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.