docs(dart): decouple from TS .jade files temporarily (#2044)
Addresses #2043.
This commit is contained in:
parent
b796d4b97d
commit
5ce8304227
|
@ -1,4 +1,4 @@
|
|||
extends ../../../ts/latest/guide/architecture.jade
|
||||
extends ../../../ts/_cache/guide/architecture.jade
|
||||
|
||||
block includes
|
||||
include ../_util-fns
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends ../../../ts/latest/guide/attribute-directives.jade
|
||||
extends ../../../ts/_cache/guide/attribute-directives.jade
|
||||
|
||||
block includes
|
||||
include ../_util-fns
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends ../../../ts/latest/guide/component-styles.jade
|
||||
extends ../../../ts/_cache/guide/component-styles.jade
|
||||
|
||||
block includes
|
||||
include ../_util-fns
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends ../../../ts/latest/guide/dependency-injection.jade
|
||||
extends ../../../ts/_cache/guide/dependency-injection.jade
|
||||
|
||||
block includes
|
||||
include ../_util-fns
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends ../../../ts/latest/guide/displaying-data.jade
|
||||
extends ../../../ts/_cache/guide/displaying-data.jade
|
||||
|
||||
block includes
|
||||
include ../_util-fns
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends ../../../ts/latest/guide/hierarchical-dependency-injection.jade
|
||||
extends ../../../ts/_cache/guide/hierarchical-dependency-injection.jade
|
||||
|
||||
block includes
|
||||
include ../_util-fns
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends ../../../ts/latest/guide/lifecycle-hooks.jade
|
||||
extends ../../../ts/_cache/guide/lifecycle-hooks.jade
|
||||
|
||||
block includes
|
||||
include ../_util-fns
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends ../../../ts/latest/guide/pipes.jade
|
||||
extends ../../../ts/_cache/guide/pipes.jade
|
||||
|
||||
block includes
|
||||
include ../_util-fns
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends ../../../ts/latest/guide/server-communication.jade
|
||||
extends ../../../ts/_cache/guide/server-communication.jade
|
||||
|
||||
block includes
|
||||
include ../_util-fns
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends ../../../ts/latest/guide/structural-directives.jade
|
||||
extends ../../../ts/_cache/guide/structural-directives.jade
|
||||
|
||||
block includes
|
||||
include ../_util-fns
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends ../../../ts/latest/guide/template-syntax.jade
|
||||
extends ../../../ts/_cache/guide/template-syntax.jade
|
||||
|
||||
block includes
|
||||
include ../_util-fns
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends ../../ts/latest/quickstart.jade
|
||||
extends ../../ts/_cache/quickstart.jade
|
||||
|
||||
block includes
|
||||
include _util-fns
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends ../../../ts/latest/tutorial/toh-pt6.jade
|
||||
extends ../../../ts/_cache/tutorial/toh-pt6.jade
|
||||
|
||||
block includes
|
||||
include ../_util-fns
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
include ../latest/_quickstart_repo
|
|
@ -0,0 +1 @@
|
|||
include ../latest/_util-fns
|
|
@ -0,0 +1,597 @@
|
|||
block includes
|
||||
include ../_util-fns
|
||||
- var _library_module = 'library module'
|
||||
- var _at_angular = '@angular'
|
||||
|
||||
:marked
|
||||
Angular 2 is a framework to help us build client applications in HTML and
|
||||
either JavaScript or a language (like Dart or TypeScript) that compiles to JavaScript.
|
||||
|
||||
block angular-parts
|
||||
:marked
|
||||
The framework consists of several cooperating libraries, some of them core and some optional.
|
||||
|
||||
:marked
|
||||
With Angular, we write applications by composing HTML *templates* with Angularized-markup,
|
||||
writing *component* classes to manage those templates, adding application logic in *services*,
|
||||
and handing the top root component to Angular's *bootstrapper*.
|
||||
|
||||
Angular takes over, presenting our application content in a browser and
|
||||
responding to user interactions according to the instructions we provided.
|
||||
|
||||
Of course there is more to it than this.
|
||||
We'll learn the details when we dive into the guide chapters.
|
||||
Let's get the big picture first.
|
||||
|
||||
figure
|
||||
img(src="/resources/images/devguide/architecture/overview2.png" alt="overview" style="margin-left:-40px;" width="700")
|
||||
|
||||
:marked
|
||||
The architecture diagram identifies the eight main building blocks of an Angular 2 application:
|
||||
|
||||
1. [Modules](#modules)
|
||||
1. [Components](#components)
|
||||
1. [Templates](#templates)
|
||||
1. [Metadata](#metadata)
|
||||
1. [Data binding](#data-binding)
|
||||
1. [Directives](#directives)
|
||||
1. [Services](#services)
|
||||
1. [Dependency injection](#dependency-injection)
|
||||
|
||||
Learn these, and we're on our way.
|
||||
|
||||
.l-sub-section
|
||||
p The code referenced in this chapter is available as a #[+liveExampleLink2()].
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Modules
|
||||
figure
|
||||
img(src="/resources/images/devguide/architecture/module.png" alt="Component" align="left" style="width:240px; margin-left:-40px;margin-right:10px" )
|
||||
:marked
|
||||
Angular apps are modular.
|
||||
|
||||
In general we assemble our application from many **modules**.
|
||||
|
||||
A typical module is a cohesive block of code dedicated to a single purpose.
|
||||
A module **exports** something of value in that code, typically one thing such as a class.
|
||||
<br clear="all"><br>
|
||||
|
||||
block modules-in-dart
|
||||
//- N/A
|
||||
|
||||
block modules-are-optional
|
||||
.l-sub-section
|
||||
:marked
|
||||
### Modules are optional
|
||||
We highly recommend modular design. TypeScript has great support for ES2015 module syntax and our chapters assume we're taking a modular
|
||||
approach using that syntax. That's why we list *Module* among the basic building blocks.
|
||||
|
||||
Angular itself doesn't require a modular approach nor this particular syntax. Don't use it if you don't want it.
|
||||
Each chapter has plenty to offer after you steer clear of the `import` and `export` statements.
|
||||
|
||||
Find setup and organization clues in the JavaScript track (select it from the combo-box at the top of this page)
|
||||
which demonstrates Angular 2 development with plain old JavaScript and no module system.
|
||||
|
||||
- var _app_comp_filename = _docsFor == 'dart' ? 'app_component.dart' : 'app.component.ts';
|
||||
:marked
|
||||
Perhaps the first module we meet is a module that exports a *component* class.
|
||||
The component is one of the basic Angular blocks, we write a lot of them,
|
||||
and we'll talk about components in the next segment. For the moment it is enough to know that a
|
||||
component class is the kind of thing we'd export from a module.
|
||||
|
||||
Most applications have an `AppComponent`. By convention, we'll find it in a file named `!{_app_comp_filename}`.
|
||||
Look inside such a file and we'll see a declaration such as this one.
|
||||
|
||||
+makeExcerpt('app/app.component.ts ()', 'export')
|
||||
|
||||
block export-qualifier
|
||||
:marked
|
||||
The `export` statement tells TypeScript that this is a module whose
|
||||
`AppComponent` class is public and accessible to other modules of the application.
|
||||
|
||||
:marked
|
||||
When we need a reference to the `AppComponent`, we **import** it like this:
|
||||
|
||||
+makeExcerpt('app/main.ts', 'import')
|
||||
|
||||
block ts-import
|
||||
:marked
|
||||
The `import` statement tells the system it can get an `AppComponent` from a module named `app.component`
|
||||
located in a neighboring file.
|
||||
The **module name** (AKA module id) is often the same as the filename without its extension.
|
||||
|
||||
:marked
|
||||
### Libraries
|
||||
|
||||
figure
|
||||
img(src="/resources/images/devguide/architecture/library-module.png" alt="Component" align="left" style="width:240px; margin-left:-40px;margin-right:10px" )
|
||||
|
||||
block angular-library-modules
|
||||
:marked
|
||||
Some modules are _libraries_ of other modules.
|
||||
Angular itself ships as a collection of library modules within several npm packages.
|
||||
Their names begin with the `!{_at_angular}` prefix.
|
||||
|
||||
Each Angular library contains a [barrel](../glossary.html#barrel) module
|
||||
that is actually a public façade over several logically-related private modules.
|
||||
|
||||
:marked
|
||||
`!{_at_angular}/core` is the primary Angular library from which we get most of what we need.
|
||||
<br clear="all">
|
||||
|
||||
There are other important Angular libraries too, such as `!{_at_angular}/common`<span if-docs="ts">,
|
||||
`!{_at_angular}/http`</span> and `!{_at_angular}/router`.
|
||||
We import what we need from an Angular !{_library_module}.
|
||||
|
||||
block angular-imports
|
||||
:marked
|
||||
For example, we import the Angular **`Component` *function*** from `@angular/core` like this:
|
||||
+makeExcerpt('app/app.component.ts', 'import')
|
||||
:marked
|
||||
Compare that syntax to our previous import of `AppComponent`.
|
||||
+makeExcerpt('app/main.ts', 'import')
|
||||
|
||||
:marked
|
||||
Notice the difference?
|
||||
In the first case, when importing from an Angular library module,
|
||||
the import statement refers to the bare module name, `@angular/core`, *without a path prefix*.
|
||||
|
||||
When we import from one of *our* own files, we prefix the module name with the file path.
|
||||
In this example we specify a relative file path (`./`). That means the
|
||||
source module is in the same folder (`./`) as the module importing it.
|
||||
We could path up and around the application folder structure if the source module were somewhere else.
|
||||
.l-sub-section
|
||||
:marked
|
||||
We import and export in the ECMAScript 2015 (ES2015) module syntax.
|
||||
Learn more about that syntax [here](http://www.2ality.com/2014/09/es6-modules-final.html)
|
||||
and many other places on the web.
|
||||
|
||||
The infrastructure *behind* module loading and importing is an important subject.
|
||||
But it's a subject outside the scope of this introduction to Angular.
|
||||
While we're focused on our application, *import* and *export*
|
||||
is about all we need to know.
|
||||
|
||||
- var _export = _docsFor == 'dart' ? 'publicly declare' : 'export';
|
||||
- var _declare = _docsFor == 'dart' ? 'declare' : 'export';
|
||||
:marked
|
||||
The key take-aways are:
|
||||
* Angular apps are composed of modules.
|
||||
* Modules !{_export} things — classes, function, values — that other modules import.
|
||||
* We prefer to write our application as a collection of modules, each module exporting one thing.
|
||||
|
||||
The first module we write will most likely !{_declare} a component.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Components
|
||||
figure
|
||||
img(src="/resources/images/devguide/architecture/hero-component.png" alt="Component" align="left" style="width:200px; margin-left:-40px;margin-right:10px" )
|
||||
|
||||
:marked
|
||||
A **component** controls a patch of screen real estate that we could call a *view*.
|
||||
The shell at the application root with navigation links, a list of heroes, a hero editor ...
|
||||
they're all views controlled by components.
|
||||
|
||||
We define a component's application logic — what it does to support the view — inside a class.
|
||||
The class interacts with the view through an API of properties and methods.
|
||||
|
||||
<a id="component-code"></a>
|
||||
A `HeroListComponent`, for example, might have a `heroes` property that returns !{_an} !{_array} of heroes
|
||||
that it acquired from a service.
|
||||
It might have a `selectHero()` method that sets a `selectedHero` property when the user clicks to choose a hero from that list.
|
||||
The component might be a class like this:
|
||||
|
||||
+makeExcerpt('app/hero-list.component.ts', 'class')
|
||||
:marked
|
||||
Angular creates, updates, and destroys components as the user moves through the application.
|
||||
The developer can take action at each moment in this lifecycle through optional [lifecycle hooks](lifecycle-hooks.html), like `ngOnInit()` declared above.
|
||||
.l-sub-section
|
||||
:marked
|
||||
We may wonder who is calling the component's constructor? Who provides the service parameter?
|
||||
For the moment, have faith that Angular will call the constructor and deliver an
|
||||
appropriate `HeroService` when we need it.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Templates
|
||||
figure
|
||||
img(src="/resources/images/devguide/architecture/template.png" alt="Template" align="left" style="width:200px; margin-left:-40px;margin-right:10px" )
|
||||
:marked
|
||||
We define a component's view with its companion **template**. A template is a form of HTML
|
||||
that tells Angular how to render the component.
|
||||
|
||||
A template looks like regular HTML much of the time ... and then it gets a bit strange. Here is a
|
||||
template for our `HeroListComponent`:
|
||||
|
||||
+makeExample('app/hero-list.component.html')
|
||||
|
||||
:marked
|
||||
This template features typical HTML elements like `<h2>` and `<p>`.
|
||||
But what are `*ngFor`, `{{hero.name}}`, `(click)`, `[hero]`, and `<hero-detail>`?
|
||||
|
||||
These are examples of Angular's [template syntax](template-syntax.html).
|
||||
We will grow accustomed to that syntax and may even learn to love it.
|
||||
We'll begin to explain it in a moment.
|
||||
|
||||
Before we do, focus attention on the last line.
|
||||
The `<hero-detail>` tag is a custom element representing the `HeroDetailComponent`.
|
||||
|
||||
The `HeroDetailComponent` is a *different* component than the `HeroListComponent` we've been reviewing.
|
||||
The `HeroDetailComponent` (code not shown) presents facts about a particular hero, the
|
||||
hero that the user selects from the list presented by the `HeroListComponent`.
|
||||
The `HeroDetailComponent` is a **child** of the `HeroListComponent`.
|
||||
|
||||
figure
|
||||
img(src="/resources/images/devguide/architecture/component-tree.png" alt="Metadata" align="left" style="width:300px; margin-left:-40px;margin-right:10px" )
|
||||
:marked
|
||||
Notice how `<hero-detail>` rests comfortably among native HTML elements.
|
||||
We can and _will_ mix our custom components with native HTML in the same layouts.
|
||||
|
||||
In this manner we'll compose complex component trees to build out our richly featured application.
|
||||
<br clear="all">
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Metadata
|
||||
figure
|
||||
img(src="/resources/images/devguide/architecture/metadata.png" alt="Metadata" align="left" style="width:150px; margin-left:-40px;margin-right:10px" )
|
||||
|
||||
:marked
|
||||
<p style="padding-top:10px">Metadata tells Angular how to process a class.</p>
|
||||
<br clear="all">
|
||||
:marked
|
||||
[Looking back at the code](#component-code) for `HeroListComponent`, we see that it's just a class.
|
||||
There is no evidence of a framework, no "Angular" in it at all.
|
||||
|
||||
In fact, it really is *just a class*. It's not a component until we *tell Angular about it*.
|
||||
|
||||
We tell Angular that `HeroListComponent` is a component by attaching **metadata** to the class.
|
||||
|
||||
In !{_Lang}, we attach metadata by using !{_a} **!{_decorator}**.
|
||||
Here's some metadata for `HeroListComponent`:
|
||||
|
||||
+makeExcerpt('app/hero-list.component.ts', 'metadata')
|
||||
|
||||
:marked
|
||||
Here we see the `@Component` !{_decorator} which (no surprise) identifies the class
|
||||
immediately below it as a component class.
|
||||
|
||||
block ts-decorator
|
||||
:marked
|
||||
A decorator is a function. Decorators often have a configuration parameter.
|
||||
The `@Component` decorator takes a required configuration object with the
|
||||
information Angular needs to create and present the component and its view.
|
||||
|
||||
Here are a few of the possible `@Component` configuration options:
|
||||
|
||||
:marked
|
||||
- `selector`: CSS selector that tells Angular to create and insert an instance of this component
|
||||
where it finds a `<hero-list>` tag in *parent* HTML.
|
||||
For example, if an app's HTML contains `<hero-list></hero-list>`, then
|
||||
Angular inserts an instance of the `HeroListComponent` view between those tags.
|
||||
|
||||
- `templateUrl`: address of this component's template, which we showed [above](#templates).
|
||||
|
||||
- `directives`: !{_array} of the components or directives that *this* template requires.
|
||||
We saw in the last line of our template that we expect Angular to insert a `HeroDetailComponent`
|
||||
in the space indicated by `<hero-detail>` tags.
|
||||
Angular will do so only if we mention the `HeroDetailComponent` in this `directives` !{_array}.
|
||||
|
||||
- `providers`: !{_array} of **dependency injection providers** for services that the component requires.
|
||||
This is one way to tell Angular that our component's constructor requires a `HeroService`
|
||||
so it can get the list of heroes to display. We'll get to dependency injection later.
|
||||
|
||||
figure
|
||||
img(src="/resources/images/devguide/architecture/template-metadata-component.png" alt="Metadata" align="left" style="height:200px; margin-left:-40px;margin-right:10px" )
|
||||
|
||||
:marked
|
||||
Angular reads the metadata specified by the `@Component`
|
||||
annotation. That's how Angular learns to do "the right thing".
|
||||
|
||||
The template, metadata, and component together describe a view.
|
||||
|
||||
We apply other metadata !{_decorator}s in a similar fashion to guide Angular behavior.
|
||||
`@Injectable`, `@Input`, and `@Output` are a few of the more popular !{_decorator}s
|
||||
we'll master as our Angular knowledge grows.
|
||||
<br clear="all">
|
||||
:marked
|
||||
The architectural takeaway is that we must add metadata to our code
|
||||
so that Angular knows what to do.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Data binding
|
||||
Without a framework, we would be responsible for pushing data values into the HTML controls and turning user responses
|
||||
into actions and value updates. Writing such push/pull logic by hand is tedious, error-prone, and a nightmare to
|
||||
read as any experienced jQuery programmer can attest.
|
||||
figure
|
||||
img(src="/resources/images/devguide/architecture/databinding.png" alt="Data Binding" style="width:220px; float:left; margin-left:-40px;margin-right:20px" )
|
||||
:marked
|
||||
Angular supports **data binding**,
|
||||
a mechanism for coordinating parts of a template with parts of a component.
|
||||
We add binding markup to the template HTML to tell Angular how to connect both sides.
|
||||
|
||||
There are four forms of data binding syntax. Each form has a direction — to the DOM, from the DOM, or in both directions —
|
||||
as indicated by the arrows in the diagram.
|
||||
<br clear="all">
|
||||
:marked
|
||||
We saw three forms of data binding in our [example](#templates) template:
|
||||
|
||||
+makeExcerpt('app/hero-list.component.1.html', 'binding')
|
||||
|
||||
:marked
|
||||
* The `{{hero.name}}` [*interpolation*](displaying-data.html#interpolation)
|
||||
displays the component's `hero.name` property value within the `<li>` tags.
|
||||
|
||||
* The `[hero]` [*property binding*](template-syntax.html#property-binding) passes the value of `selectedHero` from
|
||||
the parent `HeroListComponent` to the `hero` property of the child `HeroDetailComponent`.
|
||||
|
||||
* The `(click)` [*event binding*](user-input.html#click) calls the component's `selectHero` method when the user clicks a hero's name.
|
||||
|
||||
**Two-way data binding** is an important fourth form
|
||||
that combines property and event binding in a single notation, using the `ngModel` directive.
|
||||
We didn't have a two-way binding in the `HeroListComponent` template;
|
||||
here's an example from the `HeroDetailComponent` template:
|
||||
|
||||
+makeExcerpt('app/hero-detail.component.html', 'ngModel')
|
||||
|
||||
:marked
|
||||
In two-way binding, a data property value flows to the input box from the component as with property binding.
|
||||
The user's changes also flow back to the component, resetting the property to the latest value,
|
||||
as with event binding.
|
||||
|
||||
Angular processes *all* data bindings once per JavaScript event cycle,
|
||||
from the root of the application component tree down to the leaves.
|
||||
|
||||
figure
|
||||
img(src="/resources/images/devguide/architecture/component-databinding.png" alt="Data Binding" style="float:left; width:300px; margin-left:-40px;margin-right:10px" )
|
||||
:marked
|
||||
We don't know all the details yet,
|
||||
but it's clear from these examples that data binding plays an important role in communication
|
||||
between a template and its component.
|
||||
<br clear="all">
|
||||
figure
|
||||
img(src="/resources/images/devguide/architecture/parent-child-binding.png" alt="Parent/Child binding" style="float:left; width:300px; margin-left:-40px;margin-right:10px" )
|
||||
:marked
|
||||
Data binding is also important for communication between parent and child components.
|
||||
<br clear="all">
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Directives
|
||||
figure
|
||||
img(src="/resources/images/devguide/architecture/directive.png" alt="Parent child" style="float:left; width:150px; margin-left:-40px;margin-right:10px" )
|
||||
:marked
|
||||
Angular templates are *dynamic*. When Angular renders them, it transforms the DOM
|
||||
according to the instructions given by **directives**.
|
||||
|
||||
A directive is a class with directive metadata. In !{_Lang} we apply the `@Directive` !{_decorator}
|
||||
to attach metadata to the class.
|
||||
<br clear="all">
|
||||
:marked
|
||||
We already met one form of directive: the component. A component is a *directive-with-a-template*;
|
||||
a `@Component` !{_decorator} is actually a `@Directive` !{_decorator} extended with template-oriented features.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
While **a component is technically a directive**,
|
||||
components are so distinctive and central to Angular applications that we chose
|
||||
to separate components from directives in this architectural overview.
|
||||
:marked
|
||||
Two *other* kinds of directives exist: _structural_ and _attribute_ directives.
|
||||
|
||||
They tend to appear within an element tag as attributes do,
|
||||
sometimes by name but more often as the target of an assignment or a binding.
|
||||
|
||||
**Structural** directives alter layout by adding, removing, and replacing elements in DOM.
|
||||
|
||||
Our [example](#templates) template uses two built-in structural directives:
|
||||
|
||||
+makeExcerpt('app/hero-list.component.1.html', 'structural')
|
||||
|
||||
:marked
|
||||
* [`*ngFor`](displaying-data.html#ngFor) tells Angular to stamp out one `<li>` per hero in the `heroes` list.
|
||||
* [`*ngIf`](displaying-data.html#ngIf) includes the `HeroDetail` component only if a selected hero exists.
|
||||
|
||||
block dart-bool
|
||||
//- N/A
|
||||
|
||||
:marked
|
||||
**Attribute** directives alter the appearance or behavior of an existing element.
|
||||
In templates they look like regular HTML attributes, hence the name.
|
||||
|
||||
The `ngModel` directive, which implements two-way data binding, is
|
||||
an example of an attribute directive. `ngModel` modifies the behavior of
|
||||
an existing element (typically an `<input>`)
|
||||
by setting its display value property and responding to change events.
|
||||
|
||||
+makeExcerpt('app/hero-detail.component.html', 'ngModel')
|
||||
:marked
|
||||
Angular ships with a small number of other directives that either alter the layout structure
|
||||
(for example, [ngSwitch](template-syntax.html#ngSwitch))
|
||||
or modify aspects of DOM elements and components
|
||||
(for example, [ngStyle](template-syntax.html#ngStyle) and [ngClass](template-syntax.html#ngClass)).
|
||||
|
||||
Of course, we can also write our own directives. Components such as
|
||||
`HeroListComponent` are one kind of custom directive.
|
||||
<!-- PENDING: link to where to learn more about other kinds! -->
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Services
|
||||
figure
|
||||
img(src="/resources/images/devguide/architecture/service.png" alt="Service" style="float:left; margin-left:-40px;margin-right:10px" )
|
||||
:marked
|
||||
_Service_ is a broad category encompassing any value, function, or feature that our application needs.
|
||||
|
||||
Almost anything can be a service.
|
||||
A service is typically a class with a narrow, well-defined purpose. It should do something specific and do it well.
|
||||
<br clear="all">
|
||||
:marked
|
||||
Examples include:
|
||||
* logging service
|
||||
* data service
|
||||
* message bus
|
||||
* tax calculator
|
||||
* application configuration
|
||||
|
||||
There is nothing specifically _Angular_ about services. Angular itself has no definition of a service.
|
||||
There is no service base class, and no place to register a service.
|
||||
|
||||
Yet services are fundamental to any Angular application. Our components are big consumers of services.
|
||||
|
||||
Here's an example of a service class that logs to the browser console
|
||||
|
||||
+makeExcerpt('app/logger.service.ts', 'class')
|
||||
|
||||
:marked
|
||||
Here's a `HeroService` that fetches heroes and returns them in a resolved !{_PromiseLinked}.
|
||||
The `HeroService` depends on the `Logger` service and another `BackendService` that handles the server communication grunt work.
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'class')
|
||||
|
||||
:marked
|
||||
Services are everywhere.
|
||||
|
||||
We prefer our component classes lean. Our components don't fetch data from the server,
|
||||
they don't validate user input, and they don't log directly to the console.
|
||||
They delegate such tasks to services.
|
||||
|
||||
A component's job is to enable the user experience and nothing more. It mediates between the view (rendered by the template)
|
||||
and the application logic (which often includes some notion of a _model_).
|
||||
A good component presents properties and methods for data binding.
|
||||
It delegates everything nontrivial to services.
|
||||
|
||||
Angular doesn't *enforce* these principles.
|
||||
It won't complain if we write a "kitchen sink" component with 3000 lines.
|
||||
|
||||
Angular does help us *follow* these principles by making it easy to factor our
|
||||
application logic into services and make those services available to components through *dependency injection*.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Dependency injection
|
||||
figure
|
||||
img(src="/resources/images/devguide/architecture/dependency-injection.png" alt="Service" style="float:left; width:200px; margin-left:-40px;margin-right:10px" )
|
||||
:marked
|
||||
_Dependency injection_ is a way to supply a new instance of a class
|
||||
with the fully-formed dependencies it requires. Most dependencies are services.
|
||||
Angular uses dependency injection to provide new components with the services they need.
|
||||
<br clear="all">
|
||||
:marked
|
||||
Angular can tell which services a component needs by looking at the types of its constructor parameters.
|
||||
For example, the constructor of our `HeroListComponent` needs a `HeroService`:
|
||||
|
||||
+makeExcerpt('app/hero-list.component.ts (constructor)', 'ctor')
|
||||
|
||||
:marked
|
||||
When Angular creates a component, it first asks an **injector** for
|
||||
the services that the component requires.
|
||||
|
||||
An injector maintains a container of service instances that it has previously created.
|
||||
If a requested service instance is not in the container, the injector makes one and adds it to the container
|
||||
before returning the service to Angular.
|
||||
When all requested services have been resolved and returned,
|
||||
Angular can call the component's constructor with those services as arguments.
|
||||
This is what we mean by *dependency injection*.
|
||||
|
||||
The process of `HeroService` injection looks a bit like this:
|
||||
figure
|
||||
img(src="/resources/images/devguide/architecture/injector-injects.png" alt="Service" )
|
||||
:marked
|
||||
If the injector doesn't have a `HeroService`, how does it know how to make one?
|
||||
|
||||
In brief, we must have previously registered a **provider** of the `HeroService` with the injector.
|
||||
A provider is something that can create or return a service, typically the service class itself.
|
||||
|
||||
We can register providers at any level of the application component tree.
|
||||
We often do so at the root when we bootstrap the application so that
|
||||
the same instance of a service is available everywhere.
|
||||
|
||||
+makeExcerpt('app/main.ts', 'bootstrap')
|
||||
|
||||
:marked
|
||||
Alternatively, we might register at a component level, in the providers property of the `@Component` metadata:
|
||||
|
||||
+makeExcerpt('app/hero-list.component.ts', 'providers')
|
||||
|
||||
:marked
|
||||
Registering at a component level means we get a new instance of the
|
||||
service with each new instance of that component.
|
||||
|
||||
<!-- We've vastly oversimplified dependency injection for this overview.
|
||||
The full story is in the [Dependency Injection](dependency-injection.html) chapter. -->
|
||||
|
||||
Points to remember about dependency injection:
|
||||
|
||||
* Dependency injection is wired into the Angular framework and used everywhere.
|
||||
|
||||
* The *injector* is the main mechanism.
|
||||
* An injector maintains a *container* of service instances that it created.
|
||||
* An injector can create a new service instance from a *provider*.
|
||||
|
||||
* A *provider* is a recipe for creating a service.
|
||||
|
||||
* We register *providers* with injectors.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Wrap up
|
||||
|
||||
We've learned just a bit about the eight main building blocks of an Angular application:
|
||||
|
||||
1. [Modules](#modules)
|
||||
1. [Components](#components)
|
||||
1. [Templates](#templates)
|
||||
1. [Metadata](#metadata)
|
||||
1. [Data binding](#data-binding)
|
||||
1. [Directives](#directives)
|
||||
1. [Services](#services)
|
||||
1. [Dependency injection](#dependency-injection)
|
||||
|
||||
That's a foundation for everything else in an Angular application,
|
||||
and it's more than enough to get going.
|
||||
But it doesn't include everything we'll need or want to know.
|
||||
|
||||
Here is a brief, alphabetical list of other important Angular features and services.
|
||||
Most of them are covered in this Developers Guide (or soon will be).
|
||||
|
||||
> [**Animations**](animations.html): The animation library makes it easy for developers to animate component behavior
|
||||
without deep knowledge of animation techniques or CSS.
|
||||
|
||||
> **Bootstrap**: A method to configure and launch the root application component.
|
||||
|
||||
> **Change detection**: Learn how Angular decides that a component property value has changed and
|
||||
when to update the screen.
|
||||
Learn how it uses **zones** to intercept asynchronous activity and run its change detection strategies.
|
||||
|
||||
> **Component router**: With the component Router service, users can navigate a multi-screen application
|
||||
in a familiar web browsing style using URLs.
|
||||
|
||||
> **Events**: The DOM raises events. So can components and services. Angular offers mechanisms for
|
||||
publishing and subscribing to events.
|
||||
|
||||
> [**Forms**](forms.html): Support complex data entry scenarios with HTML-based validation and dirty checking.
|
||||
|
||||
> [**HTTP**](server-communication.html): Communicate with a server to get data, save data, and invoke server-side actions with an HTTP client.
|
||||
|
||||
> [**Lifecycle hooks**](lifecycle-hooks.html): We can tap into key moments in the lifetime of a component, from its creation to its destruction,
|
||||
by implementing the lifecycle hook interfaces.
|
||||
|
||||
> [**Pipes**](pipes.html): Services that transform values for display.
|
||||
We can put pipes in our templates to improve the user experience. Consider
|
||||
this `currency` pipe expression:
|
||||
<div style="margin-left:40px">
|
||||
code-example().
|
||||
price | currency:'USD':true
|
||||
</div>
|
||||
:marked
|
||||
> It displays a price of "42.33" as `$42.33`.
|
||||
|
||||
> [**Router**](router.html): Navigate from page to page within the client
|
||||
application and never leave the browser.
|
||||
|
||||
> [**Testing**](testing.html): Angular provides a
|
||||
[testing library](https://pub.dartlang.org/packages/angular2_testing)
|
||||
to run unit tests on our application parts as they interact with the Angular framework.
|
|
@ -0,0 +1,372 @@
|
|||
block includes
|
||||
include ../_util-fns
|
||||
|
||||
:marked
|
||||
An **Attribute** directive changes the appearance or behavior of a DOM element.
|
||||
|
||||
:marked
|
||||
In this chapter we will
|
||||
* [write an attribute directive to change the background color](#write-directive)
|
||||
* [apply the attribute directive to an element in a template](#apply-directive)
|
||||
* [respond to user-initiated events](#respond-to-user)
|
||||
* [pass values into the directive using data binding](#bindings)
|
||||
|
||||
Try the <live-example></live-example>.
|
||||
|
||||
## Directives overview
|
||||
|
||||
There are three kinds of directives in Angular:
|
||||
1. Components
|
||||
1. Structural directives
|
||||
1. Attribute directives
|
||||
|
||||
A *Component* is really a directive with a template.
|
||||
It's the most common of the three directives and we tend to write lots of them as we build applications.
|
||||
|
||||
[*Structural* directives](structural-directives.html) can change the DOM layout by adding and removing DOM elements.
|
||||
[NgFor](template-syntax.html#ngFor) and [NgIf](template-syntax.html#ngIf) are two familiar examples.
|
||||
|
||||
An *Attribute* directive can change the appearance or behavior of an element.
|
||||
The built-in [NgStyle](template-syntax.html#ngStyle) directive, for example,
|
||||
can change several element styles at the same time.
|
||||
|
||||
We are going to write our own attribute directive to set an element's background color
|
||||
when the user hovers over that element.
|
||||
.l-sub-section
|
||||
:marked
|
||||
We don't need *any* directive to simply set the background color.
|
||||
We can set it with the special [Style Binding](template-syntax.html#style-binding) like this:
|
||||
|
||||
+makeExample('attribute-directives/ts/app/app.component.1.html','p-style-background')
|
||||
|
||||
:marked
|
||||
That wouldn't be nearly as much fun as creating our own directive.
|
||||
|
||||
Besides, we're not just *setting* the color; we'll be *changing* the color
|
||||
in response to a user action, a mouse hover.
|
||||
|
||||
.l-main-section
|
||||
a#write-directive
|
||||
:marked
|
||||
## Build a simple attribute directive
|
||||
An attribute directive minimally requires building a controller class annotated with
|
||||
`@Directive`, which specifies the selector identifying
|
||||
the attribute associated with the directive.
|
||||
The controller class implements the desired directive behavior.
|
||||
|
||||
Let's build a small illustrative example together.
|
||||
|
||||
:marked
|
||||
### Our first draft
|
||||
Create a new project folder (`attribute-directives`) and follow the steps in the [QuickStart](../quickstart.html).
|
||||
|
||||
include ../_quickstart_repo
|
||||
:marked
|
||||
Create the following source file in the indicated folder with the given code:
|
||||
+makeExample('app/highlight.directive.1.ts')
|
||||
|
||||
block highlight-directive-1
|
||||
:marked
|
||||
We begin by importing some symbols from the Angular `core`.
|
||||
We need the `Directive` symbol for the `@Directive` decorator.
|
||||
We need the `ElementRef` to [inject](dependency-injection.html) into the directive's constructor
|
||||
so we can access the DOM element.
|
||||
We don't need `Input` immediately but we will need it later in the chapter.
|
||||
|
||||
Then we define the directive metadata in a configuration object passed
|
||||
as an argument to the `@Directive` decorator function.
|
||||
:marked
|
||||
`@Directive` requires a CSS selector to identify
|
||||
the HTML in the template that is associated with our directive.
|
||||
The [CSS selector for an attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors)
|
||||
is the attribute name in square brackets.
|
||||
Our directive's selector is `[myHighlight]`.
|
||||
Angular will locate all elements in the template that have an attribute named `myHighlight`.
|
||||
.l-sub-section
|
||||
:marked
|
||||
### Why not call it "highlight"?
|
||||
*highlight* is a nicer name than *myHighlight* and, technically, it would work if we called it that.
|
||||
|
||||
However, we recommend picking a selector name with a prefix to ensure
|
||||
that it cannot conflict with any standard HTML attribute, now or in the future.
|
||||
There is also less risk of colliding with a third-party directive name when we give ours a prefix.
|
||||
|
||||
We do **not** prefix our `highlight` directive name with **`ng`**.
|
||||
That prefix belongs to Angular.
|
||||
|
||||
We need a prefix of our own, preferably short, and `my` will do for now.
|
||||
p
|
||||
| After the #[code @Directive] metadata comes the directive's controller class, which contains the logic for the directive.
|
||||
+ifDocsFor('ts')
|
||||
| We export `HighlightDirective` to make it accessible to other components.
|
||||
:marked
|
||||
Angular creates a new instance of the directive's controller class for
|
||||
each matching element, injecting an Angular `ElementRef`
|
||||
into the constructor.
|
||||
`ElementRef` is a service that grants us direct access to the DOM element
|
||||
through its `nativeElement` property.
|
||||
That's all we need to set the element's background color using the browser DOM API.
|
||||
|
||||
.l-main-section
|
||||
a#apply-directive
|
||||
:marked
|
||||
## Apply the attribute directive
|
||||
The `AppComponent` in this sample is a test harness for our `HighlightDirective`.
|
||||
Let's give it a new template that
|
||||
applies the directive as an attribute to a paragraph (`p`) element.
|
||||
In Angular terms, the `<p>` element will be the attribute **host**.
|
||||
p
|
||||
| We'll put the template in its own
|
||||
code #[+adjExPath('app.component.html')]
|
||||
| file that looks like this:
|
||||
+makeExample('attribute-directives/ts/app/app.component.1.html',null,'app/app.component.html')(format=".")
|
||||
:marked
|
||||
A separate template file is clearly overkill for a 2-line template.
|
||||
Hang in there; we're going to expand it later.
|
||||
Meanwhile, we'll revise the `AppComponent` to reference this template.
|
||||
+makeExample('attribute-directives/ts/app/app.component.ts',null,'app/app.component.ts')
|
||||
:marked
|
||||
We've added an `import` statement to fetch the 'Highlight' directive and,
|
||||
added that class to a `directives` component metadata so that Angular
|
||||
will recognize our directive when it encounters `myHighlight` in the template.
|
||||
|
||||
We run the app and see that our directive highlights the paragraph text.
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/attribute-directives/first-highlight.png" alt="First Highlight")
|
||||
.l-sub-section
|
||||
:marked
|
||||
### Your directive isn't working?
|
||||
|
||||
Did you remember to set the `directives` attribute of `@Component`? It is easy to forget!
|
||||
|
||||
Open the console in the browser tools and look for an error like this:
|
||||
code-example(format="nocode").
|
||||
EXCEPTION: Template parse errors:
|
||||
Can't bind to 'myHighlight' since it isn't a known native property
|
||||
:marked
|
||||
Angular detects that we're trying to bind to *something* but it doesn't know what.
|
||||
We have to tell it by listing `HighlightDirective` in the `directives` metadata array.
|
||||
:marked
|
||||
Let's recap what happened.
|
||||
|
||||
Angular found the `myHighlight` attribute on the `<p>` element. It created
|
||||
an instance of the `HighlightDirective` class,
|
||||
injecting a reference to the element into the constructor
|
||||
where we set the `<p>` element's background style to yellow.
|
||||
|
||||
.l-main-section
|
||||
a#respond-to-user
|
||||
:marked
|
||||
## Respond to user action
|
||||
|
||||
We are not satisfied to simply set an element color.
|
||||
Our directive should set the color in response to a user action.
|
||||
Specifically, we want to set the color when the user hovers over an element.
|
||||
|
||||
We'll need to
|
||||
1. detect when the user hovers into and out of the element,
|
||||
2. respond to those actions by setting and clearing the highlight color, respectively.
|
||||
|
||||
We apply the `@HostListener` !{_decorator} to methods which are called when an event is raised.
|
||||
|
||||
+makeExample('attribute-directives/ts/app/highlight.directive.2.ts','host')(format=".")
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
The `@HostListener` !{_decorator} refers to the DOM element that hosts our attribute directive, the `<p>` in our case.
|
||||
|
||||
We could have attached event listeners by manipulating the host DOM element directly, but
|
||||
there are at least three problems with such an approach:
|
||||
|
||||
1. We have to write the listeners correctly.
|
||||
1. We must *detach* our listener when the directive is destroyed to avoid memory leaks.
|
||||
1. We'd be talking to DOM API directly which, we learned, is something to avoid.
|
||||
|
||||
Let's roll with the `@HostListener` !{_decorator}.
|
||||
:marked
|
||||
Now we implement the two mouse event handlers:
|
||||
+makeExample('attribute-directives/ts/app/highlight.directive.2.ts','mouse-methods')(format=".")
|
||||
:marked
|
||||
Notice that they delegate to a helper method that sets the color via a private local variable, `#{_priv}el`.
|
||||
We revise the constructor to capture the `ElementRef.nativeElement` in this variable.
|
||||
|
||||
+makeExample('attribute-directives/ts/app/highlight.directive.2.ts','ctor')(format=".")
|
||||
:marked
|
||||
Here's the updated directive:
|
||||
+makeExample('app/highlight.directive.2.ts')
|
||||
:marked
|
||||
We run the app and confirm that the background color appears as we move the mouse over the `p` and
|
||||
disappears as we move out.
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/attribute-directives/highlight-directive-anim.gif" alt="Second Highlight")
|
||||
.l-main-section
|
||||
a#bindings
|
||||
:marked
|
||||
## Configure the directive with binding
|
||||
|
||||
Currently the highlight color is hard-coded within the directive. That's inflexible.
|
||||
We should set the color externally with a binding like this:
|
||||
+makeExample('attribute-directives/ts/app/app.component.html','pHost')
|
||||
:marked
|
||||
We'll extend our directive class with a bindable **input** `highlightColor` property and use it when we highlight text.
|
||||
|
||||
Here is the final version of the class:
|
||||
+makeExcerpt('app/highlight.directive.ts', 'class')
|
||||
a#input
|
||||
:marked
|
||||
The new `highlightColor` property is called an *input* property because data flows from the binding expression into our directive.
|
||||
Notice the `@Input()` #{_decorator} applied to the property.
|
||||
+makeExcerpt('app/highlight.directive.ts', 'color')
|
||||
:marked
|
||||
`@Input` adds metadata to the class that makes the `highlightColor` property available for
|
||||
property binding under the `myHighlight` alias.
|
||||
We must add this input metadata or Angular will reject the binding.
|
||||
See the [appendix](#why-input) below to learn why.
|
||||
.l-sub-section
|
||||
:marked
|
||||
### @Input(_alias_)
|
||||
The developer who uses this directive expects to bind to the attribute name, `myHighlight`.
|
||||
The directive property name is `highlightColor`. That's a disconnect.
|
||||
|
||||
We could resolve the discrepancy by renaming the property to `myHighlight` and define it as follows:
|
||||
|
||||
+makeExcerpt('app/highlight.directive.ts', 'highlight', '')
|
||||
:marked
|
||||
Maybe we don't want that property name inside the directive perhaps because it
|
||||
doesn't express our intention well.
|
||||
We can **alias** the `highlightColor` property with the attribute name by
|
||||
passing `myHighlight` into the `@Input` #{_decorator}:
|
||||
+makeExcerpt('app/highlight.directive.ts', 'color', '')
|
||||
:marked
|
||||
Now that we're getting the highlight color as an input, we modify the `onMouseEnter()` method to use
|
||||
it instead of the hard-coded color name.
|
||||
We also define red as the default color to fallback on in case
|
||||
the user neglects to bind with a color.
|
||||
+makeExcerpt('attribute-directives/ts/app/highlight.directive.ts', 'mouse-enter', '')
|
||||
:marked
|
||||
Now we'll update our `AppComponent` template to let
|
||||
users pick the highlight color and bind their choice to our directive.
|
||||
|
||||
Here is the updated template:
|
||||
+makeExcerpt('attribute-directives/ts/app/app.component.html', 'v2', '')
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
### Where is the templated *color* property?
|
||||
|
||||
The eagle-eyed may notice that the radio button click handlers in the template set a `color` property
|
||||
and we are binding that `color` to the directive.
|
||||
We should expect to find a `color` on the host `AppComponent`.
|
||||
|
||||
**We never defined a color property for the host *AppComponent***!
|
||||
And yet this code works. Where is the template `color` value going?
|
||||
|
||||
Browser debugging reveals that Angular dynamically added a `color` property
|
||||
to the runtime instance of the `AppComponent`.
|
||||
|
||||
This is *convenient* behavior but it is also *implicit* behavior that could be confusing.
|
||||
While it's cool that this technique works, we recommend adding the `color` property to the `AppComponent`.
|
||||
|
||||
:marked
|
||||
Here is our second version of the directive in action.
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/attribute-directives/highlight-directive-v2-anim.gif" alt="Highlight v.2")
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Bind to a second property
|
||||
Our directive only has a single, customizable property. What if we had ***two properties***?
|
||||
|
||||
Let's allow the template developer to set the default color, the color that prevails until the user picks a highlight color.
|
||||
We'll add a second **input** property to `HighlightDirective` called `defaultColor`:
|
||||
+makeExample('attribute-directives/ts/app/highlight.directive.ts', 'defaultColor')(format=".")
|
||||
:marked
|
||||
The `defaultColor` property has a setter that overrides the hard-coded default color, "red".
|
||||
We don't need a getter.
|
||||
|
||||
How do we bind to it? We already "burned" the `myHighlight` attribute name as a binding target.
|
||||
|
||||
Remember that a *component is a directive too*.
|
||||
We can add as many component property bindings as we need by stringing them along in the template
|
||||
as in this example that sets the `a`, `b`, `c` properties to the string literals 'a', 'b', and 'c'.
|
||||
code-example(format="." ).
|
||||
<my-component [a]="'a'" [b]="'b'" [c]="'c'"><my-component>
|
||||
:marked
|
||||
We do the same thing with an attribute directive.
|
||||
+makeExample('attribute-directives/ts/app/app.component.html', 'defaultColor')(format=".")
|
||||
:marked
|
||||
Here we're binding the user's color choice to the `myHighlight` attribute as we did before.
|
||||
We're *also* binding the literal string, 'violet', to the `defaultColor`.
|
||||
|
||||
Here is the final version of the directive in action.
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/attribute-directives/highlight-directive-final-anim.gif" alt="Final Highlight")
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Summary
|
||||
We now know how to
|
||||
- [build a simple **attribute directive** to attach behavior to an HTML element](#write-directive),
|
||||
- [use that directive in a template](#apply-directive),
|
||||
- [respond to **events** to change behavior based on an event](#respond-to-user),
|
||||
- and [use **binding** to pass values to the attribute directive](#bindings).
|
||||
|
||||
The final source:
|
||||
|
||||
+makeTabs(
|
||||
`attribute-directives/ts/app/app.component.ts,
|
||||
attribute-directives/ts/app/app.component.html,
|
||||
attribute-directives/ts/app/highlight.directive.ts,
|
||||
attribute-directives/ts/app/main.ts,
|
||||
attribute-directives/ts/index.html
|
||||
`,
|
||||
',,full',
|
||||
`app.component.ts,
|
||||
app.component.html,
|
||||
highlight.directive.ts,
|
||||
main.ts,
|
||||
index.html
|
||||
`)
|
||||
|
||||
|
||||
a#why-input
|
||||
.l-main-section
|
||||
:marked
|
||||
### Appendix: Input properties
|
||||
|
||||
Earlier we declared the `highlightColor` property to be an ***input*** property of our
|
||||
`HighlightDirective`
|
||||
|
||||
We've seen properties in bindings before.
|
||||
We never had to declare them as anything. Why now?
|
||||
|
||||
Angular makes a subtle but important distinction between binding **sources** and **targets**.
|
||||
|
||||
In all previous bindings, the directive or component property was a binding ***source***.
|
||||
A property is a *source* if it appears in the template expression to the ***right*** of the equals (=).
|
||||
|
||||
A property is a *target* when it appears in **square brackets** ([ ]) to the **left** of the equals (=) ...
|
||||
as it is does when we bind to the `myHighlight` property of the `HighlightDirective`,
|
||||
+makeExample('attribute-directives/ts/app/app.component.html','pHost')(format=".")
|
||||
:marked
|
||||
The 'color' in `[myHighlight]="color"` is a binding ***source***.
|
||||
A source property doesn't require a declaration.
|
||||
|
||||
The 'myHighlight' in `[myHighlight]="color"` *is* a binding ***target***.
|
||||
We must declare it as an *input* property.
|
||||
Angular rejects the binding with a clear error if we don't.
|
||||
|
||||
Angular treats a *target* property differently for a good reason.
|
||||
A component or directive in target position needs protection.
|
||||
|
||||
Imagine that our `HighlightDirective` did truly wonderous things.
|
||||
We graciously made a gift of it to the world.
|
||||
|
||||
To our surprise, some people — perhaps naively —
|
||||
started binding to *every* property of our directive.
|
||||
Not just the one or two properties we expected them to target. *Every* property.
|
||||
That could really mess up our directive in ways we didn't anticipate and have no desire to support.
|
||||
|
||||
The *input* declaration ensures that consumers of our directive can only bind to
|
||||
the properties of our public API ... nothing else.
|
|
@ -0,0 +1,319 @@
|
|||
block includes
|
||||
include ../_util-fns
|
||||
|
||||
:marked
|
||||
Angular 2 applications are styled with regular CSS. That means we can apply
|
||||
everything we know about CSS stylesheets, selectors, rules, and media queries
|
||||
to our Angular applications directly.
|
||||
|
||||
On top of this, Angular has the ability to bundle *component styles*
|
||||
with our components enabling a more modular design than regular stylesheets.
|
||||
|
||||
In this chapter we learn how to load and apply these *component styles*.
|
||||
|
||||
# Table Of Contents
|
||||
|
||||
* [Using Component Styles](#using-component-styles)
|
||||
* [Special selectors](#special-selectors)
|
||||
* [Loading Styles into Components](#loading-styles)
|
||||
* [Controlling View Encapsulation: Emulated, Native, and None](#view-encapsulation)
|
||||
* [Appendix 1: Inspecting the generated runtime component styles](#inspect-generated-css)
|
||||
* [Appendix 2: Loading Styles with Relative URLs](#relative-urls)
|
||||
|
||||
Run the <live-example></live-example> of the code shown in this chapter.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Using Component Styles
|
||||
|
||||
For every Angular 2 component we write, we may define not only an HTML template,
|
||||
but also the CSS styles that go with that template,
|
||||
specifying any selectors, rules, and media queries that we need.
|
||||
|
||||
One way to do this is to set the `styles` property in the component metadata.
|
||||
The `styles` property takes #{_an} #{_array} of strings that contain CSS code.
|
||||
Usually we give it one string as in this example:
|
||||
|
||||
+makeExample('component-styles/ts/app/hero-app.component.ts')(format='.')
|
||||
|
||||
:marked
|
||||
Component styles differ from traditional, global styles in a couple of ways.
|
||||
|
||||
Firstly, the selectors we put into a component's styles *only apply within the template
|
||||
of that component*. The `h1` selector in the example above only applies to the `<h1>` tag
|
||||
in the template of `HeroAppComponent`. Any `<h1>` elements elsewhere in
|
||||
the application are unaffected.
|
||||
|
||||
This is a big improvement in modularity compared to how CSS traditionally works:
|
||||
|
||||
1. We can use the CSS class names and selectors that make the most sense in the context of each component.
|
||||
|
||||
1. Class names and selectors are local to the component and won't collide with
|
||||
classes and selectors used elsewhere in the application.
|
||||
|
||||
1. Our component's styles *cannot* be changed by changes to styles elsewhere in the application.
|
||||
|
||||
1. We can co-locate the CSS code of each component with the TypeScript and HTML code of the component,
|
||||
which leads to a neat and tidy project structure.
|
||||
|
||||
1. We can change or remove component CSS code in the future without trawling through the
|
||||
whole application to see where else it may have been used. We just look at the component we're in.
|
||||
|
||||
a(id="special-selectors")
|
||||
.l-main-section
|
||||
:marked
|
||||
## Special selectors
|
||||
|
||||
Component styles have a few special *selectors* from the world of
|
||||
[shadow DOM style scoping](https://www.w3.org/TR/css-scoping-1):
|
||||
|
||||
### :host
|
||||
|
||||
Use the `:host` pseudo-class selector to target styles in the element that *hosts* the component (as opposed to
|
||||
targeting elements *inside* the component's template):
|
||||
|
||||
+makeExample('component-styles/ts/app/hero-details.component.css', 'host')(format='.')
|
||||
|
||||
:marked
|
||||
This is the *only* way we can target the host element. We cannot reach
|
||||
it from inside the component with other selectors, because it is not part of the
|
||||
component's own template. It is in a parent component's template.
|
||||
|
||||
Use the *function form* to apply host styles conditionally by
|
||||
including another selector inside parentheses after `:host`.
|
||||
|
||||
In the next example we target the host element again, but only when it also has the `active` CSS class.
|
||||
|
||||
+makeExample('component-styles/ts/app/hero-details.component.css', 'hostfunction')(format=".")
|
||||
|
||||
:marked
|
||||
### :host-context
|
||||
|
||||
Sometimes it is useful to apply styles based on some condition *outside* a component's view.
|
||||
For example, there may be a CSS theme class applied to the document `<body>` element, and
|
||||
we want to change how our component looks based on that.
|
||||
|
||||
Use the `:host-context()` pseudo-class selector. It works just like the function
|
||||
form of `:host()`. It looks for a CSS class in *any ancestor* of the component host element, all the way
|
||||
up to the document root. It's useful when combined with another selector.
|
||||
|
||||
In the following example, we apply a `background-color` style to all `<h2>` elements *inside* the component, only
|
||||
if some ancestor element has the CSS class `theme-light`.
|
||||
|
||||
+makeExample('component-styles/ts/app/hero-details.component.css', 'hostcontext')(format='.')
|
||||
|
||||
:marked
|
||||
### /deep/
|
||||
|
||||
Component styles normally apply only to the HTML in the component's own template.
|
||||
|
||||
We can use the `/deep/` selector to force a style down through the child component tree into all the child component views.
|
||||
The `/deep/` selector works to any depth of nested components, and it applies *both to the view
|
||||
children and the content children* of the component.
|
||||
|
||||
In this example, we target all `<h3>` elements, from the host element down
|
||||
through this component to all of its child elements in the DOM:
|
||||
+makeExample('component-styles/ts/app/hero-details.component.css', 'deep')(format=".")
|
||||
|
||||
:marked
|
||||
The `/deep/` selector also has the alias `>>>`. We can use either of the two interchangeably.
|
||||
|
||||
.alert.is-important
|
||||
:marked
|
||||
The `/deep/` and `>>>` selectors should only be used with **emulated** view encapsulation.
|
||||
This is the default and it is what we use most of the time. See the
|
||||
[Controlling View Encapsulation](#view-encapsulation)
|
||||
section for more details.
|
||||
|
||||
a(id='loading-styles')
|
||||
.l-main-section
|
||||
:marked
|
||||
## Loading Styles into Components
|
||||
|
||||
We have several ways to add styles to a component:
|
||||
* inline in the template HTML
|
||||
* by setting `styles` or `styleUrls` metadata
|
||||
* with CSS imports
|
||||
|
||||
The scoping rules outlined above apply to each of these loading patterns.
|
||||
|
||||
### Styles in Metadata
|
||||
|
||||
We can add a `styles` #{_array} property to the `@Component` #{_decorator}.
|
||||
Each string in the #{_array} (usually just one string) defines the CSS.
|
||||
|
||||
+makeExample('component-styles/ts/app/hero-app.component.ts')
|
||||
|
||||
:marked
|
||||
### Template Inline Styles
|
||||
|
||||
We can embed styles directly into the HTML template by putting them
|
||||
inside `<style>` tags.
|
||||
|
||||
+makeExample('component-styles/ts/app/hero-controls.component.ts', 'inlinestyles')
|
||||
|
||||
:marked
|
||||
### Style URLs in Metadata
|
||||
|
||||
We can load styles from external CSS files by adding a `styleUrls` attribute
|
||||
into a component's `@Component` #{_decorator}:
|
||||
|
||||
+makeExample('component-styles/ts/app/hero-details.component.ts', 'styleurls')
|
||||
|
||||
block style-url
|
||||
.alert.is-important
|
||||
:marked
|
||||
The URL is ***relative to the application root*** which is usually the
|
||||
location of the `index.html` web page that hosts the application.
|
||||
The style file URL is *not* relative to the component file.
|
||||
That's why the example URL begins `app/`.
|
||||
See [Appendix 2](#relative-urls) to specify a URL relative to the
|
||||
component file.
|
||||
|
||||
block module-bundlers
|
||||
.l-sub-section
|
||||
:marked
|
||||
Users of module bundlers like Webpack may also use the `styles` attribute
|
||||
to load styles from external files at build time. They could write:
|
||||
|
||||
`styles: [require('my.component.css')]`
|
||||
|
||||
We set the `styles` property, **not** `styleUrls` property! The module
|
||||
bundler is loading the CSS strings, not Angular.
|
||||
Angular only sees the CSS strings *after* the bundler loads them.
|
||||
To Angular it is as if we wrote the `styles` array by hand.
|
||||
Refer to the module bundler's documentation for information on
|
||||
loading CSS in this manner.
|
||||
|
||||
:marked
|
||||
### Template Link Tags
|
||||
|
||||
We can also embed `<link>` tags into the component's HTML template.
|
||||
|
||||
As with `styleUrls`, the link tag's `href` URL is relative to the
|
||||
application root, not relative to the component file.
|
||||
|
||||
+makeExample('component-styles/ts/app/hero-team.component.ts', 'stylelink')
|
||||
|
||||
:marked
|
||||
### CSS @imports
|
||||
|
||||
We can also import CSS files into our CSS files by using the standard CSS
|
||||
[`@import` rule](https://developer.mozilla.org/en/docs/Web/CSS/@import).
|
||||
|
||||
block css-import-url
|
||||
:marked
|
||||
In *this* case the URL is relative to the CSS file into which we are importing.
|
||||
|
||||
+makeExample('component-styles/ts/app/hero-details.component.css', 'import', 'app/hero-details.component.css (excerpt)')
|
||||
|
||||
a#view-encapsulation
|
||||
.l-main-section
|
||||
:marked
|
||||
## Controlling View Encapsulation: Native, Emulated, and None
|
||||
|
||||
As discussed above, component CSS styles are *encapsulated* into the component's own view and do
|
||||
not affect the rest of the application.
|
||||
|
||||
We can control how this encapsulation happens on a *per
|
||||
component* basis by setting the *view encapsulation mode* in the component metadata. There
|
||||
are three modes to choose from:
|
||||
|
||||
* `Native` view encapsulation uses the browser's native [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM)
|
||||
implementation to attach a Shadow DOM to the component's host element, and then puts the component
|
||||
view inside that Shadow DOM. The component's styles are included within the Shadow DOM.
|
||||
|
||||
* `Emulated` view encapsulation (**the default**) emulates the behavior of Shadow DOM by preprocessing
|
||||
(and renaming) the CSS code to effectively scope the CSS to the component's view.
|
||||
See [Appendix 1](#inspect-generated-css) for details.
|
||||
|
||||
* `None` means that Angular does no view encapsulation.
|
||||
Angular adds the CSS to the global styles.
|
||||
The scoping rules, isolations, and protections discussed earlier do not apply.
|
||||
This is essentially the same as pasting the component's styles into the HTML.
|
||||
|
||||
Set the components encapsulation mode using the `encapsulation` property in the component metadata:
|
||||
|
||||
+makeExample('component-styles/ts/app/quest-summary.component.ts', 'encapsulation.native')(format='.')
|
||||
|
||||
:marked
|
||||
`Native` view encapsulation only works on [browsers that have native support
|
||||
for Shadow DOM](http://caniuse.com/#feat=shadowdom). The support is still limited,
|
||||
which is why `Emulated` view encapsulation is the default mode and recommended
|
||||
in most cases.
|
||||
|
||||
a#inspect-generated-css
|
||||
.l-main-section
|
||||
:marked
|
||||
## Appendix 1: Inspecting The CSS Generated in Emulated View Encapsulation
|
||||
|
||||
When using the default emulated view encapsulation, Angular preprocesses
|
||||
all component styles so that they approximate the standard Shadow CSS scoping rules.
|
||||
|
||||
When we inspect the DOM of a running Angular application with emulated view
|
||||
encapsulation enabled, we see that each DOM element has some extra attributes
|
||||
attached to it:
|
||||
|
||||
code-example(format="").
|
||||
<hero-details _nghost-pmm-5>
|
||||
<h2 _ngcontent-pmm-5>Mister Fantastic</h2>
|
||||
<hero-team _ngcontent-pmm-5 _nghost-pmm-6>
|
||||
<h3 _ngcontent-pmm-6>Team</h3>
|
||||
</hero-team>
|
||||
</hero-detail>
|
||||
|
||||
:marked
|
||||
We see two kinds of generated attributes:
|
||||
* An element that would be a Shadow DOM host in native encapsulation has a
|
||||
generated `_nghost` attribute. This is typically the case for component host elements.
|
||||
|
||||
* An element within a component's view has a `_ngcontent` attribute
|
||||
that identifies to which host's emulated Shadow DOM this element belongs.
|
||||
|
||||
The exact values of these attributes are not important. They are automatically
|
||||
generated and we never refer to them in application code. But they are targeted
|
||||
by the generated component styles, which we'll find in the `<head>` section of the DOM:
|
||||
|
||||
code-example(format="").
|
||||
[_nghost-pmm-5] {
|
||||
display: block;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
h3[_ngcontent-pmm-6] {
|
||||
background-color: white;
|
||||
border: 1px solid #777;
|
||||
}
|
||||
|
||||
:marked
|
||||
These are the styles we wrote, post-processed so that each selector is augmented
|
||||
with `_nghost` or `_ngcontent` attribute selectors.
|
||||
These extra selectors enable the scoping rules described in this guide.
|
||||
|
||||
We'll likely live with *emulated* mode until shadow DOM gains traction.
|
||||
|
||||
a#relative-urls
|
||||
.l-main-section
|
||||
:marked
|
||||
## Appendix 2: Loading Styles with Relative URLs
|
||||
|
||||
It's common practice to split a component's code, HTML, and CSS into three separate files in the same directory:
|
||||
|
||||
code-example(format='').
|
||||
quest-summary.component.ts
|
||||
quest-summary.component.html
|
||||
quest-summary.component.css
|
||||
|
||||
:marked
|
||||
We include the template and CSS files by setting the `templateUrl` and `styleUrls` metadata properties respectively.
|
||||
Because these files are co-located with the component,
|
||||
it would be nice to refer to them by name without also having to specify a path back to the root of the application.
|
||||
|
||||
block module-id
|
||||
:marked
|
||||
We can change the way Angular calculates the full URL be setting the component metadata's `moduleId` property to `module.id`.
|
||||
|
||||
+makeExample('component-styles/ts/app/quest-summary.component.ts','', 'app/quest-summary.component.ts')
|
||||
:marked
|
||||
Learn more about `moduleId` in the [Component-Relative Paths](../cookbook/component-relative-paths.html) chapter.
|
||||
|
|
@ -0,0 +1,897 @@
|
|||
block includes
|
||||
include ../_util-fns
|
||||
- var _thisDot = 'this.';
|
||||
|
||||
:marked
|
||||
**Dependency injection** is an important application design pattern.
|
||||
Angular has its own dependency injection framework, and
|
||||
we really can't build an Angular application without it.
|
||||
It's used so widely that almost everyone just calls it _DI_.
|
||||
|
||||
In this chapter we'll learn what DI is and why we want it.
|
||||
Then we'll learn [how to use it](#angular-di) in an Angular app.
|
||||
|
||||
- [Why dependency injection?](#why-dependency-injection)
|
||||
- [Angular dependency injection](#angular-dependency-injection)
|
||||
- [Injector providers](#injector-providers)
|
||||
- [Dependency injection tokens](#dependency-injection-tokens)
|
||||
- [Summary](#summary)
|
||||
|
||||
Run the <live-example></live-example>.
|
||||
|
||||
.l-main-section#why-di
|
||||
:marked
|
||||
## Why dependency injection?
|
||||
|
||||
Let's start with the following code.
|
||||
|
||||
+makeExample('dependency-injection/ts/app/car/car-no-di.ts', 'car', 'app/car/car.ts (without DI)')
|
||||
|
||||
:marked
|
||||
Our `Car` creates everything it needs inside its constructor.
|
||||
What's the problem?
|
||||
The problem is that our `Car` class is brittle, inflexible, and hard to test.
|
||||
|
||||
Our `Car` needs an engine and tires. Instead of asking for them,
|
||||
the `Car` constructor instantiates its own copies from
|
||||
the very specific classes `Engine` and `Tires`.
|
||||
|
||||
What if the `Engine` class evolves and its constructor requires a parameter?
|
||||
Our `Car` is broken and stays broken until we rewrite it along the lines of
|
||||
`#{_thisDot}engine = new Engine(theNewParameter)`.
|
||||
We didn't care about `Engine` constructor parameters when we first wrote `Car`.
|
||||
We don't really care about them now.
|
||||
But we'll *have* to start caring because
|
||||
when the definition of `Engine` changes, our `Car` class must change.
|
||||
That makes `Car` brittle.
|
||||
|
||||
What if we want to put a different brand of tires on our `Car`? Too bad.
|
||||
We're locked into whatever brand the `Tires` class creates. That makes our `Car` inflexible.
|
||||
|
||||
Right now each new car gets its own engine. It can't share an engine with other cars.
|
||||
While that makes sense for an automobile engine,
|
||||
we can think of other dependencies that should be shared, such as the onboard
|
||||
wireless connection to the manufacturer's service center. Our `Car` lacks the flexibility
|
||||
to share services that have been created previously for other consumers.
|
||||
|
||||
When we write tests for our `Car` we're at the mercy of its hidden dependencies.
|
||||
Is it even possible to create a new `Engine` in a test environment?
|
||||
What does `Engine`itself depend upon? What does that dependency depend on?
|
||||
Will a new instance of `Engine` make an asynchronous call to the server?
|
||||
We certainly don't want that going on during our tests.
|
||||
|
||||
What if our `Car` should flash a warning signal when tire pressure is low?
|
||||
How do we confirm that it actually does flash a warning
|
||||
if we can't swap in low-pressure tires during the test?
|
||||
|
||||
We have no control over the car's hidden dependencies.
|
||||
When we can't control the dependencies, a class becomes difficult to test.
|
||||
|
||||
How can we make `Car` more robust, flexible, and testable?
|
||||
|
||||
<a id="ctor-injection"></a>
|
||||
That's super easy. We change our `Car` constructor to a version with DI:
|
||||
|
||||
+makeTabs(
|
||||
'dependency-injection/ts/app/car/car.ts, dependency-injection/ts/app/car/car-no-di.ts',
|
||||
'car-ctor, car-ctor',
|
||||
'app/car/car.ts (excerpt with DI), app/car/car.ts (excerpt without DI)')(format=".")
|
||||
|
||||
:marked
|
||||
See what happened? We moved the definition of the dependencies to the constructor.
|
||||
Our `Car` class no longer creates an engine or tires.
|
||||
It just consumes them.
|
||||
|
||||
block ctor-syntax
|
||||
.l-sub-section
|
||||
:marked
|
||||
We also leveraged TypeScript's constructor syntax for declaring
|
||||
parameters and properties simultaneously.
|
||||
|
||||
:marked
|
||||
Now we create a car by passing the engine and tires to the constructor.
|
||||
|
||||
+makeExample('dependency-injection/ts/app/car/car-creations.ts', 'car-ctor-instantiation', '')(format=".")
|
||||
|
||||
:marked
|
||||
How cool is that?
|
||||
The definition of the engine and tire dependencies are
|
||||
decoupled from the `Car` class itself.
|
||||
We can pass in any kind of engine or tires we like, as long as they
|
||||
conform to the general API requirements of an engine or tires.
|
||||
|
||||
If someone extends the `Engine` class, that is not `Car`'s problem.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
The _consumer_ of `Car` has the problem. The consumer must update the car creation code to
|
||||
something like this:
|
||||
|
||||
- var stylePattern = { otl: /(new Car.*$)/gm };
|
||||
+makeExample('dependency-injection/ts/app/car/car-creations.ts', 'car-ctor-instantiation-with-param', '', stylePattern)(format=".")
|
||||
|
||||
:marked
|
||||
The critical point is this: `Car` itself did not have to change.
|
||||
We'll take care of the consumer's problem soon enough.
|
||||
|
||||
:marked
|
||||
The `Car` class is much easier to test because we are in complete control
|
||||
of its dependencies.
|
||||
We can pass mocks to the constructor that do exactly what we want them to do
|
||||
during each test:
|
||||
|
||||
- var stylePattern = { otl: /(new Car.*$)/gm };
|
||||
+makeExample('dependency-injection/ts/app/car/car-creations.ts', 'car-ctor-instantiation-with-mocks', '', stylePattern)(format=".")
|
||||
|
||||
:marked
|
||||
**We just learned what dependency injection is**.
|
||||
|
||||
It's a coding pattern in which a class receives its dependencies from external
|
||||
sources rather than creating them itself.
|
||||
|
||||
Cool! But what about that poor consumer?
|
||||
Anyone who wants a `Car` must now
|
||||
create all three parts: the `Car`, `Engine`, and `Tires`.
|
||||
The `Car` class shed its problems at the consumer's expense.
|
||||
We need something that takes care of assembling these parts for us.
|
||||
|
||||
We could write a giant class to do that:
|
||||
|
||||
+makeExample('dependency-injection/ts/app/car/car-factory.ts', null, 'app/car/car-factory.ts')
|
||||
|
||||
:marked
|
||||
It's not so bad now with only three creation methods.
|
||||
But maintaining it will be hairy as the application grows.
|
||||
This factory is going to become a huge spiderweb of
|
||||
interdependent factory methods!
|
||||
|
||||
Wouldn't it be nice if we could simply list the things we want to build without
|
||||
having to define which dependency gets injected into what?
|
||||
|
||||
This is where the dependency injection framework comes into play.
|
||||
Imagine the framework had something called an _injector_.
|
||||
We register some classes with this injector, and it figures out how to create them.
|
||||
|
||||
When we need a `Car`, we simply ask the injector to get it for us and we're good to go.
|
||||
|
||||
+makeExample('dependency-injection/ts/app/car/car-injector.ts','injector-call')(format=".")
|
||||
|
||||
:marked
|
||||
Everyone wins. The `Car` knows nothing about creating an `Engine` or `Tires`.
|
||||
The consumer knows nothing about creating a `Car`.
|
||||
We don't have a gigantic factory class to maintain.
|
||||
Both `Car` and consumer simply ask for what they need and the injector delivers.
|
||||
|
||||
This is what a **dependency injection framework** is all about.
|
||||
|
||||
Now that we know what dependency injection is and appreciate its benefits,
|
||||
let's see how it is implemented in Angular.
|
||||
|
||||
.l-main-section#angular-di
|
||||
:marked
|
||||
## Angular dependency injection
|
||||
|
||||
Angular ships with its own dependency injection framework. This framework can also be used
|
||||
as a standalone module by other applications and frameworks.
|
||||
|
||||
That sounds nice. What does it do for us when building components in Angular?
|
||||
Let's see, one step at a time.
|
||||
|
||||
We'll begin with a simplified version of the `HeroesComponent`
|
||||
that we built in the [The Tour of Heroes](../tutorial/).
|
||||
|
||||
+makeTabs(
|
||||
`dependency-injection/ts/app/heroes/heroes.component.1.ts,
|
||||
dependency-injection/ts/app/heroes/hero-list.component.1.ts,
|
||||
dependency-injection/ts/app/heroes/hero.ts,
|
||||
dependency-injection/ts/app/heroes/mock-heroes.ts`,
|
||||
'v1,,,',
|
||||
`app/heroes/heroes.component.ts,
|
||||
app/heroes/hero-list.component.ts,
|
||||
app/heroes/hero.ts,
|
||||
app/heroes/mock-heroes.ts`)
|
||||
|
||||
:marked
|
||||
The `HeroesComponent` is the root component of the *Heroes* feature area.
|
||||
It governs all the child components of this area.
|
||||
Our stripped down version has only one child, `HeroListComponent`,
|
||||
which displays a list of heroes.
|
||||
|
||||
:marked
|
||||
Right now `HeroListComponent` gets heroes from `HEROES`, an in-memory collection
|
||||
defined in another file.
|
||||
That may suffice in the early stages of development, but it's far from ideal.
|
||||
As soon as we try to test this component or want to get our heroes data from a remote server,
|
||||
we'll have to change the implementation of `heroes` and
|
||||
fix every other use of the `HEROES` mock data.
|
||||
|
||||
Let's make a service that hides how we get hero data.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Given that the service is a
|
||||
[separate concern](https://en.wikipedia.org/wiki/Separation_of_concerns),
|
||||
we suggest that you
|
||||
write the service code in its own file.
|
||||
+ifDocsFor('ts')
|
||||
:marked
|
||||
See [this note](#one-class-per-file) for details.
|
||||
|
||||
+makeExample('dependency-injection/ts/app/heroes/hero.service.1.ts',null, 'app/heroes/hero.service.ts' )
|
||||
|
||||
:marked
|
||||
Our `HeroService` exposes a `getHeroes` method that returns
|
||||
the same mock data as before, but none of its consumers need to know that.
|
||||
.l-sub-section
|
||||
:marked
|
||||
Notice the `@Injectable()` #{_decorator} above the service class.
|
||||
We'll discuss its purpose [shortly](#injectable).
|
||||
|
||||
- var _perhaps = _docsFor == 'dart' ? '' : 'perhaps';
|
||||
.l-sub-section
|
||||
:marked
|
||||
We aren't even pretending this is a real service.
|
||||
If we were actually getting data from a remote server, the API would have to be
|
||||
asynchronous, #{_perhaps} returning a !{_PromiseLinked}.
|
||||
We'd also have to rewrite the way components consume our service.
|
||||
This is important in general, but not to our current story.
|
||||
|
||||
:marked
|
||||
A service is nothing more than a class in Angular 2.
|
||||
It remains nothing more than a class until we register it with an Angular injector.
|
||||
|
||||
#bootstrap
|
||||
:marked
|
||||
### Configuring the injector
|
||||
|
||||
We don't have to create an Angular injector.
|
||||
Angular creates an application-wide injector for us during the bootstrap process.
|
||||
|
||||
+makeExample('dependency-injection/ts/app/main.ts', 'bootstrap', 'app/main.ts (excerpt)')(format='.')
|
||||
|
||||
:marked
|
||||
We do have to configure the injector by registering the **providers**
|
||||
that create the services our application requires.
|
||||
We'll explain what [providers](#providers) are later in this chapter.
|
||||
Before we do, let's see an example of provider registration during bootstrapping:
|
||||
|
||||
+makeExample('dependency-injection/ts/app/main.1.ts', 'bootstrap-discouraged')(format='.')
|
||||
|
||||
:marked
|
||||
The injector now knows about our `HeroService`.
|
||||
An instance of our `HeroService` will be available for injection across our entire application.
|
||||
|
||||
Of course we can't help wondering about that comment telling us not to do it this way.
|
||||
It *will* work. It's just not a best practice.
|
||||
The bootstrap provider option is intended for configuring and overriding Angular's own
|
||||
preregistered services, such as its routing support.
|
||||
|
||||
The preferred approach is to register application providers in application components.
|
||||
Because the `HeroService` is used within the *Heroes* feature area —
|
||||
and nowhere else — the ideal place to register it is in the top-level `HeroesComponent`.
|
||||
|
||||
:marked
|
||||
### Registering providers in a component
|
||||
Here's a revised `HeroesComponent` that registers the `HeroService`.
|
||||
|
||||
- var stylePattern = { otl: /(providers:.*),/ };
|
||||
+makeExample('dependency-injection/ts/app/heroes/heroes.component.1.ts', 'full','app/heroes/heroes.component.ts', stylePattern)(format='.')
|
||||
|
||||
:marked
|
||||
Look closely at the `providers` part of the `@Component` metadata.
|
||||
An instance of the `HeroService` is now available for injection in this `HeroesComponent`
|
||||
and all of its child components.
|
||||
|
||||
The `HeroesComponent` itself doesn't happen to need the `HeroService`.
|
||||
But its child `HeroListComponent` does, so we head there next.
|
||||
|
||||
:marked
|
||||
### Preparing the HeroListComponent for injection
|
||||
|
||||
The `HeroListComponent` should get heroes from the injected `HeroService`.
|
||||
Per the dependency injection pattern, the component must ask for the service in its
|
||||
constructor, [as we explained earlier](#ctor-injection).
|
||||
It's a small change:
|
||||
|
||||
+makeTabs(
|
||||
`dependency-injection/ts/app/heroes/hero-list.component.2.ts,
|
||||
dependency-injection/ts/app/heroes/hero-list.component.1.ts`,
|
||||
null,
|
||||
`app/heroes/hero-list.component (with DI),
|
||||
app/heroes/hero-list.component (without DI)`)
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
#### Focus on the constructor
|
||||
|
||||
Adding a parameter to the constructor isn't all that's happening here.
|
||||
|
||||
+makeExample('dependency-injection/ts/app/heroes/hero-list.component.2.ts', 'ctor')(format=".")
|
||||
|
||||
:marked
|
||||
Note that the constructor parameter has the type `HeroService`, and that
|
||||
the `HeroListComponent` class has an `@Component` #{_decorator}
|
||||
(scroll up to confirm that fact).
|
||||
Also recall that the parent component (`HeroesComponent`)
|
||||
has `providers` information for `HeroService`.
|
||||
|
||||
The constructor parameter type, the `@Component` #{_decorator},
|
||||
and the parent's `providers` information combine to tell the
|
||||
Angular injector to inject an instance of
|
||||
`HeroService` whenever it creates a new `HeroListComponent`.
|
||||
|
||||
#di-metadata
|
||||
:marked
|
||||
### Implicit injector creation
|
||||
|
||||
When we introduced the idea of an injector above, we showed how to
|
||||
use it to create a new `Car`. Here we also show how such an injector
|
||||
would be explicitly created:
|
||||
|
||||
+makeExample('dependency-injection/ts/app/car/car-injector.ts','injector-create-and-call')(format=".")
|
||||
|
||||
:marked
|
||||
We won't find code like that in the Tour of Heroes or any of our other samples.
|
||||
We *could* write code that [explicitly creates an injector](#explicit-injector) if we *had* to, but we rarely do.
|
||||
Angular takes care of creating and calling injectors
|
||||
when it creates components for us — whether through HTML markup, as in `<hero-list></hero-list>`,
|
||||
or after navigating to a component with the [router](./router.html).
|
||||
If we let Angular do its job, we'll enjoy the benefits of automated dependency injection.
|
||||
|
||||
:marked
|
||||
### Singleton services
|
||||
|
||||
Dependencies are singletons within the scope of an injector.
|
||||
In our example, a single `HeroService` instance is shared among the
|
||||
`HeroesComponent` and its `HeroListComponent` children.
|
||||
|
||||
However, Angular DI is an hierarchical injection
|
||||
system, which means that nested injectors can create their own service instances.
|
||||
Learn more about that in the [Hierarchical Injectors](./hierarchical-dependency-injection.html) chapter.
|
||||
|
||||
:marked
|
||||
### Testing the component
|
||||
|
||||
We emphasized earlier that designing a class for dependency injection makes the class easier to test.
|
||||
Listing dependencies as constructor parameters may be all we need to test application parts effectively.
|
||||
|
||||
For example, we can create a new `HeroListComponent` with a mock service that we can manipulate
|
||||
under test:
|
||||
|
||||
+makeExample('dependency-injection/ts/app/test.component.ts', 'spec')(format='.')
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Learn more in [Testing](../testing/index.html).
|
||||
|
||||
:marked
|
||||
### When the service needs a service
|
||||
|
||||
Our `HeroService` is very simple. It doesn't have any dependencies of its own.
|
||||
|
||||
|
||||
What if it had a dependency? What if it reported its activities through a logging service?
|
||||
We'd apply the same *constructor injection* pattern,
|
||||
adding a constructor that takes a `Logger` parameter.
|
||||
|
||||
Here is the revision compared to the original.
|
||||
|
||||
+makeTabs(
|
||||
`dependency-injection/ts/app/heroes/hero.service.2.ts,
|
||||
dependency-injection/ts/app/heroes/hero.service.1.ts`,
|
||||
null,
|
||||
`app/heroes/hero.service (v2),
|
||||
app/heroes/hero.service (v1)`)
|
||||
|
||||
:marked
|
||||
The constructor now asks for an injected instance of a `Logger` and stores it in a private property called `#{_priv}logger`.
|
||||
We call that property within our `getHeroes` method when anyone asks for heroes.
|
||||
|
||||
//- FIXME refer to Dart API when that page becomes available.
|
||||
- var injMetaUrl = 'https://angular.io/docs/ts/latest/api/core/index/InjectableMetadata-class.html';
|
||||
h3#injectable Why @Injectable()?
|
||||
:marked
|
||||
**<a href="#{injMetaUrl}">@Injectable()</a>** marks a class as available to an
|
||||
injector for instantiation. Generally speaking, an injector will report an
|
||||
error when trying to instantiate a class that is not marked as
|
||||
`@Injectable()`.
|
||||
|
||||
block injectable-not-always-needed-in-ts
|
||||
.l-sub-section
|
||||
:marked
|
||||
As it happens, we could have omitted `@Injectable()` from our first
|
||||
version of `HeroService` because it had no injected parameters.
|
||||
But we must have it now that our service has an injected dependency.
|
||||
We need it because Angular requires constructor parameter metadata
|
||||
in order to inject a `Logger`.
|
||||
|
||||
.callout.is-helpful
|
||||
header Suggestion: add @Injectable() to every service class
|
||||
:marked
|
||||
We recommend adding `@Injectable()` to every service class, even those that don't have dependencies
|
||||
and, therefore, do not technically require it. Here's why:
|
||||
|
||||
ul(style="font-size:inherit")
|
||||
li <b>Future proofing:</b> No need to remember <code>@Injectable()</code> when we add a dependency later.
|
||||
li <b>Consistency:</b> All services follow the same rules, and we don't have to wonder why #{_a} #{_decorator} is missing.
|
||||
|
||||
:marked
|
||||
Injectors are also responsible for instantiating components
|
||||
like `HeroesComponent`. Why haven't we marked `HeroesComponent` as
|
||||
`@Injectable()`?
|
||||
|
||||
We *can* add it if we really want to. It isn't necessary because the
|
||||
`HeroesComponent` is already marked with `@Component`, and this
|
||||
!{_decorator} class (like `@Directive` and `@Pipe`, which we'll learn about later)
|
||||
is a subtype of <a href="#{injMetaUrl}">InjectableMetadata</a>. It is in
|
||||
fact `InjectableMetadata` #{_decorator}s that
|
||||
identify a class as a target for instantiation by an injector.
|
||||
|
||||
+ifDocsFor('ts')
|
||||
.l-sub-section
|
||||
:marked
|
||||
At runtime, injectors can read class metadata in the transpiled JavaScript code
|
||||
and use the constructor parameter type information
|
||||
to determine what things to inject.
|
||||
|
||||
Not every JavaScript class has metadata.
|
||||
The TypeScript compiler discards metadata by default.
|
||||
If the `emitDecoratorMetadata` compiler option is true
|
||||
(as it should be in the `tsconfig.json`),
|
||||
the compiler adds the metadata to the generated JavaScript
|
||||
for _every class with at least one decorator_.
|
||||
|
||||
While any decorator will trigger this effect, mark the service class with the
|
||||
<a href="#{injMetaUrl}">InjectableMetadata</a> #{_decorator}
|
||||
to make the intent clear.
|
||||
|
||||
.callout.is-critical
|
||||
header Always include the parentheses
|
||||
block always-include-paren
|
||||
:marked
|
||||
Always write `@Injectable()`, not just `@Injectable`.
|
||||
Our application will fail mysteriously if we forget the parentheses.
|
||||
|
||||
.l-main-section#logger-service
|
||||
:marked
|
||||
## Creating and registering a logger service
|
||||
|
||||
We're injecting a logger into our `HeroService` in two steps:
|
||||
1. Create the logger service.
|
||||
1. Register it with the application.
|
||||
|
||||
Our logger service is quite simple:
|
||||
|
||||
+makeExample('dependency-injection/ts/app/logger.service.ts', null, 'app/logger.service.ts')
|
||||
|
||||
block real-logger
|
||||
//- N/A
|
||||
|
||||
:marked
|
||||
We're likely to need the same logger service everywhere in our application,
|
||||
so we put it in the project's `#{_appDir}` folder, and
|
||||
we register it in the `providers` #{_array} of the metadata for our application root component, `AppComponent`.
|
||||
|
||||
+makeExcerpt('app/providers.component.ts','providers-logger','app/app.component.ts (excerpt)')
|
||||
|
||||
:marked
|
||||
If we forget to register the logger, Angular throws an exception when it first looks for the logger:
|
||||
code-example(format="nocode").
|
||||
EXCEPTION: No provider for Logger! (HeroListComponent -> HeroService -> Logger)
|
||||
|
||||
:marked
|
||||
That's Angular telling us that the dependency injector couldn't find the *provider* for the logger.
|
||||
It needed that provider to create a `Logger` to inject into a new
|
||||
`HeroService`, which it needed to
|
||||
create and inject into a new `HeroListComponent`.
|
||||
|
||||
The chain of creations started with the `Logger` provider. *Providers* are the subject of our next section.
|
||||
|
||||
.l-main-section#providers
|
||||
:marked
|
||||
## Injector providers
|
||||
|
||||
A provider *provides* the concrete, runtime version of a dependency value.
|
||||
The injector relies on **providers** to create instances of the services
|
||||
that the injector injects into components and other services.
|
||||
|
||||
We must register a service *provider* with the injector, or it won't know how to create the service.
|
||||
|
||||
Earlier we registered the `Logger` service in the `providers` #{_array} of the metadata for the `AppComponent` like this:
|
||||
|
||||
+makeExample('dependency-injection/ts/app/providers.component.ts','providers-logger')
|
||||
|
||||
- var implements = _docsFor == 'dart' ? 'implements' : 'looks and behaves like a '
|
||||
- var objectlike = _docsFor == 'dart' ? '' : 'an object that behaves like '
|
||||
- var loggerlike = _docsFor == 'dart' ? '' : 'We could provide a logger-like object. '
|
||||
:marked
|
||||
There are many ways to *provide* something that #{implements} `Logger`.
|
||||
The `Logger` class itself is an obvious and natural provider.
|
||||
But it's not the only way.
|
||||
|
||||
We can configure the injector with alternative providers that can deliver #{objectlike} a `Logger`.
|
||||
We could provide a substitute class. #{loggerlike}
|
||||
We could give it a provider that calls a logger factory function.
|
||||
Any of these approaches might be a good choice under the right circumstances.
|
||||
|
||||
What matters is that the injector has a provider to go to when it needs a `Logger`.
|
||||
|
||||
//- Dart limitation: the provide function isn't const so it cannot be used in an annotation.
|
||||
- var _andProvideFn = _docsFor == 'dart' ? '' : 'and <i>provide</i> object literal';
|
||||
#provide
|
||||
:marked
|
||||
### The *Provider* class !{_andProvideFn}
|
||||
|
||||
:marked
|
||||
We wrote the `providers` #{_array} like this:
|
||||
|
||||
+makeExample('dependency-injection/ts/app/providers.component.ts','providers-1')
|
||||
|
||||
:marked
|
||||
This is actually a short-hand expression for a provider registration
|
||||
<span if-docs="ts">
|
||||
using a _provider_ object literal with two properties:
|
||||
</span>
|
||||
<span if-docs="dart">
|
||||
that creates a new instance of the
|
||||
[Provider](../api/core/index/Provider-class.html) class:
|
||||
</span>
|
||||
|
||||
+makeExample('dependency-injection/ts/app/providers.component.ts','providers-3')
|
||||
|
||||
block provider-ctor-args
|
||||
- var _secondParam = 'provider definition object';
|
||||
|
||||
:marked
|
||||
The first is the [token](#token) that serves as the key for both locating a dependency value
|
||||
and registering the provider.
|
||||
|
||||
The second is a !{_secondParam},
|
||||
which we can think of as a *recipe* for creating the dependency value.
|
||||
There are many ways to create dependency values ... and many ways to write a recipe.
|
||||
|
||||
#class-provider
|
||||
:marked
|
||||
### Alternative class providers
|
||||
|
||||
Occasionally we'll ask a different class to provide the service.
|
||||
The following code tells the injector
|
||||
to return a `BetterLogger` when something asks for the `Logger`.
|
||||
|
||||
+makeExample('dependency-injection/ts/app/providers.component.ts','providers-4')
|
||||
|
||||
block dart-diff-const-metadata
|
||||
//- N/A
|
||||
|
||||
:marked
|
||||
### Class provider with dependencies
|
||||
Maybe an `EvenBetterLogger` could display the user name in the log message.
|
||||
This logger gets the user from the injected `UserService`,
|
||||
which happens also to be injected at the application level.
|
||||
|
||||
+makeExample('dependency-injection/ts/app/providers.component.ts','EvenBetterLogger')(format='.')
|
||||
|
||||
:marked
|
||||
Configure it like we did `BetterLogger`.
|
||||
|
||||
+makeExample('dependency-injection/ts/app/providers.component.ts','providers-5')(format=".")
|
||||
|
||||
:marked
|
||||
### Aliased class providers
|
||||
|
||||
Suppose an old component depends upon an `OldLogger` class.
|
||||
`OldLogger` has the same interface as the `NewLogger`, but for some reason
|
||||
we can't update the old component to use it.
|
||||
|
||||
When the *old* component logs a message with `OldLogger`,
|
||||
we want the singleton instance of `NewLogger` to handle it instead.
|
||||
|
||||
The dependency injector should inject that singleton instance
|
||||
when a component asks for either the new or the old logger.
|
||||
The `OldLogger` should be an alias for `NewLogger`.
|
||||
|
||||
We certainly do not want two different `NewLogger` instances in our app.
|
||||
Unfortunately, that's what we get if we try to alias `OldLogger` to `NewLogger` with `useClass`.
|
||||
|
||||
+makeExample('dependency-injection/ts/app/providers.component.ts','providers-6a')(format=".")
|
||||
|
||||
:marked
|
||||
The solution: alias with the `useExisting` option.
|
||||
|
||||
- var stylePattern = { otl: /(useExisting: \w*)/gm };
|
||||
+makeExample('dependency-injection/ts/app/providers.component.ts','providers-6b', '', stylePattern)(format=".")
|
||||
|
||||
#value-provider
|
||||
:marked
|
||||
### Value providers
|
||||
|
||||
:marked
|
||||
Sometimes it's easier to provide a ready-made object rather than ask the injector to create it from a class.
|
||||
|
||||
block dart-diff-const-metadata-ctor
|
||||
//- N/A
|
||||
|
||||
+makeExample('dependency-injection/ts/app/providers.component.ts','silent-logger')(format=".")
|
||||
|
||||
:marked
|
||||
Then we register a provider with the `useValue` option,
|
||||
which makes this object play the logger role.
|
||||
|
||||
- var stylePattern = { otl: /(useValue: \w*)/gm };
|
||||
+makeExample('dependency-injection/ts/app/providers.component.ts','providers-7', '', stylePattern)(format=".")
|
||||
|
||||
:marked
|
||||
See more `useValue` examples in the
|
||||
[Non-class dependencies](#non-class-dependencies) and
|
||||
[OpaqueToken](#opaquetoken) sections.
|
||||
|
||||
#factory-provider
|
||||
:marked
|
||||
### Factory providers
|
||||
|
||||
Sometimes we need to create the dependent value dynamically,
|
||||
based on information we won't have until the last possible moment.
|
||||
Maybe the information changes repeatedly in the course of the browser session.
|
||||
|
||||
Suppose also that the injectable service has no independent access to the source of this information.
|
||||
|
||||
This situation calls for a **factory provider**.
|
||||
|
||||
Let's illustrate by adding a new business requirement:
|
||||
the HeroService must hide *secret* heroes from normal users.
|
||||
Only authorized users should see secret heroes.
|
||||
|
||||
Like the `EvenBetterLogger`, the `HeroService` needs a fact about the user.
|
||||
It needs to know if the user is authorized to see secret heroes.
|
||||
That authorization can change during the course of a single application session,
|
||||
as when we log in a different user.
|
||||
|
||||
Unlike `EvenBetterLogger`, we can't inject the `UserService` into the `HeroService`.
|
||||
The `HeroService` won't have direct access to the user information to decide
|
||||
who is authorized and who is not.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Why? We don't know either. Stuff like this happens.
|
||||
|
||||
:marked
|
||||
Instead the `HeroService` constructor takes a boolean flag to control display of secret heroes.
|
||||
|
||||
+makeExample('dependency-injection/ts/app/heroes/hero.service.ts','internals', 'app/heroes/hero.service.ts (excerpt)')(format='.')
|
||||
|
||||
:marked
|
||||
We can inject the `Logger`, but we can't inject the boolean `isAuthorized`.
|
||||
We'll have to take over the creation of new instances of this `HeroService` with a factory provider.
|
||||
|
||||
A factory provider needs a factory function:
|
||||
|
||||
+makeExample('dependency-injection/ts/app/heroes/hero.service.provider.ts','factory', 'app/heroes/hero.service.provider.ts (excerpt)')(format='.')
|
||||
|
||||
:marked
|
||||
Although the `HeroService` has no access to the `UserService`, our factory function does.
|
||||
|
||||
We inject both the `Logger` and the `UserService` into the factory provider and let the injector pass them along to the factory function:
|
||||
|
||||
+makeExample('dependency-injection/ts/app/heroes/hero.service.provider.ts','provider', 'app/heroes/hero.service.provider.ts (excerpt)')(format='.')
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
The `useFactory` field tells Angular that the provider is a factory function
|
||||
whose implementation is the `heroServiceFactory`.
|
||||
|
||||
The `deps` property is #{_an} #{_array} of [provider tokens](#token).
|
||||
The `Logger` and `UserService` classes serve as tokens for their own class providers.
|
||||
The injector resolves these tokens and injects the corresponding services into the matching factory function parameters.
|
||||
|
||||
- var exportedvar = _docsFor == 'dart' ? 'constant' : 'exported variable'
|
||||
- var variable = _docsFor == 'dart' ? 'constant' : 'variable'
|
||||
:marked
|
||||
Notice that we captured the factory provider in #{_an} #{exportedvar}, `heroServiceProvider`.
|
||||
This extra step makes the factory provider reusable.
|
||||
We can register our `HeroService` with this #{variable} wherever we need it.
|
||||
|
||||
In our sample, we need it only in the `HeroesComponent`,
|
||||
where it replaces the previous `HeroService` registration in the metadata `providers` #{_array}.
|
||||
Here we see the new and the old implementation side-by-side:
|
||||
|
||||
- var stylePattern = { otl: /(providers.*),$/gm };
|
||||
+makeTabs(
|
||||
`dependency-injection/ts/app/heroes/heroes.component.ts,
|
||||
dependency-injection/ts/app/heroes/heroes.component.1.ts`,
|
||||
',full',
|
||||
`app/heroes/heroes.component (v3),
|
||||
app/heroes/heroes.component (v2)`,
|
||||
stylePattern)
|
||||
|
||||
.l-main-section#token
|
||||
:marked
|
||||
## Dependency injection tokens
|
||||
|
||||
When we register a provider with an injector, we associate that provider with a dependency injection token.
|
||||
The injector maintains an internal *token-provider* map that it references when
|
||||
asked for a dependency. The token is the key to the map.
|
||||
|
||||
In all previous examples, the dependency value has been a class *instance*, and
|
||||
the class *type* served as its own lookup key.
|
||||
Here we get a `HeroService` directly from the injector by supplying the `HeroService` type as the token:
|
||||
|
||||
+makeExample('dependency-injection/ts/app/injector.component.ts','get-hero-service')(format='.')
|
||||
|
||||
:marked
|
||||
We have similar good fortune when we write a constructor that requires an injected class-based dependency.
|
||||
We define a constructor parameter with the `HeroService` class type,
|
||||
and Angular knows to inject the
|
||||
service associated with that `HeroService` class token:
|
||||
|
||||
+makeExample('dependency-injection/ts/app/heroes/hero-list.component.ts', 'ctor-signature')
|
||||
|
||||
:marked
|
||||
This is especially convenient when we consider that most dependency values are provided by classes.
|
||||
|
||||
//- TODO: if function injection is useful explain or illustrate why.
|
||||
:marked
|
||||
### Non-class dependencies
|
||||
p
|
||||
| What if the dependency value isn't a class? Sometimes the thing we want to inject is a
|
||||
block non-class-dep-eg
|
||||
span string, function, or object.
|
||||
p
|
||||
| Applications often define configuration objects with lots of small facts
|
||||
| (like the title of the application or the address of a web API endpoint)
|
||||
block config-obj-maps
|
||||
| but these configuration objects aren't always instances of a class.
|
||||
| They can be object literals
|
||||
| such as this one:
|
||||
|
||||
+makeExample('dependency-injection/ts/app/app.config.ts','config','app/app-config.ts (excerpt)')(format='.')
|
||||
|
||||
:marked
|
||||
We'd like to make this configuration object available for injection.
|
||||
We know we can register an object with a [value provider](#value-provider).
|
||||
|
||||
block what-should-we-use-as-token
|
||||
:marked
|
||||
But what should we use as the token?
|
||||
We don't have a class to serve as a token.
|
||||
There is no `AppConfig` class.
|
||||
|
||||
.l-sub-section#interface
|
||||
:marked
|
||||
### TypeScript interfaces aren't valid tokens
|
||||
|
||||
The `HERO_DI_CONFIG` constant has an interface, `AppConfig`. Unfortunately, we
|
||||
cannot use a TypeScript interface as a token:
|
||||
+makeExample('dependency-injection/ts/app/providers.component.ts','providers-9-interface')(format=".")
|
||||
+makeExample('dependency-injection/ts/app/providers.component.ts','provider-9-ctor-interface')(format=".")
|
||||
:marked
|
||||
That seems strange if we're used to dependency injection in strongly typed languages, where
|
||||
an interface is the preferred dependency lookup key.
|
||||
|
||||
It's not Angular's fault. An interface is a TypeScript design-time artifact. JavaScript doesn't have interfaces.
|
||||
The TypeScript interface disappears from the generated JavaScript.
|
||||
There is no interface type information left for Angular to find at runtime.
|
||||
|
||||
//- FIXME simplify once APIs are defined for Dart.
|
||||
- var opaquetoken = _docsFor == 'dart' ? '<b>OpaqueToken</b>' : '<a href="../api/core/index/OpaqueToken-class.html"><b>OpaqueToken</b></a>'
|
||||
:marked
|
||||
### OpaqueToken
|
||||
|
||||
One solution to choosing a provider token for non-class dependencies is
|
||||
to define and use an !{opaquetoken}.
|
||||
The definition looks like this:
|
||||
|
||||
+makeExample('dependency-injection/ts/app/app.config.ts','token')(format='.')
|
||||
|
||||
:marked
|
||||
We register the dependency provider using the `OpaqueToken` object:
|
||||
|
||||
+makeExample('dependency-injection/ts/app/providers.component.ts','providers-9')(format=".")
|
||||
|
||||
:marked
|
||||
Now we can inject the configuration object into any constructor that needs it, with
|
||||
the help of an `@Inject` #{_decorator}:
|
||||
|
||||
+makeExample('dependency-injection/ts/app/app.component.2.ts','ctor')(format=".")
|
||||
|
||||
- var configType = _docsFor == 'dart' ? '<code>Map</code>' : '<code>AppConfig</code>'
|
||||
.l-sub-section
|
||||
:marked
|
||||
Although the !{configType} interface plays no role in dependency injection,
|
||||
it supports typing of the configuration object within the class.
|
||||
|
||||
block dart-map-alternative
|
||||
:marked
|
||||
Or we can provide and inject the configuration object in our top-level `AppComponent`.
|
||||
|
||||
+makeExcerpt('app/app.component.ts','providers')
|
||||
|
||||
#optional
|
||||
:marked
|
||||
## Optional dependencies
|
||||
|
||||
Our `HeroService` *requires* a `Logger`, but what if it could get by without
|
||||
a logger?
|
||||
We can tell Angular that the dependency is optional by annotating the
|
||||
constructor argument with `@Optional()`:
|
||||
|
||||
+ifDocsFor('ts')
|
||||
+makeExample('dependency-injection/ts/app/providers.component.ts','import-optional', '')
|
||||
+makeExample('dependency-injection/ts/app/providers.component.ts','provider-10-ctor', '')(format='.')
|
||||
|
||||
:marked
|
||||
When using `@Optional()`, our code must be prepared for a null value. If we
|
||||
don't register a logger somewhere up the line, the injector will set the
|
||||
value of `logger` to null.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Summary
|
||||
|
||||
We learned the basics of Angular dependency injection in this chapter.
|
||||
We can register various kinds of providers,
|
||||
and we know how to ask for an injected object (such as a service) by
|
||||
adding a parameter to a constructor.
|
||||
|
||||
Angular dependency injection is more capable than we've described.
|
||||
We can learn more about its advanced features, beginning with its support for
|
||||
nested injectors, in the
|
||||
[Hierarchical Dependency Injection](hierarchical-dependency-injection.html) chapter.
|
||||
|
||||
.l-main-section#explicit-injector
|
||||
:marked
|
||||
## Appendix: Working with injectors directly
|
||||
|
||||
We rarely work directly with an injector, but
|
||||
here's an `InjectorComponent` that does.
|
||||
|
||||
+makeExample('dependency-injection/ts/app/injector.component.ts', 'injector', 'app/injector.component.ts')
|
||||
|
||||
:marked
|
||||
An `Injector` is itself an injectable service.
|
||||
|
||||
In this example, Angular injects the component's own `Injector` into the component's constructor.
|
||||
The component then asks the injected injector for the services it wants.
|
||||
|
||||
Note that the services themselves are not injected into the component.
|
||||
They are retrieved by calling `injector.get`.
|
||||
|
||||
The `get` method throws an error if it can't resolve the requested service.
|
||||
We can call `get` with a second parameter (the value to return if the service is not found)
|
||||
instead, which we do in one case
|
||||
to retrieve a service (`ROUS`) that isn't registered with this or any ancestor injector.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
The technique we just described is an example of the
|
||||
[service locator pattern](https://en.wikipedia.org/wiki/Service_locator_pattern).
|
||||
|
||||
We **avoid** this technique unless we genuinely need it.
|
||||
It encourages a careless grab-bag approach such as we see here.
|
||||
It's difficult to explain, understand, and test.
|
||||
We can't know by inspecting the constructor what this class requires or what it will do.
|
||||
It could acquire services from any ancestor component, not just its own.
|
||||
We're forced to spelunk the implementation to discover what it does.
|
||||
|
||||
Framework developers may take this approach when they
|
||||
must acquire services generically and dynamically.
|
||||
|
||||
+ifDocsFor('ts')
|
||||
.l-main-section#one-class-per-file
|
||||
:marked
|
||||
## Appendix: Why we recommend one class per file
|
||||
|
||||
Having multiple classes in the same file is confusing and best avoided.
|
||||
Developers expect one class per file. Keep them happy.
|
||||
|
||||
If we scorn this advice and, say,
|
||||
combine our `HeroService` class with the `HeroesComponent` in the same file,
|
||||
**define the component last!**
|
||||
If we define the component before the service,
|
||||
we'll get a runtime null reference error.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
We actually can define the component first with the help of the `forwardRef()` method as explained
|
||||
in this [blog post](http://blog.thoughtram.io/angular/2015/09/03/forward-references-in-angular-2.html).
|
||||
But why flirt with trouble?
|
||||
Avoid the problem altogether by defining components and services in separate files.
|
|
@ -0,0 +1,283 @@
|
|||
block includes
|
||||
include ../_util-fns
|
||||
- var _iterableUrl = 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols';
|
||||
- var _boolean = 'truthy/falsey';
|
||||
|
||||
:marked
|
||||
We typically display data in Angular by binding controls in an HTML template
|
||||
to properties of an Angular component.
|
||||
|
||||
In this chapter, we'll create a component with a list of heroes. Each hero has a name.
|
||||
We'll display the list of hero names and
|
||||
conditionally show a message below the list.
|
||||
|
||||
The final UI looks like this:
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/displaying-data/final.png" alt="Final UI")
|
||||
|
||||
:marked
|
||||
# Table Of Contents
|
||||
|
||||
* [Showing component properties with interpolation](#interpolation)
|
||||
* [Showing !{_an} !{_array} property with NgFor](#ngFor)
|
||||
* [Conditional display with NgIf](#ngIf)
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
The <live-example></live-example> demonstrates all of the syntax and code
|
||||
snippets described in this chapter.
|
||||
|
||||
.l-main-section#interpolation
|
||||
:marked
|
||||
## Showing component properties with interpolation
|
||||
The easiest way to display a component property
|
||||
is to bind the property name through interpolation.
|
||||
With interpolation, we put the property name in the view template, enclosed in double curly braces: `{{myHero}}`.
|
||||
|
||||
Let's build a small illustrative example together.
|
||||
|
||||
Create a new project folder (<ngio-ex path="displaying-data"></ngio-ex>) and follow the steps in the [QuickStart](../quickstart.html).
|
||||
|
||||
block quickstart-repo
|
||||
include ../_quickstart_repo
|
||||
|
||||
:marked
|
||||
Then modify the <ngio-ex path="app.component.ts"></ngio-ex> file by
|
||||
changing the template and the body of the component.
|
||||
When we're done, it should look like this:
|
||||
|
||||
+makeExample('app/app.component.1.ts')
|
||||
|
||||
:marked
|
||||
We added two properties to the formerly empty component: `title` and `myHero`.
|
||||
|
||||
Our revised template displays the two component properties using double curly brace
|
||||
interpolation:
|
||||
|
||||
+makeExcerpt('app/app.component.1.ts', 'template', '')
|
||||
|
||||
+ifDocsFor('ts')
|
||||
.l-sub-section
|
||||
:marked
|
||||
The template is a multi-line string within ECMAScript 2015 backticks (<code>\`</code>).
|
||||
The backtick (<code>\`</code>) — which is *not* the same character as a single
|
||||
quote (`'`) — has many nice features. The feature we're exploiting here
|
||||
is the ability to compose the string over several lines, which makes for
|
||||
much more readable HTML.
|
||||
|
||||
:marked
|
||||
Angular automatically pulls the value of the `title` and `myHero` properties from the component and
|
||||
inserts those values into the browser. Angular updates the display
|
||||
when these properties change.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
More precisely, the redisplay occurs after some kind of asynchronous event related to
|
||||
the view such as a keystroke, a timer completion, or an async `XHR` response.
|
||||
We don't have those in this sample.
|
||||
But then the properties aren't changing on their own either. For the moment we must operate on faith.
|
||||
|
||||
:marked
|
||||
Notice that we haven't called **new** to create an instance of the `AppComponent` class.
|
||||
Angular is creating an instance for us. How?
|
||||
|
||||
Notice the CSS `selector` in the `@Component` !{_decorator} that specifies an element named `my-app`.
|
||||
Remember back in [QuickStart](../quickstart.html) that we added the `<my-app>` element to the body of our `index.html` file:
|
||||
|
||||
+makeExcerpt('index.html', 'body')
|
||||
|
||||
:marked
|
||||
When we bootstrap with the `AppComponent` class (in <ngio-ex path="main.ts"></ngio-ex>), Angular looks for a `<my-app>`
|
||||
in the `index.html`, finds it, instantiates an instance of `AppComponent`, and renders it
|
||||
inside the `<my-app>` tag.
|
||||
|
||||
Try running the app. It should display the title and hero name:
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/displaying-data/title-and-hero.png" alt="Title and Hero")
|
||||
|
||||
+ifDocsFor('ts')
|
||||
:marked
|
||||
Let's review some of the choices we made and consider alternatives.
|
||||
|
||||
:marked
|
||||
## Template inline or template file?
|
||||
|
||||
We can store our component's template in one of two places.
|
||||
We can define it *inline* using the `template` property, as we do here.
|
||||
Or we can define the template in a separate HTML file and link to it in
|
||||
the component metadata using the `@Component` !{_decorator}'s `templateUrl` property.
|
||||
|
||||
The choice between inline and separate HTML is a matter of taste,
|
||||
circumstances, and organization policy.
|
||||
Here we're using inline HTML because the template is small, and the demo
|
||||
is simpler without the additional HTML file.
|
||||
|
||||
In either style, the template data bindings have the same access to the component's properties.
|
||||
|
||||
+ifDocsFor('ts')
|
||||
:marked
|
||||
## Constructor or variable initialization?
|
||||
|
||||
We initialized our component properties using variable assignment.
|
||||
This is a wonderfully concise and compact technique.
|
||||
|
||||
Some folks prefer to declare the properties and initialize them within a constructor like this:
|
||||
+makeExcerpt('app/app-ctor.component.ts', 'class')
|
||||
|
||||
:marked
|
||||
That's fine too. The choice is a matter of taste and organization policy.
|
||||
We'll adopt the more terse "variable assignment" style in this chapter simply because
|
||||
there will be less code to read.
|
||||
|
||||
.l-main-section#ngFor
|
||||
:marked
|
||||
## Showing !{_an} !{_array} property with ***ngFor**
|
||||
|
||||
We want to display a list of heroes. We begin by adding !{_an} !{_array} of hero names to the component and redefine `myHero` to be the first name in the !{_array}.
|
||||
|
||||
+makeExcerpt('app/app.component.2.ts', 'class')
|
||||
|
||||
:marked
|
||||
Now we use the Angular `ngFor` directive in the template to display
|
||||
each item in the `heroes` list.
|
||||
|
||||
+makeExcerpt('app/app.component.2.ts', 'template')
|
||||
|
||||
:marked
|
||||
Our presentation is the familiar HTML unordered list with `<ul>` and `<li>` tags. Let's focus on the `<li>` tag.
|
||||
|
||||
+makeExcerpt('app/app.component.2.ts ()', 'li', '')
|
||||
|
||||
:marked
|
||||
We added a somewhat mysterious `*ngFor` to the `<li>` element.
|
||||
That's the Angular "repeater" directive.
|
||||
Its presence on the `<li>` tag marks that `<li>` element (and its children) as the "repeater template".
|
||||
|
||||
.alert.is-important
|
||||
:marked
|
||||
Don't forget the leading asterisk (\*) in `*ngFor`. It is an essential part of the syntax.
|
||||
Learn more about this and `ngFor` in the [Template Syntax](./template-syntax.html#ngFor) chapter.
|
||||
|
||||
:marked
|
||||
Notice the `hero` in the `ngFor` double-quoted instruction;
|
||||
it is an example of a [template input variable](./template-syntax.html#ngForMicrosyntax).
|
||||
|
||||
Angular duplicates the `<li>` for each item in the list, setting the `hero` variable
|
||||
to the item (the hero) in the current iteration. Angular uses that variable as the
|
||||
context for the interpolation in the double curly braces.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
We happened to give `ngFor` !{_an} !{_array} to display.
|
||||
In fact, `ngFor` can repeat items for any [iterable](!{_iterableUrl})
|
||||
object.
|
||||
:marked
|
||||
Now the heroes appear in an unordered list.
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/displaying-data/hero-names-list.png" alt="After ngfor")
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Creating a class for the data
|
||||
|
||||
We are defining our data directly inside our component.
|
||||
That's fine for a demo but certainly isn't a best practice. It's not even a good practice.
|
||||
Although we won't do anything about that in this chapter, we'll make a mental note to fix this down the road.
|
||||
|
||||
At the moment, we're binding to !{_an} !{_array} of strings. We do that occasionally in real applications, but
|
||||
most of the time we're binding to more specialized objects.
|
||||
|
||||
Let's turn our !{_array} of hero names into !{_an} !{_array} of `Hero` objects. For that we'll need a `Hero` class.
|
||||
|
||||
Create a new file in the `!{_appDir}` folder called <ngio-ex path="hero.ts"></ngio-ex> with the following code:
|
||||
|
||||
+makeExcerpt('app/hero.ts')
|
||||
|
||||
block hero-class
|
||||
:marked
|
||||
We've defined a class with a constructor and two properties: `id` and `name`.
|
||||
|
||||
It might not look like we have properties, but we do. We're taking
|
||||
advantage of a TypeScript shortcut in our declaration of the constructor parameters.
|
||||
|
||||
Consider the first parameter:
|
||||
|
||||
+makeExcerpt('app/hero.ts ()', 'id')
|
||||
|
||||
:marked
|
||||
That brief syntax does a lot:
|
||||
* Declares a constructor parameter and its type
|
||||
* Declares a public property of the same name
|
||||
* Initializes that property with the corresponding argument when we "new" an instance of the class
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Using the Hero class
|
||||
Let's make the `heroes` property in our component return !{_an} !{_array} of these `Hero` objects.
|
||||
|
||||
+makeExcerpt('app/app.component.3.ts', 'heroes')
|
||||
|
||||
:marked
|
||||
We'll have to update the template.
|
||||
At the moment it displays the hero's `id` and `name`.
|
||||
Let's fix that so we display only the hero's `name` property.
|
||||
|
||||
+makeExcerpt('app/app.component.3.ts', 'template')
|
||||
|
||||
:marked
|
||||
Our display looks the same, but now we know much better what a hero really is.
|
||||
|
||||
.l-main-section#ngIf
|
||||
:marked
|
||||
## Conditional display with NgIf
|
||||
|
||||
Sometimes an app needs to display a view or a portion of a view only under specific circumstances.
|
||||
|
||||
In our example, we'd like to display a message if we have a large number of heroes, say, more than 3.
|
||||
|
||||
The Angular `ngIf` directive inserts or removes an element based on a !{_boolean} condition.
|
||||
We can see it in action by adding the following paragraph at the bottom of the template:
|
||||
|
||||
+makeExcerpt('app/app.component.ts', 'message')
|
||||
|
||||
.alert.is-important
|
||||
:marked
|
||||
Don't forget the leading asterisk (\*) in `*ngIf`. It is an essential part of the syntax.
|
||||
Learn more about this and `ngIf` in the [Template Syntax](./template-syntax.html#ngIf) chapter.
|
||||
|
||||
:marked
|
||||
The [template expression](./template-syntax.html#template-expressions) inside the double quotes
|
||||
looks much like !{_Lang}, and it _is_ much like !{_Lang}.
|
||||
When the component's list of heroes has more than 3 items, Angular adds the paragraph to the DOM and the message appears.
|
||||
If there are 3 or fewer items, Angular omits the paragraph, so no message appears.
|
||||
|
||||
.alert.is-helpful
|
||||
:marked
|
||||
Angular isn't showing and hiding the message. It is adding and removing the paragraph element from the DOM.
|
||||
That hardly matters here. But it would matter a great deal, from a performance perspective, if
|
||||
we were conditionally including or excluding a big chunk of HTML with many data bindings.
|
||||
|
||||
:marked
|
||||
Try it out. Because the !{_array} has four items, the message should appear.
|
||||
Go back into <ngio-ex path="app.component.ts"></ngio-ex> and delete or comment out one of the elements from the hero !{_array}.
|
||||
The browser should refresh automatically and the message should disappear.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Summary
|
||||
Now we know how to use:
|
||||
- **Interpolation** with double curly braces to display a component property
|
||||
- **ngFor** to display !{_an} !{_array} of items
|
||||
- A !{_Lang} class to shape the **model data** for our component and display properties of that model
|
||||
- **ngIf** to conditionally display a chunk of HTML based on a boolean expression
|
||||
|
||||
Here's our final code:
|
||||
|
||||
block final-code
|
||||
+makeTabs(`displaying-data/ts/app/app.component.ts,
|
||||
displaying-data/ts/app/hero.ts,
|
||||
displaying-data/ts/app/main.ts`,
|
||||
'final,,',
|
||||
'app/app.component.ts, app/hero.ts, main.ts')
|
|
@ -0,0 +1,178 @@
|
|||
block includes
|
||||
include ../_util-fns
|
||||
|
||||
:marked
|
||||
We learned the basics of Angular Dependency injection in the
|
||||
[Dependency Injection](./dependency-injection.html) chapter.
|
||||
|
||||
Angular has a Hierarchical Dependency Injection system.
|
||||
There is actually a tree of injectors
|
||||
that parallel an application's component tree.
|
||||
We can re-configure the injectors at any level of that component tree with
|
||||
interesting and useful results.
|
||||
|
||||
In this chapter we explore these points and write some code.
|
||||
|
||||
Try the <live-example></live-example>.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## The Injector Tree
|
||||
|
||||
In the [Dependency Injection](./dependency-injection.html) chapter
|
||||
we learned how to configure a dependency injector and how to retrieve dependencies where we need them.
|
||||
|
||||
We oversimplified. In fact, there is no such thing as ***the*** injector!
|
||||
An application may have multiple injectors!
|
||||
|
||||
An Angular application is a tree of components. Each component instance has its own injector!
|
||||
The tree of components parallels the tree of injectors.
|
||||
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Angular doesn't *literally* create a separate injector for each component.
|
||||
Every component doesn't need its own injector and it would be horribly inefficient to create
|
||||
masses of injectors for no good purpose.
|
||||
|
||||
But it is true that every component ***has an injector*** (even if it shares that injector with another component)
|
||||
and there may be many different injector instances operating at different levels of the component tree.
|
||||
|
||||
It is useful to pretend that every component has its own injector.
|
||||
:marked
|
||||
Consider a simple variation on the Tour of Heroes application consisting of three different components:
|
||||
`HeroesApp`, `HeroesListComponent` and `HeroesCardComponent`.
|
||||
The `HeroesApp` holds a single instance of `HeroesListComponent`.
|
||||
The new twist is that the `HeroesListComponent` may hold and manage multiple instances of the `HeroesCardComponent`.
|
||||
|
||||
The following diagram represents the state of the component tree when there are three instances of `HeroesCardComponent`
|
||||
open simultaneously.
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/dependency-injection/component-hierarchy.png" alt="injector tree" width="500")
|
||||
|
||||
:marked
|
||||
Each component instance gets its own injector and an injector at one level is a child injector of the injector above it in the tree.
|
||||
|
||||
When a component at the bottom requests a dependency, Angular tries to satisfy that dependency with a provider registered in that component's own injector.
|
||||
If the component's injector lacks the provider, it passes the request up to its parent component's injector.
|
||||
If that injector can't satisfy the request, it passes it along to *its* parent component's injector.
|
||||
The requests keep bubbling up until we find an injector that can handle the request or run out of component ancestors.
|
||||
If we run out of ancestors, Angular throws an error.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
There's a third possibility. An intermediate component can declare that it is the "host" component.
|
||||
The hunt for providers will climb no higher than the injector for this host component.
|
||||
We'll reserve discussion of this option for another day.
|
||||
:marked
|
||||
Such a proliferation of injectors makes little sense until we consider the possibility that injectors at different levels can be
|
||||
configured with different providers. We don't *have* to re-configure providers at every level. But we *can*.
|
||||
|
||||
If we don't re-configure, the tree of injectors appears to be flat. All requests bubble up to the root injector that we
|
||||
configured with the `bootstrap` method.
|
||||
|
||||
The ability to configure one or more providers at different levels opens up interesting and useful possibilities.
|
||||
|
||||
Let’s return to our Car example.
|
||||
Suppose we configured the root injector (marked as A) with providers for `Car`, `Engine` and `Tires`.
|
||||
We create a child component (B) that defines its own providers for `Car` and `Engine`
|
||||
This child is the parent of another component (C) that defines its own provider for `Car`.
|
||||
|
||||
Behind the scenes each component sets up its own injector with one or more providers defined for that component itself.
|
||||
|
||||
When we resolve an instance of `Car` at the deepest component (C),
|
||||
its injector produces an instance of `Car` resolved by injector (C) with an `Engine` resolved by injector (B) and
|
||||
`Tires` resolved by the root injector (A).
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/dependency-injection/injector-tree.png" alt="injector tree" width="600")
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Component Injectors
|
||||
|
||||
In the previous section, we talked about injectors and how they are organized like a tree. Lookups follow the injector tree upwards until they find the requested thing to inject. But when do we actually want to provide providers on the root injector and when do we want to provide them on a child injector?
|
||||
|
||||
Consider you are building a component to show a list of super heroes that displays each super hero in a card with its name and superpower. There should also be an edit button that opens up an editor to change the name and superpower of our hero.
|
||||
|
||||
One important aspect of the editing functionality is that we want to allow multiple heroes to be in edit mode at the same time and that one can always either commit or cancel the proposed changes.
|
||||
|
||||
Let’s take a look at the `HeroesListComponent` which is the root component for this example.
|
||||
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/heroes-list.component.ts', null, 'app/heroes-list.component.ts')
|
||||
|
||||
:marked
|
||||
Notice that it imports the `HeroService` that we’ve used before so we can skip its declaration. The only difference is that we’ve used a more formal approach for our `Hero`model and defined it upfront as such.
|
||||
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/hero.ts', null, 'app/hero.ts')(format=".")
|
||||
|
||||
:marked
|
||||
Our `HeroesListComponent` defines a template that creates a list of `HeroCardComponent`s and `HeroEditorComponent`s, each bound to an instance of hero that is returned from the `HeroService`. Ok, that’s not entirely true. It actually binds to an `EditItem<Hero>` which is a simple generic datatype that can wrap any type and indicate if the item being wrapped is currently being edited or not.
|
||||
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/edit-item.ts', null, 'app/edit-item.ts')(format=".")
|
||||
|
||||
:marked
|
||||
But how is `HeroCardComponent` implemented? Let’s take a look.
|
||||
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/hero-card.component.ts', null, 'app/hero-card.component.ts')
|
||||
|
||||
:marked
|
||||
The `HeroCardComponent` is basically a component that defines a template to render a hero. Nothing more.
|
||||
|
||||
Let’s get to the interesting part and take a look at the `HeroEditorComponent`
|
||||
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/hero-editor.component.ts', null, 'app/hero-editor.component.ts')
|
||||
|
||||
:marked
|
||||
Now here it’s getting interesting. The `HeroEditorComponent`defines a template with an input to change the name of the hero and a `cancel` and a `save` button. Remember that we said we want to have the flexibility to cancel our editing and restore the old value? This means we need to maintain two copies of our `Hero` that we want to edit. Thinking ahead, this is a perfect use case to abstract it into its own generic service since we have probably more cases like this in our app.
|
||||
|
||||
And this is where the `RestoreService` enters the stage.
|
||||
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/restore.service.ts', null, 'app/restore.service.ts')
|
||||
|
||||
:marked
|
||||
All this tiny service does is define an API to set a value of any type which can be altered, retrieved or set back to its initial value. That’s exactly what we need to implement the desired functionality.
|
||||
|
||||
Our `HeroEditComponent` uses this services under the hood for its `hero` property. It intercepts the `get` and `set` method to delegate the actual work to our `RestoreService` which in turn makes sure that we won’t work on the original item but on a copy instead.
|
||||
|
||||
At this point we may be scratching our heads asking what this has to do with component injectors?
|
||||
Look closely at the metadata for our `HeroEditComponent`. Notice the `providers` property.
|
||||
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/hero-editor.component.ts', 'providers')
|
||||
:marked
|
||||
This adds a `RestoreService` provider to the injector of the `HeroEditComponent`.
|
||||
Couldn’t we simply alter our bootstrap call to this?
|
||||
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/main.ts', 'bad-alternative')
|
||||
:marked
|
||||
Technically we could, but our component wouldn’t quite behave the way it is supposed to. Remember that each injector treats the services that it provides as singletons. However, in order to be able to have multiple instances of `HeroEditComponent` edit multiple heroes at the same time we need to have multiple instances of the `RestoreService`. More specifically, each instance of `HeroEditComponent` needs to be bound to its own instance of the `RestoreService`.
|
||||
|
||||
By configuring a provider for the `RestoreService` on the `HeroEditComponent`, we get exactly one new instance of the `RestoreService`per `HeroEditComponent`.
|
||||
|
||||
Does that mean that services aren’t singletons anymore in Angular 2? Yes and no.
|
||||
There can be only one instance of a service type in a particular injector.
|
||||
But we've learned that we can have multiple injectors operating at different levels of the application's component tree.
|
||||
Any of those injectors could have its own instance of the service.
|
||||
|
||||
If we defined a `RestoreService` provider only on the root component,
|
||||
we would have exactly one instance of that service and it would be shared across the entire application.
|
||||
|
||||
That’s clearly not what we want in this scenario. We want each component to have its own instance of the `RestoreService`.
|
||||
Defining (or re-defining) a provider at the component level creates a new instance of the service for each new instance
|
||||
of that component. We've made the `RestoreService` a kind of "private" singleton for each `HeroEditComponent`,
|
||||
scoped to that component instance and its child components.
|
||||
|
||||
<!--
|
||||
## Advanced Dependency Injection in Angular 2
|
||||
|
||||
Restrict Dependency Lookups
|
||||
[TODO] (@Host) This has been postponed for now until we come up with a decent use case
|
||||
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Dependency Visibility
|
||||
|
||||
[TODO] (providers vs viewProviders) This has been postponed for now until come up with a decent use case
|
||||
-->
|
|
@ -0,0 +1,574 @@
|
|||
block includes
|
||||
include ../_util-fns
|
||||
|
||||
- var top="vertical-align:top"
|
||||
|
||||
:marked
|
||||
# Component Lifecycle
|
||||
A Component has a lifecycle managed by Angular itself. Angular creates it, renders it, creates and renders its children,
|
||||
checks it when its data-bound properties change, and destroys it before removing it from the DOM.
|
||||
|
||||
Angular offers **component lifecycle hooks**
|
||||
that give us visibility into these key moments and the ability to act when they occur.
|
||||
|
||||
We cover these hooks in this chapter and demonstrate how they work in code.
|
||||
|
||||
* [The lifecycle hooks](#hooks-overview)
|
||||
* [The hook-call sequence](#hook-sequence)
|
||||
* [Other Angular lifecycle hooks](#other-lifecycles)
|
||||
* [The lifecycle sample](#the-sample)
|
||||
* [All](#peek-a-boo)
|
||||
* [Spying OnInit and OnDestroy](#spy)
|
||||
* [OnChanges](#onchanges)
|
||||
* [DoCheck](#docheck)
|
||||
* [AfterViewInit and AfterViewChecked](#afterview)
|
||||
* [AfterContentInit and AfterContentChecked](#aftercontent)
|
||||
|
||||
Try the <live-example></live-example>.
|
||||
|
||||
a#hooks-overview
|
||||
.l-main-section
|
||||
:marked
|
||||
## Component lifecycle Hooks
|
||||
Directive and component instances have a lifecycle
|
||||
as Angular creates, updates, and destroys them.
|
||||
|
||||
Developers can tap into key moments in that lifecycle by implementing
|
||||
one or more of the *Lifecycle Hook* interfaces in the Angular `core` library.
|
||||
|
||||
Each interface has a single hook method whose name is the interface name prefixed with `ng`.
|
||||
For example, the `OnInit` interface has a hook method named `ngOnInit`.
|
||||
We might implement it in a component class like this:
|
||||
+makeExample('lifecycle-hooks/ts/app/peek-a-boo.component.ts', 'ngOnInit', 'peek-a-boo.component.ts (excerpt)')(format='.')
|
||||
:marked
|
||||
No directive or component will implement all of them and some of the hooks only make sense for components.
|
||||
Angular only calls a directive/component hook method *if it is defined*.
|
||||
block optional-interfaces
|
||||
.l-sub-section
|
||||
:marked
|
||||
### Interface optional?
|
||||
The interfaces are optional for JavaScript and Typescript developers from a purely technical perspective.
|
||||
The JavaScript language doesn't have interfaces.
|
||||
Angular can't see TypeScript interfaces at runtime because they disappear from the transpiled JavaScript.
|
||||
|
||||
Fortunately, they aren't necessary.
|
||||
We don't have to add the lifecycle hook interfaces to our directives and components to benefit from the hooks themselves.
|
||||
|
||||
Angular instead inspects our directive and component classes and calls the hook methods *if they are defined*.
|
||||
Angular will find and call methods like `ngOnInit()`, with or without the interfaces.
|
||||
|
||||
Nonetheless, we strongly recommend adding interfaces to TypeScript directive classes
|
||||
in order to benefit from strong typing and editor tooling.
|
||||
|
||||
:marked
|
||||
Here are the component lifecycle hook methods:
|
||||
|
||||
### Directives and Components
|
||||
|
||||
table(width="100%")
|
||||
col(width="20%")
|
||||
col(width="80%")
|
||||
tr
|
||||
th Hook
|
||||
th Purpose
|
||||
tr(style=top)
|
||||
td ngOnInit
|
||||
td
|
||||
:marked
|
||||
Initialize the directive/component after Angular initializes the data-bound input properties.
|
||||
tr(style=top)
|
||||
td ngOnChanges
|
||||
td
|
||||
:marked
|
||||
Respond after Angular sets a data-bound input property.
|
||||
The method receives a `changes` object of current and previous values.
|
||||
tr(style=top)
|
||||
td ngDoCheck
|
||||
td
|
||||
:marked
|
||||
Detect and act upon changes that Angular can or won't
|
||||
detect on its own. Called every change detection run.
|
||||
tr(style=top)
|
||||
td ngOnDestroy
|
||||
td
|
||||
:marked
|
||||
Cleanup just before Angular destroys the directive/component.
|
||||
Unsubscribe observables and detach event handlers to avoid memory leaks.
|
||||
|
||||
:marked
|
||||
### Components only
|
||||
|
||||
table(width="100%")
|
||||
col(width="20%")
|
||||
col(width="80%")
|
||||
tr
|
||||
th Hook
|
||||
th Purpose
|
||||
tr(style=top)
|
||||
td ngAfterContentInit
|
||||
td
|
||||
:marked
|
||||
After Angular projects external content into its view.
|
||||
tr(style=top)
|
||||
td ngAfterContentChecked
|
||||
td
|
||||
:marked
|
||||
After Angular checks the bindings of the external content that it projected into its view.
|
||||
tr(style=top)
|
||||
td ngAfterViewInit
|
||||
td
|
||||
:marked
|
||||
After Angular creates the component's view(s).
|
||||
tr(style=top)
|
||||
td ngAfterViewChecked
|
||||
td
|
||||
:marked
|
||||
After Angular checks the bindings of the component's view(s).
|
||||
:marked
|
||||
Angular does not call the hook methods in this order.
|
||||
|
||||
a(id="hook-sequence")
|
||||
.l-main-section
|
||||
:marked
|
||||
## Lifecycle sequence
|
||||
*After* Angular creates a component/directive by `new`-ing its constructor,
|
||||
it calls the lifecycle hook methods in the following sequence at specific moments:
|
||||
table(width="100%")
|
||||
col(width="20%")
|
||||
col(width="80%")
|
||||
tr
|
||||
th Hook
|
||||
th Timing
|
||||
tr(style=top)
|
||||
td ngOnChanges
|
||||
td
|
||||
:marked
|
||||
before `ngOnInit` and when a data-bound input property value changes.
|
||||
tr(style=top)
|
||||
td ngOnInit
|
||||
td
|
||||
:marked
|
||||
after the first `ngOnChanges`.
|
||||
tr(style=top)
|
||||
td ngDoCheck
|
||||
td
|
||||
:marked
|
||||
during every Angular change detection cycle.
|
||||
tr(style=top)
|
||||
td ngAfterContentInit
|
||||
td
|
||||
:marked
|
||||
after projecting content into the component.
|
||||
tr(style=top)
|
||||
td ngAfterContentChecked
|
||||
td
|
||||
:marked
|
||||
after every check of projected component content.
|
||||
tr(style=top)
|
||||
td ngAfterViewInit
|
||||
td
|
||||
:marked
|
||||
after initializing the component's views and child views.
|
||||
tr(style=top)
|
||||
td ngAfterViewChecked
|
||||
td
|
||||
:marked
|
||||
after every check of the component's views and child views.
|
||||
tr(style=top)
|
||||
td ngOnDestroy
|
||||
td
|
||||
:marked
|
||||
just before Angular destroys the directive/component.
|
||||
|
||||
a(id="other-lifecycles")
|
||||
.l-main-section
|
||||
:marked
|
||||
## Other lifecycle hooks
|
||||
|
||||
Other Angular sub-systems may have their own lifecycle hooks apart from the component hooks we've listed.
|
||||
The router, for instance, also has it's own [router lifecycle hooks](router.html#router-lifecycle-hooks)
|
||||
that allow us to tap into specific moments in route navigation.
|
||||
|
||||
A parallel can be drawn between `ngOnInit` and `routerOnActivate`.
|
||||
Both are prefixed so as to avoid collision, and both run right when a component is 'booting' up.
|
||||
|
||||
3rd party libraries might implement their hooks as well in order to give us, the developers, more
|
||||
control over how these libraries are used.
|
||||
|
||||
.l-main-section#the-sample
|
||||
:marked
|
||||
## Lifecycle exercises
|
||||
|
||||
The <live-example></live-example>
|
||||
demonstrates the lifecycle hooks in action through a series of exercises
|
||||
presented as components under the control of the root `AppComponent`.
|
||||
|
||||
They follow a common pattern: a *parent* component serves as a test rig for
|
||||
a *child* component that illustrates one or more of the lifecycle hook methods.
|
||||
|
||||
Here's a brief description of each exercise:
|
||||
|
||||
table(width="100%")
|
||||
col(width="20%")
|
||||
col(width="80%")
|
||||
tr
|
||||
th Component
|
||||
th Description
|
||||
tr(style=top)
|
||||
td <a href="#peek-a-boo">Peek-a-boo</a>
|
||||
td
|
||||
:marked
|
||||
Demonstrates every lifecycle hook.
|
||||
Each hook method writes to the on-screen log.
|
||||
tr(style=top)
|
||||
td <a href="#spy">Spy</a>
|
||||
td
|
||||
:marked
|
||||
Directives have lifecycle hooks too.
|
||||
We create a `SpyDirective` that logs when the element it spies upon is
|
||||
created or destroyed using the `ngOnInit` and `ngOnDestroy` hooks.
|
||||
|
||||
We apply the `SpyDirective` to a `<div>` in an `ngFor` *hero* repeater
|
||||
managed by the parent `SpyComponent`.
|
||||
tr(style=top)
|
||||
td <a href="#onchanges">OnChanges</a>
|
||||
td
|
||||
:marked
|
||||
See how Angular calls the `ngOnChanges` hook with a `changes` object
|
||||
every time one of the component input properties changes.
|
||||
Shows how to interpret the `changes` object.
|
||||
tr(style=top)
|
||||
td <a href="#docheck">DoCheck</a>
|
||||
td
|
||||
:marked
|
||||
Implements an `ngDoCheck` method with custom change detection.
|
||||
See how often Angular calls this hook and watch it post changes to a log.
|
||||
tr(style=top)
|
||||
td <a href="#afterview">AfterView</a>
|
||||
td
|
||||
:marked
|
||||
Shows what Angular means by a *view*.
|
||||
Demonstrates the `ngAfterViewInit` and `ngAfterViewChecked` hooks.
|
||||
tr(style=top)
|
||||
td <a href="#aftercontent">AfterContent</a>
|
||||
td
|
||||
:marked
|
||||
Shows how to project external content into a component and
|
||||
how to distinguish projected content from a component's view children.
|
||||
Demonstrates the `ngAfterContentInit` and `ngAfterContentChecked` hooks.
|
||||
tr(style=top)
|
||||
td Counter
|
||||
td
|
||||
:marked
|
||||
Demonstrates a combination of a component and a directive
|
||||
each with its own hooks.
|
||||
|
||||
In this example, a `CounterComponent` logs a change (via `ngOnChanges`)
|
||||
every time the parent component increments its input counter property.
|
||||
Meanwhile, we apply the `SpyDirective` from the previous example
|
||||
to the `CounterComponent` log and watch log entries be created and destroyed.
|
||||
|
||||
:marked
|
||||
We discuss the exercises in further detail over this chapter as we learn more about the lifecycle hooks.
|
||||
|
||||
a(id="peek-a-boo")
|
||||
.l-main-section
|
||||
:marked
|
||||
## Peek-a-boo: all hooks
|
||||
The `PeekABooComponent` demonstrates all of the hooks in one component.
|
||||
|
||||
In real life, we'd rarely if ever implement all of the interfaces like this.
|
||||
We do so in peek-a-boo in order to watch Angular call the hooks in the expected order.
|
||||
|
||||
In this snapshot, we clicked the *Create...* button and then the *Destroy...* button.
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/lifecycle-hooks/peek-a-boo.png" alt="Peek-a-boo")
|
||||
:marked
|
||||
The sequence of log messages follows the prescribed hook calling order:
|
||||
`OnChanges`, `OnInit`, `DoCheck` (3x), `AfterContentInit`, `AfterContentChecked` (3x),
|
||||
`AfterViewInit`, `AfterViewChecked` (3x), and `OnDestroy`.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
The constructor isn't an Angular hook *per se*.
|
||||
We log in it to confirm that input properties (the `name` property in this case) have no assigned values at construction.
|
||||
:marked
|
||||
Had we clicked the *Update Hero* button, we'd have seen another `OnChanges` and two more triplets of
|
||||
`DoCheck`, `AfterContentChecked` and `AfterViewChecked`.
|
||||
Clearly these three hooks fire a *lot* and we must keep the logic we put in these hooks
|
||||
as lean as possible!
|
||||
|
||||
Our next examples focus on hook details.
|
||||
|
||||
.a(id="spy")
|
||||
.l-main-section
|
||||
:marked
|
||||
## Spying *OnInit* and *OnDestroy*
|
||||
|
||||
We're going undercover for these two hooks. We want to know when an element is initialized or destroyed,
|
||||
but we don't want *it* to know we're watching.
|
||||
|
||||
This is the perfect infiltration job for a directive.
|
||||
Our heroes will never know it's there.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Kidding aside, we're emphasizing two key points:
|
||||
|
||||
1. Angular calls hook methods for *directives* as well as components.
|
||||
|
||||
2. A spy directive can gives us insight into a DOM object that we cannot change directly.
|
||||
Obviously we can't change the implementation of a native `div`.
|
||||
We can't modify a third party component either.
|
||||
But we can watch both with a directive.
|
||||
|
||||
|
||||
:marked
|
||||
Our sneaky spy directive is simple, consisting almost entirely of `ngOnInit` and `ngOnDestroy` hooks
|
||||
that log messages to the parent via an injected `LoggerService`.
|
||||
|
||||
+makeExample('lifecycle-hooks/ts/app/spy.directive.ts', 'spy-directive')(format=".")
|
||||
|
||||
:marked
|
||||
We can apply the spy to any native or component element and it'll be initialized and destroyed
|
||||
at the same time as that element.
|
||||
Here we attach it to the repeated hero `<div>`
|
||||
+makeExample('lifecycle-hooks/ts/app/spy.component.html', 'template')(format=".")
|
||||
|
||||
:marked
|
||||
Each spy's birth and death marks the birth and death of the attached hero `<div>`
|
||||
with an entry in the *Hook Log* as we see here:
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/lifecycle-hooks/spy-directive.gif' alt="Spy Directive")
|
||||
|
||||
:marked
|
||||
Adding a hero results in a new hero `<div>`. The spy's `ngOnInit` logs that event.
|
||||
We see a new entry for each hero.
|
||||
|
||||
The *Reset* button clears the `heroes` list.
|
||||
Angular removes all hero divs from the DOM and destroys their spy directives at the same time.
|
||||
The spy's `ngOnDestroy` method reports its last moments.
|
||||
|
||||
The `ngOnInit` and `ngOnDestroy` methods have more vital roles to play in real applications.
|
||||
Let's see why we need them.
|
||||
|
||||
### OnInit
|
||||
|
||||
We turn to `ngOnInit` for two main reasons:
|
||||
1. To perform complex initializations shortly after construction
|
||||
1. To set up the component after Angular sets the input properties
|
||||
|
||||
An `ngOnInit` often fetches data for the component as shown in the
|
||||
[Tutorial](../tutorial/toh-pt4.html#oninit) and [HTTP](server-communication.html#oninit) chapters.
|
||||
|
||||
We don't fetch data in a component constructor. Why?
|
||||
Because experienced developers agree that components should be cheap and safe to construct.
|
||||
We shouldn't worry that a new component will try to contact a remote server when
|
||||
created under test or before we decide to display it.
|
||||
Constructors should do no more than set the initial local variables to simple values.
|
||||
|
||||
When a component must start working _soon_ after creation,
|
||||
we can count on Angular to call the `ngOnInit` method to jumpstart it.
|
||||
That's where the heavy initialization logic belongs.
|
||||
|
||||
Remember also that a directive's data-bound input properties are not set until _after construction_.
|
||||
That's a problem if we need to initialize the directive based on those properties.
|
||||
They'll have been set when our `ngOninit` runs.
|
||||
.l-sub-section
|
||||
:marked
|
||||
Our first opportunity to access those properties is the `ngOnChanges` method which
|
||||
Angular calls before `ngOnit`. But Angular calls `ngOnChanges` many times after that.
|
||||
It only calls `ngOnit` once.
|
||||
:marked
|
||||
### OnDestroy
|
||||
|
||||
Put cleanup logic in `ngOnDestroy`, the logic that *must* run before Angular destroys the directive.
|
||||
|
||||
This is the time to notify another part of the application that this component is going away.
|
||||
|
||||
This is the place to free resources that won't be garbage collected automatically.
|
||||
Unsubscribe from observables and DOM events. Stop interval timers.
|
||||
Unregister all callbacks that this directive registered with global or application services.
|
||||
We risk memory leaks if we neglect to do so.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## OnChanges
|
||||
|
||||
We monitor the `OnChanges` hook in this example.
|
||||
Angular calls its `ngOnChanges` method whenever it detects changes to ***input properties*** of the component (or directive).
|
||||
|
||||
Here is our implementation of the hook.
|
||||
+makeExample('lifecycle-hooks/ts/app/on-changes.component.ts', 'ng-on-changes', 'OnChangesComponent (ngOnChanges)')(format=".")
|
||||
:marked
|
||||
The `ngOnChanges` method takes an object that maps each changed property name to a
|
||||
[SimpleChange](../api/core/index/SimpleChange-class.html) object with the current and previous property values.
|
||||
We iterate over the changed properties and log them.
|
||||
|
||||
The input properties for our example `OnChangesComponent` are `hero` and `power`.
|
||||
+makeExample('lifecycle-hooks/ts/app/on-changes.component.ts', 'inputs')(format=".")
|
||||
:marked
|
||||
The parent binds to them like this:
|
||||
|
||||
+makeExample('lifecycle-hooks/ts/app/on-changes-parent.component.html', 'on-changes')
|
||||
:marked
|
||||
Here's the sample in action as we make changes.
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/lifecycle-hooks/on-changes-anim.gif' alt="OnChanges")
|
||||
|
||||
:marked
|
||||
We see log entries as the string value of the *power* property changes. But the `ngOnChanges` did not catch changes to `hero.name`
|
||||
That's surprising at first.
|
||||
|
||||
Angular only calls the hook when the value of the input property changes.
|
||||
The value of the `hero` property is the *reference to the hero object*.
|
||||
Angular doesn't care that the hero's own `name` property changed.
|
||||
The hero object *reference* didn't change so, from Angular's perspective, there is no change to report!
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## DoCheck
|
||||
We can use the `DoCheck` hook to detect and act upon changes that Angular doesn't catch on its own.
|
||||
.l-sub-section
|
||||
:marked
|
||||
With this method we can detect a change that Angular overlooked.
|
||||
What we do with that information to refresh the display is a separate matter.
|
||||
:marked
|
||||
The *DoCheck* sample extends the *OnChanges* sample with this implementation of `DoCheck`:
|
||||
+makeExample('lifecycle-hooks/ts/app/do-check.component.ts', 'ng-do-check', 'DoCheckComponent (ngDoCheck)')(format=".")
|
||||
:marked
|
||||
We manually check everything that we care about, capturing and comparing against previous values.
|
||||
We write a special message to the log when there are no substantive changes
|
||||
to the hero or the power so we can keep an eye on the method's performance characteristics.
|
||||
|
||||
The results are illuminating:
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/lifecycle-hooks/do-check-anim.gif' alt="DoCheck")
|
||||
:marked
|
||||
We now are able to detect when the hero's `name` has changed. But we must be careful.
|
||||
|
||||
The `ngDoCheck` hook is called with enormous frequency —
|
||||
after _every_ change detection cycle no matter where the change occurred.
|
||||
It's called over twenty times in this example before the user can do anything.
|
||||
|
||||
Most of these initial checks are triggered by Angular's first rendering of *unrelated data elsewhere on the page*.
|
||||
Mere mousing into another input box triggers a call.
|
||||
Relatively few calls reveal actual changes to pertinent data.
|
||||
Clearly our implementation must be very lightweight or the user experience may suffer.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
We also see that the `ngOnChanges` method is called in contradiction of the
|
||||
[incorrect API documentation](../api/core/index/DoCheck-class.html).
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## AfterView
|
||||
The *AfterView* sample explores the `AfterViewInit` and `AfterViewChecked` hooks that Angular calls
|
||||
*after* it creates a component's child views.
|
||||
|
||||
Here's a child view that displays a hero's name in an input box:
|
||||
+makeExample('lifecycle-hooks/ts/app/after-view.component.ts', 'child-view', 'ChildComponent')(format=".")
|
||||
:marked
|
||||
The `AfterViewComponent` displays this child view *within its template*:
|
||||
+makeExample('lifecycle-hooks/ts/app/after-view.component.ts', 'template', 'AfterViewComponent (template)')(format=".")
|
||||
:marked
|
||||
The following hooks take action based on changing values *within the child view*
|
||||
which we can only reach by querying for the child view via the property decorated with
|
||||
[@ViewChild](../api/core/index/ViewChild-var.html).
|
||||
|
||||
+makeExample('lifecycle-hooks/ts/app/after-view.component.ts', 'hooks', 'AfterViewComponent (class excerpts)')(format=".")
|
||||
.a(id="wait-a-tick")
|
||||
:marked
|
||||
### Abide by the unidirectional data flow rule
|
||||
The `doSomething` method updates the screen when the hero name exceeds 10 characters.
|
||||
|
||||
+makeExample('lifecycle-hooks/ts/app/after-view.component.ts', 'do-something', 'AfterViewComponent (doSomething)')(format=".")
|
||||
:marked
|
||||
Why does the `doSomething` method wait a tick before updating `comment`?
|
||||
|
||||
Because we must adhere to Angular's unidirectional data flow rule which says that
|
||||
we may not update the view *after* it has been composed.
|
||||
Both hooks fire after the component's view has been composed.
|
||||
|
||||
Angular throws an error if we update component's data-bound `comment` property immediately (try it!).
|
||||
block tick-methods
|
||||
:marked
|
||||
The `LoggerService.tick` methods, which are implemented by a call to `setTimeout`, postpone the update one turn of the of the browser's JavaScript cycle ... and that's long enough.
|
||||
|
||||
:marked
|
||||
Here's *AfterView* in action
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/lifecycle-hooks/after-view-anim.gif' alt="AfterView")
|
||||
:marked
|
||||
Notice that Angular frequently calls `AfterViewChecked`, often when there are no changes of interest.
|
||||
Write lean hook methods to avoid performance problems.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## AfterContent
|
||||
The *AfterContent* sample explores the `AfterContentInit` and `AfterContentChecked` hooks that Angular calls
|
||||
*after* Angular projects external content into the component.
|
||||
|
||||
### Content projection
|
||||
*Content projection* is a way to import HTML content from outside the component and insert that content
|
||||
into the component's template in a designated spot.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Angular 1 developers know this technique as *transclusion*.
|
||||
|
||||
:marked
|
||||
We'll illustrate with a variation on the [previous](#afterview) example
|
||||
whose behavior and output is almost the same.
|
||||
|
||||
This time, instead of including the child view within the template, we'll import it from
|
||||
the `AfterContentComponent`'s parent. Here's the parent's template.
|
||||
+makeExample('lifecycle-hooks/ts/app/after-content.component.ts', 'parent-template', 'AfterContentParentComponent (template excerpt)')(format=".")
|
||||
:marked
|
||||
Notice that the `<my-child>` tag is tucked between the `<after-content>` tags.
|
||||
We never put content between a component's element tags *unless we intend to project that content
|
||||
into the component*.
|
||||
|
||||
Now look at the component's template:
|
||||
+makeExample('lifecycle-hooks/ts/app/after-content.component.ts', 'template', 'AfterContentComponent (template)')(format=".")
|
||||
:marked
|
||||
The `<ng-content>` tag is a *placeholder* for the external content.
|
||||
They tell Angular where to insert that content.
|
||||
In this case, the projected content is the `<my-child>` from the parent.
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/lifecycle-hooks/projected-child-view.png' width="230" alt="Projected Content")
|
||||
:marked
|
||||
.l-sub-section
|
||||
:marked
|
||||
The tell-tale signs of *content projection* are (a) HTML between component element tags
|
||||
and (b) the presence of `<ng-content>` tags in the component's template.
|
||||
:marked
|
||||
### AfterContent hooks
|
||||
*AfterContent* hooks are similar to the *AfterView* hooks. The key difference is the kind of child component
|
||||
that we're looking for.
|
||||
|
||||
* The *AfterView* hooks concern `ViewChildren`, the child components whose element tags
|
||||
appear *within* the component's template.
|
||||
|
||||
* The *AfterContent* hooks concern `ContentChildren`, the child components that Angular
|
||||
projected into the component.
|
||||
|
||||
The following *AfterContent* hooks take action based on changing values in a *content child*
|
||||
which we can only reach by querying for it via the property decorated with
|
||||
[@ContentChild](../api/core/index/ContentChild-var.html).
|
||||
|
||||
+makeExample('lifecycle-hooks/ts/app/after-content.component.ts', 'hooks', 'AfterContentComponent (class excerpts)')(format=".")
|
||||
|
||||
:marked
|
||||
### No unidirectional flow worries
|
||||
|
||||
This component's `doSomething` method update's the component's data-bound `comment` property immediately.
|
||||
There's no [need to wait](#wait-a-tick).
|
||||
|
||||
Recall that Angular calls both *AfterContent* hooks before calling either of the *AfterView* hooks.
|
||||
Angular completes composition of the projected content *before* finishing the composition of this component's view.
|
||||
We still have a window of opportunity to modify that view.
|
|
@ -0,0 +1,487 @@
|
|||
block includes
|
||||
include ../_util-fns
|
||||
|
||||
:marked
|
||||
Every application starts out with what seems like a simple task: get data, transform them, and show them to users.
|
||||
Getting data could be as simple as creating a local variable or as complex as streaming data over a Websocket.
|
||||
|
||||
Once data arrive, we could push their raw `toString` values directly to the view.
|
||||
That rarely makes for a good user experience.
|
||||
E.g., almost everyone prefers a simple birthday date like
|
||||
<samp>April 15, 1988</samp> to the original raw string format
|
||||
— <samp>Fri Apr 15 1988 00:00:00 GMT-0700 (Pacific Daylight Time)</samp>.
|
||||
|
||||
Clearly some values benefit from a bit of massage. We soon discover that we
|
||||
desire many of the same transformations repeatedly, both within and across many applications.
|
||||
We almost think of them as styles.
|
||||
In fact, we'd like to apply them in our HTML templates as we do styles.
|
||||
|
||||
Introducing Angular pipes, a way to write display-value transformations that we can declare in our HTML!
|
||||
Try the <live-example></live-example>.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Using Pipes
|
||||
|
||||
A pipe takes in data as input and transforms it to a desired output.
|
||||
We'll illustrate by transforming a component's birthday property into
|
||||
a human-friendly date.
|
||||
|
||||
+makeExample('pipes/ts/app/hero-birthday1.component.ts', null, 'app/hero-birthday1.component.ts')(format='.')
|
||||
|
||||
:marked
|
||||
Focus on the component's template.
|
||||
|
||||
+makeExample('pipes/ts/app/app.component.html', 'hero-birthday-template')(format=".")
|
||||
|
||||
:marked
|
||||
Inside the interpolation expression we flow the component's `birthday` value through the
|
||||
[pipe operator](./template-syntax.html#pipe) ( | ) to the [Date pipe](../api/common/index/DatePipe-class.html)
|
||||
function on the right. All pipes work this way.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
The `Date` and `Currency` pipes need the **ECMAScript Internationalization API**.
|
||||
Safari and other older browsers don't support it. We can add support with a polyfill.
|
||||
|
||||
code-example(language="html").
|
||||
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.en"></script>
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Built-in pipes
|
||||
Angular comes with a stock of pipes such as
|
||||
`DatePipe`, `UpperCasePipe`, `LowerCasePipe`, `CurrencyPipe`, and `PercentPipe`.
|
||||
They are all immediately available for use in any template.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Learn more about these and many other built-in pipes in the [API Reference](../api/#!?apiFilter=pipe);
|
||||
filter for entries that include the word "pipe".
|
||||
|
||||
Angular 2 doesn't have a `FilterPipe` or an `OrderByPipe` for reasons explained in an [appendix below](#no-filter-pipe).
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Parameterizing a Pipe
|
||||
|
||||
A pipe may accept any number of optional parameters to fine-tune its output.
|
||||
We add parameters to a pipe by following the pipe name with a colon ( : ) and then the parameter value
|
||||
(e.g., `currency:'EUR'`). If our pipe accepts multiple parameters, we separate the values with colons (e.g. `slice:1:5`)
|
||||
|
||||
We'll modify our birthday template to give the date pipe a format parameter.
|
||||
After formatting the hero's April 15th birthday, it should render as **<samp>04/15/88</samp>**:
|
||||
|
||||
+makeExample('pipes/ts/app/app.component.html', 'format-birthday')(format=".")
|
||||
|
||||
:marked
|
||||
The parameter value can be any valid
|
||||
[template expression](./template-syntax.html#template-expressions)
|
||||
such as a string literal or a component property.
|
||||
In other words, we can control the format through a binding the same way we control the birthday value through a binding.
|
||||
|
||||
Let's write a second component that *binds* the pipe's format parameter
|
||||
to the component's `format` property. Here's the template for that component:
|
||||
|
||||
+makeExample('pipes/ts/app/hero-birthday2.component.ts', 'template', 'app/hero-birthday2.component.ts (template)')(format=".")
|
||||
|
||||
:marked
|
||||
We also added a button to the template and bound its click event to the component's `toggleFormat()` method.
|
||||
That method toggles the component's `format` property between a short form
|
||||
(`'shortDate'`) and a longer form (`'fullDate'`).
|
||||
|
||||
+makeExample('pipes/ts/app/hero-birthday2.component.ts', 'class', 'app/hero-birthday2.component.ts (class)')(format='.')
|
||||
|
||||
:marked
|
||||
As we click the button, the displayed date alternates between
|
||||
"**<samp>04/15/1988</samp>**" and
|
||||
"**<samp>Friday, April 15, 1988</samp>**".
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/pipes/date-format-toggle-anim.gif' alt="Date Format Toggle")
|
||||
:marked
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Learn more about the `DatePipes` format options in the [API Docs](../api/common/index/DatePipe-class.html).
|
||||
|
||||
:marked
|
||||
## Chaining pipes
|
||||
|
||||
We can chain pipes together in potentially useful combinations.
|
||||
In the following example, we chain the birthday to the `DatePipe` and on to the `UpperCasePipe`
|
||||
so we can display the birthday in uppercase. The following birthday displays as
|
||||
**<samp>APR 15, 1988</samp>**.
|
||||
|
||||
+makeExample('pipes/ts/app/app.component.html', 'chained-birthday')(format=".")
|
||||
|
||||
:marked
|
||||
This example — which displays **<samp>FRIDAY, APRIL 15, 1988</samp>** —
|
||||
chains the same pipes as above, but passes in a parameter to `date` as well.
|
||||
|
||||
+makeExample('pipes/ts/app/app.component.html', 'chained-parameter-birthday')(format=".")
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Custom Pipes
|
||||
|
||||
We can write our own custom pipes.
|
||||
Here's a custom pipe named `ExponentialStrengthPipe` that can boost a hero's powers:
|
||||
|
||||
+makeExample('pipes/ts/app/exponential-strength.pipe.ts', null, 'app/exponential-strength.pipe.ts')(format=".")
|
||||
|
||||
:marked
|
||||
This pipe definition reveals several key points:
|
||||
|
||||
* A pipe is a class decorated with pipe metadata.
|
||||
|
||||
* The pipe class implements the `PipeTransform` interface's `transform` method that
|
||||
accepts an input value followed by optional parameters and returns the transformed value.
|
||||
|
||||
* There will be one additional argument to the `transform` method for each parameter passed to the pipe.
|
||||
Our pipe has one such parameter: the `exponent`.
|
||||
|
||||
* We tell Angular that this is a pipe by applying the
|
||||
`@Pipe` #{_decorator} which we import from the core Angular library.
|
||||
|
||||
* The `@Pipe` #{_decorator} allows us to define the
|
||||
pipe name that we'll use within template expressions. It must be a valid JavaScript identifier.
|
||||
Our pipe's name is `exponentialStrength`.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
### The *PipeTransform* Interface
|
||||
|
||||
The `transform` method is essential to a pipe.
|
||||
The `PipeTransform` *interface* defines that method and guides both tooling and the compiler.
|
||||
It is technically optional; Angular looks for and executes the `transform` method regardless.
|
||||
|
||||
:marked
|
||||
Now we need a component to demonstrate our pipe.
|
||||
+makeExample('pipes/ts/app/power-booster.component.ts',null,'app/power-booster.component.ts')(format='.')
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/pipes/power-booster.png' alt="Power Booster")
|
||||
|
||||
:marked
|
||||
Two things to note:
|
||||
1. We use our custom pipe the same way we use the built-in pipes.
|
||||
|
||||
1. We must include our pipe in the `pipes` #{_array} of the `@Component` #{_decorator}.
|
||||
|
||||
.callout.is-helpful
|
||||
header Remember the pipes #{_array}!
|
||||
:marked
|
||||
Angular reports an error if we neglect to list our custom pipe.
|
||||
We didn't list the `DatePipe` in our previous example because all
|
||||
Angular built-in pipes are pre-registered.
|
||||
Custom pipes must be registered manually.
|
||||
|
||||
:marked
|
||||
If we try the <live-example></live-example>,
|
||||
we can probe its behavior by changing the value and the optional exponent in the template.
|
||||
|
||||
## Power Boost Calculator (extra-credit)
|
||||
|
||||
It's not much fun updating the template to test our custom pipe.
|
||||
We could upgrade the example to a "Power Boost Calculator" that combines
|
||||
our pipe and two-way data binding with `ngModel`.
|
||||
|
||||
+makeExample('pipes/ts/app/power-boost-calculator.component.ts', null, '/app/power-boost-calculator.component.ts')(format='.')
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/pipes/power-boost-calculator-anim.gif' alt="Power Boost Calculator")
|
||||
|
||||
.l-main-section
|
||||
a#change-detection
|
||||
:marked
|
||||
## Pipes and Change Detection
|
||||
|
||||
Angular looks for changes to data-bound values through a *change detection* process that runs after every JavaScript event:
|
||||
every keystroke, mouse move, timer tick, and server response. This could be expensive.
|
||||
Angular strives to lower the cost whenever possible and appropriate.
|
||||
|
||||
Angular picks a simpler, faster change detection algorithm when we use a pipe. Let's see how.
|
||||
|
||||
### No pipe
|
||||
|
||||
The component in our next example uses the default, aggressive change detection strategy to monitor and update
|
||||
its display of every hero in the `heroes` #{_array}. Here's the template:
|
||||
|
||||
+makeExample('pipes/ts/app/flying-heroes.component.html', 'template-1', 'app/flying-heroes.component.html (v1)')(format='.')
|
||||
|
||||
:marked
|
||||
The companion component class provides heroes, adds new heroes into the #{_array}, and can reset the #{_array}.
|
||||
+makeExample('pipes/ts/app/flying-heroes.component.ts', 'v1', 'app/flying-heroes.component.ts (v1)')(format='.')
|
||||
|
||||
:marked
|
||||
We can add a new hero and Angular updates the display when we do.
|
||||
The `reset` button replaces `heroes` with a new #{_array} of the original heroes and Angular updates the display when we do.
|
||||
If we added the ability to remove or change a hero, Angular would detect those changes too and update the display as well.
|
||||
|
||||
### Flying Heroes pipe
|
||||
|
||||
Let's add a `FlyingHeroesPipe` to the `*ngFor` repeater that filters the list of heroes to just those heroes who can fly.
|
||||
+makeExample('pipes/ts/app/flying-heroes.component.html', 'template-flying-heroes', 'app/flying-heroes.component.html (flyers)')(format='.')
|
||||
:marked
|
||||
Here's the `FlyingHeroesPipe` implementation which follows the pattern for custom pipes we saw earlier.
|
||||
+makeExample('pipes/ts/app/flying-heroes.pipe.ts', 'pure', 'app/flying-heroes.pipe.ts')(format='.')
|
||||
|
||||
:marked
|
||||
When we run the sample now we see odd behavior (try it in the <live-example></live-example>).
|
||||
Every hero we add is a flying hero but none of them are displayed.
|
||||
|
||||
Although we're not getting the behavior we want, Angular isn't broken.
|
||||
It's just using a different change detection algorithm — one that ignores changes to the list or any of its items.
|
||||
|
||||
Look at how we're adding a new hero:
|
||||
+makeExample('pipes/ts/app/flying-heroes.component.ts', 'push')(format='.')
|
||||
:marked
|
||||
We're adding the new hero into the `heroes` #{_array}. The reference to the #{_array} hasn't changed.
|
||||
It's the same #{_array}. That's all Angular cares about. From its perspective, *same #{_array}, no change, no display update*.
|
||||
|
||||
We can fix that. Let's create a new #{_array} with the new hero appended and assign that to `heroes`.
|
||||
This time Angular detects that the #{_array} reference has changed.
|
||||
It executes the pipe and updates the display with the new #{_array} which includes the new flying hero.
|
||||
|
||||
*If we **mutate** the #{_array}, no pipe is invoked and no display updated;
|
||||
if we **replace** the #{_array}, then the pipe executes and the display is updated*.
|
||||
The *Flying Heroes* extends the
|
||||
code with checkbox switches and additional displays to help us experience these effects.
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/pipes/flying-heroes-anim.gif' alt="Flying Heroes")
|
||||
|
||||
:marked
|
||||
Replacing the #{_array} is an efficient way to signal to Angular that it should update the display.
|
||||
When do we replace the #{_array}? When the data change.
|
||||
That's an easy rule to follow in *this toy* example
|
||||
where the only way to change the data is by adding a new hero.
|
||||
|
||||
More often we don't know when the data have changed,
|
||||
especially in applications that mutate data in many ways,
|
||||
perhaps in application locations far away.
|
||||
A component in such an application usually can't know about those changes.
|
||||
Moreover, it's unwise to distort our component design to accommodate a pipe.
|
||||
We strive as much as possible to keep the component class independent of the HTML.
|
||||
The component should be unaware of pipes.
|
||||
|
||||
Perhaps we should consider a different kind of pipe for filtering flying heroes, an *impure pipe*.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Pure and Impure Pipes
|
||||
|
||||
There are two categories of pipes: **pure** and **impure**.
|
||||
Pipes are pure by default. Every pipe we've seen so far has been pure.
|
||||
We make a pipe impure by setting its pure flag to false. We could make the `FlyingHeroesPipe`
|
||||
impure like this:
|
||||
|
||||
+makeExample('pipes/ts/app/flying-heroes.pipe.ts', 'pipe-decorator')(format='.')
|
||||
|
||||
:marked
|
||||
Before we do that, let's understand the difference between *pure* and *impure*, starting with a *pure* pipe.
|
||||
|
||||
### Pure pipes
|
||||
|
||||
block pure-change
|
||||
:marked
|
||||
Angular executes a *pure pipe* only when it detects a *pure change* to the input value.
|
||||
A ***pure change*** is *either* a change to a primitive input value (`String`, `Number`, `Boolean`, `Symbol`)
|
||||
*or* a changed object reference (`Date`, `Array`, `Function`, `Object`).
|
||||
|
||||
:marked
|
||||
Angular ignores changes *within* (composite) objects.
|
||||
It won't call a pure pipe if we change an input month, add to an input #{_array}, or update an input object property.
|
||||
|
||||
This may seem restrictive but is is also fast.
|
||||
An object reference check is fast — much faster than a deep check for
|
||||
differences — so Angular can quickly determine if it can skip both the
|
||||
pipe execution and a view update.
|
||||
|
||||
For this reason, we prefer a pure pipe if we can live with the change detection strategy.
|
||||
When we can't, we *may* turn to the impure pipe.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Or we might not use a pipe at all.
|
||||
It may be better to pursue the pipe's purpose with a property of the component,
|
||||
a point we take up later.
|
||||
|
||||
:marked
|
||||
### Impure pipes
|
||||
|
||||
Angular executes an *impure pipe* during *every* component change detection cycle.
|
||||
An impure pipe will be called a lot, as often as every keystroke or mouse-move.
|
||||
|
||||
With that concern in mind, we must implement an impure pipe with great care.
|
||||
An expensive, long-running pipe could destroy the user experience.
|
||||
|
||||
<a id="impure-flying-heroes"></a>
|
||||
### An impure *FlyingHeroesPipe*
|
||||
|
||||
A flip of the switch turns our `FlyingHeroesPipe` into a `FlyingHeroesImpurePipe`.
|
||||
Here's the complete implementation:
|
||||
+makeTabs(
|
||||
'pipes/ts/app/flying-heroes.pipe.ts, pipes/ts/app/flying-heroes.pipe.ts',
|
||||
'impure, pure',
|
||||
'FlyingHeroesImpurePipe, FlyingHeroesPipe')(format='.')
|
||||
|
||||
:marked
|
||||
We inherit from `FlyingHeroesPipe` to prove the point that nothing changed internally.
|
||||
The only difference is the `pure` flag in the pipe metadata.
|
||||
|
||||
This is a good candidate for an impure pipe because the `transform` function is trivial and fast.
|
||||
+makeExample('pipes/ts/app/flying-heroes.pipe.ts','filter')(format='.')
|
||||
|
||||
We can derive a `FlyingHeroesImpureComponent` that we derive from the `FlyingHeroesComponent`.
|
||||
+makeExample('pipes/ts/app/flying-heroes.component.ts','impure-component','app/flying-heroes.component.ts (FlyingHeroesImpureComponent)')(format='.')
|
||||
:marked
|
||||
The only substantive change is the pipe.
|
||||
We can confirm in the <live-example></live-example> that the _flying heroes_
|
||||
display updates as we enter new heroes even when we mutate the `heroes` #{_array}.
|
||||
|
||||
- var _dollar = _docsFor === 'ts' ? '$' : '';
|
||||
h3#async-pipe The impure #[i AsyncPipe]
|
||||
:marked
|
||||
The Angular `AsyncPipe` is an interesting example of an impure pipe.
|
||||
The `AsyncPipe` accepts a `#{_Promise}` or `#{_Observable}` as input
|
||||
and subscribes to the input automatically, eventually returning the emitted value(s).
|
||||
|
||||
It is also stateful.
|
||||
The pipe maintains a subscription to the input `#{_Observable}` and
|
||||
keeps delivering values from that `#{_Observable}` as they arrive.
|
||||
|
||||
In this next example, we bind an `#{_Observable}` of message strings
|
||||
(`message#{_dollar}`) to a view with the `async` pipe.
|
||||
|
||||
+makeExample('pipes/ts/app/hero-async-message.component.ts', null, 'app/hero-async-message.component.ts')
|
||||
|
||||
:marked
|
||||
The Async pipe saves boilerplate in the component code.
|
||||
The component doesn't have to subscribe to the async data source,
|
||||
it doesn't extract the resolved values and expose them for binding,
|
||||
and the component doesn't have to unsubscribe when it is destroyed
|
||||
(a potent source of memory leaks).
|
||||
|
||||
### An impure caching pipe
|
||||
|
||||
Let's write one more impure pipe, a pipe that makes an HTTP request to the server.
|
||||
Normally, that's a horrible idea.
|
||||
It's probably a horrible idea no matter what we do.
|
||||
We're forging ahead anyway to make a point.
|
||||
Remember that impure pipes are called every few microseconds.
|
||||
If we're not careful, this pipe will punish the server with requests.
|
||||
|
||||
We are careful. Our pipe only makes a server call if the request URL has changed.
|
||||
It caches the request URL and waits for a result which it also caches when it arrives.
|
||||
The pipe returns the cached result (which is null while a request is in flight)
|
||||
after every Angular call and only contacts the server as necessary.
|
||||
|
||||
Here's the code, which uses the [Angular http](server-communication.html) facility
|
||||
to retrieve a `heroes.json` file:
|
||||
|
||||
+makeExample('pipes/ts/app/fetch-json.pipe.ts', null, 'app/fetch-json.pipe.ts')
|
||||
:marked
|
||||
Then we demonstrate it in a harness component whose template defines two bindings to this pipe.
|
||||
+makeExample('pipes/ts/app/hero-list.component.ts', 'template', 'app/hero-list.component.ts (template)')
|
||||
:marked
|
||||
Despite the two bindings and what we know to be frequent pipe calls,
|
||||
the nework tab in the browser developer tools confirms that there is only one request for the file.
|
||||
|
||||
The component renders like this:
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/pipes/hero-list.png' alt="Hero List")
|
||||
|
||||
:marked
|
||||
### *JsonPipe*
|
||||
|
||||
The second binding involving the `FetchPipe` uses more pipe chaining.
|
||||
We take the same fetched results displayed in the first binding
|
||||
and display them again, this time in JSON format by chaining through to the built-in `JsonPipe`.
|
||||
|
||||
.callout.is-helpful
|
||||
header Debugging with the json pipe
|
||||
:marked
|
||||
The [JsonPipe](../api/common/index/JsonPipe-class.html)
|
||||
provides an easy way to diagnosis a mysteriously failing data binding or
|
||||
inspect an object for future binding.
|
||||
|
||||
:marked
|
||||
Here's the complete component implementation:
|
||||
|
||||
+makeExample('pipes/ts/app/hero-list.component.ts', null, 'app/hero-list.component.ts')
|
||||
|
||||
a(id="pure-pipe-pure-fn")
|
||||
:marked
|
||||
### Pure pipes and pure functions
|
||||
|
||||
A pure pipe uses pure functions.
|
||||
Pure functions process inputs and return values without detectable side-effects.
|
||||
Given the same input they should always return the same output.
|
||||
|
||||
The pipes we saw earlier in this chapter were implemented with pure functions.
|
||||
The built-in `DatePipe` is a pure pipe with a pure function implementation.
|
||||
So is our `ExponentialStrengthPipe`.
|
||||
So is our `FlyingHeroesPipe`.
|
||||
A few steps back we reviewed the `FlyingHeroesImpurePipe` — *an impure pipe with a pure function*.
|
||||
|
||||
But a *pure pipe* must always be implemented with a *pure function*. Failure to heed this warning will bring about many a console errors regarding expressions that have changed after they were checked.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Next Steps
|
||||
|
||||
Pipes are a great way to encapsulate and share common display-value
|
||||
transformations. We use them like styles, dropping them
|
||||
into our templates expressions to enrich the appeal and usability
|
||||
of our views.
|
||||
|
||||
Explore Angular's inventory of built-in pipes in the [API Reference](../api/#!?apiFilter=pipe).
|
||||
Try writing a custom pipe and perhaps contributing it to the community.
|
||||
|
||||
a(id="no-filter-pipe")
|
||||
.l-main-section
|
||||
:marked
|
||||
## No *FilterPipe* or *OrderByPipe*
|
||||
|
||||
Angular does not ship with pipes for filtering or sorting lists.
|
||||
Developers familiar with Angular 1 know these as `filter` and `orderBy`.
|
||||
There are no equivalents in Angular 2.
|
||||
|
||||
This is not an oversight. Angular 2 is unlikely to offer such pipes because
|
||||
(a) they perform poorly and (b) they prevent aggressive minification.
|
||||
Both `filter` and `orderBy` require parameters that reference object properties.
|
||||
We learned earlier that such pipes must be [*impure*](#pure-and-impure-pipes) and that
|
||||
Angular calls impure pipes in almost every change detection cycle.
|
||||
|
||||
Filtering and especially sorting are expensive operations.
|
||||
The user experience can degrade severely for even moderate sized lists when Angular calls these pipe methods many times per second.
|
||||
The `filter` and `orderBy` have often been abused in Angular 1 apps, leading to complaints that Angular itself is slow.
|
||||
That charge is fair in the indirect sense that Angular 1 prepared this performance trap
|
||||
by offering `filter` and `orderBy` in the first place.
|
||||
|
||||
The minification hazard is also compelling if less obvious. Imagine a sorting pipe applied to a list of heroes.
|
||||
We might sort the list by hero `name` and `planet` of origin properties something like this:
|
||||
code-example(language="html")
|
||||
<!-- NOT REAL CODE! -->
|
||||
<div *ngFor="let hero of heroes | orderBy:'name,planet'"></div>
|
||||
:marked
|
||||
We identify the sort fields by text strings, expecting the pipe to reference a property value by indexing
|
||||
(e.g., `hero['name']`).
|
||||
Unfortunately, aggressive minification *munges* the `Hero` property names so that `Hero.name` and `Hero.planet`
|
||||
becomes something like `Hero.a` and `Hero.b`. Clearly `hero['name']` is not going to work.
|
||||
|
||||
Some of us may not care to minify this aggressively. That's *our* choice.
|
||||
But the Angular product should not prevent someone else from minifying aggressively.
|
||||
Therefore, the Angular team decided that everything shipped in Angular will minify safely.
|
||||
|
||||
The Angular team and many experienced Angular developers strongly recommend that you move
|
||||
filtering and sorting logic into the component itself.
|
||||
The component can expose a `filteredHeroes` or `sortedHeroes` property and take control
|
||||
over when and how often to execute the supporting logic.
|
||||
Any capabilities that you would have put in a pipe and shared across the app can be
|
||||
written in a filtering/sorting service and injected into the component.
|
||||
|
||||
If these performance and minification considerations do not apply to you, you can always create your own such pipes
|
||||
(along the lines of the [FlyingHeroesPipe](#impure-flying-heroes)) or find them in the community.
|
|
@ -0,0 +1,263 @@
|
|||
block includes
|
||||
include ../_util-fns
|
||||
:marked
|
||||
Web application security has many aspects. This chapter describes Angular's built in
|
||||
protections against common web application vulnerabilities and attacks, such as Cross Site
|
||||
Scripting Attacks. It does not cover application level security, such as authentication (_Who is
|
||||
this user?_) or authorization (_What can this user do?_).
|
||||
|
||||
The [Open Web Application Security Project (OWASP)](https://www.owasp.org/index.php/Category:OWASP_Guide_Project)
|
||||
has further information on the attacks and mitigations described below.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
# Table Of Contents
|
||||
|
||||
* [Reporting Vulnerabilities](#report-issues)
|
||||
* [Best Practices](#best-practices)
|
||||
* [Preventing Cross-Site Scripting (XSS)](#xss)
|
||||
* [Trusting Safe Values](#bypass-security-apis)
|
||||
* [HTTP-level Vulnerabilities](#http)
|
||||
* [Auditing Angular Applications](#code-review)
|
||||
|
||||
Try the <live-example></live-example> of the code shown in this chapter.
|
||||
|
||||
.l-main-section
|
||||
h2#report-issues Reporting Vulnerabilities
|
||||
:marked
|
||||
Email us at [security@angular.io](mailto:security@angular.io) to report vulnerabilities in
|
||||
Angular itself.
|
||||
|
||||
For further details on how Google handles security issues please refer to [Google's security
|
||||
philosophy](https://www.google.com/about/appsecurity/).
|
||||
|
||||
.l-main-section
|
||||
h2#best-practices Best Practices
|
||||
:marked
|
||||
* **Keep current with the latest Angular library releases.**
|
||||
We regularly update our Angular libraries and these updates may fix security defects discovered in
|
||||
previous version. Check the Angular [change
|
||||
log](https://github.com/angular/angular/blob/master/CHANGELOG.md) for security-related updates.
|
||||
|
||||
* **Don't modify your copy of Angular.**
|
||||
Private, customized versions of Angular tend to fall behind the current version and may neglect
|
||||
important security fixes and enhancements. Instead, share your Angular improvements with the
|
||||
community and make a pull request.
|
||||
|
||||
* **Avoid Angular APIs marked in the documentation as “[_Security Risk_](#bypass-security-apis)”.**
|
||||
|
||||
.l-main-section
|
||||
h2#xss Preventing Cross-Site Scripting (XSS)
|
||||
:marked
|
||||
[Cross-Site Scripting (XSS)](https://en.wikipedia.org/wiki/Cross-site_scripting) enables attackers
|
||||
to inject malicious code into web pages. Such code can then, for example, steal user's data (in
|
||||
particular their login data), or perform actions impersonating the user. This is one of the most
|
||||
common attacks on the web.
|
||||
|
||||
To block XSS attacks, we must prevent malicious code from entering the DOM. For example, if an
|
||||
attacker can trick us into inserting a `<script>` tag in the DOM, they can run arbitrary code on
|
||||
our website. The attack is not limited to `<script>` tags - many elements and properties in the
|
||||
DOM allow code execution, for example `<img onerror="...">`, `<a href="javascript:...">`. If
|
||||
attacker controlled data enters the DOM, we have to expect security vulnerabilities.
|
||||
|
||||
### Angular’s Cross-site Scripting Security Model
|
||||
|
||||
To systematically block XSS bugs, Angular treats all values as untrusted by default. When a value
|
||||
is inserted into the DOM from a template, via property, attribute, style, or class binding, or via
|
||||
interpolation, Angular will sanitize and escape untrusted values.
|
||||
|
||||
**Angular templates are the same as executable code**: HTML, attributes, and binding expressions
|
||||
(but not the values bound!) in templates are trusted to be safe. That means applications must
|
||||
prevent potentially attacker controlled values from ever making it into the source code of a
|
||||
template. Never generate template source code by concatenating user input and templates! Using
|
||||
the [offline template compiler](#offline-template-compiler) is an effective way to prevent these
|
||||
vulnerabilities, also known as template injection.
|
||||
|
||||
### Sanitization and security contexts
|
||||
|
||||
Sanitization inspects an untrusted value and turns it into a value that is safe to insert into
|
||||
the DOM. In many cases, values do not get changed by this at all. Sanitization depends on context:
|
||||
a value that is harmless in CSS is potentially dangerous in a URL.
|
||||
|
||||
Angular defines four security contexts: HTML, style, URL, and resource URL.
|
||||
|
||||
* HTML is used when interpreting a value as HTML, e.g., when binding to `innerHtml`
|
||||
* Style is used when binding CSS into the `style` property
|
||||
* URL is used for URL properties such as `<a href>`
|
||||
* Resource URLs are URLs that will be loaded and executed as code, e.g., in `<script src>`
|
||||
|
||||
Angular sanitizes untrusted values for the first three items; sanitizing resource URLs is not
|
||||
possible as they contain arbitrary code. In development mode, Angular prints a console warning
|
||||
when it has to change a value during sanitization.
|
||||
|
||||
### Sanitization example
|
||||
|
||||
The template below binds the value of `htmlSnippet`, once by interpolating it into an element's
|
||||
content, and once by binding it to the `innerHTML` property of an element.
|
||||
|
||||
+makeExample('app/inner-html-binding.component.html')
|
||||
|
||||
:marked
|
||||
Interpolated content is always escaped - the HTML is not interpreted, and the browser displays
|
||||
angle brackets in the elements text content.
|
||||
|
||||
For the HTML to be interpreted, we must bind to an HTML property, such as `innerHTML`. But binding
|
||||
a potentially attacker controlled value into `innerHTML` would normally cause an XSS
|
||||
vulnerability. For example, code contained in a `<script>` tag would be executed.
|
||||
|
||||
+makeExcerpt('app/inner-html-binding.component.ts ()', 'inner-html-controller')
|
||||
|
||||
:marked
|
||||
Angular recognizes the value as unsafe, and automatically sanitizes it. It removes the `<script>`
|
||||
tag but keeps safe content, such as the text content of the `<script>` tag, or the `<b>` element.
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/security/binding-inner-html.png'
|
||||
alt='A screenshot showing interpolated and bound HTML values')
|
||||
:marked
|
||||
### Avoid direct use of the DOM APIs
|
||||
|
||||
The built-in browser DOM APIs do not automatically protect you from security vulnerabilities.
|
||||
For example, `document`, the node available through `ElementRef`, and many third party APIs
|
||||
contain unsafe methods. Avoid directly interacting with the DOM, and instead use Angular
|
||||
templates where possible.
|
||||
|
||||
### Content Security Policy
|
||||
|
||||
A [Content Security Policy (CSP)](http://www.html5rocks.com/en/tutorials/security/content-security-policy/) is a defense-in-depth
|
||||
technique to prevent XSS. To enable CSP, configure your web server to return an appropriate
|
||||
`Content-Security-Policy` HTTP header.
|
||||
|
||||
<a id="offline-template-compiler"></a>
|
||||
### Use the Offline Template Compiler
|
||||
|
||||
The offline template compiler prevents a whole class of vulnerabilities called template injection,
|
||||
and also greatly improves application performance. Use the offline template compiler in production
|
||||
deployments. Do not dynamically generate templates. Angular trusts template code, so generating
|
||||
templates, in particular containing user data, circumvents Angular's built-in protections. See the
|
||||
[Dynamic Forms Cookbook](../cookbook/dynamic-form.html) on how to dynamically construct forms in a
|
||||
safe way.
|
||||
|
||||
### Server side XSS protection
|
||||
|
||||
HTML constructed on the server is vulnerable to injection attacks. Injecting template code into an
|
||||
Angular application is the same as injecting executable code into the
|
||||
application; it gives the attacker full control over the application. To prevent this, make sure
|
||||
to use a templating language that automatically escapes values to prevent XSS vulnerabilities on
|
||||
the server. Do not generate Angular templates on the server side using a templating language, this
|
||||
carries a high risk of introducing template injection vulnerabilities.
|
||||
|
||||
.l-main-section
|
||||
h2#bypass-security-apis Trusting Safe Values
|
||||
:marked
|
||||
Sometimes applications genuinely need to include executable code, display an `<iframe>` from some
|
||||
URL, or construct potentially dangerous URLs. To prevent automatic sanitization in this situation,
|
||||
you can tell Angular that you inspected a value, checked how it is generated, and made sure it is
|
||||
always secure. But **be careful**! If you trust a value that can be malicious, you will likely
|
||||
introduce a security vulnerability into your application. If in doubt, find a professional
|
||||
security reviewer.
|
||||
|
||||
You can mark a value as trusted by injecting `DomSanitizationService`, and calling one of the
|
||||
following methods.
|
||||
|
||||
* `bypassSecurityTrustHtml`
|
||||
* `bypassSecurityTrustScript`
|
||||
* `bypassSecurityTrustStyle`
|
||||
* `bypassSecurityTrustUrl`
|
||||
* `bypassSecurityTrustResourceUrl`
|
||||
|
||||
Remember, whether a value is safe depends on context, so you need to choose the right context for
|
||||
your intended use of the value. Imagine the following template needs to bind a URL to a
|
||||
`javascript:alert(...)` call.
|
||||
|
||||
+makeExcerpt('app/bypass-security.component.html ()', 'dangerous-url')
|
||||
|
||||
:marked
|
||||
Normally, Angular automatically sanitizes the URL, disables the dangerous code and,
|
||||
in development mode, logs this action to the console. To prevent
|
||||
this, we can mark the URL value as a trusted URL using the `bypassSecurityTrustUrl` call:
|
||||
|
||||
+makeExcerpt('app/bypass-security.component.ts ()', 'trust-url')
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/security/bypass-security-component.png'
|
||||
alt='A screenshot showing an alert box created from a trusted URL')
|
||||
|
||||
:marked
|
||||
If we need to convert user input into a trusted value, it can be convenient to do so in a
|
||||
controller method. The template below allows users to enter a YouTube video ID, and load the
|
||||
corresponding video in an `<iframe>`. The `<iframe src>` attribute is a resource URL security
|
||||
context, because an untrusted source can, e.g., smuggle in file downloads that unsuspecting users
|
||||
would execute. So we call a method on the controller to construct a trusted video URL, which
|
||||
Angular then allows binding into `<iframe src>`.
|
||||
|
||||
+makeExcerpt('app/bypass-security.component.html ()', 'iframe-videoid')
|
||||
+makeExcerpt('app/bypass-security.component.ts ()', 'trust-video-url')
|
||||
|
||||
.l-main-section
|
||||
h2#http HTTP-level Vulnerabilities
|
||||
:marked
|
||||
Angular has built in support to help prevent two common HTTP vulnerabilities, Cross-site Request
|
||||
Forgery (XSRF) and Cross-site Script Inclusion (XSSI). Both of these must be primarily mitigated
|
||||
on the server side, but Angular ships helpers to make integration on the client side easier.
|
||||
|
||||
h3#xsrf Cross-site Request Forgery (XSRF)
|
||||
:marked
|
||||
In a Cross-site Request Forgery (XSRF or CSRF), an attacker tricks the user into visiting a
|
||||
_different_ page, and has them, e.g., submit a form that sends a request to your application's
|
||||
web server. If the user is logged into your application, the browser will send authentication
|
||||
cookies, and the attacker could — for example — cause a bank transfer in the user's name with
|
||||
the right request.
|
||||
|
||||
To prevent this, your application must ensure that user requests originate in your own
|
||||
application, not on a different site. A common technique is that the server sends a randomly
|
||||
generated authentication token in a cookie, often with the name `XSRF-TOKEN`. Cookies can only
|
||||
be read by the website on which they are set, so only your own application can read this token. On
|
||||
each API request, the server then validates the client by checking that the token is sent back,
|
||||
usually in an HTTP header called `X-XSRF-TOKEN`.
|
||||
|
||||
The Angular `http` client has built-in support for this technique. The default
|
||||
`CookieXSRFStrategy` looks for a cookie called `XSRF-TOKEN` and sets an HTTP request header named
|
||||
`X-XSRF-TOKEN` with the value of that cookie on every request. The server must set the
|
||||
`XSRF-TOKEN` cookie, and validate the response header for each state modifying request.
|
||||
|
||||
XSRF tokens should be unique per user and session, have a large random value generated by a
|
||||
cryptographically secure random number generator, and expire.
|
||||
|
||||
Angular applications can customize cookie and header names by binding their own
|
||||
`CookieXSRFStrategy` value, or implement an entirely custom `XSRFStrategy` by providing a custom
|
||||
binding for that type, by adding either of the following to your providers list:
|
||||
|
||||
code-example(language="typescript").
|
||||
{ provide: XSRFStrategy, useValue: new CookieXSRFStrategy('myCookieName', 'My-Header-Name')}
|
||||
{ provide: XSRFStrategy, useClass: MyXSRFStrategy}
|
||||
|
||||
:marked
|
||||
Learn about Cross Site Request Forgery (XSRF) at the Open Web Application Security Project (OWASP)
|
||||
[here](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29) and
|
||||
[here](https://www.owasp.org/index.php/CSRF_Prevention_Cheat_Sheet). This [Stanford University
|
||||
paper](https://seclab.stanford.edu/websec/csrf/csrf.pdf) is also a rich source of detail.
|
||||
|
||||
h3#xssi Cross-site Script Inclusion (XSSI)
|
||||
:marked
|
||||
Cross-site Script Inclusion, also known as JSON vulnerability, can allow an attacker's website to
|
||||
read data from a JSON API. The attack works on older browser by overriding native JavaScript
|
||||
object constructors, and then including an API URL using a `<script>` tag.
|
||||
|
||||
This attack is only successful if the returned JSON is executable as JavaScript. Servers can
|
||||
prevent it by prefixing all JSON responses to make them non-executable, by convention using the
|
||||
well-known string `")]}',\n"`.
|
||||
|
||||
Angular's `Http` library recognizes this convention and automatically strips the string
|
||||
`")]}',\n"` from all responses before further parsing.
|
||||
|
||||
Learn more in the XSSI section of this [Google web security blog
|
||||
post](https://security.googleblog.com/2011/05/website-security-for-webmasters.html)
|
||||
|
||||
.l-main-section
|
||||
h2#code-review Auditing Angular Applications
|
||||
:marked
|
||||
Angular applications should follow the same security principles as regular web applications, and
|
||||
should be audited as such. Angular specific APIs that should be audited in a security review,
|
||||
such as the [_bypassSecurityTrust_](#bypass-security-apis) APIs, are marked in the documentation
|
||||
as security sensitive.
|
|
@ -0,0 +1,702 @@
|
|||
block includes
|
||||
include ../_util-fns
|
||||
- var _Http = 'Http'; // Angular `Http` library name.
|
||||
- var _Angular_Http = 'Angular <code>Http</code>'
|
||||
- var _Angular_http_library = 'Angular HTTP library'
|
||||
|
||||
:marked
|
||||
[HTTP](https://tools.ietf.org/html/rfc2616) is the primary protocol for browser/server communication.
|
||||
.l-sub-section
|
||||
:marked
|
||||
The [`WebSocket`](https://tools.ietf.org/html/rfc6455) protocol is another important communication technology;
|
||||
we won't cover it in this chapter.
|
||||
:marked
|
||||
Modern browsers support two HTTP-based APIs:
|
||||
[XMLHttpRequest (XHR)](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) and
|
||||
[JSONP](https://en.wikipedia.org/wiki/JSONP). A few browsers also support
|
||||
[Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
|
||||
|
||||
The !{_Angular_http_library} simplifies application programming of the **XHR** and **JSONP** APIs
|
||||
as we'll learn in this chapter covering:
|
||||
|
||||
- [HTTP client sample overview](#http-client)
|
||||
- [Fetch data with http.get](#fetch-data)
|
||||
<li if-docs="ts"> [RxJS Observable of HTTP Responses](#rxjs)</li>
|
||||
<li if-docs="ts"> [Enabling RxJS Operators](#enable-rxjs-operators)</li>
|
||||
- [Extract JSON data](#extract-data)
|
||||
- [Error handling](#error-handling)
|
||||
- [Send data to the server](#update)
|
||||
<li if-docs="ts"> [Promises instead of observables](#promises)</li>
|
||||
- [Cross-origin requests: Wikipedia example](#cors)
|
||||
<ul if-docs="ts">
|
||||
<li> [Set query string parameters](#search-parameters)</li>
|
||||
<li> [Debounce search term input](#more-observables)</li>
|
||||
</ul>
|
||||
- [Appendix: the in-memory web api service](#in-mem-web-api)
|
||||
|
||||
We illustrate these topics with code that you can <live-example>run live</live-example>.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
# Demos
|
||||
|
||||
This chapter describes server communication with the help of the following demos
|
||||
|
||||
block demos-list
|
||||
:marked
|
||||
- [HTTP client: Tour of Heroes with Observables](#http-client)
|
||||
- [HTTP client: Tour of Heroes with !{_Promise}s](#promises)
|
||||
- [JSONP client: Wikipedia to fetch data from a service that does not support CORS](#cors)
|
||||
- [JSONP client: Wikipedia using observable operators to reduce server calls](#more-observables)
|
||||
|
||||
:marked
|
||||
These demos are orchestrated by the root `AppComponent`
|
||||
+makeExample('server-communication/ts/app/app.component.ts', null, 'app/app.component.ts')
|
||||
|
||||
+ifDocsFor('ts')
|
||||
:marked
|
||||
There is nothing remarkable here _except_ for the import of RxJS operators.
|
||||
+makeExample('server-communication/ts/app/app.component.ts', 'import-rxjs')(format='.')
|
||||
:marked
|
||||
We'll talk about that [below](#rxjs) when we're ready to explore observables.
|
||||
:marked
|
||||
First, we have to configure our application to use server communication facilities.
|
||||
|
||||
.l-main-section#http-providers
|
||||
:marked
|
||||
# Providing HTTP Services
|
||||
|
||||
We use the !{_Angular_Http} client to communicate with a server using a familiar HTTP request/response protocol.
|
||||
The `!{_Http}` client is one of a family of services in the !{_Angular_http_library}.
|
||||
|
||||
+ifDocsFor('ts')
|
||||
.l-sub-section
|
||||
:marked
|
||||
SystemJS knows how to load services from the !{_Angular_http_library} when we import from the `@angular/http` module
|
||||
because we registered that module name in the `system.config` file.
|
||||
|
||||
:marked
|
||||
Before we can use the `!{_Http}` client , we'll have to register it as a service provider with the Dependency Injection system.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Learn about providers in the [Dependency Injection](dependency-injection.html) chapter.
|
||||
|
||||
:marked
|
||||
In this demo, we register providers in the `bootstrap()` method of
|
||||
<span ngio-ex>app/main.ts</span>.
|
||||
|
||||
+makeExample('server-communication/ts/app/main.ts', 'v1', 'app/main.ts (v1)')(format='.')
|
||||
|
||||
block http-providers
|
||||
:marked
|
||||
We begin by importing the symbols we need, most of them familiar by now. The newcomer is `HTTP_PROVIDERS`,
|
||||
a collection of service providers from the !{_Angular_http_library}.
|
||||
|
||||
We register HTTP providers in the bootstrap method by passing them in an array as the second parameter after the root component.
|
||||
|
||||
### Why register in *bootstrap*?
|
||||
|
||||
We prefer to register application-wide providers in the metadata `providers` array
|
||||
of the root `AppComponent` like this:
|
||||
+makeExample('server-communication/ts/app/app.component.ts','http-providers')(format='.')
|
||||
:marked
|
||||
Here we register the providers in the `bootstrap` method in the `main.ts` file. Why?
|
||||
|
||||
This is a *sample application* that doesn't talk to a real server.
|
||||
We're going to reconfigure the (typically-hidden) `XhrBackend` service with a fake provider
|
||||
that fetches and saves sample data from an in-memory data store.
|
||||
This replacement service is called the [*in-memory web api*](#in-mem-web-api).
|
||||
|
||||
Such sleight-of-hand is something the root application component should *not* know about.
|
||||
For this reason, and this reason *only*, we hide it *above* the `AppComponent` in `main.ts`.
|
||||
|
||||
.l-main-section#http-client
|
||||
:marked
|
||||
# The Tour of Heroes _HTTP_ Client Demo
|
||||
|
||||
Our first demo is a mini-version of the [tutorial](../tutorial)'s "Tour of Heroes" (ToH) application.
|
||||
This version gets some heroes from the server, displays them in a list, lets us add new heroes, and saves them to the server.
|
||||
We use the !{_Angular_Http} client to communicate via `XMLHttpRequest (XHR)`.
|
||||
|
||||
It works like this.
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/server-communication/http-toh.gif' alt="ToH mini app" width="250")
|
||||
:marked
|
||||
This demo has a single component, the `HeroListComponent`. Here's its template:
|
||||
+makeExample('server-communication/ts/app/toh/hero-list.component.html', null, 'app/toh/hero-list.component.html (template)')
|
||||
:marked
|
||||
It presents the list of heroes with an `ngFor`.
|
||||
Below the list is an input box and an *Add Hero* button where we can enter the names of new heroes
|
||||
and add them to the database.
|
||||
We use a [template reference variable](template-syntax.html#ref-vars), `newHeroName`, to access the
|
||||
value of the input box in the `(click)` event binding.
|
||||
When the user clicks the button, we pass that value to the component's `addHero` method and then
|
||||
clear it to make it ready for a new hero name.
|
||||
|
||||
Below the button is an area for an error message.
|
||||
|
||||
a#oninit
|
||||
a#HeroListComponent
|
||||
:marked
|
||||
## The *HeroListComponent* class
|
||||
Here's the component class:
|
||||
+makeExample('server-communication/ts/app/toh/hero-list.component.ts','component', 'app/toh/hero-list.component.ts (class)')
|
||||
:marked
|
||||
Angular [injects](dependency-injection.html) a `HeroService` into the constructor
|
||||
and the component calls that service to fetch and save data.
|
||||
|
||||
The component **does not talk directly to the !{_Angular_Http} client**!
|
||||
The component doesn't know or care how we get the data.
|
||||
It delegates to the `HeroService`.
|
||||
|
||||
This is a golden rule: **always delegate data access to a supporting service class**.
|
||||
|
||||
Although _at runtime_ the component requests heroes immediately after creation,
|
||||
we do **not** call the service's `get` method in the component's constructor.
|
||||
We call it inside the `ngOnInit` [lifecycle hook](lifecycle-hooks.html) instead
|
||||
and count on Angular to call `ngOnInit` when it instantiates this component.
|
||||
.l-sub-section
|
||||
:marked
|
||||
This is a *best practice*.
|
||||
Components are easier to test and debug when their constructors are simple and all real work
|
||||
(especially calling a remote server) is handled in a separate method.
|
||||
block getheroes-and-addhero
|
||||
:marked
|
||||
The service's `getHeroes()` and `addHero()` methods return an `Observable` of hero data that the !{_Angular_Http} client fetched from the server.
|
||||
|
||||
*Observables* are a big topic, beyond the scope of this chapter.
|
||||
But we need to know a little about them to appreciate what is going on here.
|
||||
|
||||
We should think of an `Observable` as a stream of events published by some source.
|
||||
We listen for events in this stream by ***subscribing*** to the `Observable`.
|
||||
In these subscriptions we specify the actions to take when the web request
|
||||
produces a success event (with the hero data in the event payload) or a fail event (with the error in the payload).
|
||||
|
||||
:marked
|
||||
With our basic intuitions about the component squared away, we're ready to look inside the `HeroService`.
|
||||
|
||||
a#HeroService
|
||||
.l-main-section#fetch-data
|
||||
:marked
|
||||
## Fetch data with the **HeroService**
|
||||
|
||||
In many of our previous samples we faked the interaction with the server by
|
||||
returning mock heroes in a service like this one:
|
||||
+makeExample('toh-4/ts/app/hero.service.ts', 'just-get-heroes')(format=".")
|
||||
:marked
|
||||
In this chapter, we revise that `HeroService` to get the heroes from the server using the !{_Angular_Http} client service:
|
||||
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'v1', 'app/toh/hero.service.ts (revised)')
|
||||
|
||||
:marked
|
||||
Notice that the !{_Angular_Http} client service is
|
||||
[injected](dependency-injection.html) into the `HeroService` constructor.
|
||||
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'ctor')
|
||||
:marked
|
||||
Look closely at how we call `!{_priv}http.get`
|
||||
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'http-get', 'app/toh/hero.service.ts (getHeroes)')(format=".")
|
||||
:marked
|
||||
We pass the resource URL to `get` and it calls the server which should return heroes.
|
||||
.l-sub-section
|
||||
:marked
|
||||
It *will* return heroes once we've set up the [in-memory web api](#in-mem-web-api)
|
||||
described in the appendix below.
|
||||
Alternatively, we can (temporarily) target a JSON file by changing the endpoint URL:
|
||||
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'endpoint-json')(format=".")
|
||||
|
||||
+ifDocsFor('ts')
|
||||
:marked
|
||||
<a id="rxjs"></a>
|
||||
The return value may surprise us.
|
||||
Many of us who are familiar with asynchronous methods in modern JavaScript would expect the `get` method to return a
|
||||
[promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
|
||||
We'd expect to chain a call to `then()` and extract the heroes.
|
||||
Instead we're calling a `map()` method.
|
||||
Clearly this is not a promise.
|
||||
|
||||
In fact, the `http.get` method returns an **Observable** of HTTP Responses (`Observable<Response>`) from the RxJS library
|
||||
and `map` is one of the RxJS *operators*.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
# RxJS Library
|
||||
[RxJS](https://github.com/ReactiveX/RxJS) ("Reactive Extensions") is a 3rd party library, endorsed by Angular,
|
||||
that implements the [*asynchronous observable*](https://www.youtube.com/watch?v=UHI0AzD_WfY "Rob Wormald on observables") pattern.
|
||||
|
||||
All of our Developer Guide samples have installed the RxJS npm package and loaded via `system.js`
|
||||
because observables are used widely in Angular applications.
|
||||
We certainly need it now when working with the HTTP client.
|
||||
And we must take a critical extra step to make RxJS observables usable.
|
||||
|
||||
### Enable RxJS Operators
|
||||
The RxJS library is quite large.
|
||||
Size matters when we build a production application and deploy it to mobile devices.
|
||||
We should include only those features that we actually need.
|
||||
|
||||
Accordingly, Angular exposes a stripped down version of `Observable` in the `rxjs/Observable` module,
|
||||
a version that lacks most of the operators including some we'd like to use here
|
||||
such as the `map` method we called above in `getHeroes`.
|
||||
|
||||
It's up to us to add the operators we need.
|
||||
|
||||
We could add _every_ RxJS operators with a single import statement.
|
||||
While that is the easiest thing to do, we'd pay a penalty in extended launch time and application size
|
||||
because the full library is so big. We only use a few operators in our app.
|
||||
|
||||
Instead, we'll import each `Observable` operator and static class method, one-by-one, until we have a custom *Observable* implementation tuned
|
||||
precisely to our requirements. We'll put the `import` statements in one `app/rxjs-operators.ts` file.
|
||||
+makeExample('server-communication/ts/app/rxjs-operators.ts', null, 'app/rxjs-operators.ts')(format=".")
|
||||
:marked
|
||||
If we forget an operator, the TypeScript compiler will warn that it's missing and we'll update this file.
|
||||
.l-sub-section
|
||||
:marked
|
||||
We don't need _all_ of these particular operators in the `HeroService` — just `map`, `catch` and `throw`.
|
||||
We'll need the other operators later, in a *Wiki* example [below](#more-observables).
|
||||
:marked
|
||||
Finally, we import `rxjs-operator`_itself_ in our `app.component.ts`:
|
||||
+makeExample('server-communication/ts/app/app.component.ts', 'import-rxjs', 'app/app.component.ts (import rxjs)')(format=".")
|
||||
:marked
|
||||
Let's return to our study of the `HeroService`.
|
||||
|
||||
l-main-section
|
||||
a#extract-data
|
||||
:marked
|
||||
## Process the response object
|
||||
Remember that our `getHeroes()` method mapped the `!{_priv}http.get` response object to heroes with an `!{_priv}extractData` helper method:
|
||||
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'extract-data', 'app/toh/hero.service.ts (excerpt)')(format=".")
|
||||
:marked
|
||||
The `response` object does not hold our data in a form we can use directly.
|
||||
To make it useful in our application we must parse the response data into a JSON object
|
||||
|
||||
#### Parse to JSON
|
||||
block parse-json
|
||||
:marked
|
||||
The response data are in JSON string form.
|
||||
We must parse that string into JavaScript objects which we do by calling `response.json()`.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
This is not Angular's own design.
|
||||
The Angular HTTP client follows the ES2015 specification for the
|
||||
[response object](https://fetch.spec.whatwg.org/#response-class) returned by the `Fetch` function.
|
||||
That spec defines a `json()` method that parses the response body into a JavaScript object.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
We shouldn't expect the decoded JSON to be the heroes !{_array} directly.
|
||||
The server we're calling always wraps JSON results in an object with a `data`
|
||||
property. We have to unwrap it to get the heroes.
|
||||
This is conventional web api behavior, driven by
|
||||
[security concerns](https://www.owasp.org/index.php/OWASP_AJAX_Security_Guidelines#Always_return_JSON_with_an_Object_on_the_outside).
|
||||
.alert.is-important
|
||||
:marked
|
||||
Make no assumptions about the server API.
|
||||
Not all servers return an object with a `data` property.
|
||||
:marked
|
||||
### Do not return the response object
|
||||
Our `getHeroes()` could have returned the HTTP response. Bad idea!
|
||||
The point of a data service is to hide the server interaction details from consumers.
|
||||
The component that calls the `HeroService` wants heroes.
|
||||
It has no interest in what we do to get them.
|
||||
It doesn't care where they come from.
|
||||
And it certainly doesn't want to deal with a response object.
|
||||
|
||||
+ifDocsFor('ts')
|
||||
.callout.is-important
|
||||
header HTTP GET is delayed
|
||||
:marked
|
||||
The `!{_priv}http.get` does **not send the request just yet!** This observable is
|
||||
[*cold*](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/creating.md#cold-vs-hot-observables)
|
||||
which means the request won't go out until something *subscribes* to the observable.
|
||||
That *something* is the [HeroListComponent](#subscribe).
|
||||
|
||||
a#error-handling
|
||||
:marked
|
||||
### Always handle errors
|
||||
|
||||
Whenever we deal with I/O we must be prepared for something to go wrong as it surely will.
|
||||
We should catch errors in the `HeroService` and do something with them.
|
||||
We may also pass an error message back to the component for presentation to the user
|
||||
but only if we can say something the user can understand and act upon.
|
||||
|
||||
In this simple app we provide rudimentary error handling in both the service and the component.
|
||||
block error-handling
|
||||
:marked
|
||||
The eagle-eyed reader may have spotted our use of the `catch` operator in conjunction with a `handleError` method.
|
||||
We haven't discussed so far how that actually works.
|
||||
|
||||
We use the Observable `catch` operator on the service level.
|
||||
It takes an error handling function with an error object as the argument.
|
||||
Our service handler, `handleError`, logs the response to the console,
|
||||
transforms the error into a user-friendly message, and returns the message in a new, failed observable via `Observable.throw`.
|
||||
|
||||
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'error-handling', 'app/toh/hero.service.ts (excerpt)')(format=".")
|
||||
|
||||
a#subscribe
|
||||
a#hero-list-component
|
||||
h4 #[b HeroListComponent] error handling
|
||||
block hlc-error-handling
|
||||
:marked
|
||||
Back in the `HeroListComponent`, where we called `!{_priv}heroService.getHeroes()`,
|
||||
we supply the `subscribe` function with a second function parameter to handle the error message.
|
||||
It sets an `errorMessage` variable which we've bound conditionally in the `HeroListComponent` template.
|
||||
|
||||
+makeExample('server-communication/ts/app/toh/hero-list.component.ts', 'getHeroes', 'app/toh/hero-list.component.ts (getHeroes)')(format=".")
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Want to see it fail? Reset the api endpoint in the `HeroService` to a bad value. Remember to restore it!
|
||||
|
||||
|
||||
<a id="update"></a>
|
||||
<a id="post"></a>
|
||||
.l-main-section
|
||||
:marked
|
||||
## Send data to the server
|
||||
|
||||
So far we've seen how to retrieve data from a remote location using an HTTP service.
|
||||
Let's add the ability to create new heroes and save them in the backend.
|
||||
|
||||
We'll create an easy method for the `HeroListComponent` to call, an `addHero()` method that takes
|
||||
just the name of a new hero:
|
||||
|
||||
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'addhero-sig')(format=".")
|
||||
|
||||
:marked
|
||||
To implement it, we need to know some details about the server's api for creating heroes.
|
||||
|
||||
[Our data server](#server) follows typical REST guidelines.
|
||||
It expects a [`POST`](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5) request
|
||||
at the same endpoint where we `GET` heroes.
|
||||
It expects the new hero data to arrive in the body of the request,
|
||||
structured like a `Hero` entity but without the `id` property.
|
||||
The body of the request should look like this:
|
||||
|
||||
code-example(format="." language="javascript").
|
||||
{ "name": "Windstorm" }
|
||||
:marked
|
||||
The server will generate the `id` and return the entire `JSON` representation
|
||||
of the new hero including its generated id. The hero arrives tucked inside a response object
|
||||
with its own `data` property.
|
||||
|
||||
Now that we know how the API works, we implement `addHero()`like this:
|
||||
|
||||
+ifDocsFor('ts')
|
||||
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'import-request-options', 'app/toh/hero.service.ts (additional imports)')(format=".")
|
||||
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'addhero', 'app/toh/hero.service.ts (addHero)')(format=".")
|
||||
|
||||
:marked
|
||||
### Headers
|
||||
|
||||
The `Content-Type` header allows us to inform the server that the body will represent JSON.
|
||||
|
||||
+ifDocsFor('ts')
|
||||
:marked
|
||||
[Headers](../api/http/index/Headers-class.html) are one of the [RequestOptions](../api/http/index/RequestOptions-class.html).
|
||||
Compose the options object and pass it in as the *third* parameter of the `post` method, as shown above.
|
||||
|
||||
:marked
|
||||
### Body
|
||||
|
||||
Despite the content type being specified as JSON, the POST body must actually be a *string*.
|
||||
Hence, we explicitly encode the JSON hero content before passing it in as the body argument.
|
||||
|
||||
+ifDocsFor('ts')
|
||||
.l-sub-section
|
||||
:marked
|
||||
We may be able to skip the `JSON.stringify` step in the near future.
|
||||
|
||||
:marked
|
||||
### JSON results
|
||||
|
||||
As with `getHeroes()`, we [extract the data](#extract-data) from the response using the
|
||||
`!{_priv}extractData()` helper.
|
||||
|
||||
block hero-list-comp-add-hero
|
||||
:marked
|
||||
Back in the `HeroListComponent`, we see that *its* `addHero()` method subscribes to the observable returned by the *service's* `addHero()` method.
|
||||
When the data, arrive it pushes the new hero object into its `heroes` array for presentation to the user.
|
||||
+makeExample('server-communication/ts/app/toh/hero-list.component.ts', 'addHero', 'app/toh/hero-list.component.ts (addHero)')(format=".")
|
||||
|
||||
+ifDocsFor('ts')
|
||||
h2#promises Fall back to Promises
|
||||
:marked
|
||||
Although the Angular `http` client API returns an `Observable<Response>` we can turn it into a
|
||||
[Promise<Response>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) if we prefer.
|
||||
It's easy to do and a promise-based version looks much like the observable-based version in simple cases.
|
||||
.l-sub-section
|
||||
:marked
|
||||
While promises may be more familiar, observables have many advantages.
|
||||
Don't rush to promises until you give observables a chance.
|
||||
:marked
|
||||
Let's rewrite the `HeroService` using promises , highlighting just the parts that are different.
|
||||
+makeTabs(
|
||||
'server-communication/ts/app/toh/hero.service.promise.ts,server-communication/ts/app/toh/hero.service.ts',
|
||||
'methods, methods',
|
||||
'app/toh/hero.service.promise.ts (promise-based), app/toh/hero.service.ts (observable-based)')
|
||||
:marked
|
||||
Converting from an observable to a promise is as simple as calling `toPromise(success, fail)`.
|
||||
|
||||
We move the observable's `map` callback to the first *success* parameter and its `catch` callback to the second *fail* parameter
|
||||
and we're done!
|
||||
Or we can follow the promise `then.catch` pattern as we do in the second `addHero` example.
|
||||
|
||||
Our `errorHandler` forwards an error message as a failed promise instead of a failed Observable.
|
||||
|
||||
The diagnostic *log to console* is just one more `then` in the promise chain.
|
||||
|
||||
We have to adjust the calling component to expect a `Promise` instead of an `Observable`.
|
||||
|
||||
+makeTabs(
|
||||
'server-communication/ts/app/toh/hero-list.component.promise.ts, server-communication/ts/app/toh/hero-list.component.ts',
|
||||
'methods, methods',
|
||||
'app/toh/hero-list.component.promise.ts (promise-based), app/toh/hero-list.component.ts (observable-based)')
|
||||
:marked
|
||||
The only obvious difference is that we call `then` on the returned promise instead of `subscribe`.
|
||||
We give both methods the same functional arguments.
|
||||
.l-sub-section
|
||||
:marked
|
||||
The less obvious but critical difference is that these two methods return very different results!
|
||||
|
||||
The promise-based `then` returns another promise. We can keep chaining more `then` and `catch` calls, getting a new promise each time.
|
||||
|
||||
The `subscribe` method returns a `Subscription`. A `Subscription` is not another `Observable`.
|
||||
It's the end of the line for observables. We can't call `map` on it or call `subscribe` again.
|
||||
The `Subscription` object has a different purpose, signified by its primary method, `unsubscribe`.
|
||||
|
||||
Learn more about observables to understand the implications and consequences of subscriptions.
|
||||
|
||||
h2#cors Cross-origin requests: Wikipedia example
|
||||
:marked
|
||||
We just learned how to make `XMLHttpRequests` using the !{_Angular_Http} service.
|
||||
This is the most common approach for server communication.
|
||||
It doesn't work in all scenarios.
|
||||
|
||||
For security reasons, web browsers block `XHR` calls to a remote server whose origin is different from the origin of the web page.
|
||||
The *origin* is the combination of URI scheme, hostname and port number.
|
||||
This is called the [Same-origin Policy](https://en.wikipedia.org/wiki/Same-origin_policy).
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Modern browsers do allow `XHR` requests to servers from a different origin if the server supports the
|
||||
[CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) protocol.
|
||||
If the server requires user credentials, we'll enable them in the [request headers](#headers).
|
||||
|
||||
:marked
|
||||
Some servers do not support CORS but do support an older, read-only alternative called [JSONP](https://en.wikipedia.org/wiki/JSONP).
|
||||
Wikipedia is one such server.
|
||||
.l-sub-section
|
||||
:marked
|
||||
This [StackOverflow answer](http://stackoverflow.com/questions/2067472/what-is-jsonp-all-about/2067584#2067584) covers many details of JSONP.
|
||||
:marked
|
||||
### Search wikipedia
|
||||
|
||||
Let's build a simple search that shows suggestions from wikipedia as we type in a text box.
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/server-communication/wiki-1.gif' alt="Wikipedia search app (v.1)" width="250")
|
||||
|
||||
block wikipedia-jsonp+
|
||||
:marked
|
||||
Wikipedia offers a modern `CORS` API and a legacy `JSONP` search API. Let's use the latter for this example.
|
||||
The Angular `Jsonp` service both extends the `!{_Http}` service for JSONP and restricts us to `GET` requests.
|
||||
All other HTTP methods throw an error because JSONP is a read-only facility.
|
||||
|
||||
As always, we wrap our interaction with an Angular data access client service inside a dedicated service, here called `WikipediaService`.
|
||||
|
||||
+makeExample('server-communication/ts/app/wiki/wikipedia.service.ts',null,'app/wiki/wikipedia.service.ts')
|
||||
:marked
|
||||
The constructor expects Angular to inject its `jsonp` service.
|
||||
We register that service with `JSONP_PROVIDERS` in the [component below](#wikicomponent) that calls our `WikipediaService`.
|
||||
|
||||
<a id="query-parameters"></a>
|
||||
:marked
|
||||
### Search parameters
|
||||
The [Wikipedia 'opensearch' API](https://www.mediawiki.org/wiki/API:Opensearch)
|
||||
expects four parameters (key/value pairs) to arrive in the request URL's query string.
|
||||
The keys are `search`, `action`, `format`, and `callback`.
|
||||
The value of the `search` key is the user-supplied search term to find in Wikipedia.
|
||||
The other three are the fixed values "opensearch", "json", and "JSONP_CALLBACK" respectively.
|
||||
.l-sub-section
|
||||
:marked
|
||||
The `JSONP` technique requires that we pass a callback function name to the server in the query string: `callback=JSONP_CALLBACK`.
|
||||
The server uses that name to build a JavaScript wrapper function in its response which Angular ultimately calls to extract the data.
|
||||
All of this happens under the hood.
|
||||
:marked
|
||||
If we're looking for articles with the word "Angular", we could construct the query string by hand and call `jsonp` like this:
|
||||
+makeExample('server-communication/ts/app/wiki/wikipedia.service.1.ts','query-string')(format='.')
|
||||
:marked
|
||||
In more parameterized examples we might prefer to build the query string with the Angular `URLSearchParams` helper as shown here:
|
||||
+makeExample('server-communication/ts/app/wiki/wikipedia.service.ts','search-parameters','app/wiki/wikipedia.service.ts (search parameters)')(format=".")
|
||||
:marked
|
||||
This time we call `jsonp` with *two* arguments: the `wikiUrl` and an options object whose `search` property is the `params` object.
|
||||
+makeExample('server-communication/ts/app/wiki/wikipedia.service.ts','call-jsonp','app/wiki/wikipedia.service.ts (call jsonp)')(format=".")
|
||||
:marked
|
||||
`Jsonp` flattens the `params` object into the same query string we saw earlier before putting the request on the wire.
|
||||
|
||||
<a id="wikicomponent"></a>
|
||||
:marked
|
||||
### The WikiComponent
|
||||
|
||||
Now that we have a service that can query the Wikipedia API,
|
||||
we turn to the component that takes user input and displays search results.
|
||||
|
||||
+makeExample('server-communication/ts/app/wiki/wiki.component.ts', null, 'app/wiki/wiki.component.ts')
|
||||
:marked
|
||||
The `providers` array in the component metadata specifies the Angular `JSONP_PROVIDERS` collection that supports the `Jsonp` service.
|
||||
We register that collection at the component level to make `Jsonp` injectable in the `WikipediaService`.
|
||||
|
||||
The component presents an `<input>` element *search box* to gather search terms from the user.
|
||||
and calls a `search(term)` method after each `keyup` event.
|
||||
|
||||
The `search(term)` method delegates to our `WikipediaService` which returns an observable array of string results (`Observable<string[]>`).
|
||||
Instead of subscribing to the observable inside the component as we did in the `HeroListComponent`,
|
||||
we forward the observable result to the template (via `items`) where the [async pipe](pipes.html#async-pipe)
|
||||
in the `ngFor` handles the subscription.
|
||||
.l-sub-section
|
||||
:marked
|
||||
We often use the [async pipe](pipes.html#async-pipe) in read-only components where the component has no need to interact with the data.
|
||||
We couldn't use the pipe in the `HeroListComponent` because the "add hero" feature pushes newly created heroes into the list.
|
||||
|
||||
:marked
|
||||
## Our wasteful app
|
||||
|
||||
Our wikipedia search makes too many calls to the server.
|
||||
It is inefficient and potentially expensive on mobile devices with limited data plans.
|
||||
|
||||
### 1. Wait for the user to stop typing
|
||||
At the moment we call the server after every key stroke.
|
||||
The app should only make requests when the user *stops typing* .
|
||||
Here's how it *should* work — and *will* work — when we're done refactoring:
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/server-communication/wiki-2.gif' alt="Wikipedia search app (v.2)" width="250")
|
||||
:marked
|
||||
### 2. Search when the search term changes
|
||||
|
||||
Suppose the user enters the word *angular* in the search box and pauses for a while.
|
||||
The application issues a search request for *Angular*.
|
||||
|
||||
Then the user backspaces over the last three letters, *lar*, and immediately re-types *lar* before pausing once more.
|
||||
The search term is still "angular". The app shouldn't make another request.
|
||||
|
||||
### 3. Cope with out-of-order responses
|
||||
|
||||
The user enters *angular*, pauses, clears the search box, and enters *http*.
|
||||
The application issues two search requests, one for *angular* and one for *http*.
|
||||
|
||||
Which response will arrive first? We can't be sure.
|
||||
A load balancer could dispatch the requests to two different servers with different response times.
|
||||
The results from the first *angular* request might arrive after the later *http* results.
|
||||
The user will be confused if we display the *angular* results to the *http* query.
|
||||
|
||||
When there are multiple requests in-flight, the app should present the responses
|
||||
in the original request order. That won't happen if *angular* results arrive last.
|
||||
|
||||
<a id="more-observables"></a>
|
||||
## More fun with Observables
|
||||
We can address these problems and improve our app with the help of some nifty observable operators.
|
||||
|
||||
We could make our changes to the `WikipediaService`.
|
||||
But we sense that our concerns are driven by the user experience so we update the component class instead.
|
||||
|
||||
+makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', null, 'app/wiki/wiki-smart.component.ts')
|
||||
:marked
|
||||
We made no changes to the template or metadata, confining them all to the component class.
|
||||
Let's review those changes.
|
||||
|
||||
### Create a stream of search terms
|
||||
|
||||
We're binding to the search box `keyup` event and calling the component's `search` method after each keystroke.
|
||||
|
||||
We turn these events into an observable stream of search terms using a `Subject`
|
||||
which we import from the RxJS observable library:
|
||||
+makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', 'import-subject')
|
||||
:marked
|
||||
Each search term is a string, so we create a new `Subject` of type `string` called `searchTermStream`.
|
||||
After every keystroke, the `search` method adds the search box value to that stream
|
||||
via the subject's `next` method.
|
||||
+makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', 'subject')(format='.')
|
||||
:marked
|
||||
### Listen for search terms
|
||||
|
||||
Earlier, we passed each search term directly to the service and bound the template to the service results.
|
||||
Now we listen to the *stream of terms*, manipulating the stream before it reaches the `WikipediaService`.
|
||||
+makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', 'observable-operators')(format='.')
|
||||
:marked
|
||||
We wait for the user to stop typing for at least 300 milliseconds
|
||||
([debounceTime](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/debounce.md)).
|
||||
Only changed search values make it through to the service
|
||||
([distinctUntilChanged](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/distinctuntilchanged.md)).
|
||||
|
||||
The `WikipediaService` returns a separate observable of string arrays (`Observable<string[]>`) for each request.
|
||||
We could have multiple requests *in flight*, all awaiting the server's reply,
|
||||
which means multiple *observables-of-strings* could arrive at any moment in any order.
|
||||
|
||||
The [switchMap](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md)
|
||||
(formerly known as `flatMapLatest`) returns a new observable that combines these `WikipediaService` observables,
|
||||
re-arranges them in their original request order,
|
||||
and delivers to subscribers only the most recent search results.
|
||||
|
||||
The displayed list of search results stays in sync with the user's sequence of search terms.
|
||||
.l-sub-section
|
||||
:marked
|
||||
We added the `debounceTime`, `distinctUntilChanged`, and `switchMap` operators to the RxJS `Observable` class
|
||||
in `rxjs-operators` as [described above](#rxjs)
|
||||
|
||||
a#in-mem-web-api
|
||||
.l-main-section
|
||||
:marked
|
||||
## Appendix: Tour of Heroes in-memory server
|
||||
|
||||
If we only cared to retrieve data, we could tell Angular to get the heroes from a `heroes.json` file like this one:
|
||||
+makeJson('server-communication/ts/app/heroes.json', null, 'app/heroes.json')(format=".")
|
||||
.l-sub-section
|
||||
:marked
|
||||
We wrap the heroes array in an object with a `data` property for the same reason that a data server does:
|
||||
to mitigate the [security risk](http://stackoverflow.com/questions/3503102/what-are-top-level-json-arrays-and-why-are-they-a-security-risk)
|
||||
posed by top-level JSON arrays.
|
||||
:marked
|
||||
We'd set the endpoint to the JSON file like this:
|
||||
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'endpoint-json')(format=".")
|
||||
|
||||
- var _a_ca_class_with = _docsFor === 'ts' ? 'a custom application class with' : ''
|
||||
:marked
|
||||
The *get heroes* scenario would work.
|
||||
But we want to *save* data too. We can't save changes to a JSON file. We need a web API server.
|
||||
We didn't want the hassle of setting up and maintaining a real server for this chapter.
|
||||
So we turned to an *in-memory web API simulator* instead.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
The in-memory web api is not part of the Angular core.
|
||||
It's an optional service in its own `angular2-in-memory-web-api` library
|
||||
that we installed with npm (see `package.json`) and
|
||||
registered for module loading by SystemJS (see `systemjs.config.js`)
|
||||
|
||||
:marked
|
||||
The in-memory web API gets its data from !{_a_ca_class_with} a `createDb()`
|
||||
method that returns a map whose keys are collection names and whose values
|
||||
are !{_array}s of objects in those collections.
|
||||
|
||||
Here's the class we created for this sample based on the JSON data:
|
||||
+makeExample('server-communication/ts/app/hero-data.ts', null, 'app/hero-data.ts')(format=".")
|
||||
:marked
|
||||
Ensure that the `HeroService` endpoint refers to the web API:
|
||||
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'endpoint')(format=".")
|
||||
:marked
|
||||
Finally, we need to redirect client HTTP requests to the in-memory web API.
|
||||
block redirect-to-web-api
|
||||
:marked
|
||||
This redirection is easy to configure because Angular's `http` service delegates the client/server communication tasks
|
||||
to a helper service called the `XHRBackend`.
|
||||
|
||||
To enable our server simulation, we replace the default `XHRBackend` service with
|
||||
the in-memory web API service using standard Angular provider registration techniques.
|
||||
We initialize the in-memory web API with *seed data* from the mock hero dataset at the same time.
|
||||
:marked
|
||||
Here is the revised (and final) version of <span ngio-ex>app/main.ts></span> demonstrating these steps.
|
||||
|
||||
+makeExcerpt('app/main.ts', 'final')
|
||||
|
||||
:marked
|
||||
See the full source code in the <live-example></live-example>.
|
|
@ -0,0 +1,336 @@
|
|||
block includes
|
||||
include ../_util-fns
|
||||
|
||||
:marked
|
||||
One of the defining features of a single page application is its manipulation
|
||||
of the DOM tree. Instead of serving a whole new page every time a user
|
||||
navigates, whole sections of the DOM appear and disappear according
|
||||
to the application state. In this chapter we'll to look at how Angular
|
||||
manipulates the DOM and how we can do it ourselves in our own directives.
|
||||
|
||||
In this chapter we will
|
||||
- [learn what structural directives are](#definition)
|
||||
- [study *ngIf*](#ngIf)
|
||||
- [discover the <template> element](#template)
|
||||
- [understand the asterisk (\*) in **ngFor*](#asterisk)
|
||||
- [write our own structural directive](#unless)
|
||||
|
||||
Try the <live-example></live-example>.
|
||||
|
||||
<a id="definition"></a>
|
||||
.l-main-section
|
||||
:marked
|
||||
## What are structural directives?
|
||||
|
||||
There are three kinds of Angular directives:
|
||||
1. Components
|
||||
1. Attribute directives
|
||||
1. Structural directives
|
||||
|
||||
The *Component* is really a directive with a template.
|
||||
It's the most common of the three directives and we write lots of them as we build our application.
|
||||
|
||||
The [*Attribute* directive](attribute-directives.html) changes the appearance or behavior of an element.
|
||||
The built-in [NgStyle](template-syntax.html#ngStyle) directive, for example,
|
||||
can change several element styles at the same time.
|
||||
We can use it to render text bold, italic, and lime green by binding to a
|
||||
component property that requests such a sickening result.
|
||||
|
||||
A *Structural* directive changes the DOM layout by adding and removing DOM elements.
|
||||
We've seen three of the built-in structural directives in other chapters: [ngIf](template-syntax.html#ngIf),
|
||||
[ngSwitch](template-syntax.html#ngSwitch) and [ngFor](template-syntax.html#ngFor).
|
||||
|
||||
+makeExample('structural-directives/ts/app/structural-directives.component.html', 'structural-directives')(format=".")
|
||||
|
||||
|
||||
<a id="ngIf"></a>
|
||||
.l-main-section
|
||||
:marked
|
||||
## NgIf Case Study
|
||||
|
||||
Let’s focus on `ngIf`. It's a great example of a structural
|
||||
directive: it takes a boolean and makes an entire chunk of DOM appear
|
||||
or disappear.
|
||||
|
||||
+makeExample('structural-directives/ts/app/structural-directives.component.html', 'ngIf')(format=".")
|
||||
|
||||
:marked
|
||||
The `ngIf` directive does not hide the element.
|
||||
Using browser developer tools we can see that, when the condition is true, the top
|
||||
paragraph is in the DOM and the bottom disused paragraph is completely
|
||||
absent from the DOM! In its place are empty `<script>` tags.
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/structural-directives/element-not-in-dom.png' alt="element not in dom")
|
||||
|
||||
:marked
|
||||
### Why *remove* rather than *hide*?
|
||||
We could hide the unwanted paragraph by setting its css `display` style to `none`.
|
||||
The element would remain in the DOM while invisible. Instead we removed it with `ngIf`.
|
||||
|
||||
The difference matters. When we hide an element,
|
||||
the component's behavior continues.
|
||||
It remains attached to its DOM element. It continues to listen to events.
|
||||
Angular keeps checking for changes that could affect data bindings.
|
||||
Whatever the component was doing it keeps doing.
|
||||
|
||||
Although invisible, the component — and all of its descendent components —
|
||||
tie up resources that might be more useful elsewhere.
|
||||
The performance and memory burden can be substantial and the user may not benefit at all.
|
||||
|
||||
On the positive side, showing the element again is very quick.
|
||||
The component's previous state is preserved and ready to display.
|
||||
The component doesn't re-initialize — an operation that could be expensive.
|
||||
|
||||
`ngIf` is different.
|
||||
Setting `ngIf` to false **does** affect the component's resource consumption.
|
||||
Angular removes the element from DOM, stops change detection for the associated component,
|
||||
detaches it from DOM events (the attachments that it made) and destroys the component.
|
||||
The component can be garbage-collected (we hope) and free up memory.
|
||||
|
||||
Components often have child components which themselves have children.
|
||||
All of them are destroyed when `ngIf` destroys the common ancestor.
|
||||
This cleanup effort is usually a good thing.
|
||||
|
||||
Of course it isn't *always* a good thing.
|
||||
It might be a bad thing if we need that particular component again soon.
|
||||
|
||||
The component's state might be expensive to re-construct.
|
||||
When `ngIf` becomes `true` again, Angular recreates the component and its subtree.
|
||||
Angular runs every component's initialization logic again. That could be expensive ... as when
|
||||
a component re-fetches data that had been in memory just moments ago.
|
||||
.l-sub-section
|
||||
:marked
|
||||
*Design thought*: minimize initialization effort and consider caching state in a
|
||||
companion service.
|
||||
:marked
|
||||
Although there are pros and cons to each approach,
|
||||
in general it is best to use `ngIf` to remove unwanted components rather than
|
||||
hide them.
|
||||
|
||||
**These same considerations apply to every structural directive, whether built-in or custom.**
|
||||
We should ask ourselves — and the users of our directives — to think carefully
|
||||
about the consequences of adding and removing elements and of creating and destroying components.
|
||||
|
||||
Let's see these dynamics at work. For fun, we'll stack the deck *against*
|
||||
our recommendation and consider a component called `heavy-loader` that
|
||||
***pretends*** to load a ton of data when initialized.
|
||||
|
||||
We'll display two instances of the component. We toggle the visibility of the first one with CSS.
|
||||
We toggle the second into and out of the DOM with `ngIf`.
|
||||
|
||||
+makeTabs(
|
||||
`structural-directives/ts/app/structural-directives.component.html,
|
||||
structural-directives/ts/app/heavy-loader.component.ts`,
|
||||
'message-log,',
|
||||
'template (excerpt), heavy-loader.component.ts')
|
||||
|
||||
:marked
|
||||
We also log when a component is created or destroyed
|
||||
using the built-in `ngOnInit` and `ngOnDestroy` [lifecycle hooks](lifecycle-hooks.html).
|
||||
Here it is in action:
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/structural-directives/heavy-loader-toggle.gif' alt="heavy loader toggle")
|
||||
|
||||
:marked
|
||||
Both components are in the DOM at the start.
|
||||
First we toggle the component's visibility repeatedly. The component never leaves the DOM.
|
||||
When visible it's always the same instance and the log is quiet.
|
||||
|
||||
Then we toggle the second component with `ngIf`.
|
||||
We create a new instance every time and the log shows that we're paying
|
||||
a heavy price to create and destroy it.
|
||||
|
||||
If we really expected to "wink" the component like this, toggling visibility would be the better choice.
|
||||
In most UIs, when we "close" a component we're unlikely see it again for a long time, if ever.
|
||||
The `ngIf` would be preferred in that case.
|
||||
|
||||
<a id="template"></a>
|
||||
.l-main-section
|
||||
:marked
|
||||
## The *<template>* tag
|
||||
|
||||
Structural directives, like `ngIf`, do their magic by using the
|
||||
[HTML 5 template tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).
|
||||
|
||||
Outside of an Angular app, the `<template>` tag's default CSS `display` property is `none`.
|
||||
It's contents are ***invisible*** within
|
||||
a hidden [document fragment](https://developer.mozilla.org/en/docs/Web/API/DocumentFragment).
|
||||
|
||||
Inside of an app, Angular ***removes*** the`<template>` tags and their children.
|
||||
The contents are gone — but not forgotten as we'll see soon.
|
||||
|
||||
We can confirm these effects by wrapping the middle "hip" of the phrase "Hip! Hip! Hooray!" within a `<template>` tag.
|
||||
+makeExample('structural-directives/ts/app/structural-directives.component.html', 'template-tag')(format=".")
|
||||
:marked
|
||||
The display is a 'Hip! Hooray!', short of perfect enthusiasm. The DOM effects are different when Angular is in control.
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/structural-directives/template-in-out-of-a2.png' alt="template outside angular")
|
||||
|
||||
:marked
|
||||
Evidently Angular replaces the `<template>` tag and its contents with empty `<script>` tags.
|
||||
That's just its default behavior.
|
||||
It can do something different as we saw when applying a variety of `ngSwitch` directives to `<template>` tags:
|
||||
|
||||
+makeExample('structural-directives/ts/app/structural-directives.component.html', 'ngSwitch')(format=".")
|
||||
:marked
|
||||
When one of those `ngSwitch` conditions is true, Angular inserts the template's content into the DOM.
|
||||
|
||||
What does this have to do with `ngIf` and `ngFor`? We didn't use a `<template>` tag with those directives.
|
||||
|
||||
<a id="asterisk"></a>
|
||||
.l-main-section
|
||||
:marked
|
||||
## The asterisk (\*) effect
|
||||
Here are those directives again. See the difference?
|
||||
|
||||
+makeExample('structural-directives/ts/app/structural-directives.component.html', 'asterisk')(format=".")
|
||||
:marked
|
||||
We're prefixing these directive names with an asterisk (\*).
|
||||
|
||||
The asterisk is "syntactic sugar". It simplifies `ngIf` and `ngFor` for both the writer and the reader.
|
||||
Under the hood, Angular replaces the asterisk version with a more verbose `<template>` form.
|
||||
|
||||
The next two `ngIf` examples are effectively the same and we may write in either style:
|
||||
|
||||
+makeExample('structural-directives/ts/app/structural-directives.component.html', 'ngIf-template')(format=".")
|
||||
|
||||
:marked
|
||||
Most of us would rather write in style (A).
|
||||
|
||||
It's worth knowing that Angular expands style (A) into style (B).
|
||||
It moves the paragraph and its contents inside a `<template>` tag.
|
||||
It moves the directive up to the `<template>` tag where it becomes a property binding,
|
||||
surrounded in square brackets. The boolean value of the host component's `condition` property
|
||||
determines whether the templated content is displayed or not.
|
||||
|
||||
Angular transforms `*ngFor` in a similar manner:
|
||||
|
||||
+makeExample('structural-directives/ts/app/structural-directives.component.html', 'ngFor-template')(format=".")
|
||||
:marked
|
||||
The basic pattern is the same: create a `<template>`, relocate the content,
|
||||
and move the directive onto the `<template>`.
|
||||
|
||||
There are extra nuances stemming from
|
||||
Angular's [ngFor micro-syntax](template-syntax.html#ngForMicrosyntax) which expands
|
||||
into an additional `ngForOf` property binding (the iterable) and
|
||||
the `hero` template input variable (the current item in each iteration).
|
||||
|
||||
<a id="unless"></a>
|
||||
.l-main-section
|
||||
:marked
|
||||
## Make a structural directive
|
||||
Let's write our own structural directive, an `Unless` directive, the not-so-evil twin of `ngIf`.
|
||||
|
||||
Unlike `ngIf` which displays the template content when `true`,
|
||||
our directive displays the content when the condition is ***false***.
|
||||
|
||||
block unless-intro
|
||||
:marked
|
||||
Creating a directive is similar to creating a component.
|
||||
* import the `Directive` decorator.
|
||||
|
||||
* add a CSS **attribute selector** (in brackets) that identifies our directive.
|
||||
|
||||
* specify the name of the public `input` property for binding
|
||||
(typically the name of the directive itself).
|
||||
|
||||
* apply the decorator to our implementation class.
|
||||
|
||||
Here is how we begin:
|
||||
|
||||
+makeExample('structural-directives/ts/app/unless.directive.ts', 'unless-declaration', 'unless.directive.ts (excerpt)')(format=".")
|
||||
.l-sub-section
|
||||
:marked
|
||||
### Selector brackets [ ]
|
||||
The CSS syntax for selecting an attribute is a name in square brackets.
|
||||
We surround our directive name in square brackets. See *Directive configuration* on the
|
||||
[cheatsheet](cheatsheet.html).
|
||||
|
||||
### Selector name prefixes
|
||||
|
||||
We recommend picking a selector name with a prefix to ensure
|
||||
that it cannot conflict with any standard HTML attribute, now or in the future.
|
||||
|
||||
We do **not** prefix our `unless` directive name with **`ng`**.
|
||||
That prefix belongs to Angular and
|
||||
we don't want to confuse our directives with their directives.
|
||||
|
||||
Our prefix is `my`.
|
||||
:marked
|
||||
We'll need access to the template *and* something that can render its contents.
|
||||
We access the template with a `TemplateRef`. The renderer is a `ViewContainerRef`.
|
||||
We inject both into our constructor as private variables.
|
||||
|
||||
+makeExample('structural-directives/ts/app/unless.directive.ts', 'unless-constructor')(format=".")
|
||||
|
||||
:marked
|
||||
The consumer of our directive will bind a boolean value to our directive's `myUnless` input property.
|
||||
The directive adds or removes the template based on that value.
|
||||
|
||||
Let's add the `myUnless` property now as a setter-only property.
|
||||
|
||||
+makeExample('structural-directives/ts/app/unless.directive.ts', 'unless-set')(format=".")
|
||||
.l-sub-section
|
||||
:marked
|
||||
The `@Input()` annotation marks this property as an input for the directive.
|
||||
:marked
|
||||
Nothing fancy here: if the condition is false,
|
||||
we render the template, otherwise we clear the element content.
|
||||
|
||||
The end result should look like this:
|
||||
|
||||
+makeExample('structural-directives/ts/app/unless.directive.ts', null, 'unless.directive.ts')
|
||||
|
||||
:marked
|
||||
Now we add it to the `directives`array of the host component and try it.
|
||||
First we add some test HTML to the template:
|
||||
|
||||
+makeExample('structural-directives/ts/app/structural-directives.component.html', 'myUnless')(format=".")
|
||||
:marked
|
||||
We run it and it behaves as expected, doing the opposite of `ngIf`.
|
||||
When `condition` is `true`, the top paragraph is removed (replaced by `<script>` tags) and the bottom paragraph appears.
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/structural-directives/myUnless-is-true.png' alt="myUnless is true" )
|
||||
|
||||
:marked
|
||||
Our `myUnless` directive is dead simple. Surely we left something out.
|
||||
Surely `ngIf` is more complex?
|
||||
|
||||
[Look at the source code](https://github.com/angular/angular/blob/master/modules/%40angular/common/src/directives/ng_if.ts).
|
||||
It's well documented and we shouldn't be shy
|
||||
about consulting the source when we want to know how something works.
|
||||
|
||||
`ngIf` isn't much different! There are a few
|
||||
additional checks to improve performance (don't clear or recreate the
|
||||
view unless necessary) but otherwise it's much the same.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Wrap up
|
||||
Here is the pertinent source for this chapter.
|
||||
|
||||
+makeTabs(`
|
||||
structural-directives/ts/app/unless.directive.ts,
|
||||
structural-directives/ts/app/heavy-loader.component.ts,
|
||||
structural-directives/ts/app/structural-directives.component.ts,
|
||||
structural-directives/ts/app/structural-directives.component.html
|
||||
`,
|
||||
null,
|
||||
`unless.directive.ts,
|
||||
heavy-loader.component.ts,
|
||||
structural-directives.component.ts,
|
||||
structural-directives.component.html
|
||||
`)
|
||||
:marked
|
||||
We learned that we can manipulate our HTML layout with
|
||||
structural directives like `ngFor` and `ngIf` and we
|
||||
wrote our own structural directive, `myUnless`, to do something similar.
|
||||
|
||||
Angular offers more sophisticated techniques for managing layout
|
||||
such as *structural components* that can take external content
|
||||
and incorporate that content within their own templates.
|
||||
Tab and tab pane controls are good examples.
|
||||
|
||||
We'll learn about structural components in a future chapter.
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,582 @@
|
|||
block includes
|
||||
include _util-fns
|
||||
- var _Install = 'Install'
|
||||
- var _prereq = 'Node.js'
|
||||
- var _angular_browser_uri = '@angular/platform-browser-dynamic'
|
||||
- var _angular_core_uri = '@angular/core'
|
||||
|
||||
:marked
|
||||
Our QuickStart goal is to build and run a super-simple
|
||||
Angular 2 application in #{_Lang}, and
|
||||
establish a development environment for the remaining documentation samples
|
||||
that also can be the foundation for real world applications.
|
||||
|
||||
.callout.is-helpful
|
||||
header Don't want #{_Lang}?
|
||||
p.
|
||||
Although we're getting started in #{_Lang}, you can also write Angular 2 apps
|
||||
in #{_docsFor == 'ts' ? 'Dart' : 'TypeScript'} and JavaScript.
|
||||
Just select either of those languages from the combo-box in the banner.
|
||||
|
||||
:marked
|
||||
# Try it!
|
||||
|
||||
Try the <live-example></live-example> which loads the sample app
|
||||
<span if-docs="ts">
|
||||
in <a href="http://plnkr.co/" title="Plunker" target="_blank">plunker</a>
|
||||
</span>
|
||||
and displays the simple message:
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/quickstart/my-first-app.png' alt="Output of QuickStart app")
|
||||
|
||||
:marked
|
||||
# Build this app!
|
||||
|
||||
- [Prerequisite](#prereq): Install #{_prereq}
|
||||
- [Step 1](#create-and-configure): Create the app’s project folder and
|
||||
define package dependencies and special project setup
|
||||
- [Step 2](#root-component): Create the app’s Angular root component
|
||||
- [Step 3](#main): Add <span ngio-ex>main.ts</span>, identifying the root component to Angular
|
||||
- [Step 4](#index): Add `index.html`, the web page that hosts the application
|
||||
- [Step 5](#build-and-run): Build and run the app
|
||||
- [Make some changes to the app](#make-some-changes)
|
||||
- [Wrap up](#wrap-up)
|
||||
|
||||
.l-main-section
|
||||
h2#prereq Prerequisite: #{_prereq}
|
||||
|
||||
block setup-tooling
|
||||
:marked
|
||||
Install **[Node.js® and npm](https://nodejs.org/en/download/)**
|
||||
if they are not already on your machine.
|
||||
.l-sub-section
|
||||
:marked
|
||||
**Verify that you are running at least node `v4.x.x` and npm `3.x.x`**
|
||||
by running `node -v` and `npm -v` in a terminal/console window.
|
||||
Older versions produce errors.
|
||||
|
||||
block download-source
|
||||
.l-main-section
|
||||
.callout.is-helpful
|
||||
header Download the source
|
||||
:marked
|
||||
Instead of following each step of these instructions, we can
|
||||
[download the QuickStart source](https://github.com/angular/quickstart/blob/master/README.md)
|
||||
from github and follow its brief instructions.
|
||||
|
||||
.l-main-section
|
||||
button(class="verbose off md-primary md-button md-ink-ripple", type="button", onclick="verbose(false)").
|
||||
Hide explanations
|
||||
button(class="verbose on md-primary md-button md-ink-ripple", type="button", onclick="verbose(true)").
|
||||
View explanations
|
||||
.l-verbose-section
|
||||
:marked
|
||||
*Explanations* describe the concepts and reasons behind the instructions.
|
||||
Explanations have a thin border on the left like *this* block of text.
|
||||
|
||||
Click *Hide Explanations* to show only the instructions.
|
||||
Click *View Explanations* to see everything again.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
We'll see many code blocks as we build the QuickStart app. They're all easy to copy and paste:
|
||||
code-example(format="nocode").
|
||||
Click the glyph on the right to copy code snippets to the clipboard ==>
|
||||
|
||||
.l-main-section
|
||||
h2#create-and-configure Step 1: Create and configure the project
|
||||
|
||||
- var _package_and_config_files = _docsFor == 'dart' ? 'pubspec.yaml' : 'package definition and configuration files'
|
||||
|
||||
:marked
|
||||
In this step we:
|
||||
* [(a) Create the project folder](#create-the-project-folder)
|
||||
* [(b) Add #{_package_and_config_files}](#add-config-files)
|
||||
* [(c) #{_Install} packages](#install-packages)
|
||||
|
||||
h3 (a) Create the project folder
|
||||
|
||||
- var _ = _docsFor == 'dart' ? '_' : '-';
|
||||
code-example(language="sh").
|
||||
mkdir angular2!{_}quickstart
|
||||
cd angular2!{_}quickstart
|
||||
|
||||
h3#add-config-files (b) Add #{_package_and_config_files}
|
||||
block package-and-config-files
|
||||
- var _tsconfigUri = 'guide/typescript-configuration.html#tsconfig'
|
||||
- var _typingsUri = 'guide/typescript-configuration.html#!#typings'
|
||||
|
||||
p Add the following package definition and configuration files to the project folder:
|
||||
ul
|
||||
li.
|
||||
#[b package.json] lists packages the QuickStart app depends on and
|
||||
defines some useful scripts.
|
||||
See #[a(href="guide/npm-packages.html") Npm Package Configuration] for details.
|
||||
li.
|
||||
#[b tsconfig.json] is the TypeScript compiler configuration file.
|
||||
See #[a(href="#{_tsconfigUri}") TypeScript Configuration] for details.
|
||||
li.
|
||||
#[b typings.json] identifies TypeScript definition files.
|
||||
See #[a(href="#{_typingsUri}") TypeScript Configuration] for details.
|
||||
li.
|
||||
#[b systemjs.config.js], the SystemJS configuration file.
|
||||
See discussion #[a(href="#systemjs") below].
|
||||
|
||||
a#config-files
|
||||
+makeTabs(`
|
||||
quickstart/ts/package.1.json,
|
||||
quickstart/ts/tsconfig.1.json,
|
||||
quickstart/ts/typings.1.json,
|
||||
quickstart/ts/systemjs.config.1.js
|
||||
`, '', `
|
||||
package.json,
|
||||
tsconfig.json,
|
||||
typings.json,
|
||||
systemjs.config.js
|
||||
`)
|
||||
|
||||
h3#install-packages (c) #{_Install} packages
|
||||
block install-packages
|
||||
:marked
|
||||
We install the packages listed in `package.json` using `npm`. Enter the
|
||||
following command in a terminal window (command window in Windows):
|
||||
|
||||
code-example(language="sh").
|
||||
npm install
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
The `typings` folder could not show up after `npm install`. If so, please install them manually.
|
||||
|
||||
code-example(language="sh").
|
||||
npm run typings install
|
||||
|
||||
.alert.is-important
|
||||
:marked
|
||||
Scary <span style="color:red; font-weight: bold">error messages in red</span> may appear **during** install.
|
||||
The install typically recovers from these errors and finishes successfully.
|
||||
.l-verbose-section(class="l-verbose-inherit")
|
||||
:marked
|
||||
#### npm errors and warnings
|
||||
|
||||
All is well if there are no console messages starting with `npm ERR!` *at the end* of **npm install**.
|
||||
There might be a few `npm WARN` messages along the way — and that is perfectly fine.
|
||||
|
||||
We often see an `npm WARN` message after a series of `gyp ERR!` messages.
|
||||
Ignore them. A package may try to recompile itself using `node-gyp`.
|
||||
If the recompile fails, the package recovers (typically with a pre-built version)
|
||||
and everything works.
|
||||
|
||||
Just make sure there are no `npm ERR!` messages at the end of `npm install`.
|
||||
|
||||
.l-verbose-section
|
||||
:marked
|
||||
#### Adding the libraries and packages we need with *npm*
|
||||
Angular application developers rely on the _[npm](https://docs.npmjs.com)_
|
||||
package manager to install the libraries and packages their apps require.
|
||||
The Angular team recommends the starter-set of packages specified in the
|
||||
`dependencies` and `devDependencies` sections.
|
||||
See the [npm packages](guide/npm-packages.html) chapter for details.
|
||||
|
||||
#### Helpful scripts
|
||||
We've included a number of npm scripts in our suggested `package.json` to handle common development tasks:
|
||||
+makeJson('quickstart/ts/package.1.json',{ paths: 'scripts'}, 'package.json (scripts)')(format=".")
|
||||
|
||||
:marked
|
||||
We execute most npm scripts in the following way: `npm run` followed by a *script-name*.
|
||||
Some commands (such as `start`) don't require the `run` keyword.
|
||||
|
||||
Here's what these scripts do:
|
||||
|
||||
* `npm start` - runs the compiler and a server at the same time, both in "watch mode"
|
||||
|
||||
* `npm run tsc` - runs the TypeScript compiler once
|
||||
|
||||
* `npm run tsc:w` - runs the TypeScript compiler in watch mode;
|
||||
the process keeps running, awaiting changes to TypeScript files and recompiling when it sees them
|
||||
|
||||
* `npm run lite` - runs the <a href="https://www.npmjs.com/package/lite-server" target="_blank">lite-server</a>,
|
||||
a light-weight, static file server with excellent support for Angular apps that use routing
|
||||
|
||||
* `npm run typings` - runs the [*typings* tool](#{_typingsUri}) separately
|
||||
|
||||
* `npm run postinstall` - called by *npm* automatically *after* it successfully completes package installation.
|
||||
This script installs the [TypeScript definition files](#{_typingsUri}) defined in `typings.json`
|
||||
|
||||
:marked
|
||||
**We're all set.** Let's write some code.
|
||||
|
||||
.l-main-section
|
||||
h2#root-component Step 2: Our first Angular component
|
||||
:marked
|
||||
Let's create a folder to hold our application and add a super-simple Angular component.
|
||||
|
||||
**Create #{_an} #{_appDir} subfolder** off the project root directory:
|
||||
|
||||
code-example.
|
||||
mkdir #{_appDir}
|
||||
|
||||
a#app-component
|
||||
p.
|
||||
#[b Create the component file]
|
||||
#[code #[+adjExPath('app/app.component.ts')]] (in this newly created directory) with the following content:
|
||||
|
||||
+makeExample('app/app.component.ts')
|
||||
|
||||
.l-verbose-section
|
||||
:marked
|
||||
### AppComponent is the root of the application
|
||||
|
||||
Every Angular app has at least one **root component**, conventionally named `AppComponent`,
|
||||
that hosts the client user experience.
|
||||
Components are the basic building blocks of Angular applications.
|
||||
A component controls a portion of the screen — a *view* — through its associated template.
|
||||
|
||||
This QuickStart has only one, extremely simple component.
|
||||
But it has the essential structure of every component we'll ever write:
|
||||
|
||||
* One or more [import](#component-import)
|
||||
statements to reference the things we need.
|
||||
* A [@Component #{_decorator}](#component-decorator)
|
||||
that tells Angular what template to use and how to create the component.
|
||||
* A [component class](#component-class)
|
||||
that controls the appearance and behavior of a view through its template.
|
||||
|
||||
a#component-import
|
||||
:marked
|
||||
### Import
|
||||
|
||||
Angular apps are modular. They consist of many files each dedicated to a purpose.
|
||||
Angular itself is modular. It is a collection of library modules
|
||||
each made up of several, related features that we'll use to build our application.
|
||||
|
||||
When we need something from a module or library, we import it.
|
||||
Here we import the Angular 2 core so that our component code can have access to
|
||||
the `@Component` #{_decorator}.
|
||||
|
||||
+makeExcerpt('app/app.component.ts', 'import')
|
||||
|
||||
h3#component-decorator @Component #{_decorator}
|
||||
+ifDocsFor('ts')
|
||||
:marked
|
||||
`Component` is a *decorator function* that takes a *metadata object* as argument.
|
||||
We apply this function to the component class by prefixing the function with the
|
||||
**@** symbol and invoking it with a metadata object, just above the class.
|
||||
:marked
|
||||
`@Component` is #{_a} *#{_decorator}* that allows us to associate *metadata* with the
|
||||
component class.
|
||||
The metadata tells Angular how to create and use this component.
|
||||
|
||||
+makeExcerpt('app/app.component.ts', 'metadata')
|
||||
|
||||
block annotation-fields
|
||||
:marked
|
||||
This particular metadata object has two fields, a `selector` and a `template`.
|
||||
:marked
|
||||
The **selector** specifies a simple CSS selector for an HTML element that represents the component.
|
||||
|
||||
>The element for this component is named `my-app`.
|
||||
Angular creates and displays an instance of our `AppComponent`
|
||||
wherever it encounters a `my-app` element in the host HTML.
|
||||
|
||||
The **template** specifies the component's companion template,
|
||||
written in an enhanced form of HTML that tells Angular how to render this component's view.
|
||||
|
||||
>Our template is a single line of HTML announcing "*My First Angular 2 App*".
|
||||
|
||||
>A more advanced template could contain data bindings to component properties
|
||||
and might identify other application components which have their own templates.
|
||||
These templates might identify yet other components.
|
||||
In this way an Angular application becomes a tree of components.
|
||||
|
||||
:marked
|
||||
### Component class
|
||||
At the bottom of the file is an empty, do-nothing class named `AppComponent`.
|
||||
+makeExcerpt('app/app.component.ts', 'class')
|
||||
:marked
|
||||
When we're ready to build a substantive application,
|
||||
we can expand this class with properties and application logic.
|
||||
Our `AppComponent` class is empty because we don't need it to do anything in this QuickStart.
|
||||
+ifDocsFor('ts')
|
||||
:marked
|
||||
We **export** `AppComponent` so that we can **import** it elsewhere in our application,
|
||||
as we'll see when we create `main.ts`.
|
||||
|
||||
.l-main-section
|
||||
h2#main Step 3: Add #[code #[+adjExPath('main.ts')]]
|
||||
|
||||
block create-main
|
||||
p.
|
||||
Now we need something to tell Angular to load the root component.
|
||||
Create the file #[code #[+adjExPath('app/main.ts')]] with the following content:
|
||||
|
||||
+makeExample('app/main.ts')
|
||||
|
||||
.l-verbose-section
|
||||
:marked
|
||||
We import the two things we need to launch the application:
|
||||
|
||||
1. Angular's browser `bootstrap` function
|
||||
1. The application root component, `AppComponent`.
|
||||
|
||||
Then we call `bootstrap` with `AppComponent`.
|
||||
|
||||
### Bootstrapping is platform-specific
|
||||
Notice that we import the `bootstrap` function from `#{_angular_browser_uri}`,
|
||||
not `#{_angular_core_uri}`.
|
||||
Bootstrapping isn't core because there isn't a single way to bootstrap the app.
|
||||
True, most applications that run in a browser call the bootstrap function from
|
||||
this library.
|
||||
|
||||
But it is possible to load a component in a different environment.
|
||||
We might load it on a mobile device with [Apache Cordova](https://cordova.apache.org/) or [NativeScript](https://www.nativescript.org/).
|
||||
We might wish to render the first page of our application on the server
|
||||
to improve launch performance or facilitate
|
||||
[SEO](http://www.google.com/webmasters/docs/search-engine-optimization-starter-guide.pdf).
|
||||
These targets require a different kind of bootstrap function that we'd import from a different library.
|
||||
|
||||
### Why create separate *<span ngio-ex>main.ts</span>* and app component files?
|
||||
|
||||
Both <span ngio-ex>main.ts</span> and the app component files are tiny.
|
||||
This is just a QuickStart.
|
||||
We could have merged these two files into one
|
||||
and spared ourselves some complexity.
|
||||
|
||||
We'd rather demonstrate the proper way to structure an Angular application.
|
||||
App bootstrapping is a separate concern from presenting a view.
|
||||
Mixing concerns creates difficulties down the road.
|
||||
We might launch the `AppComponent` in multiple environments with different bootstrappers.
|
||||
Testing the component is much easier if it doesn't also try to run the entire application.
|
||||
Let's make the small extra effort to do it *the right way*.
|
||||
|
||||
.l-main-section
|
||||
h2#index Step 4: Add #[code index.html]
|
||||
:marked
|
||||
In the *#{_indexHtmlDir}* folder
|
||||
create an `index.html` file and paste the following lines into it:
|
||||
|
||||
+makeExample('index.html')
|
||||
|
||||
.l-verbose-section
|
||||
:marked
|
||||
The `index.html` file defines the web page that hosts the application.
|
||||
|
||||
block index-html-commentary-for-ts
|
||||
:marked
|
||||
The noteworthy sections of HTML are:
|
||||
|
||||
1. The JavaScript [libraries](#libraries)
|
||||
2. Configuration file for [SystemJS](#systemjs), and a script
|
||||
where we import and run the `app` module which refers to the `main` file that we just wrote.
|
||||
3. The [`<my-app>`](#my-app) tag in the `<body>` which is *where our app lives!*
|
||||
|
||||
:marked
|
||||
### Libraries
|
||||
We loaded the following scripts
|
||||
+makeExcerpt('index.html', 'libraries')
|
||||
:marked
|
||||
We begin with `core-js`'s ES2015/ES6 shim which monkey patches the global context (window) with essential features of ES2015 (ES6).
|
||||
Next are the polyfills for Angular2, `zone.js` and `reflect-metadata`.
|
||||
Then the [SystemJS](#systemjs) library for module loading.
|
||||
|
||||
We'll make different choices as we gain experience and
|
||||
become more concerned about production qualities such as
|
||||
load times and memory footprint.
|
||||
|
||||
h3#systemjs SystemJS
|
||||
:marked
|
||||
QuickStart uses <a href="https://github.com/systemjs/systemjs" target="_blank">SystemJS</a>
|
||||
to load application and library modules. [Earlier](#add-config-files) we
|
||||
added the `systemjs.config.js` file to the project root.
|
||||
There are alternatives that work just fine including the well-regarded
|
||||
[webpack](guide/webpack.html).
|
||||
SystemJS happens to be a good choice.
|
||||
But we want to be clear that it was a *choice* and not a *preference*.
|
||||
|
||||
All module loaders require configuration and all loader configuration
|
||||
becomes complicated rather quickly as soon as the file structure diversifies and
|
||||
we start thinking about building for production and performance.
|
||||
|
||||
We suggest becoming well-versed in the loader of your choice.
|
||||
Learn more about SystemJS configuration
|
||||
<a href="https://github.com/systemjs/systemjs/blob/master/docs/config-api.md" target="_blank">here</a>.
|
||||
|
||||
With those cautions in mind, what are we doing in the
|
||||
QuickStart [`systemjs.config.js` configuration file we added earlier](#config-files)?
|
||||
First, we create a map to tell SystemJS where to look when we import some module.
|
||||
Then, we register all our packages to SystemJS:
|
||||
all the project dependencies and our application package, `app`.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Our QuickStart doesn't use all of the listed packages
|
||||
but any substantial application will want many of them
|
||||
and all of the listed packages are required by at least one of the documentation samples.
|
||||
|
||||
There is no runtime harm in listing packages that we don't need as they will only be loaded when requested.
|
||||
:marked
|
||||
The `app` package tells SystemJS what to do when it sees a request for a
|
||||
module from the `app/` folder.
|
||||
|
||||
Our QuickStart makes such requests when one of its
|
||||
application TypeScript files has an import statement like this:
|
||||
+makeExcerpt('app/main.ts', 'import')
|
||||
:marked
|
||||
Notice that the module name (after `from`) does not mention a filename extension.
|
||||
In the configuration we tell SystemJS to default the extension to `js`, a JavaScript file.
|
||||
|
||||
That makes sense because we transpile TypeScript to JavaScript
|
||||
*before* running the application.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
#### Transpiling in the browser
|
||||
In the live example on plunker we transpile (AKA compile) to JavaScript in the browser
|
||||
on the fly. _That's fine for a demo_.
|
||||
|
||||
**Do not transpile in the browser during development or for production**.
|
||||
|
||||
We strongly recommend transpiling (AKA compiling) to JavaScript during a build phase
|
||||
before running the application for several reasons including:
|
||||
|
||||
* We see compiler warnings and errors that are hidden from us in the browser.
|
||||
|
||||
* Precompilation simplifies the module loading process and
|
||||
it's much easier to diagnose problems when this is a separate, external step.
|
||||
|
||||
* Precompilation means a faster user experience because the browser doesn't waste time compiling.
|
||||
|
||||
* We iterate development faster because we only recompile changed files.
|
||||
We notice the difference as soon as the app grows beyond a handful of files.
|
||||
|
||||
* Precompilation fits into a continuous integration process of build, test, deploy.
|
||||
|
||||
:marked
|
||||
The `System.import` call tells SystemJS to import the `main` file
|
||||
(`main.js` ... after transpiling `main.ts`, remember?);
|
||||
`main` is where we tell Angular to launch the application.
|
||||
We also catch and log launch errors to the console.
|
||||
|
||||
All other modules are loaded upon request
|
||||
either by an import statement or by Angular itself.
|
||||
|
||||
### *<my-app>*
|
||||
|
||||
a(id="my-app")
|
||||
:marked
|
||||
When Angular calls the `bootstrap` function in <span ngio-ex>main.ts</span>, it reads the `AppComponent`
|
||||
metadata, finds the `my-app` selector, locates an element tag named `my-app`,
|
||||
and renders our application's view between those tags.
|
||||
|
||||
:marked
|
||||
### Add some style
|
||||
Styles aren't essential but they're nice, and `index.html` assumes we have
|
||||
a stylesheet called `styles.css`.
|
||||
|
||||
Create a `styles.css` file in the *#{_indexHtmlDir}* folder and start styling, perhaps with the minimal
|
||||
styles shown below. For the full set of master styles used by the documentation samples,
|
||||
see [styles.css](https://github.com/angular/angular.io/blob/master/public/docs/_examples/styles.css).
|
||||
+makeExcerpt('styles.1.css')
|
||||
|
||||
.l-main-section
|
||||
h2#build-and-run Step 5: Build and run the app!
|
||||
block run-app
|
||||
:marked
|
||||
Open a terminal window and enter this command:
|
||||
code-example.
|
||||
npm start
|
||||
:marked
|
||||
That command runs two parallel node processes
|
||||
1. The TypeScript compiler in watch mode
|
||||
1. A static server called **lite-server** that loads `index.html` in a browser
|
||||
and refreshes the browser when application files change
|
||||
|
||||
In a few moments, a browser tab should open and display
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/quickstart/my-first-app.png' alt="Output of QuickStart app")
|
||||
|
||||
:marked
|
||||
**Great job!**
|
||||
|
||||
block build-app
|
||||
//- Nothing for ts.
|
||||
|
||||
:marked
|
||||
## Make some changes
|
||||
|
||||
Try changing the message to "My SECOND Angular 2 app".
|
||||
block server-watching
|
||||
:marked
|
||||
The TypeScript compiler and `lite-server` are watching.
|
||||
They should detect the change, recompile the TypeScript into JavaScript,
|
||||
refresh the browser, and display the revised message.
|
||||
It's a nifty way to develop an application!
|
||||
|
||||
We close the terminal window when we're done to terminate both the compiler and the server.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
# Wrap up
|
||||
|
||||
Our final project folder structure looks like this:
|
||||
block project-file-structure
|
||||
.filetree
|
||||
.file angular2-quickstart
|
||||
.children
|
||||
.file app
|
||||
.children
|
||||
.file app.component.ts
|
||||
.file main.ts
|
||||
.file node_modules ...
|
||||
.file typings ...
|
||||
.file index.html
|
||||
.file package.json
|
||||
.file styles.css
|
||||
.file systemjs.config.js
|
||||
.file tsconfig.json
|
||||
.file typings.json
|
||||
:marked
|
||||
Here are the file contents:
|
||||
|
||||
block project-files
|
||||
+makeTabs(`
|
||||
quickstart/ts/app/app.component.ts,
|
||||
quickstart/ts/app/main.ts,
|
||||
quickstart/ts/index.html,
|
||||
quickstart/ts/package.1.json,
|
||||
quickstart/ts/tsconfig.1.json,
|
||||
quickstart/ts/typings.1.json,
|
||||
quickstart/ts/styles.1.css,
|
||||
quickstart/ts/systemjs.config.1.js`
|
||||
,null,
|
||||
`app/app.component.ts,
|
||||
app/main.ts,
|
||||
index.html,
|
||||
package.json,
|
||||
tsconfig.json,
|
||||
typings.json,
|
||||
styles.css,
|
||||
systemjs.config.js`)
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## What next?
|
||||
Our first application doesn't do much. It's basically "Hello, World" for Angular 2.
|
||||
|
||||
We kept it simple in our first pass: we wrote a little Angular component,
|
||||
created a simple `index.html`, and launched with a
|
||||
static file server. That's about all we'd expect to do for a "Hello, World" app.
|
||||
|
||||
**We have greater ambitions!**
|
||||
block what-next-ts-overhead
|
||||
:marked
|
||||
The good news is that the overhead of setup is (mostly) behind us.
|
||||
We'll probably only touch the `package.json` to update libraries.
|
||||
We'll likely open `index.html` only if we need to add a library or some css stylesheets.
|
||||
:marked
|
||||
We're about to take the next step and build a small application that
|
||||
demonstrates the great things we can build with Angular 2.
|
||||
|
||||
Join us on the [Tour of Heroes Tutorial](./tutorial)!
|
|
@ -0,0 +1,632 @@
|
|||
- var _example = 'toh-6';
|
||||
|
||||
block includes
|
||||
include ../_util-fns
|
||||
- var _Http = 'Http'; // Angular `Http` library name.
|
||||
- var _Angular_Http = 'Angular <code>Http</code>'
|
||||
- var _Angular_http_library = 'Angular HTTP library'
|
||||
- var _HTTP_PROVIDERS = 'HTTP_PROVIDERS'
|
||||
- var _JSON_stringify = 'JSON.stringify'
|
||||
|
||||
:marked
|
||||
# Getting and Saving Data with HTTP
|
||||
|
||||
Our stakeholders appreciate our progress.
|
||||
Now they want to get the hero data from a server, let users add, edit, and delete heroes,
|
||||
and save these changes back to the server.
|
||||
|
||||
In this chapter we teach our application to make the corresponding HTTP calls to a remote server's web API.
|
||||
|
||||
Run the <live-example></live-example> for this part.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Where We Left Off
|
||||
In the [previous chapter](toh-pt5.html), we learned to navigate between the dashboard and the fixed heroes list, editing a selected hero along the way.
|
||||
That's our starting point for this chapter.
|
||||
|
||||
block start-server-and-watch
|
||||
:marked
|
||||
### Keep the app transpiling and running
|
||||
Open a terminal/console window and enter the following command to
|
||||
start the TypeScript compiler, start the server, and watch for changes:
|
||||
|
||||
code-example(language="bash").
|
||||
npm start
|
||||
|
||||
:marked
|
||||
The application runs and updates automatically as we continue to build the Tour of Heroes.
|
||||
|
||||
.l-main-section#http-providers
|
||||
h1 Providing HTTP Services
|
||||
block http-library
|
||||
:marked
|
||||
`Http` is ***not*** a core Angular module.
|
||||
It's Angular's optional approach to web access and it exists as a separate add-on module called `@angular/http`,
|
||||
shipped in a separate script file as part of the Angular npm package.
|
||||
|
||||
Fortunately we're ready to import from `@angular/http` because `systemjs.config` configured *SystemJS* to load that library when we need it.
|
||||
|
||||
:marked
|
||||
### Register (provide) *HTTP* services
|
||||
|
||||
block http-providers
|
||||
:marked
|
||||
Our app will depend upon the Angular `http` service which itself depends upon other supporting services.
|
||||
The `HTTP_PROVIDERS` array from `@angular/http` library holds providers for the complete set of http services.
|
||||
|
||||
:marked
|
||||
We should be able to access `!{_Http}` services from anywhere in the application.
|
||||
So we register them in the `bootstrap` call of <span ngio-ex>main.ts</span> where we
|
||||
launch the application and its root `AppComponent`.
|
||||
|
||||
+makeExcerpt('app/main.ts','v1')
|
||||
|
||||
:marked
|
||||
Notice that we supply `!{_HTTP_PROVIDERS}` in !{_an} !{_array} as the second parameter to the `bootstrap` method.
|
||||
This has the same effect as the `providers` !{_array} in `@Component` !{_decorator}.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Simulating the web API
|
||||
|
||||
We generally recommend registering application-wide services in the root `AppComponent` *providers*.
|
||||
Here we're registering in `main` for a special reason.
|
||||
|
||||
Our application is in the early stages of development and far from ready for production.
|
||||
We don't even have a web server that can handle requests for heroes.
|
||||
Until we do, *we'll have to fake it*.
|
||||
|
||||
We're going to *trick* the HTTP client into fetching and saving data from
|
||||
a mock service, the *in-memory web API*.
|
||||
|
||||
The application itself doesn't need to know and shouldn't know about this.
|
||||
So we'll slip the in-memory web API into the configuration *above* the `AppComponent`.
|
||||
|
||||
Here is a version of `main` that performs this trick
|
||||
+makeExcerpt('app/main.ts', 'final')
|
||||
|
||||
block backend
|
||||
:marked
|
||||
We're replacing the default `XHRBackend`, the service that talks to the remote server,
|
||||
with the in-memory web API service after priming it as follows:
|
||||
|
||||
+makeExample('app/in-memory-data.service.ts', 'init')
|
||||
|
||||
p This file replaces the #[code #[+adjExPath('mock-heroes.ts')]] which is now safe to delete.
|
||||
|
||||
block dont-be-distracted-by-backend-subst
|
||||
.alert.is-helpful
|
||||
:marked
|
||||
This chapter is an introduction to the !{_Angular_http_library}.
|
||||
Please don't be distracted by the details of this backend substitution. Just follow along with the example.
|
||||
|
||||
Learn more later about the in-memory web API in the [HTTP client chapter](../guide/server-communication.html#!#in-mem-web-api).
|
||||
Remember, the in-memory web API is only useful in the early stages of development and for demonstrations such as this Tour of Heroes.
|
||||
Skip it when you have a real web API server.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Heroes and HTTP
|
||||
|
||||
Look at our current `HeroService` implementation
|
||||
|
||||
+makeExcerpt('toh-4/ts/app/hero.service.ts (old getHeroes)', 'get-heroes')
|
||||
|
||||
:marked
|
||||
We returned a !{_Promise} resolved with mock heroes.
|
||||
It may have seemed like overkill at the time, but we were anticipating the
|
||||
day when we fetched heroes with an HTTP client and we knew that would have to be an asynchronous operation.
|
||||
|
||||
That day has arrived! Let's convert `getHeroes()` to use HTTP:
|
||||
|
||||
+makeExcerpt('app/hero.service.ts (new constructor and revised getHeroes)', 'getHeroes')
|
||||
|
||||
:marked
|
||||
### HTTP !{_Promise}
|
||||
|
||||
We're still returning a !{_Promise} but we're creating it differently.
|
||||
|
||||
block get-heroes-details
|
||||
:marked
|
||||
The Angular `http.get` returns an RxJS `Observable`.
|
||||
*Observables* are a powerful way to manage asynchronous data flows.
|
||||
We'll learn about [Observables](#observables) later in this chapter.
|
||||
|
||||
For *now* we get back on familiar ground by immediately by
|
||||
converting that `Observable` to a `Promise` using the `toPromise` operator.
|
||||
+makeExcerpt('app/hero.service.ts', 'to-promise', '')
|
||||
:marked
|
||||
Unfortunately, the Angular `Observable` doesn't have a `toPromise` operator ... not out of the box.
|
||||
The Angular `Observable` is a bare-bones implementation.
|
||||
|
||||
There are scores of operators like `toPromise` that extend `Observable` with useful capabilities.
|
||||
If we want those capabilities, we have to add the operators ourselves.
|
||||
That's as easy as importing them from the RxJS library like this:
|
||||
+makeExcerpt('app/hero.service.ts', 'rxjs', '')
|
||||
|
||||
:marked
|
||||
### Extracting the data in the *then* callback
|
||||
In the *promise*'s `then` callback we call the `json` method of the http `Response` to extract the
|
||||
data within the response.
|
||||
+makeExcerpt('app/hero.service.ts', 'to-data', '')
|
||||
|
||||
:marked
|
||||
That response JSON has a single `data` property.
|
||||
The `data` property holds the !{_array} of *heroes* that the caller really wants.
|
||||
So we grab that !{_array} and return it as the resolved !{_Promise} value.
|
||||
|
||||
.alert.is-important
|
||||
:marked
|
||||
Pay close attention to the shape of the data returned by the server.
|
||||
This particular *in-memory web API* example happens to return an object with a `data` property.
|
||||
Your API might return something else.
|
||||
|
||||
Adjust the code to match *your web API*.
|
||||
:marked
|
||||
The caller is unaware of these machinations. It receives a !{_Promise} of *heroes* just as it did before.
|
||||
It has no idea that we fetched the heroes from the (mock) server.
|
||||
It knows nothing of the twists and turns required to convert the HTTP response into heroes.
|
||||
Such is the beauty and purpose of delegating data access to a service like this `HeroService`.
|
||||
:marked
|
||||
### Error Handling
|
||||
|
||||
At the end of `getHeroes()` we `catch` server failures and pass them to an error handler:
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'catch', '')
|
||||
|
||||
:marked
|
||||
This is a critical step!
|
||||
We must anticipate HTTP failures as they happen frequently for reasons beyond our control.
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'handleError', '')
|
||||
|
||||
- var rejected_promise = _docsFor == 'dart' ? 'propagated exception' : 'rejected promise';
|
||||
:marked
|
||||
In this demo service we log the error to the console; we should do better in real life.
|
||||
|
||||
We've also decided to return a user friendly form of the error to
|
||||
the caller in a !{rejected_promise} so that the caller can display a proper error message to the user.
|
||||
|
||||
### !{_Promise}s are !{_Promise}s
|
||||
Although we made significant *internal* changes to `getHeroes()`, the public signature did not change.
|
||||
We still return a !{_Promise}. We won't have to update any of the components that call `getHeroes()`.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Add, Edit, Delete
|
||||
|
||||
Our stakeholders are incredibly pleased with the added flexibility from the API integration, but it doesn't stop there. Next we want to add the capability to add, edit and delete heroes.
|
||||
|
||||
We'll complete `HeroService` by creating `post`, `put` and `delete` methods to meet our new requirements.
|
||||
|
||||
:marked
|
||||
### Post
|
||||
|
||||
We will be using `post` to add new heroes. Post requests require a little bit more setup than Get requests:
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'post')
|
||||
|
||||
:marked
|
||||
For Post requests we create a header and set the content type to `application/json`. We'll call `!{_JSON_stringify}` before we post to convert the hero object to a string.
|
||||
|
||||
### Put
|
||||
|
||||
Put will be used to update an individual hero. Its structure is very similar to Post requests. The only difference is that we have to change the URL slightly by appending the id of the hero we want to update.
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'put')
|
||||
|
||||
:marked
|
||||
### Delete
|
||||
Delete will be used to delete heroes and its format is like `put` except for the function name.
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'delete')
|
||||
|
||||
:marked
|
||||
We add a `catch` to handle errors for all three methods.
|
||||
|
||||
:marked
|
||||
### Save
|
||||
|
||||
We combine the call to the private `post` and `put` methods in a single `save` method. This simplifies the public API and makes the integration with `HeroDetailComponent` easier. `HeroService` determines which method to call based on the state of the `hero` object. If the hero already has an id we know it's an edit. Otherwise we know it's an add.
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'save')
|
||||
|
||||
:marked
|
||||
After these additions our `HeroService` looks like this:
|
||||
|
||||
+makeExample('app/hero.service.ts')
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Updating Components
|
||||
|
||||
Loading heroes using `Http` required no changes outside of `HeroService`, but we added a few new features as well.
|
||||
In the following section we will update our components to use our new methods to add, edit and delete heroes.
|
||||
|
||||
block hero-detail-comp-extra-imports-and-vars
|
||||
:marked
|
||||
Before we can add those methods, we need to initialize some variables with their respective imports.
|
||||
|
||||
+makeExcerpt('app/hero-detail.component.ts ()', 'variables-imports')
|
||||
|
||||
block hero-detail-comp-updates
|
||||
:marked
|
||||
### Add/Edit in the *HeroDetailComponent*
|
||||
|
||||
We already have `HeroDetailComponent` for viewing details about a specific hero.
|
||||
Add and Edit are natural extensions of the detail view, so we are able to reuse `HeroDetailComponent` with a few tweaks.
|
||||
|
||||
The original component was created to render existing data, but to add new data we have to initialize the `hero` property to an empty `Hero` object.
|
||||
|
||||
+makeExcerpt('app/hero-detail.component.ts', 'ngOnInit')
|
||||
|
||||
:marked
|
||||
In order to differentiate between add and edit we are adding a check to see if an id is passed in the URL. If the id is absent we bind `HeroDetailComponent` to an empty `Hero` object. In either case, any edits made through the UI will be bound back to the same `hero` property.
|
||||
|
||||
:marked
|
||||
Add a save method to `HeroDetailComponent` and call the corresponding save method in `HeroesService`.
|
||||
|
||||
+makeExcerpt('app/hero-detail.component.ts', 'save')
|
||||
|
||||
block hero-detail-comp-save-and-goback
|
||||
:marked
|
||||
The same save method is used for both add and edit since `HeroService` will know when to call `post` vs `put` based on the state of the `Hero` object.
|
||||
|
||||
After we save a hero, we redirect the browser back to the previous page using the `goBack()` method.
|
||||
|
||||
+makeExcerpt('app/hero-detail.component.ts', 'goBack')
|
||||
|
||||
:marked
|
||||
Here we call `emit` to notify that we just added or modified a hero. `HeroesComponent` is listening for this notification and will automatically refresh the list of heroes to include our recent updates.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
The `emit` "handshake" between `HeroDetailComponent` and `HeroesComponent` is an example of component to component communication. This is a topic for another day, but we have detailed information in our <a href="/docs/ts/latest/cookbook/component-communication.html#!#child-to-parent">Component Interaction Cookbook</a>
|
||||
|
||||
:marked
|
||||
Here is `HeroDetailComponent` with its new save button and the corresponding HTML.
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/toh/hero-details-save-button.png' alt="Hero Details With Save Button")
|
||||
|
||||
+makeExcerpt('app/hero-detail.component.html', 'save')
|
||||
|
||||
:marked
|
||||
### Add/Delete in the *HeroesComponent*
|
||||
|
||||
We'll be reporting propagated HTTP errors, let's start by adding the following
|
||||
field to the `HeroesComponent` class:
|
||||
|
||||
+makeExcerpt('app/heroes.component.ts', 'error', '')
|
||||
|
||||
:marked
|
||||
The user can *add* a new hero by clicking a button and entering a name.
|
||||
|
||||
block add-new-hero-via-detail-comp
|
||||
:marked
|
||||
When the user clicks the *Add New Hero* button, we display the `HeroDetailComponent`.
|
||||
We aren't navigating to the component so it won't receive a hero `id`;
|
||||
as we noted above, that is the component's cue to create and present an empty hero.
|
||||
|
||||
- var _below = _docsFor == 'dart' ? 'before' : 'below';
|
||||
:marked
|
||||
Add the following to the heroes component HTML, just !{_below} the hero list (`<ul class="heroes">...</ul>`).
|
||||
+makeExcerpt('app/heroes.component.html', 'add-and-error')
|
||||
:marked
|
||||
The first line will display an error message if there is any. The remaining HTML is for adding heroes.
|
||||
|
||||
The user can *delete* an existing hero by clicking a delete button next to the hero's name.
|
||||
Add the following to the heroes component HTML right after the hero name in the repeated `<li>` tag:
|
||||
+makeExcerpt('app/heroes.component.html', 'delete')
|
||||
:marked
|
||||
Add the following to the bottom of the `HeroesComponent` CSS file:
|
||||
+makeExcerpt('app/heroes.component.css', 'additions')
|
||||
:marked
|
||||
Now let's fix-up the `HeroesComponent` to support the *add* and *delete* actions used in the template.
|
||||
Let's start with *add*.
|
||||
|
||||
block heroes-comp-directives
|
||||
:marked
|
||||
We're using the `HeroDetailComponent` to capture the new hero information.
|
||||
We have to tell Angular about that by importing the `HeroDetailComponent` and referencing it in the component metadata `directives` array.
|
||||
+makeExcerpt('app/heroes.component.ts (HeroDetailComponent)', 'hero-detail-component')
|
||||
.l-sub-section
|
||||
:marked
|
||||
These are the same lines that we removed in the previous [Routing](toh-pt5.html) chapter.
|
||||
We didn't know at the time that we'd need the *HeroDetailComponent* again. So we tidied up.
|
||||
|
||||
Now we *must* put these lines back. If we don't, Angular will ignore the `<my-hero-detail>`
|
||||
tag and pushing the *Add New Hero* button will have no visible effect.
|
||||
:marked
|
||||
Implement the click handler for the *Add New Hero* button.
|
||||
|
||||
+makeExcerpt('app/heroes.component.ts', 'addHero')
|
||||
|
||||
block heroes-comp-add
|
||||
:marked
|
||||
The `HeroDetailComponent` does most of the work. All we do is toggle an `*ngIf` flag that
|
||||
swaps it into the DOM when we add a hero and removes it from the DOM when the user is done.
|
||||
|
||||
:marked
|
||||
The *delete* logic is a bit trickier.
|
||||
+makeExcerpt('app/heroes.component.ts', 'deleteHero')
|
||||
|
||||
:marked
|
||||
Of course we delegate the persistence of hero deletion to the `HeroService`.
|
||||
But the component is still responsible for updating the display.
|
||||
So the *delete* method removes the deleted hero from the list.
|
||||
|
||||
block review
|
||||
:marked
|
||||
### Let's see it
|
||||
Here are the fruits of labor in action:
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/toh/toh-http.anim.gif' alt="Heroes List Editing w/ HTTP")
|
||||
|
||||
:marked
|
||||
## !{_Observable}s
|
||||
|
||||
block observables-section-intro
|
||||
:marked
|
||||
Each `Http` method returns an `Observable` of HTTP `Response` objects.
|
||||
|
||||
Our `HeroService` converts that `Observable` into a `Promise` and returns the promise to the caller.
|
||||
In this section we learn to return the `Observable` directly and discuss when and why that might be
|
||||
a good thing to do.
|
||||
|
||||
### Background
|
||||
An *observable* is a stream of events that we can process with array-like operators.
|
||||
|
||||
Angular core has basic support for observables. We developers augment that support with
|
||||
operators and extensions from the [RxJS Observables](http://reactivex.io/rxjs/) library.
|
||||
We'll see how shortly.
|
||||
|
||||
Recall that our `HeroService` quickly chained the `toPromise` operator to the `Observable` result of `http.get`.
|
||||
That operator converted the `Observable` into a `Promise` and we passed that promise back to the caller.
|
||||
|
||||
Converting to a promise is often a good choice. We typically ask `http` to fetch a single chunk of data.
|
||||
When we receive the data, we're done.
|
||||
A single result in the form of a promise is easy for the calling component to consume
|
||||
and it helps that promises are widely understood by JavaScript programmers.
|
||||
|
||||
:marked
|
||||
But requests aren't always "one and done". We may start one request,
|
||||
then cancel it, and make a different request before the server has responded to the first request.
|
||||
Such a _request-cancel-new-request_ sequence is difficult to implement with *!{_Promise}s*.
|
||||
It's easy with *!{_Observable}s* as we'll see.
|
||||
|
||||
### Search-by-name
|
||||
We're going to add a *hero search* feature to the Tour of Heroes.
|
||||
As the user types a name into a search box, we'll make repeated HTTP requests for heroes filtered by that name.
|
||||
|
||||
We start by creating `HeroSearchService` that sends search queries to our server's web api.
|
||||
|
||||
+makeExample('app/hero-search.service.ts')
|
||||
|
||||
:marked
|
||||
The `!{_priv}http.get()` call in `HeroSearchService` is similar to the one
|
||||
in the `HeroService`, although the URL now has a query string.
|
||||
<span if-docs="ts">Another notable difference: we no longer call `toPromise`,
|
||||
we simply return the *observable* instead.</span>
|
||||
|
||||
### HeroSearchComponent
|
||||
|
||||
Let's create a new `HeroSearchComponent` that calls this new `HeroSearchService`.
|
||||
|
||||
The component template is simple — just a text box and a list of matching search results.
|
||||
|
||||
+makeExample('app/hero-search.component.html')
|
||||
:marked
|
||||
We'll also want to add styles for the new component.
|
||||
+makeExample('app/hero-search.component.css')
|
||||
:marked
|
||||
As the user types in the search box, a *keyup* event binding calls the component's `search` method with the new search box value.
|
||||
|
||||
The `*ngFor` repeats *hero* objects from the component's `heroes` property. No surprise there.
|
||||
|
||||
But, as we'll soon see, the `heroes` property is now !{_an} *!{_Observable}* of hero !{_array}s, rather than just a hero !{_array}.
|
||||
The `*ngFor` can't do anything with !{_an} `!{_Observable}` until we flow it through the `async` pipe (`AsyncPipe`).
|
||||
The `async` pipe subscribes to the `!{_Observable}` and produces the !{_array} of heroes to `*ngFor`.
|
||||
|
||||
Time to create the `HeroSearchComponent` class and metadata.
|
||||
|
||||
+makeExample('app/hero-search.component.ts')
|
||||
|
||||
:marked
|
||||
#### Search terms
|
||||
|
||||
Let's focus on the `!{_priv}searchTerms`:
|
||||
|
||||
+makeExcerpt('app/hero-search.component.ts', 'searchTerms', '')
|
||||
|
||||
block search-criteria-intro
|
||||
:marked
|
||||
A `Subject` is a producer of an _observable_ event stream;
|
||||
`searchTerms` produces an `Observable` of strings, the filter criteria for the name search.
|
||||
|
||||
Each call to `search` puts a new string into this subject's _observable_ stream by calling `next`.
|
||||
|
||||
:marked
|
||||
<a id="ngoninit"></a>
|
||||
#### Initialize the _**heroes**_ property (_**ngOnInit**_)
|
||||
|
||||
<span if-docs="ts">A `Subject` is also an `Observable`.</span>
|
||||
We're going to turn the stream
|
||||
of search terms into a stream of `Hero` !{_array}s and assign the result to the `heroes` property.
|
||||
|
||||
+makeExcerpt('app/hero-search.component.ts', 'search', '')
|
||||
|
||||
:marked
|
||||
If we passed every user keystroke directly to the `HeroSearchService`, we'd unleash a storm of HTTP requests.
|
||||
Bad idea. We don't want to tax our server resources and burn through our cellular network data plan.
|
||||
|
||||
block observable-transformers
|
||||
:marked
|
||||
Fortunately, we can chain `Observable` operators to the string `Observable` that reduce the request flow.
|
||||
We'll make fewer calls to the `HeroSearchService` and still get timely results. Here's how:
|
||||
|
||||
* `debounceTime(300)` waits until the flow of new string events pauses for 300 milliseconds
|
||||
before passing along the latest string. We'll never make requests more frequently than 300ms.
|
||||
|
||||
* `distinctUntilChanged` ensures that we only send a request if the filter text changed.
|
||||
There's no point in repeating a request for the same search term.
|
||||
|
||||
* `switchMap` calls our search service for each search term that makes it through the `debounce` and `distinctUntilChanged` gauntlet.
|
||||
It cancels and discards previous search observables, returning only the latest search service observable.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
The [switchMap operator](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md)
|
||||
(formerly known as "flatMapLatest") is very clever.
|
||||
|
||||
Every qualifying key event can trigger an http call.
|
||||
Even with a 300ms pause between requests, we could have multiple http requests in flight
|
||||
and they may not return in the order sent.
|
||||
|
||||
`switchMap` preserves the original request order while returning
|
||||
only the observable from the most recent http call.
|
||||
Results from prior calls are canceled and discarded.
|
||||
|
||||
We also short-circuit the http call and return an observable containing an empty array
|
||||
if the search text is empty.
|
||||
|
||||
Note that _canceling_ the `HeroSearchService` observable won't actually abort a pending http request
|
||||
until the service supports that feature, a topic for another day.
|
||||
We are content for now to discard unwanted results.
|
||||
:marked
|
||||
* `catch` intercepts a failed observable.
|
||||
Our simple example prints the error to the console; a real life application should do better.
|
||||
Then we return an observable containing an empty array to clear the search result.
|
||||
|
||||
### Import RxJS operators
|
||||
The RxJS operators are not available in Angular's base `Observable` implementation.
|
||||
We have to extend `Observable` by *importing* them.
|
||||
|
||||
We could extend `Observable` with just the operators we need here by
|
||||
including the pertinent `import` statements at the top of this file.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Many authorities say we should do just that.
|
||||
:marked
|
||||
We take a different approach in this example.
|
||||
We combine all of the RxJS `Observable` extensions that _our entire app_ requires into a single RxJS imports file.
|
||||
|
||||
+makeExample('app/rxjs-extensions.ts')
|
||||
|
||||
:marked
|
||||
We load them all at once by importing `rxjs-extensions` in `AppComponent`.
|
||||
|
||||
+makeExcerpt('app/app.component.ts', 'rxjs-extensions')
|
||||
|
||||
:marked
|
||||
### Add the search component to the dashboard
|
||||
|
||||
We add the hero search HTML element to the bottom of the `DashboardComponent` template.
|
||||
|
||||
+makeExample('app/dashboard.component.html')
|
||||
|
||||
:marked
|
||||
And finally, we import the `HeroSearchComponent` and add it to the `directives` !{_array}.
|
||||
|
||||
+makeExcerpt('app/dashboard.component.ts', 'search')
|
||||
|
||||
:marked
|
||||
Run the app again, go to the *Dashboard*, and enter some text in the search box.
|
||||
At some point it might look like this.
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/toh/toh-hero-search.png' alt="Hero Search Component")
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Application structure and code
|
||||
|
||||
Review the sample source code in the <live-example></live-example> for this chapter.
|
||||
Verify that we have the following structure:
|
||||
|
||||
block filetree
|
||||
.filetree
|
||||
.file angular2-tour-of-heroes
|
||||
.children
|
||||
.file app
|
||||
.children
|
||||
.file app.component.ts
|
||||
.file app.component.css
|
||||
.file app.routes.ts
|
||||
.file dashboard.component.css
|
||||
.file dashboard.component.html
|
||||
.file dashboard.component.ts
|
||||
.file hero.ts
|
||||
.file hero-detail.component.css
|
||||
.file hero-detail.component.html
|
||||
.file hero-detail.component.ts
|
||||
.file hero-search.component.html (new)
|
||||
.file hero-search.component.css (new)
|
||||
.file hero-search.component.ts (new)
|
||||
.file hero-search.service.ts (new)
|
||||
.file rxjs-operators.ts
|
||||
.file hero.service.ts
|
||||
.file heroes.component.css
|
||||
.file heroes.component.html
|
||||
.file heroes.component.ts
|
||||
.file main.ts
|
||||
.file in-memory-data.service.ts (new)
|
||||
.file node_modules ...
|
||||
.file typings ...
|
||||
.file index.html
|
||||
.file package.json
|
||||
.file styles.css
|
||||
.file systemjs.config.json
|
||||
.file tsconfig.json
|
||||
.file typings.json
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Home Stretch
|
||||
|
||||
We are at the end of our journey for now, but we have accomplished a lot.
|
||||
- We added the necessary dependencies to use HTTP in our application.
|
||||
- We refactored `HeroService` to load heroes from a web API.
|
||||
- We extended `HeroService` to support post, put and delete methods.
|
||||
- We updated our components to allow adding, editing and deleting of heroes.
|
||||
- We configured an in-memory web API.
|
||||
- We learned how to use !{_Observable}s.
|
||||
|
||||
Here are the files we added or changed in this chapter.
|
||||
|
||||
block file-summary
|
||||
+makeTabs(
|
||||
`toh-6/ts/app/app.component.ts,
|
||||
toh-6/ts/app/heroes.component.ts,
|
||||
toh-6/ts/app/heroes.component.html,
|
||||
toh-6/ts/app/heroes.component.css,
|
||||
toh-6/ts/app/hero-detail.component.ts,
|
||||
toh-6/ts/app/hero-detail.component.html,
|
||||
toh-6/ts/app/hero.service.ts,
|
||||
toh-6/ts/app/in-memory-data.service.ts`,
|
||||
null,
|
||||
`app.comp...ts,
|
||||
heroes.comp...ts,
|
||||
heroes.comp...html,
|
||||
heroes.comp...css,
|
||||
hero-detail.comp...ts,
|
||||
hero-detail.comp...html,
|
||||
hero.service.ts,
|
||||
in-memory-data.service.ts`
|
||||
)
|
||||
|
||||
+makeTabs(
|
||||
`toh-6/ts/app/hero-search.service.ts,
|
||||
toh-6/ts/app/hero-search.component.ts,
|
||||
toh-6/ts/app/hero-search.component.html,
|
||||
toh-6/ts/app/hero-search.component.css,
|
||||
toh-6/ts/app/rxjs-operators.ts`,
|
||||
null,
|
||||
`hero-search.service.ts,
|
||||
hero-search.component.ts,
|
||||
hero-search.service.html,
|
||||
hero-search.component.css,
|
||||
rxjs-operators.ts`
|
||||
)
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e -o pipefail
|
||||
|
||||
BASE="public/docs/ts"
|
||||
LATEST="$BASE/latest"
|
||||
CACHE="$BASE/_cache"
|
||||
|
||||
FILES="
|
||||
guide/architecture.jade
|
||||
guide/attribute-directives.jade
|
||||
guide/component-styles.jade
|
||||
guide/dependency-injection.jade
|
||||
guide/displaying-data.jade
|
||||
guide/hierarchical-dependency-injection.jade
|
||||
guide/lifecycle-hooks.jade
|
||||
guide/pipes.jade
|
||||
guide/security.jade
|
||||
guide/server-communication.jade
|
||||
guide/structural-directives.jade
|
||||
guide/template-syntax.jade
|
||||
quickstart.jade
|
||||
tutorial/toh-pt6.jade"
|
||||
|
||||
function main() {
|
||||
local allFound=true;
|
||||
|
||||
for f in $FILES; do
|
||||
local srcPath="$LATEST/$f";
|
||||
local destPath="$CACHE/$f";
|
||||
local destDir=`dirname $destPath`;
|
||||
if [[ -e $srcPath ]]; then
|
||||
[[ -d "$destDir" ]] || (set -x; mkdir $destDir);
|
||||
(set -x; cp $srcPath $destPath)
|
||||
else
|
||||
echo Cannot find $srcPath
|
||||
allFound=false;
|
||||
fi
|
||||
done
|
||||
|
||||
[[ $allFound ]] || exit 1;
|
||||
}
|
||||
|
||||
main;
|
Loading…
Reference in New Issue