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>