(docs) Move ToH-pt2 to Jade and break toh-pt1 out of the tutorial index
This commit is contained in:
parent
b2de9a1067
commit
c25314a2c4
|
@ -0,0 +1,7 @@
|
|||
include ../../../../_includes/_util-fns
|
||||
|
||||
.l-main-section
|
||||
:markdown
|
||||
# Template Syntax
|
||||
|
||||
<h1>Coming Soon</h1>
|
|
@ -2,7 +2,15 @@
|
|||
"_listtype": "ordered",
|
||||
|
||||
"index": {
|
||||
"title": "Tutorial",
|
||||
"title": "Tutorial: Tour of Heroes",
|
||||
"intro": "The Tour of Heroes tutorial takes us through the steps of creating an Angular application in TypeScript."
|
||||
},
|
||||
"toh-pt1": {
|
||||
"title": "The Hero Editor",
|
||||
"intro": "We build a simple hero editor"
|
||||
},
|
||||
"toh-pt2": {
|
||||
"title": "Master/Detail",
|
||||
"intro": "We build a master/detail page with a list of heroes "
|
||||
}
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
include ../../../../_includes/_util-fns
|
||||
|
||||
.l-main-section
|
||||
|
||||
:markdown
|
||||
# Tour of Heroes
|
||||
# Tour of Heroes: the vision
|
||||
|
||||
Our grand vision is to build an app to help a staffing agency manage its stable of heroes.
|
||||
Our grand plan is to build an app to help a staffing agency manage its stable of heroes.
|
||||
Even heroes need to find work.
|
||||
|
||||
Of course we'll only make a little progress in this tutorial. What we do build will
|
||||
|
@ -28,7 +27,7 @@ include ../../../../_includes/_util-fns
|
|||
|
||||
## The End Game
|
||||
|
||||
Let's get a visual idea of where we're going in this tour, beginning with the "Heroes"
|
||||
Here's a visual idea of where we're going in this tour, beginning with the "Heroes"
|
||||
view and its list of heroes:
|
||||
|
||||
figure.image-display
|
||||
|
@ -56,312 +55,5 @@ include ../../../../_includes/_util-fns
|
|||
met in countless applications. Everything has a reason.
|
||||
|
||||
And we’ll meet many of the core fundamentals of Angular along the way.
|
||||
|
||||
.l-main-section
|
||||
|
||||
:markdown
|
||||
# Once Upon a Time
|
||||
|
||||
Every story starts somewhere. Our story starts where the [QuickStart](../quickstart) ends.
|
||||
|
||||
Follow the "QuickStart" steps. They provide the prerequisites, the folder structure,
|
||||
and the core files for our Tour of Heroes.
|
||||
|
||||
Copy the "QuickStart" code to a new folder and rename the folder `angular2-tour-of-heroes`.
|
||||
We should have the following structure:
|
||||
|
||||
code-example.
|
||||
angular2-tour-of-heroes
|
||||
├── node_modules
|
||||
├── src
|
||||
| ├── app
|
||||
| | └── app.ts
|
||||
| ├── index.html
|
||||
| └── tsconfig.json
|
||||
└── package.json
|
||||
|
||||
:markdown
|
||||
## Keep the App Running
|
||||
Start the TypeScript compiler and have it watch for changes in one terminal window by typing
|
||||
|
||||
pre.prettyprint.lang-bash
|
||||
code tsc -p src -w
|
||||
|
||||
:markdown
|
||||
Now open another terminal window and start the server by typing
|
||||
|
||||
pre.prettyprint.lang-bash
|
||||
code live-server --open=src
|
||||
|
||||
:markdown
|
||||
This command starts the server, launches the app in a browser,
|
||||
and keeps the app running while we continue to build the Tour of Heroes.
|
||||
|
||||
.alert.is-helpful
|
||||
:markdown
|
||||
These two steps watch all project files. They recompile TypeScript files and re-run
|
||||
the app when any file changes.
|
||||
If the watchers fail to detect renamed or new files,
|
||||
stop these commands in each terminal by typing `CTRL+C` and then re-run them.
|
||||
|
||||
.l-main-section
|
||||
|
||||
:markdown
|
||||
# Show our Hero
|
||||
We want to display Hero data in our app
|
||||
|
||||
Let's add two properties to our `AppComponent`, a `title` property for the application name and a `hero` property
|
||||
for a hero named "Windstorm".
|
||||
|
||||
```
|
||||
class AppComponent {
|
||||
public title = 'Tour of Heroes';
|
||||
public hero = 'Windstorm';
|
||||
}
|
||||
```
|
||||
|
||||
:markdown
|
||||
Now we update the template in the `@Component` decoration with data bindings to these new properties.
|
||||
|
||||
code-example(format="linenums").
|
||||
template: '<h1>{{title}}</h1><h2>{{hero}} details!</h2>'
|
||||
:markdown
|
||||
The browser should refresh and display our title and hero.
|
||||
|
||||
The double curly braces tell our app to read the `title` and `hero` properties from the component and render them.
|
||||
This is the "interpolation" form of one-way data binding;
|
||||
we can learn more about interpolation in the [Displaying Data chapter](displaying-data).
|
||||
|
||||
## Hero Object
|
||||
|
||||
At the moment, our hero is just a name. Our hero needs more properties.
|
||||
Let's convert the `hero` from a literal string to a class.
|
||||
|
||||
Create a `Hero` class with `id` and `name` properties.
|
||||
Keep this near the top of hte `app.ts` file for now.
|
||||
|
||||
```
|
||||
class Hero {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
```
|
||||
|
||||
Now that we have a `Hero` class, let’s refactor our component’s `hero` property to be of type `Hero`.
|
||||
Then initialize it with an id of `1` and the name, "Windstorm".
|
||||
|
||||
```
|
||||
public hero: Hero = {
|
||||
id: 1,
|
||||
name: 'Windstorm'
|
||||
};
|
||||
```
|
||||
|
||||
Because we changed the hero from a string to an object,
|
||||
we update the binding in the template to refer to the hero’s `name` property.
|
||||
|
||||
code-example(format="linenums").
|
||||
template: '<h1>{{title}}</h1><h2>{{hero.name}} details!</h2>'
|
||||
:markdown
|
||||
The browser refreshes and continues to display our hero’s name.
|
||||
|
||||
## **Adding more HTML**
|
||||
Displaying a name is good, but we want to see all of our hero’s properties.
|
||||
We’ll add a `<div>` for our hero’s `id` property and another `<div>` for our hero’s `name`.
|
||||
|
||||
code-example(format="linenums").
|
||||
template: '<h1>{{title}}</h1><h2>{{hero.name}} details!</h2><div><label>id: </label>{{hero.id}}</div><div><label>name: </label>{{hero.name}}</div>'
|
||||
:markdown
|
||||
Uh oh, our template string is getting long. We better take care of that to avoid the risk of making a typo in the template.
|
||||
|
||||
### Multi-line Template Strings
|
||||
|
||||
We could make a more readable template with string concatenation
|
||||
but that gets ugly fast, it is harder to read, and
|
||||
it is easy to make a spelling error. Instead,
|
||||
let’s take advantage of the template strings feature
|
||||
in ES2015 and TypeScript to maintain our sanity.
|
||||
|
||||
Change the quotes around the template to back-ticks and
|
||||
put the `<h1>`, `<h2>` and `<div>` elements on their own lines.
|
||||
|
||||
code-example(format="linenums").
|
||||
template:`
|
||||
<h1>{{title}}</h1>
|
||||
<h2>{{hero.name}} details!</h2>
|
||||
<div><label>id: </label>{{hero.id}}</div>
|
||||
<div><label>name: </label>{{hero.name}}</div>
|
||||
`
|
||||
|
||||
.callout.is-important
|
||||
header A back-tick is not a single quote
|
||||
:markdown
|
||||
**Be careful!** A back-tick (`) looks a lot like a single quote (').
|
||||
It's actually a completely different character.
|
||||
Back-ticks can do more than demarcate a string.
|
||||
Here we use them in a limited way to spread the template over multiple lines.
|
||||
Everything between the back-ticks at the beginning and end of the template
|
||||
is part of a single template string.
|
||||
|
||||
.l-main-section
|
||||
|
||||
:markdown
|
||||
# Editing Our Hero
|
||||
|
||||
We want to be able to edit the hero name in a textbox.
|
||||
|
||||
Replace the hero name `<label>` with an `<input>` element as shown below:
|
||||
code-example(format="linenums").
|
||||
template:`
|
||||
<h1>{{title}}</h1>
|
||||
<h2>{{hero.name}} details!</h2>
|
||||
<div><label>id: </label>{{hero.id}}</div>
|
||||
<div>
|
||||
<label>name: </label>
|
||||
<div><input value="{{hero.name}}" placeholder="name"></input></div>
|
||||
</div>
|
||||
`
|
||||
:markdown
|
||||
We see in the browser that the hero’s name does appear in the `<input>` textbox.
|
||||
But something doesn’t feel right.
|
||||
When we change the name, we notice that our change
|
||||
is not reflected in the `<h2>`. We won't get the desired behavior
|
||||
with a one-way binding to `<input>`.
|
||||
|
||||
## Two-Way Binding
|
||||
|
||||
We intend to display the name of the hero in the `<input>`, change it,
|
||||
and see those changes wherever we bind to the hero’s name.
|
||||
In short, we want two-way data binding.
|
||||
|
||||
Let’s update the template to use the **`ng-model`** built-in directive for two-way binding.
|
||||
|
||||
<!-- TODO style with a class and add a link
|
||||
*Sidenote: Learn more about ng-model in the [Using Forms chapter]*
|
||||
-->
|
||||
|
||||
Replace the `<input>` with the following HTML
|
||||
|
||||
```
|
||||
<input [(ng-model)]="hero.name" placeholder="name"></input>
|
||||
```
|
||||
Unfortunately, that change broke our application and we're no longer displaying the hero in the browser.
|
||||
Let’s fix that next.
|
||||
|
||||
.l-main-section
|
||||
|
||||
:markdown
|
||||
# Declaring Template Directives
|
||||
|
||||
We added the `ng-model` directive but we didn't tell Angular about it.
|
||||
A component must disclose every directive that appears in its template.
|
||||
|
||||
Let’s first gain access to the `NgModel` directive class by importing it from Angular as shown below:
|
||||
|
||||
````
|
||||
import {bootstrap, Component, NgModel} from 'angular2/angular2';
|
||||
```
|
||||
|
||||
Now tell the component that we will use the `ng-model` directive in the template
|
||||
by adding the `directives` property to the `@Component` decoration
|
||||
immediately below the `template` string:
|
||||
|
||||
```
|
||||
directives: [NgModel]
|
||||
```
|
||||
|
||||
The `directives` property is an array holding all directive classes that
|
||||
are used by the component’s template.
|
||||
|
||||
Unfortunately when we view the app in the browser we still have an error:
|
||||
|
||||
```
|
||||
*EXCEPTION: No value accessor for ' ' in [null]*
|
||||
```
|
||||
|
||||
Apparently declaring the `NgModel` is not quite enough.
|
||||
|
||||
## Declare Multiple Form Directives
|
||||
|
||||
We learned from our latest error message that we can’t the import `NgModel` alone.
|
||||
We need additional directives to enable two-way data binding with `NgModel`.
|
||||
|
||||
We could hunt them down and add each of them to the `directives` array one by one.
|
||||
That's painful. No one wants to remember all of the necessary directives and
|
||||
type them correctly. Fortunately, there is a shortcut.
|
||||
|
||||
The `ng-model` directive is one of many Forms directives which happen to be
|
||||
bundled in a convenient array called `FORM_DIRECTIVES`.
|
||||
|
||||
<!-- TODO *Sidenote: Learn more about Angular Forms in the [Forms chapter]* -->
|
||||
|
||||
Let’s forget about importing `NgModel` and import the `FORM_DIRECTIVES` array instead:
|
||||
```
|
||||
import {bootstrap, Component, FORM_DIRECTIVES} from 'angular2/angular2';
|
||||
```
|
||||
Now we tell the component that our template can use `FORM_DIRECTIVES`
|
||||
by updating the `directives` property of the `@Component` decorator.
|
||||
```
|
||||
directives: [FORM_DIRECTIVES]
|
||||
```
|
||||
The browser refreshes. We see our hero again. We can edit the hero’s name and
|
||||
see the changes reflected immediately in the `<h2>`.
|
||||
|
||||
### Bundled Directives
|
||||
Angular bundled the Form-related directives together in a convenient `FORM_DIRECTIVES` array.
|
||||
That's all we need to remember to light up our template.
|
||||
|
||||
We may wish to use this trick ourselves someday.
|
||||
We too can bundle a collection of directives in an array, give it a catchy name,
|
||||
and plug that array into the `directives` property.
|
||||
|
||||
# The Road We’ve Travelled
|
||||
Let’s take stock of what we’ve built.
|
||||
|
||||
* Our Tour of Heroes uses the double curly braces of interpolation (a form of one-way data binding)
|
||||
to display the application title and properties of a `Hero` object.
|
||||
* We wrote a multi-line template using ES2015’s template strings to make our template readable.
|
||||
* We can both display and change the hero’s name after adding a two-way data binding to the `<input>` element
|
||||
using the built-in `ng-model` directive.
|
||||
* The `ng-model` directive also propagates changes to every other binding of the `hero.name`.
|
||||
* We declared our use of `NgModel` and other Form directives
|
||||
by setting the component's `directives` metadata property to the `FORMS_DIRECTIVES` array.
|
||||
|
||||
Here's the complete `AppComponent.ts` as it stands now:
|
||||
|
||||
code-example(format="linenums").
|
||||
import {bootstrap, Component, FORM_DIRECTIVES} from 'angular2/angular2';
|
||||
|
||||
class Hero {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template:`
|
||||
<h1>{{title}}</h1>
|
||||
<h2>{{hero.name}} details!</h2>
|
||||
<div><label>id: </label>{{hero.id}}</div>
|
||||
<div>
|
||||
<label>name: </label>
|
||||
<div><input value="{{hero.name}}" placeholder="name"></input></div>
|
||||
</div>
|
||||
`,
|
||||
directives: [FORM_DIRECTIVES]
|
||||
})
|
||||
class AppComponent {
|
||||
public title = 'Tour of Heroes';
|
||||
public hero: Hero = {
|
||||
id: 1,
|
||||
name: 'Windstorm'
|
||||
};
|
||||
}
|
||||
|
||||
bootstrap(AppComponent);
|
||||
<!-- TODO: Add this in when the next chapter exists
|
||||
# **The Road Ahead**
|
||||
Our Tour of Heroes only displays one hero and we really want to display a list of heroes.
|
||||
We also want to allow the user to select a hero and display their details.
|
||||
We’ll learn more about how to retrieve lists, bind them to the
|
||||
template, and allow a user to select it in the next chapter.
|
||||
-->
|
||||
|
||||
[Let's get started!](./toh-pt1).
|
||||
|
|
|
@ -0,0 +1,314 @@
|
|||
include ../../../../_includes/_util-fns
|
||||
|
||||
.l-main-section
|
||||
|
||||
:markdown
|
||||
# Once Upon a Time
|
||||
|
||||
Every story starts somewhere. Our story starts where the [QuickStart](../quickstart) ends.
|
||||
|
||||
Follow the "QuickStart" steps. They provide the prerequisites, the folder structure,
|
||||
and the core files for our Tour of Heroes.
|
||||
|
||||
Copy the "QuickStart" code to a new folder and rename the folder `angular2-tour-of-heroes`.
|
||||
We should have the following structure:
|
||||
|
||||
code-example.
|
||||
angular2-tour-of-heroes
|
||||
├── node_modules
|
||||
├── src
|
||||
| ├── app
|
||||
| | └── app.ts
|
||||
| ├── index.html
|
||||
| └── tsconfig.json
|
||||
└── package.json
|
||||
|
||||
:markdown
|
||||
## Keep the App Running
|
||||
Start the TypeScript compiler and have it watch for changes in one terminal window by typing
|
||||
|
||||
pre.prettyprint.lang-bash
|
||||
code npm run tsc
|
||||
|
||||
:markdown
|
||||
Now open another terminal window and start the server by typing
|
||||
|
||||
pre.prettyprint.lang-bash
|
||||
code npm start
|
||||
|
||||
:markdown
|
||||
This command starts the server, launches the app in a browser,
|
||||
and keeps the app running while we continue to build the Tour of Heroes.
|
||||
|
||||
.alert.is-helpful
|
||||
:markdown
|
||||
These two steps watch all project files. They recompile TypeScript files and re-run
|
||||
the app when any file changes.
|
||||
If the watchers fail to detect renamed or new files,
|
||||
stop these commands in each terminal by typing `CTRL+C` and then re-run them.
|
||||
|
||||
.l-main-section
|
||||
|
||||
:markdown
|
||||
# Show our Hero
|
||||
We want to display Hero data in our app
|
||||
|
||||
Let's add two properties to our `AppComponent`, a `title` property for the application name and a `hero` property
|
||||
for a hero named "Windstorm".
|
||||
|
||||
```
|
||||
class AppComponent {
|
||||
public title = 'Tour of Heroes';
|
||||
public hero = 'Windstorm';
|
||||
}
|
||||
```
|
||||
|
||||
:markdown
|
||||
Now we update the template in the `@Component` decoration with data bindings to these new properties.
|
||||
|
||||
code-example(format="linenums").
|
||||
template: '<h1>{{title}}</h1><h2>{{hero}} details!</h2>'
|
||||
:markdown
|
||||
The browser should refresh and display our title and hero.
|
||||
|
||||
The double curly braces tell our app to read the `title` and `hero` properties from the component and render them.
|
||||
This is the "interpolation" form of one-way data binding;
|
||||
we can learn more about interpolation in the [Displaying Data chapter](displaying-data).
|
||||
|
||||
## Hero Object
|
||||
|
||||
At the moment, our hero is just a name. Our hero needs more properties.
|
||||
Let's convert the `hero` from a literal string to a class.
|
||||
|
||||
Create a `Hero` class with `id` and `name` properties.
|
||||
Keep this near the top of hte `app.ts` file for now.
|
||||
|
||||
```
|
||||
class Hero {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
```
|
||||
|
||||
Now that we have a `Hero` class, let’s refactor our component’s `hero` property to be of type `Hero`.
|
||||
Then initialize it with an id of `1` and the name, "Windstorm".
|
||||
|
||||
```
|
||||
public hero: Hero = {
|
||||
id: 1,
|
||||
name: 'Windstorm'
|
||||
};
|
||||
```
|
||||
|
||||
Because we changed the hero from a string to an object,
|
||||
we update the binding in the template to refer to the hero’s `name` property.
|
||||
|
||||
code-example(format="linenums").
|
||||
template: '<h1>{{title}}</h1><h2>{{hero.name}} details!</h2>'
|
||||
:markdown
|
||||
The browser refreshes and continues to display our hero’s name.
|
||||
|
||||
## **Adding more HTML**
|
||||
Displaying a name is good, but we want to see all of our hero’s properties.
|
||||
We’ll add a `<div>` for our hero’s `id` property and another `<div>` for our hero’s `name`.
|
||||
|
||||
code-example(format="linenums").
|
||||
template: '<h1>{{title}}</h1><h2>{{hero.name}} details!</h2><div><label>id: </label>{{hero.id}}</div><div><label>name: </label>{{hero.name}}</div>'
|
||||
:markdown
|
||||
Uh oh, our template string is getting long. We better take care of that to avoid the risk of making a typo in the template.
|
||||
|
||||
### Multi-line Template Strings
|
||||
|
||||
We could make a more readable template with string concatenation
|
||||
but that gets ugly fast, it is harder to read, and
|
||||
it is easy to make a spelling error. Instead,
|
||||
let’s take advantage of the template strings feature
|
||||
in ES2015 and TypeScript to maintain our sanity.
|
||||
|
||||
Change the quotes around the template to back-ticks and
|
||||
put the `<h1>`, `<h2>` and `<div>` elements on their own lines.
|
||||
|
||||
code-example(format="linenums").
|
||||
template:`
|
||||
<h1>{{title}}</h1>
|
||||
<h2>{{hero.name}} details!</h2>
|
||||
<div><label>id: </label>{{hero.id}}</div>
|
||||
<div><label>name: </label>{{hero.name}}</div>
|
||||
`
|
||||
|
||||
.callout.is-important
|
||||
header A back-tick is not a single quote
|
||||
:markdown
|
||||
**Be careful!** A back-tick (`) looks a lot like a single quote (').
|
||||
It's actually a completely different character.
|
||||
Back-ticks can do more than demarcate a string.
|
||||
Here we use them in a limited way to spread the template over multiple lines.
|
||||
Everything between the back-ticks at the beginning and end of the template
|
||||
is part of a single template string.
|
||||
|
||||
.l-main-section
|
||||
|
||||
:markdown
|
||||
# Editing Our Hero
|
||||
|
||||
We want to be able to edit the hero name in a textbox.
|
||||
|
||||
Replace the hero name `<label>` with an `<input>` element as shown below:
|
||||
code-example(format="linenums").
|
||||
template:`
|
||||
<h1>{{title}}</h1>
|
||||
<h2>{{hero.name}} details!</h2>
|
||||
<div><label>id: </label>{{hero.id}}</div>
|
||||
<div>
|
||||
<label>name: </label>
|
||||
<div><input value="{{hero.name}}" placeholder="name"></input></div>
|
||||
</div>
|
||||
`
|
||||
:markdown
|
||||
We see in the browser that the hero’s name does appear in the `<input>` textbox.
|
||||
But something doesn’t feel right.
|
||||
When we change the name, we notice that our change
|
||||
is not reflected in the `<h2>`. We won't get the desired behavior
|
||||
with a one-way binding to `<input>`.
|
||||
|
||||
## Two-Way Binding
|
||||
|
||||
We intend to display the name of the hero in the `<input>`, change it,
|
||||
and see those changes wherever we bind to the hero’s name.
|
||||
In short, we want two-way data binding.
|
||||
|
||||
Let’s update the template to use the **`ng-model`** built-in directive for two-way binding.
|
||||
|
||||
.alert.is-helpful
|
||||
:markdown
|
||||
Learn more about `ng-model` in the [Template Syntax](../guide/template-syntax#ng-model)
|
||||
:markdown
|
||||
Replace the `<input>` with the following HTML
|
||||
|
||||
```
|
||||
<input [(ng-model)]="hero.name" placeholder="name"></input>
|
||||
```
|
||||
Unfortunately, that change broke our application and we're no longer displaying the hero in the browser.
|
||||
Let’s fix that next.
|
||||
|
||||
.l-main-section
|
||||
|
||||
:markdown
|
||||
# Declaring Template Directives
|
||||
|
||||
We added the `ng-model` directive but we didn't tell Angular about it.
|
||||
A component must disclose every directive that appears in its template.
|
||||
|
||||
Let’s first gain access to the `NgModel` directive class by importing it from Angular as shown below:
|
||||
|
||||
````
|
||||
import {bootstrap, Component, NgModel} from 'angular2/angular2';
|
||||
```
|
||||
|
||||
Now tell the component that we will use the `ng-model` directive in the template
|
||||
by adding the `directives` property to the `@Component` decoration
|
||||
immediately below the `template` string:
|
||||
|
||||
```
|
||||
directives: [NgModel]
|
||||
```
|
||||
|
||||
The `directives` property is an array holding all directive classes that
|
||||
are used by the component’s template.
|
||||
|
||||
Unfortunately when we view the app in the browser we still have an error:
|
||||
|
||||
```
|
||||
*EXCEPTION: No value accessor for ' ' in [null]*
|
||||
```
|
||||
|
||||
Apparently declaring the `NgModel` is not quite enough.
|
||||
|
||||
## Declare Multiple Form Directives
|
||||
|
||||
We learned from our latest error message that we can’t the import `NgModel` alone.
|
||||
We need additional directives to enable two-way data binding with `NgModel`.
|
||||
|
||||
We could hunt them down and add each of them to the `directives` array one by one.
|
||||
That's painful. No one wants to remember all of the necessary directives and
|
||||
type them correctly. Fortunately, there is a shortcut.
|
||||
|
||||
The `ng-model` directive is one of many Forms directives which happen to be
|
||||
bundled in a convenient array called `FORM_DIRECTIVES`.
|
||||
<!-- TODO
|
||||
.alert.is-helpful
|
||||
:markdown
|
||||
Learn more about Angular Forms in the [Forms chapter]()
|
||||
:markdown
|
||||
-->
|
||||
Let’s forget about importing `NgModel` and import the `FORM_DIRECTIVES` array instead:
|
||||
```
|
||||
import {bootstrap, Component, FORM_DIRECTIVES} from 'angular2/angular2';
|
||||
```
|
||||
Now we tell the component that our template can use `FORM_DIRECTIVES`
|
||||
by updating the `directives` property of the `@Component` decorator.
|
||||
```
|
||||
directives: [FORM_DIRECTIVES]
|
||||
```
|
||||
The browser refreshes. We see our hero again. We can edit the hero’s name and
|
||||
see the changes reflected immediately in the `<h2>`.
|
||||
|
||||
### Bundled Directives
|
||||
Angular bundled the Form-related directives together in a convenient `FORM_DIRECTIVES` array.
|
||||
That's all we need to remember to light up our template.
|
||||
|
||||
We may wish to use this trick ourselves someday.
|
||||
We too can bundle a collection of directives in an array, give it a catchy name,
|
||||
and plug that array into the `directives` property.
|
||||
|
||||
# The Road We’ve Travelled
|
||||
Let’s take stock of what we’ve built.
|
||||
|
||||
* Our Tour of Heroes uses the double curly braces of interpolation (a form of one-way data binding)
|
||||
to display the application title and properties of a `Hero` object.
|
||||
* We wrote a multi-line template using ES2015’s template strings to make our template readable.
|
||||
* We can both display and change the hero’s name after adding a two-way data binding to the `<input>` element
|
||||
using the built-in `ng-model` directive.
|
||||
* The `ng-model` directive also propagates changes to every other binding of the `hero.name`.
|
||||
* We declared our use of `NgModel` and other Form directives
|
||||
by setting the component's `directives` metadata property to the `FORMS_DIRECTIVES` array.
|
||||
|
||||
Here's the complete `AppComponent.ts` as it stands now:
|
||||
|
||||
code-example(format="linenums").
|
||||
import {bootstrap, Component, FORM_DIRECTIVES} from 'angular2/angular2';
|
||||
|
||||
class Hero {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template:`
|
||||
<h1>{{title}}</h1>
|
||||
<h2>{{hero.name}} details!</h2>
|
||||
<div><label>id: </label>{{hero.id}}</div>
|
||||
<div>
|
||||
<label>name: </label>
|
||||
<div><input value="{{hero.name}}" placeholder="name"></input></div>
|
||||
</div>
|
||||
`,
|
||||
directives: [FORM_DIRECTIVES]
|
||||
})
|
||||
class AppComponent {
|
||||
public title = 'Tour of Heroes';
|
||||
public hero: Hero = {
|
||||
id: 1,
|
||||
name: 'Windstorm'
|
||||
};
|
||||
}
|
||||
|
||||
bootstrap(AppComponent);
|
||||
|
||||
:markdown
|
||||
# The Road Ahead
|
||||
Our Tour of Heroes only displays one hero and we really want to display a list of heroes.
|
||||
We also want to allow the user to select a hero and display their details.
|
||||
We’ll learn more about how to retrieve lists, bind them to the
|
||||
template, and allow a user to select it in the
|
||||
[next tutorial chapter](./toh-pt2).
|
|
@ -0,0 +1,441 @@
|
|||
include ../../../../_includes/_util-fns
|
||||
|
||||
.l-main-section
|
||||
:markdown
|
||||
# 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
|
||||
:markdown
|
||||
# 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).
|
||||
If not, we’ll need to go back to Part 1 and figure out what we missed.
|
||||
|
||||
code-example.
|
||||
angular2-tour-of-heroes
|
||||
├── node_modules
|
||||
├── src
|
||||
| ├── app
|
||||
| | └── app.ts
|
||||
| ├── index.html
|
||||
| └── tsconfig.json
|
||||
└── package.json
|
||||
:markdown
|
||||
## Keep the App Running
|
||||
Start the TypeScript compiler and have it watch for changes in one terminal window by typing
|
||||
|
||||
pre.prettyprint.lang-bash
|
||||
code npm run tsc
|
||||
|
||||
:markdown
|
||||
Now open another terminal window and start the server by typing
|
||||
|
||||
pre.prettyprint.lang-bash
|
||||
code npm start
|
||||
|
||||
:markdown
|
||||
This will keep the application running while we continue to build the Tour of Heroes.
|
||||
|
||||
# Displaying Our Heroes
|
||||
## Creating Heroes
|
||||
Let’s create an array of ten heroes at the bottom of `app.ts`.
|
||||
```
|
||||
var HEROES: Hero[] = [
|
||||
{ "id": 11, "name": "Mr. Nice" },
|
||||
{ "id": 12, "name": "Narco" },
|
||||
{ "id": 13, "name": "Bombasto" },
|
||||
{ "id": 14, "name": "Celeritas" },
|
||||
{ "id": 15, "name": "Magneta" },
|
||||
{ "id": 16, "name": "RubberMan" },
|
||||
{ "id": 17, "name": "Dynama" },
|
||||
{ "id": 18, "name": "Dr IQ" },
|
||||
{ "id": 19, "name": "Magma" },
|
||||
{ "id": 20, "name": "Tornado" }
|
||||
];
|
||||
```
|
||||
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.
|
||||
```
|
||||
public heroes = HEROES;
|
||||
```
|
||||
We did not have to define the `heroes` type. TypeScript can infer it from the `HEROES` array.
|
||||
.alert.is-helpful
|
||||
:markdown
|
||||
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 know where we are heading, it makes sense to separate the hero data
|
||||
from the class implementation from the start.
|
||||
:markdown
|
||||
## 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.
|
||||
```
|
||||
<h2>My Heroes</h2>
|
||||
<ul class="heroes">
|
||||
<li>
|
||||
<!-- each hero goes here -->
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
Now we have a template that we can fill with our heroes.
|
||||
|
||||
## Iterating Over Our Heroes with `ng-for`
|
||||
|
||||
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 `*ng-for`.
|
||||
```
|
||||
<li *ng-for="#hero of heroes">
|
||||
```
|
||||
.alert.is-important
|
||||
:markdown
|
||||
Do not neglect the leading asterisk (`*`) in front of `ng-for`!
|
||||
|
||||
Although it is not part of the `ng-for` directive name,
|
||||
it is a critical part of the syntax in this usage of `ng-for`.
|
||||
:markdown
|
||||
The (`*`) prefix to the `ng-for` indicates that the `<li>` element and its children
|
||||
constitute a master template.
|
||||
|
||||
The `ng-for` directive iterates over the `heroes` array returned by the `AppComponent.heroes` property
|
||||
and stamps out instances of this template.
|
||||
|
||||
The quoted text assigned to `ng-for` 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.
|
||||
|
||||
.alert.is-helpful
|
||||
:markdown
|
||||
Learn more about `ng-for` and local template variables in the
|
||||
[Template Syntax](../guide/template-syntax#ng-for) chapter.
|
||||
|
||||
:markdown
|
||||
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.
|
||||
code-example.
|
||||
<li *ng-for="#hero of heroes">
|
||||
<span class="badge">{{hero.id}}</span> {{hero.name}}</a>
|
||||
</li>
|
||||
:markdown
|
||||
## Declaring NgFor
|
||||
When we view the running app in the browser we see nothing … no heroes.
|
||||
We open the developer tools and see an error in the console.
|
||||
```
|
||||
EXCEPTION:
|
||||
Can't bind to 'ngForOf' since it isn't a known property of the '<template>' element and
|
||||
there are no matching directives with a corresponding property
|
||||
```
|
||||
Thankfully we have a clear error message that indicates where we went wrong.
|
||||
We used `ng-for` in the template but we didn’t tell the component about it.
|
||||
From Angular's perspective, `ng-for` is a meaningless attribute.
|
||||
When it tries to render the view, it doesn’t recognize `ng-for` and gives up.
|
||||
|
||||
We need to say “*hey component, I’m going to use this NgFor directive. OK?*”
|
||||
|
||||
To that end, we first import the `NgFor` symbol
|
||||
```
|
||||
import {bootstrap, Component, FORM_DIRECTIVES, NgFor} from 'angular2/angular2';
|
||||
```
|
||||
and then declare `NgFor` to be one of the view’s directives in the `@Component` decorator.
|
||||
```
|
||||
directives: [FORM_DIRECTIVES, NgFor]
|
||||
```
|
||||
After 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:
|
||||
```
|
||||
styles: `
|
||||
.heroes {list-style-type: none; margin-left: 1em; padding: 0; width: 10em;}
|
||||
.heroes li { cursor: pointer; position: relative; left: 0; transition: all 0.2s ease; }
|
||||
.heroes li:hover {color: #369; background-color: #EEE; left: .2em;}
|
||||
.heroes .badge {
|
||||
font-size: small;
|
||||
color: white;
|
||||
padding: 0.1em 0.7em;
|
||||
background-color: #369;
|
||||
line-height: 1em;
|
||||
position: relative;
|
||||
left: -1px;
|
||||
top: -1px;
|
||||
}
|
||||
.selected { background-color: #EEE; color: #369; }
|
||||
`
|
||||
```
|
||||
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:
|
||||
|
||||
code-example(format="linenums").
|
||||
<h2>My Heroes</h2>
|
||||
<ul class="heroes">
|
||||
<li *ng-for="#hero of heroes">
|
||||
<span class="badge">{{hero.id}}</span> {{hero.name}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
:markdown
|
||||
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
|
||||
:markdown
|
||||
# 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.
|
||||
|
||||
code-example(format="linenums").
|
||||
<li *ng-for="#hero of heroes" (click)="onSelect(hero)">
|
||||
<span class="badge">{{hero.id}}</span> {{hero.name}}</a>
|
||||
</li>
|
||||
:markdown
|
||||
Focus on the event binding
|
||||
pre.prettyprint.lang-bash
|
||||
code (click)="onSelect(hero)">
|
||||
:markdown
|
||||
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 `ng-for`.
|
||||
.alert.is-helpful
|
||||
:markdown
|
||||
Learn more about Event Binding in the [Templating Syntax](../guide/template-syntax#event-binding) chapter.
|
||||
:markdown
|
||||
## 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:
|
||||
```
|
||||
public selectedHero: Hero;
|
||||
```
|
||||
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.
|
||||
```
|
||||
onSelect(hero: Hero) { this.selectedHero = hero; }
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
code-example(format="linenums").
|
||||
<h2>{{selectedHero.name}} details!</h2>
|
||||
<div><label>id: </label>{{selectedHero.id}}</div>
|
||||
<div>
|
||||
<label>name: </label>
|
||||
<input [(ng-model)]="selectedHero.name" placeholder="name"></input>
|
||||
</div>
|
||||
|
||||
:markdown
|
||||
### 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:
|
||||
```
|
||||
EXCEPTION: TypeError: Cannot read property 'name' of undefined in [null]
|
||||
```
|
||||
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 `ng-if` built-in directive and set it to the `selectedHero` property of our component.
|
||||
|
||||
code-example(format="linenums").
|
||||
<div *ng-if="selectedHero">
|
||||
<h2>{{selectedHero.name}} details!</h2>
|
||||
<div><label>id: </label>{{selectedHero.id}}</div>
|
||||
<div>
|
||||
<label>name: </label>
|
||||
<input [(ng-model)]="selectedHero.name" placeholder="name"></input>
|
||||
</div>
|
||||
</div>
|
||||
.alert.is-important
|
||||
:markdown
|
||||
Do not neglect the leading asterisk (`*`) in front of `ng-if`.
|
||||
It's a critical part of the syntax in this usage of `ng-if`.
|
||||
:markdown
|
||||
When there is no `selectedHero`, the `ng-if` directive removes the hero detail HTML from the DOM.
|
||||
There will no hero detail elements and no bindings to worry about.
|
||||
|
||||
When the user picks a hero, `selectedHero` becomes "truthy" and
|
||||
`ng-if` puts the hero detail content into the DOM and evaluates the nested bindings.
|
||||
|
||||
.alert.is-helpful
|
||||
:markdown
|
||||
`ng-if` and `ng-for` 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 `ng-if`, `ng-for` and other structural directives in the
|
||||
[Template Syntax](../guide/template-syntax#directives) chapter
|
||||
|
||||
:markdown
|
||||
We learned previously with `NgFor` that we must declare every directive we use in the component’s `@Component` decorator.
|
||||
Let’s do that again for `NgIf`.
|
||||
|
||||
Add the `NgIf` symbol to our imports at the top of our `app.ts` file, keeping them sorted
|
||||
alphabetically to make them easier to find:
|
||||
```
|
||||
import {bootstrap, Component, FORM_DIRECTIVES, NgFor, NgIf} from 'angular2/angular2';
|
||||
```
|
||||
:markdown
|
||||
Now add `NgIf` to the directives array in the `@Component` decorator:
|
||||
```
|
||||
directives: [FORM_DIRECTIVES, NgFor, NgIf]
|
||||
```
|
||||
The browser refreshes and we see the list of heroes but not the selected hero detail.
|
||||
The `ng-if` 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")
|
||||
:markdown
|
||||
First we’ll add a `getSelectedClass` method to the component that compares the current `selectedHero` to a hero parameter
|
||||
and returns an object with a single key/value pair.
|
||||
|
||||
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*”.
|
||||
Here is that method.
|
||||
```
|
||||
getSelectedClass(hero: Hero) {
|
||||
return { 'selected': hero === this.selectedHero };
|
||||
}
|
||||
```
|
||||
What do we do with this method and its peculiar result?
|
||||
|
||||
### NgClass
|
||||
We’ll add the `ng-class`built-in directive to the `<li>` element in our template and bind it to `getSelectedClass`.
|
||||
It’s no coincidence that the value returned by `getSelectedClass` is exactly what the `ng-class` requires
|
||||
to add or remove the `selected` class to each hero’s display.
|
||||
code-example(format="linenums").
|
||||
<li *ng-for="#hero of heroes"
|
||||
[ng-class]="getSelectedClass(hero)"
|
||||
(click)="onSelect(hero)">
|
||||
<span class="badge">{{hero.id}}</span> {{hero.name}}</a>
|
||||
</li>
|
||||
|
||||
.alert.is-helpful
|
||||
:markdown
|
||||
Learn more about `ng-class` in the
|
||||
[Template Syntax](../guide/template-syntax#ng-class) chapter
|
||||
|
||||
:markdown
|
||||
Notice in the template that the `ng-class` name is surrounded in square brackets (`[]`).
|
||||
This is the syntax for a Property Binding, a binding in which data flow one way
|
||||
from the data source (the `getSelectedClass`) to a property of the `ng-class` directive.
|
||||
|
||||
.alert.is-helpful
|
||||
:markdown
|
||||
Learn more about about one-way property data binding in the
|
||||
[Template Syntax](../guide/template-syntax#property-binding) chapter
|
||||
:markdown
|
||||
We've added yet another new directive to our template that we have to import and declare
|
||||
in the component’s `directives` array as we’ve done twice before.
|
||||
```
|
||||
import {bootstrap, Component,
|
||||
FORM_DIRECTIVES, NgClass, NgFor, NgIf} from 'angular2/angular2';
|
||||
```
|
||||
|
||||
```
|
||||
directives: [FORM_DIRECTIVES, NgClass, NgFor, NgIf]
|
||||
```
|
||||
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")
|
||||
|
||||
:markdown
|
||||
We select a different hero and the tell-tale color switches to that hero.
|
||||
|
||||
## Declaring Built-In Directives
|
||||
|
||||
Every time we used a directive, we imported it and declared it in the component.
|
||||
We only used three directives but we can easily envision a component that uses many more.
|
||||
The `directives` array grows quickly and the process of importing the directive and adding it to the array is tedious.
|
||||
We can make this easier.
|
||||
|
||||
Remember how we imported the `FORM_DIRECTIVES` array to help us apply `ng-model`to our template in the previous chapter?
|
||||
The `FORM_DIRECTIVES` array held all the directives we needed for `ng-model` (and a few more).
|
||||
We didn’t have to list them. We simply added the `FORM_DIRECTIVES` array to the component’s `directives` array.
|
||||
|
||||
The `NgClass`, `NgFor`, and `NgIf` are extremely common directives used by many components in many applications.
|
||||
Fortunately they are all exported from Angular as part of the `CORE_DIRECTIVES` array.
|
||||
|
||||
Let’s replace all of those separate import variables with `CORE_DIRECTIVES`:
|
||||
```
|
||||
import {bootstrap, Component, CORE_DIRECTIVES, FORM_DIRECTIVES} from 'angular2/angular2';
|
||||
```
|
||||
Then replace `NgClass`, `NgFor`, and `NgIf` in the `directives` array with `CORE_DIRECTIVES`:
|
||||
```
|
||||
directives: [CORE_DIRECTIVES, FORM_DIRECTIVES]
|
||||
```
|
||||
Everything still works and we have a convenient way to import and declare the most commonly used directives.
|
||||
Cleaner code for the win!
|
||||
|
||||
.l-main-section
|
||||
:markdown
|
||||
# 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 `ng-if`, `ng-for` and `ng-class` 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.
|
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
Loading…
Reference in New Issue