Our customers repeatedly told us about the challenges they are facing in their Angular projects. This Advanced Angular Concepts Tutorial directly addresses some of those issues.
1.1 Detecting Change to @Input Binding
When a parent component updates a property of a child bound using @Input the child may like to get notified of the change. This is necessary when a change in a component’s state requires further processing. Example: When you change the sorting order property of a product list component it needs to sort the products differently.
There are two ways a child can intercept changes to an @Input property:
- Using a custom setter
- Using ngOnChanges
1.2 Example Child and Parent Components
We will use these components to illustrate both approaches. The parent provides a GUI to increment a counter. The child simply displays the counter value.
@Component({ selector: 'app-child', template: `{{counter}}
` }) export class ChildComponent { @Input() counter = 0 } @Component({ selector: 'app-parent', template: `` }) export class ParentComponent { p_counter = 0 update() { this.p_counter += 1 } }
1.3 Using a Custom Setter
@Component({ selector: 'app-child', template: `{{counter}}
` }) export class ChildComponent { _counter = 0 @Input() set counter(newValue:number) { console.log("Intercepted update:", newValue) this._counter = newValue } get counter() { return this._counter } }
Notes:
In the example above “counter” is now a computed property with a custom getter and setter. It internally uses the _counter member variable to store state.
Code for the parent component doesn’t change. We still bind to the counter property.
1.4 Using ngOnChanges
import { Component, Input, OnChanges, SimpleChange } from '@angular/core'; @Component({...}) export class ChildComponent implements OnChanges { @Input() counter = 0 ngOnChanges(changes: {: SimpleChange}) { let c = changes console.log(c.previousValue, c.currentValue, c.firstChange) } } //Prints undefined 0 true 0 1 false 1 2 false
Notes:
ngOnChanges receives a dictionary where the key is a string with the same name as the property that was modified. The value is a SimpleChange object. That object has these properties:
- firstChange – If the property is being set for the first time.
- previousValue – The value before the change. It is normally undefined when firstChange is true.
- currentValue – The value after the update.
1.5 Advanced Inter Component Communication
Data and event binding using @Input and @Output work very well for immediate parent-child components. But this becomes cumbersome for deeply nested components or for communication between siblings.
Angular provides a couple of ways to deal with these situations:
- A component can directly access an instance of a child component.
- A component can directly access an instance of an ancestor.
- For truly complex situations use the Subject API discussed later in this chapter.
Notes:
For example, these situations become difficult to implement using @Input and @Output:
- A parent wants to set a property of a grand child or a child further down the descendants hierarchy. Every component in the chain has to set up @Input binding for the property.
- A component needs to raise an event for a grand parent or a component further higher in the hierarchy. Every component in the chain needs to setup an @Output binding.
- A component needs to communicate with a sibling.
- A component needs to invoke a method of another component.
1.6 Direct Access to Child
You can access an immediate child directive or component in a few ways:
- Using a template local variable for the child component instance.
- Using the @ViewChild decorator. This is strongly typed and recommended over template local variables.
- Use the @ViewChildren decorator to get a collection of children and monitor changes in that collection.
Notes:
These give you access to immediate children and not any arbitrary descendants down the hierarchy.
1.7 Using a Template Local Variable
@Component({ selector: 'app-parent', template: `` }) export class ParentComponent { }
Notes:
In the example above we declare a local variable called “ch” for an instance of ChildComponent. From the button click handler, we access the counter variable of the class. You could also call a method of ChildComponent from the template this way.
1.8 Using the @ViewChild Decorator
import { Component, ViewChild } from '@angular/core'; import { ChildComponent } from '../child/child.component'; @Component({ selector: 'app-parent', template: `` }) export class ParentComponent { @ViewChild(ChildComponent, {static: false}) child:ChildComponent update() { this.child.counter += 1 } }
- Access to the 'child' property depends the type of the childComponent as well as the timing.
- In the case above the variable is available after the view initializes (from ngAfterViewInit()) but is not yet available when ngOnInit() is called.
- If the DOM changes and children are added or removed the variable is kept up to date.
Notes:
The @ViewChild decorator locates the child component by its class. This works well when there is only one instance of the child component inside the parent. If there are multiple instances then @ViewChild returns the first one. To get a specific instance you need to declare a template local variable for the child and refer to the variable name from @ViewChild. In the example below we update the counter of the second child.
@Component({ selector: 'app-parent', template: `` }) export class ParentComponent { @ViewChild("ch2", {static: true}) child:ChildComponent update() { this.child.counter += 1 } }
1.9 Static and Dynamic Children with @ViewChild
Children referenced by @ViewChild have two types:
- Dynamic {static:false} – If they are wrapped by an ngFor or ngInit and are thus subject to change.
- Static {static:true} – If they are not dynamic
The child’s static state needs to be declared in the second parameter (options) of the @ViewChild annotation.
@ViewChild('ch1', {static:true} ) prop1: StaticChildComponent; @ViewChild('ch2', {static:false} ) prop2: DynamicChildComponent;
As a rule properties based on Dynamic view children should be used only after the view initializes (for example from within ngAfterViewInit()) and not before.
1.10 More About @ViewChild
There may be several objects associated with the same element in the template. For example, an element can have many attribute directives. You can specify the object you need by supplying its type in the options object parameter of @ViewChild.
@Component({ selector: 'app-parent', template: `` }) export class ParentComponent implements OnInit { @ViewChild("d", {static: false, read: ViewContainerRef}) childContainer @ViewChild("d", {static: false, read: ElementRef}) childElement ngOnInit() { console.log(this.childContainer) console.log(this.childElement) } }
Notes:
You specify the type of object instance you are querying using the “read” property. If you do not supply the read property Angular behaves this way:
- If the child is a component tag then the component instance is returned.
- Otherwise, the ElementRef instance for the tag is returned.
1.11 The @ViewChildren Decorator
Similar to @ViewChild but gives you access to a collection of all matching children.
import { Component, AfterViewInit, ViewChildren, QueryList } from '@angular/core'; @Component({ selector: 'app-parent', template: `` }) export class ParentComponent implements AfterViewInit { @ViewChildren(ChildComponent) childList: QueryList ngAfterViewInit() { this.childList.forEach(child => console.log(child.counter)) } }
Notes:
The @ViewChildren decorator does not require the static state {static:boolean} option parameter like we saw with @ViewChild
1.12 Live Monitoring of Children
You can monitor any dynamic addition or removal of children.
@Component({ selector: 'app-parent', template: `` }) export class ParentComponent implements AfterViewInit { showTwo = false @ViewChildren(ChildComponent) childList: QueryList ngAfterViewInit() { this.childList.changes.subscribe(queryList => queryList.forEach(child => console.log(child.counter))) window.setTimeout(_ => this.showTwo = true, 1000) } }
Notes:
In the example above we add the second child after one second. This change is picked up by our subscription callback. The callback receives a new QueryList which you can iterate over.
1.13 Direct Access to the Parent Component
You can inject an instance of an ancestor component.
export class ChildComponent implements OnInit { constructor(private parent:ParentComponent, private grandParent:AppComponent) { } }
Since @ViewChild and @ViewChildren do not give access to deeply nested children it is better for these children to lookup an ancestor and initiate communication.
To restrict the search to the immediate parent use @Host.
constructor(@Host() private parent:ParentComponent) { }
You can make a component more flexible by not requiring it to have a specific parent. Use @Optional for that.
constructor(@Optional() @Host() private parent:ParentComponent) { }
Notes:
@ViewChild and @ViewChildren limit access to the immediate children only. They are not very suitable when a parent needs to communicate with a deeply nested descendent. In a situation like that it will be much easier for the descendants to lookup an ancestor component and then make method calls to initiate communication.
1.14 Communication Using Subject API
When deeply nested components need to notify events to each other data and event binding may become complex.
In a situation like this RxJS Subject API can considerably simplify the implementation. The basic steps are as follows:
- Use a singleton service to create a subject (or topic of conversation).
- To notify an event a component will post a message to the subject.
- Any other component can subscribe to the subject and be notified.
1.15 Creating a Subject
import { Subject } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class TopicManagerService { public cartTopic = new Subject() }
A subject is strongly typed. You can specify the data type of messages posted in the subject when creating the subject.
1.16 Publishing a Message in a Subject
In this example, C1 component is publishing a message.
@Component({ selector: 'app-c1', template: `` }) export class C1Component { constructor(private tm:TopicManagerService) { } publish() { this.tm.cartTopic.next({itemId: 1001, qty: 2}) } }
1.17 Subscribing to the Subject
In this example C2 is subscribing. Code will be same for C3.
export class C2Component implements OnInit { constructor(private tm:TopicManagerService) { } ngOnInit() { this.tm.cartTopic.subscribe(item => console.log(item)) } }
1.18 Problem With Ordering
If you subscribe after a message is posted you will not receive the message. This can be a problem if components are posting and subscribing from ngOnInt since the order of initialization can be hard to control.
To receive the last posted message even if you have subscribed late, use the BehaviorSubject class instead of a plain Subject. The constructor needs a starter message which can be undefined.
public cartTopic = new BehaviorSubject(undefined)
To receive more than one last posted messages use ReplaySubject. The constructor takes the number of past messages.
public cartTopic = new ReplaySubject(5)
1.19 Content Projection
Content projection lets a parent component (i.e., the user of the component) provide parts of the template of a child component.
This has many useful benefits:
- The parent can customize how the component should render data.
- UI toolkit components can let users decide what content gets displayed. For example, a card component can simply focus on showing a shaded border and a title but the parent component can decide what gets displayed in the card.
1.20 Setup Projection Using ng-content
The ng-content directive lets you specify a placeholder. A user of the component can supply the actual template for that area. Below is a card component that has two placeholders.
@Component({ selector: 'app-card', template: `` }) export class ChildComponent {}
ng-content takes a CSS selector which helps the parent supply different templates for different placeholders. We see that next.
1.21 Supplying Template for ng-content
Here a user of the card component supplies its templates using matching CSS selectors.
@Component({ selector: 'app-parent', template: `<app-card> <div class="title">First Card</div> <div class="body">First card body here.</div> </app-card> <app-card> <div class="title">{{title2}}</div> <div class="body"><button (click)="greet()">Hello</button></div> </app-card>` }) export class ParentComponent { title2 = "Second Title" greet(){} }
Notes:
The template supplied by the parent has full access to its own properties and methods. We see that above in the second card. This works just fine even though the template will actually be executed inside the card component. Without this feature it will be very difficult to create a generic card component that can host any content. Content projection makes it all very easy.
1.22 The Host Element
Angular creates a DOM element for every component on the page. This is called the host element. The host contains as child the elements generated by the component's template.
In the example of the previous slide the element created for the <app-card%gt; tag will be the host element for the card component.
Sometimes you need to access the host element. For example
- A card component needs to show a border around it.
- A popup menu component needs to position the host where the mouse was clicked.
1.23 Static Styling of the Host Element
Use the :host pseudo selector from the component's CSS.
:host { display: block; border: 1px solid black; width: 200px; position: absolute; }
1.24 Setting DOM Properties of the Host
Use the @HostBinding decorator to one-way bind a component property to a DOM property of the host element.
The following example shows the host element and sets its position.
export class PopupMenuComponent { @HostBinding("style.top") y = "0px" @HostBinding("style.left") x = "0px" @HostBinding("style.visibility") visibility = "hidden" openAt(x:number, y:number) { this.x = `${x}px` this.y = `${y}px` this.visibility = "visible" } }
1.25 Dynamically Loading a Component
Some applications may need to dynamically decide what component to show without directly referring to its tag in a template. For example, different products may need to be displayed using different components.
This can be done by a parent component following these steps:
- Create a ComponentFactory for the component class that you need to display. This is done using the ComponentFactoryResolver service.
- In the parent’s template add a placeholder element where the child component will be dynamically inserted.
- Using the ViewContainerRef object for the placeholder element create an instance of the child component. This will add the child to the component tree and make it visible.
- Add all the child components that are dynamically displayed in the entryComponents list of the application module. Without this the tree shaker will exclude the child components from the build process (since they are never directly used in any template).
1.26 Dynamic Loading Example
In this example the parent component – HostComponent – dynamically loads either BasicViewComponent or FullViewComponent and adds it to the component tree.
User can use a checkbox to toggle between the two components.
The template of the parent component:
@Component({ selector: 'app-host', template: ` <div> <input type="checkbox" (change)="changeView()" ="isBasic"> Basic view <div #compHost></div> <!-- Placeholder for child--> </div> ` }) export class HostComponent {...}
1.27 HostComponent Code
export class HostComponent { isBasic = true @ViewChild("compHost", {static: true, read: ViewContainerRef}) container:ViewContainerRef constructor(private resolver: ComponentFactoryResolver){} changeView() { let compClass = this.isBasic ? BasicViewComponent : FullViewComponent let factory = this.resolver.resolveComponentFactory(compClass) this.container.clear() //Remove old component let compRef = this.container.createComponent(factory) } }
Notes:
Two key players here are ComponentFactoryResolver and ViewContainerRef. A ViewContainerRef is used to insert a component instance into the component tree. Every element in a template has an associated ViewContainerRef. Here we look it up for the tag with the local variable “compHost”.
A ComponentFactoryResolver gives us a component factory appropriate for a given component class. The createComponent() method of ViewContainerRef takes the factory as input. The method uses the factory to create a new instance of the component and adds it to the component tree.
The createComponent() method returns a ComponentRef object. You can get the actual component instance using the “instance” property of that object. The instance here will be either a BasicViewComponent or FullViewComponent. Once you have the instance you can even set its properties to set its input. There is no other way to do this since you can’t really use data binding to set input for dynamically loaded components.
1.28 Setting entryComponents
Register the dynamically loaded components to the entryComponents list of the application module.
@NgModule({ declarations: , imports: , entryComponents: [ BasicViewComponent, FullViewComponent ], ... }) export class AppModule { }
1.29 Optimizing Change Detection
By default almost any user interaction will cause templates for all components on the page to be re-executed. For a complex application with hundreds of components on a page, this can slow down the screen update or cause flickers.
There are two ways you can take control of the situation and only re-execute a component’s template if it is truly necessary:
- Set the change detection strategy to OnPush. This is the simplest way and should be considered first.
- By programmatically detaching or attaching a component from the change detection process. This gives you more precise control over when a component’s template should be re-run. Not discussed here.
1.30 Example Excessive Template Execution
@Component({ selector: 'app-a', template: `<input type="text" ="todoItem"/> <button (click)="addItem()">Add</button> <app-b ="list"></app-b>` }) export class AComponent { todoItem = "" list = [] addItem() { this.list.push(this.todoItem) } } @Component({ selector: 'app-b', template: `<p *ngFor="let item of todoList">{{item}}</p>` }) export class BComponent { @Input() todoList = [] }
Notes:
As things stand right now every keystroke in the input field of AComponent will cause the template for both components to be re-executed. As you can imagine; if the list is long then rebuilding the DOM for BComponent can take some time.
1.31 Using OnPush Change Detection Strategy
Component B does not need to run its template for every key stroke in the parent component (A). It can use the OnPush strategy.
@Component({ ... changeDetection: ChangeDetectionStrategy.OnPush }) export class BComponent {@Input() todoList = []}
Now the template of B will be run only if any of its bound @Input variables have changed.
Unfortunately, this will not solve our problem. Component A adds new items to the existing array. Angular only does shallow checks for changes in the object reference. It does not check if the contents of a variable (such as an array or an object) have changed. We will see next how to work around this.
1.32 Properly Changing @Input Variables
A parent needs to set an entirely new object to a bound property of a child for Angular to realize that the property has changed. In our case AComponent needs to create a new list. Now the template of B will be run only after user has clicked on the Add button.
addItem() { this.list = }
Notes:
Recreating a large array like this can be expensive. To solve that issue, store the array inside an object and just recreate the object. Pass the object as input to the child.
1.33 Additional Notes on OnPush
OnPush setting only takes effect if a parent changes variable bound via @Input. However, if the child itself directly changes that variable then the child’s template will be always executed. OnPush has no effect in that case. If change detection is skipped for a child due to the use of OnPush then the entire component tree from the child onwards will be skipped.
1.34 Summary
In this tutorial we covered:
- Sometimes you need to detect changes to the variables bound by @Input to perform certain work.
- Basic @Input and @Output based inter-component communication only works between direct parent and children. For communication between arbitrary components it is easier to use direct access or the Subject API.
- Content projection allows a parent component to supply parts of a child’s template. This is often used by GUI framework components like modal dialog.
- By default Angular re-executes templates of all components on a page if any change is detected in their state. This can slow down page refresh if you have many components. OnPush is an easy way to limit re-execution of a component’s template only if a @Input bound variable has changed