Select Page

The Website

This lesson is the follow-up post to Get Started with Angular 2 by Building a Simple Website where we walked through building out a simple website in Angular 2.

In this lesson, we are going to expand on the idea of composition through components by introducing subcomponents. Angular 2 components, by nature, offer a consistent experience; which means that this will seem more like we are reinforcing existing concepts as opposed to throwing new-fangled monkey wrenches into the mix. This is good! My personal experience is that Angular 2 is much easier to learn because the Angular team has distilled AngularJS 1 best practices into a much simpler, less nuanced framework.

I wrote a post on AngularClass called Angular 2 Components for AngularJS Developers where I talk about the evolution of Angular components. It is an easy read and a great companion to this post.

Angular 2 SubComponents

We are going to enhance the Experiments component by adding in an ExperimentDetails subcomponent to encapsulate the details of a single experiment. Along the way, we will build out the service to provide us with the experiments collection, build out the Experiment interface and, most importantly, show how to include the ExperimentDetails component in our Experiments component.

Check out the first post in the series if you haven’t, grab the code, and let’s go!

Code

Starting Point

We are going to use the code below as our starting point. If you read the first post in the series, the code below should be relatively familiar. In a nutshell, we have an Experiments component that is targeting the experiments element and using experiments.component.html as its template. We are injecting the StateService into our component via its constructor and then using the ngOnInit lifecycle hook to pull the message from the service.

import {Component, OnInit} from '@angular/core';
import {StateService} from '../common/state.service';

@Component({
  selector: 'experiments',
  templateUrl: require('app/experiments/experiments.component.html')
})
export class ExperimentsComponent implements OnInit {
  title: string = 'Experiments Page';
  body: string = 'This is the about experiments body';
  message: string = '';

constructor(private stateService: StateService) {}

ngOnInit() {
    this.message = this.stateService.getMessage();
  }

updateMessage(m: string): void {
    this.stateService.setMessage(m);
  }
}

At this point, the home, about and experiments features are almost identical and the perfect starting point for digging into subcomponents.

Getting the Experiments

To display a list of experiments, we need to create a service to provide us with the appropriate collection. We are going to create a class called ExperimentsService that exposes a method called getExperiments, which returns an array of experiment objects.

import {Experiment} from './experiment.model';

export class ExperimentsService {
  private experiments: Experiment[] = [
    {name: 'Experiment 1', description: 'This is an experiment', completed: 0},
    {name: 'Experiment 2', description: 'This is an experiment', completed: 0},
    {name: 'Experiment 3', description: 'This is an experiment', completed: 0},
    {name: 'Experiment 4', description: 'This is an experiment', completed: 0}
  ];

getExperiments(): Experiment[] {
    return this.experiments;
  };
}

Two of the main features of TypeScript are the ability to make members of a class private with the private modifier as well as declare data types. These features are not mandatory, and you can use them to whatever degree you are comfortable. Here are a few variations that we could have used to define our experiments collection.

experiments = [...] // Public with no type

private experiments = [...] // Private with no type

private experiments: Object[] = [...] // Private with an array of generic objects

private experiments: Experiment[] = [...] // Private with an array of objects of type Experiment

How does TypeScript even know what an Experiment is so that it can enforce that type on the array? Notice in the code below that we are importing in an Experiment module that grants us access to the Experiment type.

import {Injectable} from '@angular/core';
import {Experiment} from './experiment.model';

@Injectable()
export class ExperimentsService {
  private experiments: Experiment[] = [
    {name: 'Experiment 1', description: 'This is an experiment', completed: 0},
    {name: 'Experiment 2', description: 'This is an experiment', completed: 0},
    {name: 'Experiment 3', description: 'This is an experiment', completed: 0},
    {name: 'Experiment 4', description: 'This is an experiment', completed: 0}
  ];

getExperiments(): Experiment[] {
    return this.experiments;
  };
}

But what is it, man!? Okay! We have created an interface that allows us to define the “shape” of how an Experiment should look. By declaring our interface, TypeScript knows how to type check an object against its intended shape.

export interface Experiment {
  name: string;
  description: string;
  completed: number;
}

Interfaces are an architectural cornerstone and out of scope of this lesson; however, at a minimum I recommend taking a few minutes to read up on TypeScript interfaces here

Now that we have created ExperimentsService and Experiment, we are now ready to integrate with our Experiments component. We are going to import our two new modules.

import {Experiment} from '../common/experiment.model';
import {ExperimentsService} from '../common/experiments.service';

We will then define an experiments collection with a typed Experiment array. We are going to inject ExperimentsService into our component in the constructor function just like we did with the StateService. To complete the circuit, we will populate our experiments collection by calling this.experimentsService.getExperiments() from the ngOnInit method.

experiments: Experiment[];

constructor(
  private stateService: StateService,
  private experimentsService: ExperimentsService) {}

ngOnInit() {
  this.experiments = this.experimentsService.getExperiments();
  this.message = this.stateService.getMessage();
}

Now that we have our experiments, it is time for us to do something with them.

Displaying the Experiments

We are now going to create the experiment detail component, and I want you to be mindful of how many of the steps are familiar. We will start out by creating the class for our components and calling it ExperimentDetailComponent.

export class ExperimentDetailComponent { }

Then we will import our modules.

import {Component, Input} from '@angular/core';
import {Experiment} from '../../common/experiment.model';

export class ExperimentDetailComponent { }

And then we are going to decorate the class to target the experiment element and use the experiment.detail.component.html template.

import {Component, Input} from '@angular/core';
import {Experiment} from '../../common/experiment.model';

@Component({
  selector: 'experiment',
  templateUrl: require('app/experiments/experiment-details/experiment.detail.component.html')
})
export class ExperimentDetailComponent { }

We are now going to enhance our basic class to do actual work. Notice in the code above that we are importing the Component module and then annotating our class with @Component(). Leading question alert! If we are also importing the Input module, would it not be reasonable to expect to see something like @Input() somewhere in our code? Why yes! Yes, it would!

export class ExperimentDetailComponent {
  @Input() experiment: Experiment;

doExperiment(): void {
    this.experiment.completed += 1;
  };
}

In Angular 2, @Input declares a data-bound property so that it is automatically updated during change detection. This is very similar, albeit much simpler, to isolated scope in Angular 1.

Our experiment detail template is comprised mostly of classic curly brace data binding with the exception of our (click) handler which we use to call doExperiment.


<div class="experiment" (click)="doExperiment()">
<h3>
    {{ experiment.name }}
  </h3>
    {{ experiment.description }}

    <strong>{{experiment.completed}}</strong>

</div>

Our ExperimentDetailComponent is complete, and it is now time to integrate it into our ExperimentsComponent.

Experiments Meet ExperimentsDetail

We will update our imports to include ExperimentDetailComponent.

import {Component} from '@angular/core';
import {NgFor} from '@angular/common';
import {Experiment} from '../common/experiment.model';
import {ExperimentsService} from '../common/experiments.service';
import {StateService} from '../common/state.service';
import {ExperimentDetailComponent} from './experiment-details/experiment.detail.component';

This is important! For us to use a subcomponent within a parent component, we have to define it in the directives property on our component annotation. Emphasis added to the previous statement because I have wasted a bunch of time by spacing out that critical step.

@Component({
  selector: 'experiments',
  templateUrl: require('app/experiments/experiments.component.html'),
  directives: [ExperimentDetailComponent]
})

Angular 2 Template Primer

Home stretch! We need to update our HTML to render the experiment subcomponent. All of this happens in a single line of code that, as we can see below, is quite dense.

<experiment *ngFor="let experiment of experiments" [experiment]="experiment"></experiment>

Let’s break this down. First things first, we want to render the ExperimentDetailComponent and so we will add this experiment selector to our page.

<experiment></experiment>

We want to render a bunch of experiment components, and so we will use ngFor to repeat over our experiments collection and stamp out N number of elements. In Angular 2, we use ngFor much like we used ngRepeat in Angular 1.

<experiment *ngFor="let experiment of experiments"></experiment>

There are two things that I would like to call out; these are new additions to the Angular templating that will seem “odd” at first. The first oddity is the asterisk that we see in front of ngFor. This is a shorthand way for us to work with directives that modify the DOM. Angular sees the asterisk and knows to expand the HTML into whatever defined as our template. The oddity is the let keyword. This is providing a local variable for us to hook on to inside of the template.

Remember that one really funny time when we used @Input on that one component? You know!? That ONE time! It looked like this @Input() experiment: Experiment;. We are now going to make use of that by adding a property binding in the form of [experiment] to our element and then setting the value to the local variable experiment we created in our ngFor loop.

<experiment *ngFor="let experiment of experiments" [experiment]="experiment"></experiment>

One line of code and yet so many important things are happening when we stop and think about it. And there you have it! We have successfully created and integrated a subcomponent into our app.

Review

Let’s do a quick review of what we covered in this post.

  • To make a service injectable, we annotate our class with @Injectable.
  • To make a component, we annotate our class with @Component.
  • To make a property bindable, we annotate our property with @Input.
  • We can use the private modifier in TypeScript to restrict access to class members.
  • We can use TypeScript interfaces to define the shape of an object and provide richer typing.
  • We must add our subcomponents to the directives array in our @Component annotation.
  • The asterisk (*) is a shorthand method to work with directives that modify the DOM by inserting templates.
  • We create a local variable inside an *ngFor loop by using the key word let.
  • Double brackets ([]) binds an element property to an expression.
  • At the risk of an oversimplification, ngFor is the new ngRepeat.

Thanks for joining me as we built out an Angular 2 subcomponent. I have a few ideas for what I want to tackle next in this series but let me know what you would like to see in the comments below. #highFive

Resources

Get Started with Angular 2 by Building a Simple Website

Angular 2 Components for AngularJS Developers

TypeScript Interfaces

Angular 2 Template Syntax Guide

Code