One very useful piece of behavior in components is the ability to track changes to the collections of children in the view. In many ways, this is quite a nebulous subject, as the number of ways in which view collections can be altered is numerous and subtle. Thankfully, Angular 2 provides a solid foundation for tracking these changes.
The code, links, and a live example of this are available at .
Suppose you begin with the following skeleton application:
[app/inner.component.ts] import {Component, Input} from '@angular/core'; @Component({ selector: 'inner', template: `<p>{{val}}` }) export class InnerComponent { @Input() val:number; } [app/outer.component.ts] import {Component} from '@angular/core'; @Component({ selector: 'outer', template: ` <button (click)="add()">Moar</button> <button (click)="remove()">Less</button> <button (click)="shuffle()">Shuffle</button> <inner *ngFor="let i of list" val="{{i}}"> </inner> ` }) export class OuterComponent { list:Array<number> = []; add():void { this.list.push(this.list.length) } remove():void { this.list.pop(); } shuffle():void { // simple assignment shuffle this.list = this.list.sort(() => (4*Math.random()>2)?1:-1); } }
As is, this is a very simple list manager that gives you the ability to add, remove, and shuffle a list interpolated as InnerComponent
instances. You want the ability to track when this list undergoes changes and keep references to the component instances that correspond to the view collection.
Begin by using ViewChildren
to collect the InnerComponent
instances into a single QueryList
:
[app/outer.component.ts] import {Component, ViewChildren, QueryList} from '@angular/core'; import {InnerComponent} from './inner.component'; @Component({ selector: 'outer', template: ` <button (click)="add()">Moar</button> <button (click)="remove()">Less</button> <button (click)="shuffle()">Shuffle</button> <inner *ngFor="let i of list" val="{{i}}"> </inner> ` }) export class OuterComponent { @ViewChildren(InnerComponent) innerComponents: QueryList<InnerComponent>; list:Array<number> = []; add():void { this.list.push(this.list.length) } remove():void { this.list.pop(); } shuffle():void { // simple assignment shuffle this.list = this.list.sort(() => (4*Math.random()>2)?1:-1); } }
Easy! Now, once the view of OuterComponent
is initialized, you will be able to use this.innerComponents
to reference QueryList
.
QueryLists
are strange birds in Angular 2, but like many other facets of the framework, they are just a convention that you will have to learn. In this case, they are an immutable and iterable collection that exposes a handful of methods to inspect what they contain and when these contents are altered.
In this case, the two instance properties you care about are last
and changes
. last
, as you might expect, will return the last instance of QueryList
—in this case, an instance of InnerComponent
if QueryList
is not empty. changes
will return an Observable
that will emit QueryList
whenever a change occurs inside it. In the case of a collection of InnerComponent
instances, the addition, removal, and shuffling options will all be registered as changes.
Using these properties, you can very easily set up OuterComponent
to keep track of what the value of the last InnerComponent
instance is:
import {Component, ViewChildren, QueryList} from '@angular/core'; import {InnerComponent} from './inner.component'; @Component({ selector: 'app-outer', template: ` <button (click)="add()">Moar</button> <button (click)="remove()">Less</button> <button (click)="shuffle()">Shuffle</button> <app-inner *ngFor="let i of list" val="{{i}}"> </app-inner> <p>Value of last: {{lastVal}}</p> ` }) export class OuterComponent { @ViewChildren(InnerComponent) innerComponents: QueryList<InnerComponent>; list: Array<number> = []; lastVal: number; constructor() {} add() { this.list.push(this.list.length) } remove() { this.list.pop(); } shuffle() { this.list = this.list.sort(() => (4*Math.random()>2)?1:-1); } ngAfterViewInit() { this.innerComponents.changes .subscribe(e => this.lastVal = (e.last || {}).val); } }
With all of this, you should be able to find that lastVal
will stay up to date with any changes you would trigger in the InnerComponent
collection.
If you run the application as is, you will notice that an error is thrown after you click on the Moar button the first time:
Expression has changed after it was checked
This is an error you will most likely see frequently in Angular 2. The meaning is simple: since you are, by default, operating in development mode, Angular will check twice to see that any bound values do not change after all of the change detection logic has been resolved. In the case of this recipe, the emission by QueryList
modifies lastVal
, which Angular does not expect. Thus, you'll need to explicitly inform the framework that the value is expected to change again. This can be accomplished by injecting ChangeDetectorRef
, which allows you to trigger a change detection cycle once the value is changed:
import {Component, ViewChildren, QueryList, ngAfterViewInit, ChangeDetectorRef} from '@angular/core'; import {InnerComponent} from './inner.component'; @Component({ selector: 'outer', template: ` <button (click)="add()">Moar</button> <button (click)="remove()">Less</button> <button (click)="shuffle()">Shuffle</button> <inner *ngFor="let i of list" val="{{i}}"> </inner> <p>Value of last: {{lastVal}}</p> ` }) export class OuterComponent implements AfterViewInit { @ViewChildren(InnerComponent) innerComponents: QueryList<InnerComponent>; list:Array<number> = []; lastVal:number; constructor(private changeDetectorRef_:ChangeDetectorRef) {} add():void { this.list.push(this.list.length) } remove():void { this.list.pop(); } shuffle():void { // simple assignment shuffle this.list = this.list.sort(() => (4*Math.random()>2)?1:-1); } ngAfterViewInit() { this.innerComponents.changes .subscribe(innerComponents => { this.lastVal = (innerComponents.last || {}).val; this.changeDetectorRef_.detectChanges(); }); } }
At this point, everything should work correctly with no errors.
Once the OuterComponent
view is initialized, you will be able to interact with QueryList
that is obtained using ViewChildren
. Each time the collection that QueryList
wraps is modified, the Observable
exposed by its changes property will emit QueryList
, signaling that something has changed.
Importantly, Observable<QueryList>
does not track changes in the array of numbers. It tracks the generated collection of InnerComponents
. The ngFor
structural directive is responsible for generating the list of InnerComponent
instances in the view. It is this collection that QueryList
is concerned with, not the original array.
This is a good thing! ViewChildren
should only be concerned with the components as they have been rendered inside the view, not the data that caused them to be rendered in such a fashion.
One important consideration of this is that upon each emission, it is entirely possible that QueryList
will be empty. As shown above, since the Observer
of the QueryList.changes Observable
tries to reference a property of last
, it is necessary to have a fallback object literal in the event that last
returns undefined
.