Note: This post is part of an eleven-part series on Angular development. You can find the series overview here.
Overview
Input, Output, and services are often all that is needed to communicate between components in Angular, however there are times when using these methods do not provide the control and flexibility needed to provide the necessary component interaction. In some cases, especially when components are tightly linked within the same feature (e.g. label / control directives) that direct access to the component or directive class can facilitate the interactivity required. Angular provides the ViewChild / ViewChildren and ContentChild / ContentChildren decorators to facilitate this type of binding and interaction. This article aims to provide a high level overview of the functionality provided just to give a taste of what is possible. There are many various ways to use these features and much of that will be left to the imagination and as an exercise to the reader. Reviewing the Angular documentation on these decorators is a good place to start. Usage of these tools is often so highly situational that makes providing examples relevant to the situations you will likely face is quite challenging. Awareness of the capability as a whole is often the major hurdle needed to investigate the implementation for a specific need.
To follow along with the articles there a two repositories created: a starting point and final solution.
Child vs. Children
The ViewChild / ViewChildren decorators and ContentChild / ContentChildren decorators are, as is expected by the similar names, very similar in functionality. The only notable difference is the Child implementation is relevant to a single reference whereas the Children returns an array of references (specifically a QueryList which is an Angular managed iterable class). Understanding the Child implementation sets up an easy transition to the Children implementation, so this article will focus on the Child level for ease of understanding and the reduction of complexity. If your situation calls for multiple references then you can consider the Children implementation. For this article, I will only refer to ViewChild and ContentChild, but just know that everything applies to it’s associated Children decorator as well.
ViewChild
Selection Types
The ViewChild decorator gives the ability to obtain a reference to an item in the template of a component. By providing a selector to the decorator, Angular will search for and bind a property on the component to the reference found by the selector. As mentioned previously, there are many different situations where this can be used. This is further highlighted by the list of valid selectors that can be used – from the Angular documentation:
The following selectors are supported.
- Any class with the
@Component
or@Directive
decorator- A template reference variable as a string (e.g. query
<my-component #cmp></my-component>
with@ViewChild('cmp')
)- Any provider defined in the child component tree of the current component (e.g.
@ViewChild(SomeService) someService: SomeService
)- Any provider defined through a string token (e.g.
@ViewChild('someToken') someTokenVal: any
)- A
TemplateRef
(e.g. query<ng-template></ng-template>
with@ViewChild(TemplateRef) template;
)
We will focus on the most commonly used selector, the component selector.
Sample Setup
Let’s set up an example to demonstrate how the decorator is used. The first thing we need is a child element – for this example we will create a wrapper element for a list group that allows for the selection of some elements:
export interface ListItemInterface {
display: string;
isSelected: boolean;
}
@Component({
selector: 'app-child-list',
template: `
<div class="list-group">
<div
class="list-group-item list-group-item-action"
*ngFor="let item of items; let i = index"
[ngClass]="{'active': item.isSelected}"
(click)="toggleItem(i)"
>{{item.display}}</div>
</div>
`,
styles: [`
.list-group-item-action {
color: inherit;
}
`]
})
export class ChildListComponent implements ListComponent {
@Input() items: ListItemInterface[];
public toggleItem(i: number): void {
this.items[i].isSelected = !this.items[i].isSelected;
}
}
@Component({
selector: 'app-view-child',
template: `<app-child-list [items]="items"></app-child-list>`
})
export class ViewChildComponent {
public items: ListItemInterface[] = [
{display: 'Apple', isSelected: false},
{display: 'Orange', isSelected: false},
{display: 'Banana', isSelected: false},
{display: 'Kiwi', isSelected: false},
{display: 'Lemon', isSelected: false}
];
}
Implementing ViewChild
For the sake of our example, let’s say we want to select items in the list component from outside of the component. We could use a service to interact with the data, but there are times that may be challenging – for example, what if the list component came from a third party library? With ViewChild, we can get a reference to the component class and directly interact with the component. First, we need to add the ViewChild decorator to our parent level component:
export class ViewChildComponent {
public items: ListItemInterface[] = ..SNIP
@ViewChild(ListComponent) listComponent: ListComponent;
}
We tell Angular we are looking for an instance of the ChildListComponent by passing the type into the decorator as the selector. As mentioned earlier, there are many forms the selector can take so review the list from the Angular documents to see the various options. Angular will search the template for an instance of the component and bind the reference to the listComponent property in the class.
Note: the binding of the ViewChild may be available in the ngOnInit life-cycle hook, but it is not guaranteed and should not be relied upon. Angular provides the ngAfterViewInit life-cycle hook that fires after the ViewChild binding takes place – so any actions that happen during the load of the component should take place here.
We can now interact with any property or method on the instance of the ChildListComponent. Let’s add a method to remotely select an item and bind that to a list of buttons we create:
@Component({
selector: 'app-view-child',
template: `
<div class="btn-group pb-5">
<button class="btn btn-secondary"
*ngFor="let item of items; let i = index"
(click)="toggleItem(i)">
{{item.display}}
</button>
</div>
<app-child-list [items]="items"></app-child-list>`
})
export class ViewChildComponent {
public items: ListItemInterface[] = ...SNIP
@ViewChild('list') listComponent: ListComponent;
public toggleItem(index: number): void {
this.listComponent.toggleItem(index);
}
}
A Note on Abstraction
The downside of this approach is that we have now created a tight coupling between the two components. While there are times when the components are so intrinsically linked that this isn’t a problem (e.g. a form group wrapper and a form control). But when the two components are not so tightly linked, it can be beneficial to use an interface to control what methods are available to other components. As all methods on a component that need to be accessible to the template must be public, this can create a situation where a method is changed and cause unexpected results throughout the application. For our example, let’s replace the list implementation with a new component that uses a table instead.
First, we create an interface we can implement with our child components that let us remotely toggle the selected items.
export interface ListComponent {
toggleItem(index: number): void;
}
<app-child-list [items]="items" #list></app-child-list>
@ViewChild('list') listComponent: ListComponent;
@Component({
selector: 'app-child-table',
template: `
<table class="table table-hover">
<tbody>
<tr *ngFor="let item of items; let i = index">
<td [ngClass]="{'bg-success': item.isSelected}" (click)="toggleItem(i)">{{item.display}}</td>
</tr>
<tbody>
</table>
`
})
export class ChildTableComponent implements ListComponent {
@Input() items: ListItemInterface[];
public toggleItem(i: number): void {
this.items[i].isSelected = !this.items[i].isSelected;
}
}
<app-child-table [items]="items" #list></app-child-table>
While this is overkill for this situation, it’s a useful pattern to keep in mind to help control interaction between components when directly accessing them. Where I have really seen this approach shine is in combination with Dynamic Component Generation to create some powerful user interfaces!
ContentChild
Selection Types
ContentChild is similar in function and usage to ViewChild – the major difference is it is used to select elements within the projected content of a component. If you are not familiar with content projection, see the article: 4 – Content Projection vs. Inputs. ContentChild has some additional restrictions to ViewChild in the types of selectors that can be used. It only supports Component or Directive types to be used as a selector.
Sample Setup
To demonstrate the usage of ContentChild, we will need to create a few items. First, a simple directive to use as a selector:
@Directive({
selector: '[appLabel]'
})
export class ControlLabelDirective { }
@Component({
selector: 'app-control-formatter',
template: `<ng-content></ng-content>`
})
export class ControlFormatterComponent { }
<app-control-formatter>
<div class="form-group">
<label for="exampleInput" appLabel>A field label</label>
<input type="text" class="form-control" id="exampleInput">
</div>
</app-control-formatter>
ContentChild Implementation
Our goal is for the wrapper component to implement some formatting on the label element inside of it. To start, we need to add the ContentChild decorator targeting the ControlLabelDirective we created and applied to the label element:
@Component({
selector: 'app-control-formatter',
template: `<ng-content></ng-content>`
})
export class ControlFormatterComponent {
@ContentChild(ControlLabelDirective) labelRef: ControlLabelDirective;
}
Just as was the case with ViewChild, we don’t have access to this reference in ngOnInit. Again, Angular gives a life-cycle hook we can use that is fired after the content binding is complete: ngAfterContentInit. In case you are wondering, this hook occurs after the ngAfterViewInit hook. To add a class to the element, we will inject the Angular Renderer2 class (if you are not familiar with Renderer2 see the Angular docs and this article: Angular: Stop Manipulating DOM With ElementRef). We can then add a class the the element in the ngAfterContentInit life-cycle hook.
@Component({
selector: 'app-control-formatter',
template: `<ng-content></ng-content>`
})
export class ControlFormatterComponent implements AfterContentInit {
@ContentChild(ControlLabelDirective) labelRef: ControlLabelDirective;
constructor(private renderer: Renderer2) { }
ngAfterContentInit(): void {
this.renderer.addClass(this.labelRef.nativeElement, 'text-info');
}
}
@Component({
selector: 'app-control-formatter',
template: `<ng-content></ng-content>`
})
export class ControlFormatterComponent implements AfterContentInit {
@ContentChild(ControlLabelDirective, {read: ElementRef}) labelRef: ElementRef;
constructor(private renderer: Renderer2) { }
ngAfterContentInit(): void {
this.renderer.addClass(this.labelRef.nativeElement, 'text-info');
}
}
<app-my-component myDirective #myElement></app-my-component>
We can now test this out and see the results:
As you can see, the ‘text-info’ class has been applied to the label. As this example shows, ContentChild is often utilized to help control formatting or DOM manipulation when a component or directive expects a specific structure. While this example applies the class at startup, this could easily be extended to change the class based on form validation errors, animations when selecting or interacting with the input, etc.
Conclusion
ViewChild and ContentChild provide an additional way to interact with components in Angular. These tools provide ability for a high level of interaction between a component and it’s children on the DOM tree. While there are dangers in the amount of coupling between components that can be created, careful architecture can help mitigate the issues in future maintenance and is well worth it for the abilities it provides.
About Intertech
Founded in 1991, Intertech delivers software development consulting to Fortune 500, Government, and Leading Technology institutions, along with real-world based corporate education services. Whether you are a company looking to partner with a team of technology leaders who provide solutions, mentor staff and add true business value, or a developer interested in working for a company that invests in its employees, we’d like to meet you. Learn more about us.