As of late, I’ve really been getting more and more into Angular. For me, the important aspects are learning how to do what I’ve done with v1.x in the newer framework. This involves a great deal of understanding of the difference between v1.x and v2.x. Since one of my most popular Angular 1.x directives is the Multiselect Dropdown w/ Checkboxes, I decided to tackle converting it in incremental steps. From the top, the first piece of significant functionality is filtering the list of items.
EDIT: Be sure to check out my newer article on Angular Pipes.
Even this one small piece of functionality requires understanding a significant chunk of how Angular works: two-way binding, attribute binding, events, Components, Observables, debouncing an input, templates, filtered pipes, Typescript conventions, and the list goes on and on.
I jumped right in and a starting working on a prototype. My basic program structure will contain the Application component and then a FilteredList component. The Application component’s template will contain a FilteredList element. We’ll pass our list of items to the FilteredList and let it do all of the work.
In my app.component.ts, I am defining an array of items and creating an Observable (Observable.of()) against that array. This Observable is what will be passed to the FilteredList component. Passing the data in this way provides an ideal mechanism to allow the FilteredList to subscribe to changes in the underlying list.
@Component({ selector: 'my-app', templateUrl: 'templates/app.html' }) export class AppComponent implements OnInit { public items: Observable<Array<any>>; private _items: Array<any>; private _lipsum: any; constructor(private changeRef: ChangeDetectorRef, private appRef: ApplicationRef) { declare var LoremIpsum: any; this._lipsum = new LoremIpsum(); this._items = []; this.items = Observable.of(this._items); } createItems() { this._items.length = 0; var numItems: int = Math.random() * (200 - 10) + 10; console.log("Adding " + numItems.toString() + " items"); var i: int; for (i =0; i < numItems; i++) { var label: string = this._lipsum.singleWord(); this._items.push({ label: label, value: i.toString()}); } } ngOnInit() { this.createItems(); } }
I use the LoremIpsum generator to populate the list OnInit. That’s it for the app.component except for its HTML template. Its template only contains the FilteredList element.
<filteredlist [items]="items"></filteredlist>
Things get more interesting with the FilteredList element. I started out with a plain text input and immediately wanted to know how to capture its input and “debounce” it. In order to track changes occurring with an element, we can use Angular’s ElementRef API, which is cumbersome in this case, or we can use the Angular FormsControl types.
<input class="form-control" type="text" [value]="filterText" [placeholder]="filterPlaceholder" [formControl]="filterInput" />
By specifying a [formControl], we get the element bound to a variable in our Typescript of the same name. This, in turn, gives us a handle to the events that occur on the DOM element.
The basic class for the component, with its imports will look a bit like this:
import {Observable} from 'rxjs/Rx'; import 'rxjs/add/operator/debounceTime'; import { FormGroup, FormControl } from '@angular/forms'; export class FilteredList implements OnInit { public filterText: string; public filterPlaceholder: string; public filterInput = new FormControl(); ngOnInit() { this.enableFilter = true; this.filterText = ""; this.filterPlaceholder = "Filter..";
Within the ngOnInit, this is where we can look at the “valueChanges” Observable provided by the FormControl (filterInput). React provides a debouce mechanism out of the box. In this case, we debounce the the “change event,” and after it stabilizes, we use a subscription to set our local variable value.
this.filterInput .valueChanges .debounceTime(200) .subscribe(term => { this.filterText = term; console.log(term); }); } }
That’s debouncing a form input in a nutshell. Back to the list of items, though. How do we get those into the UI? We’re passing the items as an @Input() into the component. We only need to bind them. However, we want them to be filtered passed on the filterText we’re debouncing above. We need “filter pipes.”
What is a filter pipe? A pipe takes an input and transforms it to our desired output. In Angular, similarly to Angular 1.x, we can define a pipe to be used as a filter.
In order to implement a Pipe, we have to import Pipe and PipeTransform, and then implement PipeTransform. PipeTransform has a single method called “transform.” Below is a very simple implementation that will take an array of items and a filter object of type any. The expectation is that the filter object will contain a key (property name) and value (filter value) as a JavaScript object in the form {key:val}. The transform method will iterate over the keys and perform a basic RegEx comparison against each item in the Array.
import {Pipe, PipeTransform} from '@angular/core'; @Pipe({ name: 'filter' }) export class FilterPipe implements PipeTransform { transform(items: any, filter: any): any { if (filter && Array.isArray(items)) { let filterKeys = Object.keys(filter); return items.filter(item => filterKeys.reduce((memo, keyName) => (memo && new RegExp(filter[keyName], 'gi').test(item[keyName])) || filter[keyName] === "", true)); } else { return items; } } }
Once we have our named pipe defined and in our declarations (see module declarations), it can be used in our template. In regards to that list that was passed in, remember how we passed it in as an Observable? We’ll subscribe to the observable within our component in order to bind the underlying array to a local instance of the array:
this.items.subscribe(res => this._items = res);
With that subscription setting the “_items” array, we can finally bind to it in our UI. The UI will be a simple ngFor using the filter pipe that we previously defined.
<input class="form-control" type="text" [value]="filterText" [placeholder]="filterPlaceholder" [formControl]="filterInput" /> <div> <span>You typed this:</span><span class="bold" [innerHtml]="filterText"></span> </div> <ul> <li *ngFor="let item of _items | filter:{label: filterText}">{{ item.label }}</li> </ul>
As the user types into the filter text box, the items will be filtered. Angular will update the UI as it detects these changes.
Phew! That was a lot to explain for what, really, should be pretty simple. I sometimes question if JavaScript development isn’t, to some extent, continually getting more verbose/tedious without a ton of benefit. That’s rant for another day, though. I actually enjoyed creating this demo since it caused me to become a bit more familiar with React and to start playing with Angular’s two-way binding.
Below is a working sample plunk showing all of the pieces in motion for the filtered list.