Tutorial: The Hero Editor

REFERENCES
[1]: https://angular.io/tutorial/toh-pt1 "The Hero Editor"

The application now has a basic title. Next you will create a new component to display hero information and place that component in the application shell.

Create the heroes component

Using the Angular CLI, generate a new component named heroes.

$ ng generate component heroes

The CLI creates a new folder, src/app/heroes/ and generates the three files of the HeroesComponent.

The HeroesComponent class file is as follows:

src/app/heroes/heroes.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {

  constructor() { }

  ngOnInit() { }

}

You always import the Component symbol from the Angular core library and annotate the component class with @Component.

@Component is a decorator function that specifies the Angular metadata for the component.

The CLI generated three metadata properties:

  1. selector— the components CSS element selector
  2. templateUrl— the location of the component's template file.
  3. styleUrls— the location of the component's private CSS styles.

The CSS element selector, 'app-heroes', matches the name of the HTML element that identifies this component within a parent component's template.

The ngOnInit is a lifecycle hook Angular calls ngOnInit shortly after creating a component. It's a good place to put initialization logic.

Always export the component class so you can import it elsewhere ... like in the AppModule.

Add a hero property

Add a hero property to the HeroesComponent for a hero named "Iron Man."

src/app/heroes/heroes.component.ts
export class HeroesComponent implements OnInit {
  hero = 'Captain America';

  constructor() { }

  ngOnInit() { }
}

Show the hero

Open the heroes.component.html template file. Delete the default text generated by the Angular CLI and replace it with a data binding to the new hero property.

src/app/heroes/heroes.component.html
{{ hero }}

Show the HeroesComponent view

To display the HeroesComponent, you must add it to the template of the shell AppComponent.

Remember that app-heroes is the element selector for the HeroesComponent. So add an <app-heroes> element to the AppComponenttemplate file, just below the title.

src/app/app.component.html
<app-mat-navbar></app-mat-navbar>

<div class="row red pt-5">
  <div class="col-sm-12 no-gutters">
    <h1 class="mat-display-1 text-center white-text">{{ title }}</h1>
  </div>
</div>

<div class="container">
  <div class="row">
    <div class="col">
      <app-heroes></app-heroes>
    </div>
  </div>
</div>

Assuming that the CLI ng serve command is still running, the browser should refresh and display both the application title and the hero name.

All selectors are from the stylsheets imported in the src/styles.css.

Create a Hero class

A real hero is more than a name.

Create a Hero class in its own file in the src/app folder. Give it id and name properties.

src/app/hero.ts
export class Hero {
  id: number;
  name: string;
}

Return to the HeroesComponent class and import the Hero class.

Refactor the component's hero property to be of type Hero. Initialize it with an id of 1 and the name Windstorm.

The revised HeroesComponent class file should look like this:

src/app/heroes/heroes.component.ts
import { Component, OnInit } from '@angular/core';
import {Hero} from '../hero';

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})

export class HeroesComponent implements OnInit {
  hero: Hero = {
    id: 1,
    name: 'Captain America',
  };

  constructor() { }

  ngOnInit() { }
}

The page no longer displays properly because you changed the hero from a string to an object.

Introducing the <mat-card> component

REFERENCES
[2]: https://material.angular.io/components/card/overview "Card"

<mat-card> is a content container for text, photos, and actions in the context of a single subject.

Basic card sections

The most basic card needs only an <mat-card> element with some content. However, Angular Material provides a number of preset sections that you can use inside of an <mat-card>:

Element Description
<mat-card-title> Card title
<mat-card-subtitle> Card subtitle
<mat-card-content> Primary card content. Intended for blocks of text
<img mat-card-image> Card image. Stretches the image to the container width
<mat-card-actions> Container for buttons at the bottom of the card
<mat-card-footer> Section anchored to the bottom of the card

These elements primary serve as pre-styled content containers without any additional APIs. However, the align property on <mat-card-actions> can be used to position the actions at the 'start' or 'end of the container.

Card headers

In addition to the aforementioned sections, <mat-card-header> gives the ability to add a rich header to a card. This header can contain:

Element Description
<mat-card-title> A title within the header
<mat-card-subtitle> A subtitle within the header
<img mat-card-avatar> An image used as an avatar within the header
Title groups

<mat-card-title-group> can be used to combine a title, subtitle, and image into a single section. This element can contain:

  • <mat-card-title>
  • <mat-card-subtitle>
  • One of:
    • <img mat-card-sm-image>
    • <img mat-card-md-image>
    • <img mat-card-lg-image>

Show the hero object

Update the binding in the template to announce the hero's name and show both id and name in a details layout like this:

src/app/heroes/heroes.component.html
<div class="row">
  <div class="col-sm-6">
    <mat-card class="mat-elevation-z4">
      <mat-card-header>
        <div mat-card-avatar class="header-image"></div>
        <mat-card-title>{{ hero.name }}</mat-card-title>
        <mat-card-subtitle><span>ID:</span>{{ hero.id }}</mat-card-subtitle>
      </mat-card-header>
      <img mat-card-image src="https://i.annihil.us/u/prod/marvel/i/mg/9/80/537ba5b368b7d.jpg" alt="Captain America Image">
      <mat-card-content>
        <p>
          {{hero.name}}
        </p>
      </mat-card-content>
      <mat-card-actions>
        <button mat-button>LIKE</button>
        <button mat-button>SHARE</button>
      </mat-card-actions>
    </mat-card>
  </div>
</div>
src/app/heroes/heroes.component.css
.header-image {
  background-image: url("https://i.annihil.us/u/prod/marvel/i/mg/9/80/537ba5b368b7d.jpg");
  -webkit-background-size: cover;
  background-size: cover;
}

The browser refreshes and display's the hero's information.

Format with the UppercasePipe

Modify the hero.name binding like this.

src/app/heroes/heroes.component.html
<mat-card-title>{{ hero.name | uppercase }}</mat-card-title>

The browser refreshes and now the hero's name is displayed in capital letters.

The word uppercase in the interpolation binding, right after the pipe operator ( | ), activates the built-in UppercasePipe.

Pipes are a good way to format strings, currency amounts, dates and other display data. Angular ships with several built-in pipes and you can create your own.

Angular Material Form Controls

REFERENCES
[3]: https://material.angular.io/components/form-field/overview "Form field"
[4]: https://material.angular.io/components/input/overview "Input"

<mat-form-field> is a component used to wrap several Angular Material components and apply common Text field styles such as the underline, floating label, and hint messages.

In this document, "form field" refers to the wrapper component <mat-form-field> and "form field control" refers to the component that the <mat-form-field> is wrapping (e.g. the input, textarea, select, etc.)

The following Angular Material components are designed to work inside a <mat-form-field>:

<matInput>

matInput is a directive that allows native <input> and <textarea> elements to work with<mat-form-fields>.

<input> and <textarea> attributes

All of the attributes that can be used with normal <input> and <textarea> elements can be used on elements inside <mat-form-field> as well. This includes Angular directives such as ngModel and formControl.

The only limitations are that the type attribute can only be one of the values supported bymatInput and the native element cannot specify a placeholder attribute if the <mat-form-field> also contains an <mat-placeholder> element.

Supported <input> types

The following input types can be used with matInput:

  • date
  • datetime-local
  • email
  • month
  • number
  • password
  • search
  • tel
  • text
  • time
  • url
  • week
Form field features

There are a number of <mat-form-field> features that can be used with any <input matInput> or <textarea matInput>. These include error messages, hint text, prefix & suffix, and theming. For additional information about these features, see the form field documentation.

Placeholder

A placeholder is a text label displayed in the input area when the input does not contain text. When text is present, the placeholder will float above the input area. The placeholder can be specified either via a placeholder attribute on the input or a <mat-placeholder> element in the same form field as the matInput. The <mat-form-field> also has additional options for changing the behavior of the placeholder. For more information see the form field placeholder documentation.

Changing when error messages are shown

The <mat-form-field> allows you to associate error messages with your matInput. By default, these error messages are shown when the control is invalid and either the user has interacted with (touched) the element or the parent form has been submitted. If you wish to override this behavior (e.g. to show the error as soon as the invalid control is dirty or when a parent form group is invalid), you can use the errorStateMatcher property of thematInput. The property takes an instance of an ErrorStateMatcher object. An ErrorStateMatcher must implement a single method isErrorState which takes the FormControl for this matInput as well as the parent form and returns a boolean indicating whether errors should be shown. (true indicating that they should be shown, and falseindicating that they should not.)

Edit the hero

Users should be able to edit the hero name in an <input> textbox.

The textbox should both display the hero's name property and update that property as the user types. That means data flow from the component class out to the screen and from the screen back to the class.

To automate that data flow, setup a two-way data binding between the <input> form element and the hero.name property.

Two-way binding

Refactor the details area in the HeroesComponent template so it looks like this:

src/app/heroes/heroes.component.html
<div class="row">
  <div class="col-sm-12 form-container">
    <mat-card class="form-container mat-elevation-z4">
      <mat-form-field [floatPlaceholder]="auto">
        <input type="text" placeholder="Name:" matInput [(ngModel)]="hero.name">
        <mat-hint>Give your hero a name.</mat-hint>
      </mat-form-field>
    </mat-card>
  </div>
</div>

[(ngModel)] is Angular's two-way data binding syntax.

Here it binds the hero.name property to the HTML textbox so that data can flow in both directions: from the hero.name property to the textbox, and from the textbox back to the hero.name.

This example also includes some custom css.

src/app/heroes/heroes.component.css
.form-container {
  display: flex;
  flex-direction: column;
}

.form-container > * {
  width: 100%;
}
The missing FormsModule

Notice that the app stopped working when you added [(ngModel)].

To see the error, open the browser development tools and look in the console for a message like:

Template parse errors:
Can't bind to 'ngModel' since it isn't a known property of 'input'.

Although ngModel is a valid Angular directive, it isn't available by default.

It belongs to the optional FormsModule and you must opt-in to using it.

AppModule

Angular needs to know how the pieces of your application fit together and what other files and libraries the app requires. This information is called metadata

Some of the metadata is in the @Component decorators that you added to your component classes. Other critical metadata is in @NgModule decorators.

The most important @NgModule decorator annotates the top-level AppModule class.

The Angular CLI generated an AppModule class in src/app/app.module.ts when it created the project. This is where you opt-in to the FormsModule.

Import FormsModule

Open AppModule (app.module.ts) and import the FormsModule symbol from the @angular/forms library.

Then add FormsModule to the @NgModule metadata's imports array, which contains a list of external modules that the app needs.

src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { MaterialModule } from './material.module';
import { MatNavbarComponent  } from './mat-navbar/mat-navbar.component';
import { HeroesComponent } from './heroes/heroes.component';

@NgModule({
  declarations: [
    AppComponent,
    MatNavbarComponent,
    HeroesComponent
  ],
  imports: [
    BrowserModule,
    MaterialModule,
    FormsModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})

export class AppModule { }

When the browser refreshes, the app should work again. You can edit the hero's name and see the changes reflected immediately in the <mat-card-title> in the <mat-card>.

Declare HeroesComponent

Every component must be declared in exactly one NgModule.

You didn't declare the HeroesComponent. So why did the application work?

It worked because the Angular CLI declared HeroesComponent in the AppModule when it generated that component.

Open src/app/app.module.ts and find HeroesComponent imported near the top.

Note that AppModule declares all application components, AppComponent, HeroesComponent, and MatNavbarComponent.

Summary

  • You used the CLI to create a second HeroesComponent.
  • You displayed the HeroesComponent by adding it to the AppComponent shell.
  • You applied the UppercasePipe to format the name.
  • You used two-way data binding with the ngModel directive.
  • You learned about the AppModule.
  • You imported the FormsModule in the AppModule so that Angular would recognize and apply the ngModel directive.
  • You learned the importance of declaring components in the AppModule and appreciated that the CLI declared it for you.

results matching ""

    No results matching ""