651.288.7000 info@intertech.com

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)
  • 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;
   }
}
Now we can also create a component that uses this list and provides some data:
@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}
    ];
}
Here is the result after selecting a few items:

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('list') 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);
   }
 }
We now have a group of buttons outside of the component we can use to trigger selection via the method inside the component along with still being able to toggle from the list itself:

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;
}
Next, in our template we can specify a template variable on the component we aim to select:
<app-child-list [items]="items" #list></app-child-list>
And in the component, we can change our ViewChild selector to be more generic:
@ViewChild('list') listComponent: ListComponent;
Instead of a type, we pass in a string of the template variable name. We also specify the property type to be the newly created interface. With this we can create a new component that mimics our list but displays values as a table:
@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;
    }
}
All we have to do is change the template in our parent component to the new selector:
<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 { }
Next, a component that uses content projection – this is often some type of wrapper component that expects a specific child structure. We will create a sample form input wrapper where we will eventually add our ContentChild:
@Component({
    selector: 'app-control-formatter',
    template: `<ng-content></ng-content>`
})
export class ControlFormatterComponent { }
Lastly, our parent level template that creates the component. Only the relevant template is shown here:
<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>
As you can see, our wrapper element sits around a form control group.  This example uses a component but it could just as easily be a directive.

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');
   }
}
But we have an issue with this code! Our property is the ControlLabelDirective class, what we actually need to pass to the renderer is the ElementRef for the element. To address this, Angular provides the ability to pass an options object to the ViewChild and ContentChild decorators. One of the properties we can pass is the read option. The read option allows us to direct Angular to bind a different related object type than the directive itself:
@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');
   }
}
Another example of where the read property is very useful is with ViewChild and using a template variable. For instance, if you have something like the following:
<app-my-component myDirective #myElement></app-my-component>
If you provide the myElement template variable to ViewChild, the read property can be used to distinguish if you are looking for the component or the directive on the element to use for the binding.

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.