Multiple Components 4.0

Send feedback

The AppComponent is doing everything at the moment. In the beginning, it showed details of a single hero. Then it became a master/detail form with both a list of heroes and the hero detail. Soon there will be new requirements and capabilities. You can’t keep piling features on top of features in one component; that’s not maintainable.

You’ll need to break it up into sub-components, each focused on a specific task or workflow. Eventually, the AppComponent could become a simple shell that hosts those sub-components.

In this page, you’ll take the first step in that direction by carving out the hero details into a separate, reusable component. When you’re done, the app should look like this .

Where you left off

Before getting started on this page, verify that you have the following structure from earlier in the Tour of Heroes. If not, go back to the previous pages.

  • angular_tour_of_heroes
    • lib
      • app_component.css
      • app_component.dart
    • test
      • app_test.dart
    • web
      • index.html
      • main.dart
      • styles.css
    • pubspec.yaml

If the app isn’t running already, launch the app. As you make changes, keep it running by reloading the browser window.

Make a hero detail component

Create the lib/src folder and add a file named hero_detail_component.dart to it. This file will hold the new HeroDetailComponent.

Angular conventions:

  • The component class name should be written in upper camel case and end in the word “Component”. The hero detail component class is HeroDetailComponent.

  • The component file name should be in snake case —lowercase with underscore separation—and end in _component.dart. The HeroDetailComponent class goes in the hero_detail_component.dart file.

  • Internal implementation files should be placed under lib/src. See the pub package layout conventions for details.

Start writing the HeroDetailComponent as follows:

lib/src/hero_detail_component.dart (initial version)

import 'package:angular/angular.dart'; import 'package:angular_forms/angular_forms.dart'; @Component( selector: 'hero-detail', directives: const [CORE_DIRECTIVES, formDirectives], ) class HeroDetailComponent { }

To define a component, you always import the main Angular library.

The @Component annotation provides the Angular metadata for the component. The CSS selector name, hero-detail, will match the element tag that identifies this component within a parent component’s template. Near the end of this tutorial page, you’ll add a <hero-detail> element to the AppComponent template.

Hero detail template

To move the hero detail view to the HeroDetailComponent, cut the hero detail content from the bottom of the AppComponent template and paste it into a new template argument of the @Component annotation.

The HeroDetailComponent has a hero, not a selected hero. Replace the word, “selectedHero”, with the word, “hero”, everywhere in the template. When you’re done, the new template should look like this:

lib/src/hero_detail_component.dart (template)

template: ''' <div *ngIf="hero != null"> <h2>{{hero.name}} details!</h2> <div><label>id: </label>{{hero.id}}</div> <div> <label>name: </label> <input [(ngModel)]="hero.name" placeholder="name"> </div> </div>''',

Add the hero property

The HeroDetailComponent template binds to the component’s hero property. Add that property to the HeroDetailComponent class like this:

lib/src/hero_detail_component.dart (hero)

Hero hero;

The hero property is typed as an instance of Hero. The Hero class is still in the app_component.dart file. Now there are two components that need to reference the Hero class.

Move the Hero class from app_component.dart to its own hero.dart file.

lib/src/hero.dart

class Hero { final int id; String name; Hero(this.id, this.name); }

Now that the Hero class is in its own file, the AppComponent and the HeroDetailComponent have to import it:

import 'src/hero.dart'; import 'hero.dart';

The hero property is an input property

Later in this page, the parent AppComponent will tell the child HeroDetailComponent which hero to display by binding its selectedHero to the hero property of the HeroDetailComponent. The binding will look like this:

<hero-detail [hero]="selectedHero"></hero-detail>

Putting square brackets around the hero property, to the left of the equal sign (=), makes it the target of a property binding expression. You must declare a target binding property to be an input property. Otherwise, Angular rejects the binding and throws an error.

Declare that hero is an input property by annotating it with @Input():

lib/src/hero_detail_component.dart (inputs)

@Input() Hero hero;

Read more about input properties in the Attribute Directives page.

That’s it. The hero property is the only thing in the HeroDetailComponent class. All it does is receive a hero object through its hero input property and then bind to that property with its template. Here’s the complete HeroDetailComponent.

lib/src/hero_detail_component.dart

import 'package:angular/angular.dart'; import 'package:angular_forms/angular_forms.dart'; import 'hero.dart'; @Component( selector: 'hero-detail', template: ''' <div *ngIf="hero != null"> <h2>{{hero.name}} details!</h2> <div><label>id: </label>{{hero.id}}</div> <div> <label>name: </label> <input [(ngModel)]="hero.name" placeholder="name"> </div> </div>''', directives: const [CORE_DIRECTIVES, formDirectives], ) class HeroDetailComponent { @Input() Hero hero; }

Add HeroDetailComponent to the AppComponent

The AppComponent is still a master/detail view. It used to display the hero details on its own, before you cut out that portion of the template. Now it will delegate to the HeroDetailComponent.

Start by importing the HeroDetailComponent so AppComponent can refer to it.

import 'src/hero_detail_component.dart';

Recall that hero-detail is the CSS selector in the HeroDetailComponent metadata. That’s the tag name of the element that represents the HeroDetailComponent.

Add a <hero-detail> element near the bottom of the AppComponent template, where the hero detail view used to be.

Coordinate the master AppComponent with the HeroDetailComponent by binding the selectedHero property of the AppComponent to the hero property of the HeroDetailComponent.

  <hero-detail [hero]="selectedHero"></hero-detail>

Now every time the selectedHero changes, the HeroDetailComponent gets a new hero to display.

The revised AppComponent template should look like this:

lib/app_component.dart (template)

template: ''' <h1>{{title}}</h1> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" [class.selected]="hero == selectedHero" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul> <hero-detail [hero]="selectedHero"></hero-detail> ''',

The detail should update every time the user picks a new hero. It’s not happening yet! Click a hero. No details. If you look for an error in the console of the browser development tools. No error.

It is as if Angular were ignoring the new tag. That’s because it is ignoring the new tag.

The directives list

A browser ignores HTML tags and attributes that it doesn’t recognize. So does Angular.

You’ve imported HeroDetailComponent, and you’ve used <hero-detail> in the template, but you haven’t told Angular about it.

Just as you’ve done for the built-in Angular directives, tell Angular about the hero detail component by listing it in the metadata directives list. You don’t need formDirectives anymore, so delete it and the angular_forms import at the top of the file:

lib/app_component.dart (directives)

directives: const [CORE_DIRECTIVES, HeroDetailComponent],

Refresh the browser. It works!

App design changes

As before, whenever a user clicks on a hero name, the hero detail appears below the hero list. But now the HeroDetailComponent is presenting those details.

Refactoring the original AppComponent into two components yields benefits, both now and in the future:

  1. You simplified the AppComponent by reducing its responsibilities.
  2. You can evolve the HeroDetailComponent into a rich hero editor without touching the parent AppComponent.
  3. You can evolve the AppComponent without touching the hero detail view.
  4. You can reuse the HeroDetailComponent in the template of some future parent component.

Review the app structure

Verify that you have the following structure:

  • angular_tour_of_heroes
    • lib
      • app_component.css
      • app_component.dart
      • src
        • hero.dart
        • hero_detail_component.dart
    • test
      • app_test.dart
    • web
      • index.html
      • main.dart
      • styles.css
    • pubspec.yaml

Here are the code files discussed in this page.

import 'package:angular/angular.dart'; import 'package:angular_forms/angular_forms.dart'; import 'hero.dart'; @Component( selector: 'hero-detail', template: ''' <div *ngIf="hero != null"> <h2>{{hero.name}} details!</h2> <div><label>id: </label>{{hero.id}}</div> <div> <label>name: </label> <input [(ngModel)]="hero.name" placeholder="name"> </div> </div>''', directives: const [CORE_DIRECTIVES, formDirectives], ) class HeroDetailComponent { @Input() Hero hero; } import 'package:angular/angular.dart'; import 'src/hero.dart'; import 'src/hero_detail_component.dart'; final List<Hero> mockHeroes = [ new Hero(11, 'Mr. Nice'), new Hero(12, 'Narco'), new Hero(13, 'Bombasto'), new Hero(14, 'Celeritas'), new Hero(15, 'Magneta'), new Hero(16, 'RubberMan'), new Hero(17, 'Dynama'), new Hero(18, 'Dr IQ'), new Hero(19, 'Magma'), new Hero(20, 'Tornado') ]; @Component( selector: 'my-app', template: ''' <h1>{{title}}</h1> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" [class.selected]="hero == selectedHero" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul> <hero-detail [hero]="selectedHero"></hero-detail> ''', styleUrls: const ['app_component.css'], directives: const [CORE_DIRECTIVES, HeroDetailComponent], ) class AppComponent { final title = 'Tour of Heroes'; final List<Hero> heroes = mockHeroes; Hero selectedHero; void onSelect(Hero hero) { selectedHero = hero; } } class Hero { final int id; String name; Hero(this.id, this.name); }

The road you’ve travelled

Here’s what you achieved in this page:

  • You created a reusable component.
  • You learned how to make a component accept input.
  • You learned to declare the application directives in a directives list.
  • You learned to bind a parent component to a child component.

Your app should look like this .

The road ahead

The Tour of Heroes app is more reusable with shared components, but its (mock) data is still hard coded within the AppComponent. That’s not sustainable. Data access should be refactored to a separate service and shared among the components that need data.

You’ll learn to create services in the next tutorial page.