import { Component, Injectable, OnInit } from '@angular/core';
import { BooksService } from './services/books.service';
import { Book } from './shared/book';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/min';

@Component({
  selector: 'my-app',
  styleUrls: [ './app.component.css' ],
  templateUrl: './app.component.html'
})
@Injectable()
export class AppComponent implements OnInit {
  /**
   * @type {string} name The page title.
   */
  name = 'Angular v4 - Applying filters to *ngFor using pipes';

  /**
   * @type {number} minYear The year of the oldest book in the json.
   */
  minYear: number = 1996;
  
  /**
   * @type {number} maxYear The year of the newest book in the json.
   */
  maxYear: number = 2017;

  /**
   * @type {number} numberOfBooks The number of books in the JSON file, used for max attribute for limit and page.
   */
  numberOfBooks: number;
  
  /**
   * @type {number} limit The number of books per page.
   */
  limit: number;

  /**
   * @type {number} page The current page.
   */
  page: number = 1;

  /**
   * @type {Book[]} books A list of books to output in a table.
   */
  books: Book[];
  
  /**
   * @type {Book} filter The object containing the filter values to apply to bookfilter.
   * Could have created another entity called BookFilter, but it would basically have the same fields.
   */
  filter: Book = new Book();

  constructor(private booksService: BooksService) {
  }

  ngOnInit() {
    // Load books from the books service on init
    this.booksService.getBooks().subscribe(
      (books: Book[]) => {
        this.books = books;
        this.numberOfBooks = this.books.length;
        this.limit = this.books.length; // Start off by showing all books on a single page.
      });
  }
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { BooksService } from './services/books.service';
import { BookFilterPipe } from './shared/book-filter.pipe';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
  ],
  declarations: [ 
    AppComponent,
    BookFilterPipe,
  ],
  providers: [
    BooksService,
  ],
  bootstrap: [
    AppComponent,
  ]
})
export class AppModule {}
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
<!DOCTYPE html>
<html>
  <head>
    <title>Angular v4 - Applying filters to *ngFor using pipes</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      body {font-family:Verdana,Arial,Helvetica,sans-serif;}
    </style>
    <!-- Polyfills -->
    <script src="https://unpkg.com/core-js/client/shim.min.js"></script>
    <script src="https://unpkg.com/zone.js@0.8.4?main=browser"></script>
    <script src="https://unpkg.com/systemjs@0.19.39/dist/system.src.js"></script>
    <script src="systemjs.config.js"></script>
    <script>
      System.import('main.js').catch(function(err){ console.error(err); });
    </script>
  </head>
  <body>
    <my-app>Loading...</my-app>
  </body>
</html>
/**
 * WEB ANGULAR VERSION
 * (based on systemjs.config.js in angular.io)
 * System configuration for Angular samples
 * Adjust as necessary for your application needs.
 */
(function (global) {
  System.config({
    // DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER
    transpiler: 'ts',
    typescriptOptions: {
      // Copy of compiler options in standard tsconfig.json
      "target": "es5",
      "module": "commonjs",
      "moduleResolution": "node",
      "sourceMap": true,
      "emitDecoratorMetadata": true,
      "experimentalDecorators": true,
      "lib": ["es2015", "dom"],
      "noImplicitAny": true,
      "suppressImplicitAnyIndexErrors": true
    },
    meta: {
      'typescript': {
        "exports": "ts"
      }
    },
    paths: {
      // paths serve as alias
      'npm:': 'https://unpkg.com/'
    },
    // map tells the System loader where to look for things
    map: {
      // our app is within the app folder
      'app': 'app',

      // angular bundles
      '@angular/animations': 'npm:@angular/animations/bundles/animations.umd.js',
      '@angular/animations/browser': 'npm:@angular/animations/bundles/animations-browser.umd.js',
      '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
      '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
      '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
      '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
      '@angular/platform-browser/animations': 'npm:@angular/platform-browser/bundles/platform-browser-animations.umd.js',
      '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
      '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
      '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
      '@angular/router/upgrade': 'npm:@angular/router/bundles/router-upgrade.umd.js',
      '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
      '@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js',
      '@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',

      // other libraries
      'rxjs':                      'npm:rxjs@5.0.1',
      'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
      'ts':                        'npm:plugin-typescript@5.2.7/lib/plugin.js',
      'typescript':                'npm:typescript@2.2.1/lib/typescript.js',

    },
    // packages tells the System loader how to load when no filename and/or no extension
    packages: {
      app: {
        main: './main.ts',
        defaultExtension: 'ts',
        meta: {
          './*.ts': {
            loader: 'systemjs-angular-loader.js'
          }
        }
      },
      rxjs: {
        defaultExtension: 'js'
      }
    }
  });

})(this);

/*
Copyright 2016 Google Inc. All Rights Reserved.
Use of this source code is governed by an MIT-style license that
can be found in the LICENSE file at http://angular.io/license
*/
var templateUrlRegex = /templateUrl\s*:(\s*['"`](.*?)['"`]\s*)/gm;
var stylesRegex = /styleUrls *:(\s*\[[^\]]*?\])/g;
var stringRegex = /(['`"])((?:[^\\]\\\1|.)*?)\1/g;

module.exports.translate = function(load){
  if (load.source.indexOf('moduleId') != -1) return load;

  var url = document.createElement('a');
  url.href = load.address;

  var basePathParts = url.pathname.split('/');

  basePathParts.pop();
  var basePath = basePathParts.join('/');

  var baseHref = document.createElement('a');
  baseHref.href = this.baseURL;
  baseHref = baseHref.pathname;

  if (!baseHref.startsWith('/base/')) { // it is not karma
    basePath = basePath.replace(baseHref, '');
  }

  load.source = load.source
    .replace(templateUrlRegex, function(match, quote, url){
      let resolvedUrl = url;

      if (url.startsWith('.')) {
        resolvedUrl = basePath + url.substr(1);
      }

      return 'templateUrl: "' + resolvedUrl + '"';
    })
    .replace(stylesRegex, function(match, relativeUrls) {
      var urls = [];

      while ((match = stringRegex.exec(relativeUrls)) !== null) {
        if (match[2].startsWith('.')) {
          urls.push('"' + basePath + match[2].substr(1) + '"');
        } else {
          urls.push('"' + match[2] + '"');
        }
      }

      return "styleUrls: [" + urls.join(', ') + "]";
    });

  return load;
};
{
  "data": [
    {
      "id": 1,
      "title": "A Game of Thrones",
      "year": 1996,
      "author": "George R. R. Martin"
    },
    {
      "id": 2,
      "title": "A Clash of Kings",
      "year": 1998,
      "author": "George R. R. Martin"
    },
    {
      "id": 3,
      "title": "A Storm of Swords",
      "year": 2000,
      "author": "George R. R. Martin"
    },
    {
      "id": 4,
      "title": "A Feast for Crows",
      "year": 2005,
      "author": "George R. R. Martin"
    },
    {
      "id": 5,
      "title": "A Dance with Dragons",
      "year": 2011,
      "author": "George R. R. Martin"
    },
    {
      "id": 6,
      "title": "The Winds of Winter",
      "year": null,
      "author": "George R. R. Martin"
    },
    {
      "id": 7,
      "title": "A Dream of Spring",
      "year": null,
      "author": "George R. R. Martin"
    },
    {
      "id": 8,
      "title": "The Philosopher's Stone",
      "year": 1997,
      "author": "J. K. Rowling"
    },
    {
      "id": 9,
      "title": "The Chamber of Secrets",
      "year": 1998,
      "author": "J. K. Rowling"
    },
    {
      "id": 10,
      "title": "The Prisoner of Azkaban",
      "year": 1999,
      "author": "J. K. Rowling"
    },
    {
      "id": 11,
      "title": "The Goblet of Fire",
      "year": 2000,
      "author": "J. K. Rowling"
    },
    {
      "id": 12,
      "title": "The Order of the Phoenix",
      "year": 2003,
      "author": "J. K. Rowling"
    },
    {
      "id": 13,
      "title": "The Half-Blood Prince",
      "year": 2005,
      "author": "J. K. Rowling"
    },
    {
      "id": 14,
      "title": "The Deathly Hallows",
      "year": 2007,
      "author": "J. K. Rowling"
    },
    {
      "id": 15,
      "title": "Flaggermusmannen",
      "year": 1997,
      "author": "Jo Nesbø"
    },
    {
      "id": 16,
      "title": "Kakerlakkene",
      "year": 1998,
      "author": "Jo Nesbø"
    },
    {
      "id": 17,
      "title": "Rødstrupe",
      "year": 2000,
      "author": "Jo Nesbø"
    },
    {
      "id": 18,
      "title": "Sorgenfri",
      "year": 2002,
      "author": "Jo Nesbø"
    },
    {
      "id": 19,
      "title": "Marekors",
      "year": 2003,
      "author": "Jo Nesbø"
    },
    {
      "id": 20,
      "title": "Frelseren",
      "year": 2005,
      "author": "Jo Nesbø"
    },
    {
      "id": 21,
      "title": "Snømannen",
      "year": 2007,
      "author": "Jo Nesbø"
    },
    {
      "id": 22,
      "title": "Panserhjerte",
      "year": 2009,
      "author": "Jo Nesbø"
    },
    {
      "id": 23,
      "title": "Gjenferd",
      "year": 2011,
      "author": "Jo Nesbø"
    },
    {
      "id": 24,
      "title": "Politi",
      "year": 2013,
      "author": "Jo Nesbø"
    },
    {
      "id": 25,
      "title": "Tørst",
      "year": 2017,
      "author": "Jo Nesbø"
    }
  ]
}
import { Pipe, PipeTransform } from '@angular/core';

import { Book } from './book';

@Pipe({
    name: 'bookfilter',
    pure: false
})
export class BookFilterPipe implements PipeTransform {
  transform(items: Book[], filter: Book): Book[] {
    if (!items || !filter) {
      return items;
    }
    // filter items array, items which match and return true will be kept, false will be filtered out
    return items.filter((item: Book) => this.applyFilter(item, filter));
  }
  
  /**
   * Perform the filtering.
   * 
   * @param {Book} book The book to compare to the filter.
   * @param {Book} filter The filter to apply.
   * @return {boolean} True if book satisfies filters, false if not.
   */
  applyFilter(book: Book, filter: Book): boolean {
    for (let field in filter) {
      if (filter[field]) {
        if (typeof filter[field] === 'string') {
          if (book[field].toLowerCase().indexOf(filter[field].toLowerCase()) === -1) {
            return false;
          }
        } else if (typeof filter[field] === 'number') {
          if (book[field] !== filter[field]) {
            return false;
          }
        }
      }
    }
    return true;
  }
}
/**
 * Book entity, used for filtering as well.
 */
export class Book {
  /**
   * @type {number} id Unique numeric identifier.
   */
  id: number;
  
  /**
   * @type {string} title The title of the book.
   */
  title: String;

  /**
   * @type {string} author The author of the book.
   */
  author: String;

  /**
   * @type {number} year The year the book was published.
   */
  year: number;
}
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';

import { Book } from '../shared/book';

@Injectable() 
export class BooksService {
  constructor(private http: Http) {
  }
  
  /**
   * Load books from the static books.json data, usually an API URL.
   * 
   * @return {Observable<Book[]>} A list of books.
   */
  getBooks(): Observable<Book[]> {
    return this.http.get('app/data/books.json')
      .map(res => res.json().data)
      .catch(res => console.log(res));
  }
}
table,footer {
  width: 550px;
}
h1 {
  color: #404040;
  font-size: 16px;
}
ul {
  margin: 0;
}
th,td,input,p,footer,li,label {
  color: #404040;
  font-size: 14px;
}
table {
  border-collapse: collapse;
  border-spacing: 0;
  margin: 1em 0;
}
table thead th, table tfoot td {
  background-color: lightgray;
  border: 1px solid gray;
  padding: 4px;
}
table td {
  border: 1px solid gray;
  padding: 2px 4px;
}
table input {
  padding: 1px 2px;
}
table input[type=number] {
  width: 60px;
}
<h1>{{name}}</h1>

<form>
<table>
  <thead>
    <tr>
      <th><input type="number" name="id" [(ngModel)]="filter.id" min="1" [attr.max]="[numberOfBooks]" step="1" placeholder="Id"></th>
      <th><input type="text" name="title" [(ngModel)]="filter.title" placeholder="Title"></th>
      <th><input type="text" name="author" [(ngModel)]="filter.author" placeholder="Author"></th>
      <th><input type="number" name="year" [(ngModel)]="filter.year" [attr.min]="minYear" [attr.max]="[maxYear]" step="1" placeholder="Year"></th>
    </tr>
  </thead>
  <tfoot>
    <tr>
      <td colspan="4">
        <label for="limit">Limit: </label><input type="number" name="limit" [(ngModel)]="limit" min="1" [attr.max]="[numberOfBooks]" step="1" placeholder="Limit">
        <label for="page">Page: </label><input type="number" name="page" [(ngModel)]="page" min="1" [attr.max]="[numberOfBooks]" step="1" placeholder="Page">
      </td>
    </tr>
  </tfoot>
  <tbody>
    <tr *ngFor="let book of books | bookfilter:filter | slice:(page-1)*limit:page*limit">
      <td>{{book.id}}</td>
      <td>{{book.title}}</td>
      <td>{{book.author}}</td>
      <td>{{book.year}}</td>
    </tr>
  </tbody>
</table>
</form>

<footer>
  <p>
    <em>
    Ps. since this is just a demo of pipes, the limit and page max attribute is just set to match the number of books in the books.json file.<br>
    Like a paper notebook, you'll get blank pages when going beyond the unwritten pages ;o)
    </em>
  </p>
  References:
  <ul>
    <li><a href="https://stackoverflow.com/questions/34164413/how-to-apply-filters-to-ngfor/">stackoverflow.com/questions/34164413/how-to-apply-filters-to-ngfor/</a></li>
    <li><a href="https://angular.io/docs/ts/latest/guide/pipes.html">angular.io/docs/ts/latest/guide/pipes.html</a></li>
    <li><a href="https://angular.io/docs/ts/latest/api/common/index/SlicePipe-pipe.html">angular.io/docs/ts/latest/api/common/index/SlicePipe-pipe.html</a></li>
  </ul>
</footer>