Lesson 2

The Role of Components and Directives

Understand the role that components and directives play in our applications

PRO

Lesson Outline

The Role of Components and Directives

In order to effectively create our own custom components and directives, it is important to understand the role that each of these concepts plays in an application. Keep in mind that we are focusing on Angular components/directives in this module. Although Ionic's own components used to be Angular components, they are now built using web components so that they can be used more generically across multiple different frameworks (or without a framework). The basic role of Ionic's web components and Angular components are still the same, and their usage will look almost identical, but they are built in different ways.

If you are an Angular developer, then you will probably feel most at home creating Angular component's to use in your Angular application (and that is what we will focus on in this module). However, just be aware that it is possible to create generic components that can be used outside of the Angular world if that is what you need.

Components

A component is actually technically a directive, it is just a special type of directive. A component is basically a directive with a template.

It seems somewhat confusing to consider a component to be a special type of directive, when components are the key building blocks of our applications, and what we would consider to be a normal directive plays a much less prominent role. For all intents and purposes, it is probably easier to consider "components" and "directives" as two distinct concepts. Throughout the rest of this module, I will be referring to components and directives as separate things.

I think it is important to understand the concept of a component first before we get into the more generalised "directives". If you are reading this, then you should already be reasonably comfortable with using various components in your Ionic applications, but we are going to take a higher level look at the role of a component.

A component is made up of the following ingredients:

  • A template
  • A class decorated with @Component to define the application logic for that template

A component has a view defined by a template. You could have multiple views on the screen at the same time, or just one. When building Ionic applications, all of our "pages" are just components, but we also put additional components inside of those pages (like lists, headers, tabs, and so on). Our entire application is basically components within components within components, which creates a tree like structure of components starting from our root component in app.component.ts that looks like this:

Component Tree

That is an example of the component structure of a blank Ionic application (with just the default home page). Each component that we add will have its template inserted into the DOM for our application. In general, a parent component is able to pass data to a child component through property bindings, and a child component can pass data back to its parent component through an event binding. If we take a look at the DOM structure for the example above, we would find something that looks like this:

<ion-app>
  <ion-router-outlet>
    <app-home>
      <ion-header>
        <ion-toolbar> </ion-toolbar>
      </ion-header>
      <ion-content></ion-content>
    </app-home>
  </ion-router-outlet>
</ion-app>

I have slightly simplified the DOM structure here to give a broad overview of the structure (there are a few more bits and pieces that are injected into the DOM).

You can see that we have our root <ion-app> component, and inside of that we have more components, and inside of those components, we have more components. Each component is added to the DOM by using the selector defined in the @Component decorator. If you have a component with a selector of app-home then it can be added to the DOM using <app-home></app-home> - the template for that component would then be injected into the DOM at that location.

Selectors do not have to match just tags like <app-home>. For example, if we were to set up selectors like the following:

@Component({
 	selector: 'my-list-header,my-item,[my-item],my-item-divider',
	template: /* etc etc... */
})

There are multiple selectors defined here, so this component would be instantiated whenever the following tags are used:

<my-list-header></my-list-header>
<my-item></my-item>
<my-item-divider></my-item-divider>

but there is also an attribute selector of [my-item] which means that this component would also be instantiated for elements with the my-item attribute:

<button my-item></button>

The @Component decorator is important because it tells Angular that this class should be treated as an Angular component and it allows us to define other associated metadata like the selector. A key part of telling Angular to treat a class as a component is that it will set up change detection for that component, which is what is responsible for propagating changes in that component throughout the application.

When a change is detected in the application (e.g. due to an event being triggered or a callback firing) Angular will check every component in the component tree to see if it needs to have its UI re-rendered. For every change, it will start at the root component and then check every single component in the tree. There are strategies we can use to help optimise this process like using the OnPush change detection strategy that is covered in the High Performance Applications module.

Without the @Component decorator, we just have a normal ES6 class. This does also mean that it is possible to do things that you would do with a standard class, like extending a class with the extends keywords to create a new class based on another class. This is possible to do, and might even play an important role for some applications, but keep in mind that extending a class will not include any of the information contained in the @Component decorator like the template.

Another important concept is that we can have data both flowing in and out of components. We can have data flowing into components, either through providing it with data through an input binding:

<my-component [someInput]="someData"></my-component>

which we would set up in that components class by using the @Input decorator:

@Input('someInput') someInput: string;

or by injecting additional services into that components constructor:

constructor(private someService: SomeService){ }

and we can also have data flowing out of components, by setting up an event binding like this:

<my-component (someEvent)="doSomething($event)"></my-component>

which would be triggered by using an @Output decorator in the components class:

@Output() someEvent = new EventEmitter();

There are other ways to have data flowing back out of a component to its parent or elsewhere in the application (like accessing an observable that the child component exposes, or using a service), so it will depend on the circumstance - this is just intended as a broad overview. We will be exploring these concepts in more depth throughout the module.

Directives

We know that our applications are made up of various components, but we also know that a component is just a special type of directive that has an associated template. So, what is the general role of a directive?

A template in Angular is dynamic, and we can attach additional directives to these templates to modify their behaviour. This is the primary role of a directive: to attach behaviour to elements in the DOM. A directive is just a class decorated with the @Directive decorator.

We could have a component added to the DOM:

<my-component></my-component>

That has its own template and set of behaviours defined. We would then add a directive to that component:

<my-component my-directive></my-component>

or even multiple directives:

<my-component my-directive my-other-directive></my-component>

By attaching a directive to a specific component, that directive will then have access to that component and be able to modify its behaviour. We could use that directive to modify the styling of that component, or perhaps set up some kind of listener on it.

However, a directive can also exist on its own, for example:

<my-directive></my-directive>

at a glance, you might assume that the above was a component, but in this case, it is a directive. Previous versions of the Ionic framework (when Angular was used to build all of Ionic's components/directives) used this approach quite a bit, and a good example of how this is different to a normal component were their implementations of <ion-list> and <ion-item>.

The <ion-list> component was actually a directive, not a component. Since <ion-list> did not have a template of its own, it did not meet the requirements to be considered a component. If you were to add an <ion-list> to your application, this is what you would find was actually added to the DOM:

<ion-list class="list list-md"></ion-list>

There is no associated template being injected into the DOM, however, the directive did have certain behaviours defined on it to allow it to behave as a list. One of those behaviours was to add the material design styling with the list-md class in response to the platform the application is running on.

An <ion-item> on the other hand was considered a component, as it would inject its associated template into the DOM. That template used "content projection" (a concept we will dive into later) to add the data that you supplied to the <ion-item> inside of that template. Although the actual code we write may look like this:

<ion-list>
  <ion-item>hello</ion-item>
</ion-list>

the structure that was injected into the DOM actually looks like this this:

<ion-list class="list list-md">
  <ion-item class="item item-block item-md">
    <div class="item-inner">
      <div class="input-wrapper">
        <ion-label class="label label-md">hello</ion-label>
      </div>
    </div>
    <div class="button-effect"></div>
  </ion-item>
</ion-list>

Our hello string is projected into the <ion-item> component's template inside of the <ion-label> (which was also another directive, since it did not have its own template).

NOTE: Although Ionic components are no longer built with Angular, this same behaviour of injecting templates into the DOM is still present in the Ionic web components - it is just achieved through a different means.

In this case, it is desirable to encapsulate the <ion-item>'s with an <ion-list> to attach certain behaviours to that group of items, but the <ion-list> itself does not have its own template, so it is a directive not a component.

In practice, however, the <ion-list> would commonly be referred to as a component. It is not common, at least in my experience, to need to use directives in this manner. Generally, you will just be using attribute directives like this:

<my-component my-directive></my-component>

However, I wanted to make it clear what constitutes a directive and what constitutes a component.

There is also another type of directive called a structural directive, which are directives like *ngIf and *ngFor that you should be reasonably familiar with. We could even create our own custom structural directives if we wanted, but we won't be doing that in this module.

Summary

Although the components Ionic provides out of the box cover a lot of use cases for us, there will come a time when it makes sense for us to create our own custom components and directives, and so understanding exactly what that means is important.