The title of this article doesn’t have quite the same air of excitement about it when compared to say Batman vs Superman, but it’s a concept I struggled with for a while and I felt that it is worthy of its own blog post.
When building an application with Ionic (or more generally, with Angular) you may come across the following annotations:
But that’s not really the “Angular way” to do it. Angular is designed to be platform agnostic (i.e. it doesn’t have to run on the web), so it’s better to avoid working directly with the DOM wherever possible. Using @ViewChildren and ContentChildren will also return a QueryList which provides an observable that you can watch for changes, so it works much more nicely in the Angular environment than using
getElementById does (as tempting as that may be).
In this tutorial, we are going to briefly cover how to grab elements using @ViewChildren and @ContentChildren, and then we will walk through what exactly the difference is between the two, and when you should use one over the other.
Before We Get Started
Before you go through this tutorial, you should have at least a basic understanding of Ionic 2 concepts. You must also already have Ionic 2 set up on your machine.
If you’re not familiar with Ionic 2 already, I’d recommend reading my Ionic 2 Beginners Guide first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic 2, then take a look at Building Mobile Apps with Ionic 2.
If you are unfamiliar with creating custom components in general, and do not understand concepts like @Input, @Output, and content projection, then it may be worthwhile watching this video first.
How do they work?
Although they look somewhat intimidating, using these annotations to grab elements is quite straight forward. You use them to set up member variables above your
constructor that you can use throughout your component.
You can supply these with either a local variable or you can provide it with the class of the component you are attempting to grab. So, you could do something like this:
so that you could access all of the
<ion-item> elements in your template through
this.items, or you could do:
to grab a reference to an element that you have set up a local template variable on, like this:
The basic idea is simple enough, but the distinction between @ViewChildren and @ContentChildren can be quite difficult to make at first.
When to use @ViewChildren
You should use @ViewChildren when you have added the element you are trying to grab directly to the component yourself, in other words, if the element you are grabbing is not added to the component you are working with through content projection with
<ng-content>. If you are working with pages in an Ionic application, then this will likely be the most common scenario, and unless you are building your own custom components then you likely would only need to use @ViewChild and @ViewChildren.
I’m going to use an application that uses a custom component that I built recently as an example. Let’s take a look at the template for the
<ion-header> <expandable-header [scrollArea]="mycontent" headerHeight="125"> <ion-item> <ion-label><ion-icon name="search"></ion-icon></ion-label> <ion-input type="text"></ion-input> </ion-item> <ion-item> <ion-label><ion-icon name="funnel-outline"></ion-icon></ion-label> <ion-input type="text"></ion-input> </ion-item> </expandable-header> <ion-navbar color="primary"> <ion-title> Expandable Header </ion-title> </ion-navbar> </ion-header> <ion-content fullscreen #mycontent> <ion-card *ngFor="let test of testData"> <ion-item> <ion-avatar item-left> <img src="https://ionicframework.com/dist/preview-app/www/assets/img/marty-avatar.png"> </ion-avatar> <h2>Marty McFly</h2> <p>November 5, 1955</p> </ion-item> <img src="https://ionicframework.com/dist/preview-app/www/assets/img/advance-card-bttf.png"> <ion-card-content> <p>Wait a minute. Wait a minute, Doc. Uhhh... Are you telling me that you built a time machine... out of a DeLorean?! Whoa. This is heavy.</p> </ion-card-content> <ion-row> <ion-col> <button ion-button icon-left clear small> <ion-icon name="thumbs-up"></ion-icon> <div>12 Likes</div> </button> </ion-col> <ion-col> <button ion-button icon-left clear small> <ion-icon name="text"></ion-icon> <div>4 Comments</div> </button> </ion-col> <ion-col center text-center> <ion-note> 11h ago </ion-note> </ion-col> </ion-row> </ion-card> </ion-content>
Everything that you see above has been added directly by me to the template, so if I want to grab any of those elements from within the context of the
HomePage component, then I can do so with @ViewChild or @ViewChildren.
If I wanted to grab every
<ion-item> on the page I could do so with:
This would return the
<ion-item> elements that are inside of the
<ion-content> but it will also grab the
<ion-item> elements that are inside of the
<expandable-header> tags. This is an important distinction, because as you will see in the next section, the elements that are inside of the
<expandable-header> custom component can also be grabbed with @ContentChildren, but only from the context of the ExpandableHeader component, not the
HomePage component (remember, a page in Ionic is still just a normal component). I will elaborate on this in the next section, but in this case, I have added those elements directly to the
HomePage template so I can grab them with @ViewChildren.
If I just wanted to grab the
<ion-content> area, I could select it in a similar way to the way that I selected the Item’s, but I could also use the
#mycontent local variable that I added by doing this:
or I could just do this:
Content is the name of the class for the
When to use @ContentChildren
You can use @ContentChildren to grab a reference to content that has been projected into a component through the use of
<ng-content>. This is a subtle but important difference. If I were to try to use @ContentChildren to grab those items in the example above, by doing this:
It wouldn’t return any results. However, let’s consider the
<expandable-header> component specifically. The template for that component looks like this:
Not much here, all it does is project whatever content is added between the
<expandable-header> tags in the
HomePage into this template. I haven’t added anything directly to the template here, so there is nothing to grab with @ViewChild or @ViewChildren.
However, if we look at how we were using
<expandable-header> in the
<expandable-header [scrollArea]="mycontent" headerHeight="125"> <ion-item> <ion-label><ion-icon name="search"></ion-icon></ion-label> <ion-input type="text"></ion-input> </ion-item> <ion-item> <ion-label><ion-icon name="funnel-outline"></ion-icon></ion-label> <ion-input type="text"></ion-input> </ion-item> </expandable-header>
We have two
<ion-item> elements that will be projected into the ExpandableHeader component’s template. When we were in the context of the
HomePage I could grab both of these items with @ViewChildren because I added them directly to the
HomePage template. If we tried to do the same from the context of the ExpandableHeader component in expandable-header.ts then we wouldn’t get any results with @ViewChildren (because these elements are not coded directly into the template for ExpandableHeader).
However, even though we haven’t added these elements directly into the template for the expandable header component, they are added through content projection. This is where @ContentChildren comes in. If we instead do this inside of expandable-header.ts:
@ContentChildren(Item) items: any;
then we can grab a reference to those elements that are being added through content projection.
The difference between @ViewChildren and @ContentChildren is one of those concepts that will just click in your head, and once you understand that difference it’s a reasonably easy distinction to make. Getting to that point can be difficult, though, so I hope I have given a clear enough example of the difference.
If you are interested in a more detailed description of the difference between these annotations, I would recommend taking a look at this talk.