ViewChild versus ContentChild

Although both concepts sound similar, they have quite different semantics. In order to understand them better, let’s take a look at the following example:

// dunebook4/ts/view-child-content-child/app.ts
@Component({
  selector: 'user-badge',
  template: '…'
})
class UserBadge {}

@Component({
  selector: 'user-rating',
  template: '…'
})
class UserRating {}

Here, we’ve defined two components: UserBadge and UserRating. Let’s define a parent component, which comprises both the components:

@Component({
  selector: 'user-panel',
  template: '<user-badge></user-badge>',
  directives: [UserBadge]
})
class UserPanel {…}

Note that the template of the view of UserPanel contains only the UserBadge component’s selector. Now, let’s use the UserPanel component in our application:

@Component({
  selector: 'app',
  template: `<user-panel>
    <user-rating></user-rating>
  </user-panel>`,
  directives: [CORE_DIRECTIVES, UserPanel, UserRating]
})
class App {
  constructor() {}
}

The template of our main App component uses the UserPanel component and nests the UserRating component inside it. Now, let’s suppose we want to get a reference to the instance of the UserRating component that is used inside the user-panel element in the App component and a reference to the UserBadge component, which is used inside the UserPanel template. In order to do this, we can add two more properties to the UserPanel controller and add the @ContentChild and @ViewChild decorators to them with the appropriate arguments:

class UserPanel {
  @ViewChild(UserBadge)
  badge: UserBadge;

  @ContentChild(UserRating)
  rating: UserRating;
  constructor() {
    //
  }
}

The semantics of the badge property declaration is this: “get the instance of the first child component of the type UserBadge, which is used inside the UserPanel template”. Accordingly, the semantics of the rating property’s declaration is this: “get the instance of the first child component of the type UserRating, which is nested inside the UserPanel host element”.

Now, if you run this code, you’ll note that the values of the badge and rating properties are still equal to the undefined value inside the controller’s constructor. This is because they are still not initialized in this phase of the component’s life cycle. The life cycle hooks that we can use in order to get a reference to these child components are ngAfterViewInit and ngAfterContentInit. We can use these hooks simply by adding definitions of the ngAfterViewInit and ngAfterContentInit methods to the component’s controller. We will make a complete overview of the life cycle hooks that Angular 2 provides shortly.

To recap, we can say that the content children of the given components are the child elements that are nested within the component’s host element. In contrast, the view children directives of the given component are the elements used within its template.

Note

In order to get platform independent reference to a DOM element, again, we can use @ContentChildren and @ViewChildren. For instance, if we have the following template: <input #todo> we can get a reference to the input by using: @ViewChild('todo').

Since we are already familiar with the core differences between view children and content children now, we can continue with our tabs implementation.

In the tabs component, instead of using the @ContentChild decorator, we use @ContentChildren. We do this because we have multiple content children and we want to get them all:

@ContentChildren(TabTitle)
tabTitles: QueryList<TabTitle>;

@ContentChildren(TabContent)
tabContents: QueryList<TabContent>;

Another main difference we can notice is that the types of the tabTitles and tabContents properties are QueryList with the respective type parameter and not the component’s type itself. We can think of the QueryList data structure as a JavaScript array—we can apply the same high-order functions (map, filter, reduce, and so on) over it and loop over its elements; however, QueryList is also observable, that is, we can observe it for changes.

As the final step of our Tabs definition, let’s take a peek at the implementation of the ngAfterContentInit and select methods:

ngAfterContentInit() {
  this.tabTitles
    .map(t => t.tabSelected)
    .forEach((t, i) => {
      t.subscribe(_ => {
        this.select(i)
      });
    });
  this.active = 0;
  this.select(0);
}

In the first line of the method’s implementation, we loop all tabTitles and take the observable’s references. These objects have a method called subscribe, which accepts a callback as an argument. Once the .emit() method of the EventEmitter instance (that is, the tabSelected property of any tab) is called, the callback passed to the subscribe method will be invoked.

Now, let’s take a look at the select method’s implementation:

select(index: number) {
  let contents: TabContent[] = this.tabContents.toArray();
  contents[this.active].isActive = false;
  this.active = index;
  contents[this.active].isActive = true;
  this.tabChanged.emit(index);
}

In the first line, we get an array representation of tabContents, which is of the type QueryList<TabContent>. After that, we set the isActive flag of the current active tab to false and select the next active one. In the last line in the select method’s implementation, we trigger the selected event of the Tabs component by invoking this.tabChanged.emit with the index of the currently selected tab.