298 lines
13 KiB
Plaintext
298 lines
13 KiB
Plaintext
include ../../../../_includes/_util-fns
|
||
|
||
:marked
|
||
# It Takes Many Heroes
|
||
Our story needs more heroes.
|
||
We’ll expand our Tour of Heroes app to display a list of heroes,
|
||
allow the user to select a hero, and display the hero’s details.
|
||
|
||
Let’s take stock of what we’ll need to display a list of heroes.
|
||
First, we need a list of heroes. We want to display those heroes in the view’s template,
|
||
so we’ll need a way to do that.
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Where We Left Off
|
||
Before we continue with Part 2 of the Tour of Heroes,
|
||
let’s verify we have the following structure after [Part 1](./toh-pt1.html).
|
||
If not, we’ll need to go back to Part 1 and figure out what we missed.
|
||
|
||
code-example(format="").
|
||
angular2-tour-of-heroes
|
||
├── node_modules
|
||
├── app
|
||
| ├── app.component.ts
|
||
| └── boot.ts
|
||
├── index.html
|
||
├── tsconfig.json
|
||
└── package.json
|
||
|
||
:marked
|
||
### Keep the app transpiling and running
|
||
We want to start the TypeScript compiler, have it watch for changes, and start our server. We'll do this by typing
|
||
|
||
code-example(format="." language="bash").
|
||
npm start
|
||
|
||
:marked
|
||
This will keep the application running while we continue to build the Tour of Heroes.
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Displaying Our Heroes
|
||
### Creating heroes
|
||
Let’s create an array of ten heroes at the bottom of `app.component.ts`.
|
||
|
||
+makeExample('toh-2/ts/app/app.component.ts', 'hero-array', 'app.component.ts (Hero array)')
|
||
|
||
:marked
|
||
The `HEROES` array is of type `Hero`.
|
||
We are taking advantage of the `Hero` class we coded previously to create an array of our heroes.
|
||
We aspire to get this list of heroes from a web service, but let’s take small steps
|
||
on this road and start by displaying these mock heroes in the browser.
|
||
|
||
### Exposing heroes
|
||
Let’s create a public property in `AppComponent` that exposes the heroes for binding.
|
||
|
||
+makeExample('toh-2/ts/app/app.component.snippets.pt2.ts', 'hero-array-1', 'app.component.ts (Hero array property)')
|
||
|
||
:marked
|
||
We did not have to define the `heroes` type. TypeScript can infer it from the `HEROES` array.
|
||
.l-sub-section
|
||
:marked
|
||
We could have defined the heroes list here in this component class.
|
||
But we know that we’ll get the heroes from a data service.
|
||
Because we know where we are heading, it makes sense to separate the hero data
|
||
from the class implementation from the start.
|
||
:marked
|
||
### Displaying heroes in a template
|
||
Our component has `heroes`. Let’s create an unordered list in our template to display them.
|
||
We’ll insert the following chunk of HTML below the title and above the hero details.
|
||
|
||
+makeExample('toh-2/ts/app/app.component.snippets.pt2.ts', 'heroes-template-1', 'app.component.ts (Heroes template)')
|
||
|
||
:marked
|
||
Now we have a template that we can fill with our heroes.
|
||
|
||
### Listing heroes with ngFor
|
||
|
||
We want to bind the array of `heroes` in our component to our template, iterate over them,
|
||
and display them individually.
|
||
We’ll need some help from Angular to do this. Let’s do this step by step.
|
||
|
||
First modify the `<li>` tag by adding the built-in directive `*ngFor`.
|
||
|
||
+makeExample('toh-2/ts/app/app.component.snippets.pt2.ts', 'heroes-ngfor-1', 'app.component.ts (ngFor)')
|
||
|
||
.alert.is-critical
|
||
:marked
|
||
The leading asterisk (`*`) in front of `ngFor` is a critical part of this syntax.
|
||
|
||
.l-sub-section
|
||
:marked
|
||
The (`*`) prefix to `ngFor` indicates that the `<li>` element and its children
|
||
constitute a master template.
|
||
|
||
The `ngFor` directive iterates over the `heroes` array returned by the `AppComponent.heroes` property
|
||
and stamps out instances of this template.
|
||
|
||
The quoted text assigned to `ngFor` means
|
||
“*take each hero in the `heroes` array, store it in the local `hero` variable,
|
||
and make it available to the corresponding template instance*”.
|
||
|
||
The `#` prefix before "hero" identifies the `hero` as a local template variable.
|
||
We can reference this variable within the template to access a hero’s properties.
|
||
|
||
Learn more about `ngFor` and local template variables in the
|
||
[Template Syntax chapter](../guide/template-syntax.html#ng-for).
|
||
|
||
:marked
|
||
With this background in mind, we now insert some content between the `<li>` tags
|
||
that uses the `hero` template variable to display the hero’s properties.
|
||
|
||
+makeExample('toh-2/ts/app/app.component.snippets.pt2.ts', 'ng-for', 'app.component.ts (ngFor template)')(format=".")
|
||
|
||
:marked
|
||
When the browser refreshes, we see a list of heroes!
|
||
|
||
### Styling our heroes
|
||
Our list of heroes looks pretty bland.
|
||
We want to make it visually obvious to a user which hero we are hovering over and which hero is selected.
|
||
|
||
Let’s add some styles to our component by setting the `styles` property on the `@Component` decorator
|
||
to the following CSS classes:
|
||
|
||
+makeExample('toh-2/ts/app/app.component.snippets.pt2.ts', 'styles-1', 'app.component.ts (Styling)')
|
||
|
||
:marked
|
||
Notice that we again use the back-tick notation for multi-line strings.
|
||
|
||
When we assign styles to a component they are scoped to that specific component.
|
||
Our styles will only apply to our `AppComponent` and won't "leak" to the outer HTML.
|
||
|
||
Our template for displaying the heroes should now look like this:
|
||
|
||
+makeExample('toh-2/ts/app/app.component.snippets.pt2.ts', 'heroes-styled', 'app.component.ts (Styled heroes)')
|
||
|
||
:marked
|
||
Our styled list of heroes should look like this:
|
||
|
||
figure.image-display
|
||
img(src='/resources/images/devguide/toh/heroes-list-2.png' alt="Output of heroes list app (no selection color)")
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Selecting a Hero
|
||
We have a list of heroes and we have a single hero displayed in our app.
|
||
The list and the single hero are not connected in any way.
|
||
We want the user to select a hero from our list, and have the selected hero appear in the details view.
|
||
This UI pattern is widely known as "master-detail".
|
||
In our case, the master is the heroes list and the detail is the selected hero.
|
||
|
||
Let’s connect the master to the detail through a `selectedHero` component property bound to a click event.
|
||
|
||
### Click event
|
||
We modify the `<li>` by inserting an Angular event binding to its click event.
|
||
|
||
+makeExample('toh-2/ts/app/app.component.snippets.pt2.ts', 'selectedHero-click', 'app.component.ts (Capturing the click event)')
|
||
|
||
:marked
|
||
Focus on the event binding
|
||
code-example(format="." language="bash").
|
||
(click)="onSelect(hero)"
|
||
:marked
|
||
The parenthesis identify the `<li>` element’s `click` event as the target.
|
||
The expression to the right of the equal sign calls the `AppComponent` method, `onSelect()`,
|
||
passing the local template variable `hero` as an argument.
|
||
That’s the same `hero` variable we defined previously in the `ngFor`.
|
||
.l-sub-section
|
||
:marked
|
||
Learn more about Event Binding in the [Templating Syntax chapter](../guide/template-syntax.html#event-binding).
|
||
:marked
|
||
### Add the click handler
|
||
Our event binding refers to an `onSelect` method that doesn’t exist yet.
|
||
We’ll add that method to our component now.
|
||
|
||
What should that method do? It should set the component’s selected hero to the hero that the user clicked.
|
||
|
||
Our component doesn’t have a “selected hero” yet either. We’ll start there.
|
||
|
||
### Expose the selected hero
|
||
We no longer need the static `hero` property of the `AppComponent`.
|
||
**Replace** it with this simple `selectedHero` property:
|
||
|
||
+makeExample('toh-2/ts/app/app.component.snippets.pt2.ts', 'selected-hero-1', 'app.component.ts (selectedHero)')
|
||
|
||
:marked
|
||
We’ve decided that none of the heroes should be selected before the user picks a hero so
|
||
we won’t initialize the `selectedHero` as we were doing with `hero`.
|
||
|
||
Now **add an `onSelect` method** that sets the `selectedHero` property to the `hero` the user clicked.
|
||
+makeExample('toh-2/ts/app/app.component.snippets.pt2.ts', 'on-select-1', 'app.component.ts (onSelect)')
|
||
|
||
:marked
|
||
We will be showing the selected hero's details in our template.
|
||
At the moment, it is still referring to the old `hero` property.
|
||
Let’s fix the template to bind to the new `selectedHero` property.
|
||
|
||
+makeExample('toh-2/ts/app/app.component.snippets.pt2.ts', 'selectedHero-details', 'app.compontent.ts (Binding to the selectedHero\'s name)')
|
||
:marked
|
||
### Hide the empty detail with ngIf
|
||
|
||
When our app loads we see a list of heroes, but a hero is not selected.
|
||
The `selectedHero` is `undefined`.
|
||
That’s why we'll see the following error in the browser’s console:
|
||
|
||
code-example(language="html").
|
||
EXCEPTION: TypeError: Cannot read property 'name' of undefined in [null]
|
||
|
||
:marked
|
||
Remember that we are displaying `selectedHero.name` in the template.
|
||
This name property does not exist because `selectedHero` itself is undefined.
|
||
|
||
We'll address this problem by keeping the hero detail out of the DOM until there is a selected hero.
|
||
|
||
We wrap the HTML hero detail content of our template with a `<div>`.
|
||
Then we add the `ngIf` built-in directive and set it to the `selectedHero` property of our component.
|
||
|
||
+makeExample('toh-2/ts/app/app.component.snippets.pt2.ts', 'ng-if', 'app.component.ts (ngIf)')
|
||
|
||
.alert.is-critical
|
||
:marked
|
||
Remember that the leading asterisk (`*`) in front of `ngIf` is
|
||
a critical part of this syntax.
|
||
:marked
|
||
When there is no `selectedHero`, the `ngIf` directive removes the hero detail HTML from the DOM.
|
||
There will be no hero detail elements and no bindings to worry about.
|
||
|
||
When the user picks a hero, `selectedHero` becomes "truthy" and
|
||
`ngIf` puts the hero detail content into the DOM and evaluates the nested bindings.
|
||
.l-sub-section
|
||
:marked
|
||
`ngIf` and `ngFor` are called “structural directives” because they can change the
|
||
structure of portions of the DOM.
|
||
In other words, they give structure to the way Angular displays content in the DOM.
|
||
|
||
Learn more about `ngIf`, `ngFor` and other structural directives in the
|
||
[Template Syntax chapter](../guide/template-syntax.html#directives).
|
||
|
||
:marked
|
||
The browser refreshes and we see the list of heroes but not the selected hero detail.
|
||
The `ngIf` keeps it out of the DOM as long as the `selectedHero` is undefined.
|
||
When we click on a hero in the list, the selected hero displays in the hero details.
|
||
Everything is working as we expect.
|
||
|
||
### Styling the selection
|
||
|
||
We see the selected hero in the details area below but we can’t quickly locate that hero in the list above.
|
||
We can fix that by applying the `selected` CSS class to the appropriate `<li>` in the master list.
|
||
For example, when we select Magneta from the heroes list,
|
||
we can make it pop out visually by giving it a subtle background color as shown here.
|
||
|
||
figure.image-display
|
||
img(src='/resources/images/devguide/toh/heroes-list-selected.png' alt="Selected hero")
|
||
:marked
|
||
We’ll add a property binding on `class` for the `selected` class to the template. We'll set this to an expression that compares the current `selectedHero` to the `hero`.
|
||
|
||
The key is the name of the CSS class (`selected`). The value is `true` if the two heroes match and `false` otherwise.
|
||
We’re saying “*apply the `selected` class if the heroes match, remove it if they don’t*”.
|
||
+makeExample('toh-2/ts/app/app.component.snippets.pt2.ts', 'class-selected-1', 'app.component.ts (Setting the CSS class)')(format=".")
|
||
:marked
|
||
Notice in the template that the `class.selected` is surrounded in square brackets (`[]`).
|
||
This is the syntax for a Property Binding, a binding in which data flows one way
|
||
from the data source (the expression `hero === selectedHero`) to a property of `class`.
|
||
+makeExample('toh-2/ts/app/app.component.snippets.pt2.ts', 'class-selected-2', 'app.component.ts (Styling each hero)')(format=".")
|
||
|
||
.l-sub-section
|
||
:marked
|
||
Learn more about [Property Binding](../guide/template-syntax.html#property-binding)
|
||
in the Template Syntax chapter.
|
||
|
||
:marked
|
||
The browser reloads our app.
|
||
We select a hero and the selection is clearly identified by the background color.
|
||
|
||
figure.image-display
|
||
img(src='/resources/images/devguide/toh/heroes-list-1.png' alt="Output of heroes list app")
|
||
|
||
:marked
|
||
We select a different hero and the tell-tale color switches to that hero.
|
||
|
||
Here's the complete `app.component.ts` as it stands now:
|
||
|
||
+makeExample('toh-2/ts/app/app.component.ts', 'pt2', 'app.component.ts')
|
||
|
||
.l-main-section
|
||
:marked
|
||
## The Road We’ve Travelled
|
||
Here’s what we achieved in this chapter:
|
||
|
||
* Our Tour of Heroes now displays a list of selectable heroes
|
||
* We added the ability to select a hero and show the hero’s details
|
||
* We learned how to use the built-in directives `ngIf` and `ngFor` in a component’s template
|
||
|
||
### The Road Ahead
|
||
Our Tour of Heroes has grown, but it’s far from complete.
|
||
We want to get data from an asynchronous source using promises, use shared services, and create reusable components.
|
||
We’ll learn more about these tasks in the coming tutorial chapters.
|