parent
8a5bcb4da7
commit
9e75006b2d
|
@ -1,6 +1,8 @@
|
|||
//
|
||||
TODO: REVIVE AUX ROUTE MATERIAL WHEN THAT FEATURE WORKS AS EXPECTED
|
||||
|
||||
PLEASE DO NOT CREATE ISSUES OR PULL REQUESTS FOR THIS PAGE
|
||||
|
||||
<a id="chat-feature"></a>
|
||||
.l-main-section
|
||||
:marked
|
||||
|
|
|
@ -1,724 +0,0 @@
|
|||
.l-main-section
|
||||
|
||||
h2#section-route-use Using the Component Router
|
||||
p There are three steps to setting up routing with Angular's Component Router
|
||||
ol
|
||||
li Install the Component Router
|
||||
li Map paths to components
|
||||
li Link to routes
|
||||
|
||||
|
||||
.l-main-section
|
||||
|
||||
h2#section-install-router Import the Component Router
|
||||
|
||||
p.
|
||||
Create two files, <code>index.html</code> and <code>app.ts</code>, both at the root of the project:
|
||||
|
||||
pre.prettyprint.lang-bash
|
||||
code.
|
||||
touch index.html app.ts
|
||||
|
||||
p Your app directory should look something like:
|
||||
pre.prettyprint.lang-bash
|
||||
code.
|
||||
app.ts
|
||||
index.html
|
||||
package.json
|
||||
node_modules/
|
||||
└── ...
|
||||
|
||||
p.
|
||||
Because the component is an addition to the core, you must install Angular's Component Router into your app.
|
||||
When using the angular2.dev.js bundle you have include the additional router.dev.js bundle.
|
||||
|
||||
p.
|
||||
Add Angular and Component Router into your app by adding the relevant <code><script></code> tags into your
|
||||
<code>index.html</code>:
|
||||
|
||||
//ANGULAR 1
|
||||
pre.prettyprint.lang-html.is-angular1.is-hidden
|
||||
code.
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<base href="/">
|
||||
<title>My app</title>
|
||||
</head>
|
||||
<body ng-app="myApp" ng-controller="AppController as app">
|
||||
<div ng-outlet></div>
|
||||
<script src="/node_modules/angular/angular.js"></script>
|
||||
<script src="/dist/router.es5.js"></script>
|
||||
<script src="/app/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
pre.prettyprint.lang-html.is-angular2
|
||||
code.
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<base href="/">
|
||||
<title>My app</title>
|
||||
</head>
|
||||
<body ng-app="myApp" ng-controller="AppController as app">
|
||||
<script src="https://jspm.io/system@0.16.js"></script>
|
||||
<script src="https://code.angularjs.org/2.0.0-alpha.21/angular2.dev.js"></script>
|
||||
<script src="https://code.angularjs.org/2.0.0-alpha.21/router.dev.js"></script>
|
||||
<script>
|
||||
System.import('main');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
p.is-angular2.
|
||||
Then you can add the router into your app by importing the Router module in your <code>app.ts</code> file:
|
||||
|
||||
.code-box.is-angular2
|
||||
pre.prettyprint.linenums.lang-typescript(data-name="typescript")
|
||||
code.
|
||||
import {Component, View, bootstrap} from 'angular2/angular2';
|
||||
import {Router, RouterOutlet, RouterLink} from 'angular2/router';
|
||||
pre.prettyprint.linenums.lang-javascript(data-name="es5")
|
||||
code.
|
||||
// self-executing bundle deploys angular APIs on the window object.
|
||||
window.angular;
|
||||
// the router APIs are part of the router sub-object.
|
||||
window.angular.router;
|
||||
|
||||
p.is-angular1.is-hidden.
|
||||
This is a pretty typical angular app, except the <code>ng-outlet</code> directive. <code>ng-outlet</code> is like
|
||||
<code>ng-view</code>; it's a placeholder for part of your app loaded dynamically based on the route configuration.
|
||||
|
||||
p.is-angular2.
|
||||
This is the same as you've seen in the rest of Angular 2, except the <code>router-outlet</code> directive.
|
||||
<code>router-outlet</code> is a placeholder for part of your app loaded dynamically based on the route configuration.
|
||||
|
||||
p So how do we configure the app? Let's open <code>app.ts</code> and find out. Add this to the file:
|
||||
|
||||
//ANGULAR 1
|
||||
pre.prettyprint.lang-javascript.is-angular1.is-hidden
|
||||
code.
|
||||
angular.module('app', ['ngNewRouter'])
|
||||
.controller('AppController', ['$router', AppController]);
|
||||
|
||||
AppController.$routeConfig = [
|
||||
{path: '/', component: 'home' }
|
||||
];
|
||||
function AppController ($router) {}
|
||||
|
||||
// ANGULAR 2
|
||||
.code-box
|
||||
pre.prettyprint.linenums.lang-typescript(data-name="typescript")
|
||||
code.
|
||||
import {Component, View, bootstrap} from 'angular2/angular2';
|
||||
import {routerInjectables, RouterOutlet} from 'angular2/router';
|
||||
|
||||
import {HomeComp} from './components/home';
|
||||
|
||||
@Component({
|
||||
selector: 'my-app'
|
||||
})
|
||||
@View({
|
||||
template: '<router-outlet></router-outlet>',
|
||||
directives: [RouterOutlet]
|
||||
})
|
||||
@RouteConfig([
|
||||
{path: '/', component: HomeComp }
|
||||
])
|
||||
class AppComp {}
|
||||
|
||||
bootstrap(AppComp, routerInjectables);
|
||||
pre.prettyprint.linenums.lang-javascript(data-name="es5")
|
||||
code.
|
||||
var HomeComp = function() {};
|
||||
...
|
||||
|
||||
var AppComp = function() {};
|
||||
AppComp.annotations = [
|
||||
new angular.ComponentAnnotation({
|
||||
selector: 'my-app'
|
||||
}),
|
||||
new angular.ViewAnnotation({
|
||||
template: '<router-outlet></router-outlet>',
|
||||
directives: [angular.router.RouterOutlet]
|
||||
}),
|
||||
new angular.router.RouteConfigAnnotation([
|
||||
{path: '/', component: HomeComp}
|
||||
])
|
||||
];
|
||||
|
||||
angular.bootstrap(AppComp, routerInjectables);
|
||||
|
||||
|
||||
p.is-angular1.is-hidden.
|
||||
The <code>ngComponentRouter</code> module provides a new service, <code>$router</code>. In the configuration, we map paths
|
||||
to components. What's a component? Let's talk about that for a bit.
|
||||
|
||||
p.is-angular2.
|
||||
The <code>angular2/router</code> module provides <code>routerInjectables</code>, which is an array of all of the services
|
||||
you'll need to use the component router in your app.
|
||||
|
||||
.l-main-section
|
||||
h2#section-map-paths-to-components Map paths to components
|
||||
|
||||
//- TODO - Alex - would it make more sense to have some paragraph styles conditionalized like this??
|
||||
p.angular1.is-hidden.
|
||||
In Angular 1, a "routable component" is a template, plus a controller, plus a router. You can configure how to map
|
||||
component names to controllers and templates in the <code>$componentLoader</code> service.
|
||||
|
||||
p.
|
||||
A component's template can have "outlets," which are holes in the DOM for loading parts of your app based on the
|
||||
route configuration and it can ask the DI system for an instance of Router. A component's router tells the component what to put
|
||||
inside the outlets based on URL. The configuration maps routes to components for each outlet.
|
||||
|
||||
p Let's make a <code>home</code> component that our app can route to:
|
||||
|
||||
pre.prettyprint.lang-bash
|
||||
code.
|
||||
mkdir -p components/home
|
||||
touch components/home/home.html components/home/home.js
|
||||
|
||||
p This creates our component directory and its corresponding files: a template and a JavaScript component.
|
||||
|
||||
p Let's open <code>home.html</code> and add some content:
|
||||
|
||||
pre.prettyprint.lang-html
|
||||
code.
|
||||
<h1>Hello {{home.name}}!</h1>
|
||||
|
||||
p.is-angular1.is-hidden.
|
||||
Components use the "controller as" syntax, so if we want to access property <code>name</code> of the controller, we
|
||||
write the binding as <code>home.name</code>.
|
||||
|
||||
p Let's make a controller:
|
||||
//ANGULAR 1
|
||||
pre.prettyprint.lang-javascript.is-angular1.is-hidden
|
||||
code.
|
||||
angular.module('app.home', [])
|
||||
.controller('HomeController', [function () {
|
||||
this.name = 'Friend';
|
||||
}]);
|
||||
|
||||
// ANGULAR 2
|
||||
.code-box
|
||||
pre.prettyprint.linenums.lang-typescript(data-name="typescript")
|
||||
code.
|
||||
@Component({
|
||||
selector: 'home-cmp'
|
||||
})
|
||||
@View({
|
||||
template: 'Hello {{name}}'
|
||||
})
|
||||
export class HomeComponent {
|
||||
name:string;
|
||||
constructor() {
|
||||
this.name = 'Friend';
|
||||
}
|
||||
}
|
||||
pre.prettyprint.linenums.lang-javascript(data-name="es5")
|
||||
code.
|
||||
function HomeComponent() {
|
||||
this.name = 'Friend';
|
||||
}
|
||||
|
||||
AppComponent.annotations = [
|
||||
new angular.ComponentAnnotation({
|
||||
selector: 'home-cmp'
|
||||
}),
|
||||
new angular.ViewAnnotation({
|
||||
template: 'Hello {{name}}'
|
||||
})
|
||||
];
|
||||
|
||||
|
||||
p.is-angular1.is-hidden.
|
||||
To wire this up, we need to add a <code><script></code> tag to our <code>index.html</code>:
|
||||
pre.prettyprint.lang-html
|
||||
code.
|
||||
...
|
||||
<script src="./components/home/home.js"></script>
|
||||
|
||||
//ANGULAR 1
|
||||
p.is-angular1.is-hidden.
|
||||
And add the controller's module as a dependency to our main module in <code>app.js</code>:
|
||||
pre.prettyprint.lang-javascript.is-angular1.is-hidden
|
||||
code.
|
||||
angular.module('app', ['ngNewRouter', 'app.home'])
|
||||
.controller('AppController', ['$router', AppController]);
|
||||
// ...
|
||||
|
||||
p.
|
||||
To wire this up, we need to import the component into the rest of our app.
|
||||
// ANGULAR 2
|
||||
.code-box.is-angular2
|
||||
pre.prettyprint.linenums.lang-typescript(data-name="typescript")
|
||||
code.
|
||||
import {HomeComp} from './components/home';
|
||||
pre.prettyprint.linenums.lang-javascript(data-name="es5")
|
||||
code.
|
||||
// Use your favorite module system / bundler for ES5.
|
||||
|
||||
|
||||
p If you load up the app, you should see <code>Hello Friend!</code>
|
||||
|
||||
.l-main-section
|
||||
h2#section-link-to-routes Link to routes
|
||||
|
||||
p Let's add another route and then link to it. This route will have a route parameter, <code>id</code>.
|
||||
|
||||
p In <code>app.js</code>:
|
||||
//ANGULAR 1
|
||||
pre.prettyprint.lang-javascript.is-hidden
|
||||
code.
|
||||
angular.module('app', ['ngNewRouter'])
|
||||
.controller('AppController', ['$router', AppController]);
|
||||
AppController.$routeConfig = [
|
||||
{ path: '/', component: 'home' },
|
||||
{ path: '/detail/:id', component: 'detail' }
|
||||
];
|
||||
function AppController ($router) {}
|
||||
|
||||
// ANGULAR 2
|
||||
.code-box
|
||||
pre.prettyprint.linenums.lang-typescript(data-name="typescript")
|
||||
code.
|
||||
...
|
||||
@RouteConfig([
|
||||
{ path: '/', component: HomeComp },
|
||||
{ path: '/detail/:id', component: DetailComp }
|
||||
])
|
||||
class AppComp {}
|
||||
pre.prettyprint.linenums.lang-javascript(data-name="es5")
|
||||
code.
|
||||
var AppComp = function() {};
|
||||
AppComp.annotations = [
|
||||
...
|
||||
new angular.router.RouteConfigAnnotation([
|
||||
{ path: '/', component: HomeComp}
|
||||
{ path: '/detail/:id', component: DetailComp }
|
||||
])
|
||||
];
|
||||
|
||||
angular.bootstrap(AppComp, routerInjectables);
|
||||
|
||||
|
||||
p.
|
||||
We can link to our detail component using the
|
||||
<code class="is-angular2">router-link</code><code class="is-angular1 is-hidden">ng-link</code> directive.
|
||||
Add this to <code class="is-angular2">template</code><code class="is-angular1 is-hidden">index.html</code>:
|
||||
|
||||
pre.prettyprint.lang-html.is-angular1.is-hidden.
|
||||
code.
|
||||
<body ng-app="myApp" ng-controller="AppController as app">
|
||||
<a ng-link="detail({id: 5})">link to detail</a>
|
||||
...
|
||||
|
||||
pre.prettyprint.lang-html.is-angular2
|
||||
code.
|
||||
<a ng-link="detail({id: 5})">link to detail</a>
|
||||
|
||||
p This directive will generate an <code>href</code> and update the browser URL.
|
||||
|
||||
p We should also implement our detail component. Let's make these new files:
|
||||
|
||||
pre.prettyprint.lang-bash
|
||||
code.
|
||||
mkdir components/detail
|
||||
touch components/detail/detail.html components/detail/detail.ts
|
||||
|
||||
p In <code>detail.ts</code>, we implement a controller that uses the <code>id</code> route parameter:
|
||||
|
||||
//ANGULAR 1
|
||||
pre.prettyprint.lang-javascript.is-hidden
|
||||
code.
|
||||
angular.module('app.detail', ['ngNewRouter'])
|
||||
.controller('DetailController', ['$routeParams', DetailController]);
|
||||
|
||||
function DetailController ($routeParams) {
|
||||
this.id = $routeParams.id;
|
||||
}
|
||||
|
||||
// ANGULAR 2
|
||||
.code-box
|
||||
pre.prettyprint.linenums.lang-typescript(data-name="typescript")
|
||||
code.
|
||||
@Component({
|
||||
selector: 'detail-cmp'
|
||||
})
|
||||
@View({
|
||||
template: 'User ID: {{id}}'
|
||||
})
|
||||
export class DetailComp {
|
||||
id: string;
|
||||
constructor(routeParams:RouteParams) {
|
||||
this.id = routeParams.get('id');
|
||||
}
|
||||
}
|
||||
pre.prettyprint.linenums.lang-javascript(data-name="es5")
|
||||
code.
|
||||
function DetailComp(routeParams) {
|
||||
this.id = routeParams.get('id');
|
||||
}
|
||||
|
||||
DetailComp.annotations = [
|
||||
new angular.ComponentAnnotation({
|
||||
selector: 'detail-cmp'
|
||||
}),
|
||||
new angular.ViewAnnotation({
|
||||
template: 'User ID: {{id}}'
|
||||
})
|
||||
];
|
||||
|
||||
DetailComp.parameters = [[RouteParams]];
|
||||
|
||||
|
||||
p.is-angular1.is-hidden.
|
||||
And then we can display the <code>id</code> in our template by adding this to <code>detail.html</code>:
|
||||
|
||||
pre.prettyprint.lang-html.is-angular1.is-hidden
|
||||
code.
|
||||
<p>detail {{detail.id}}</p>
|
||||
|
||||
p.is-angular1.is-hidden.
|
||||
Finally, we'd wire up the controller by adding a script tag and making our <code>app</code> module depend on
|
||||
<code>app.detail</code>.
|
||||
|
||||
|
||||
.l-main-section
|
||||
|
||||
h2#section-configuring-the-router Configuring the Router
|
||||
|
||||
p.
|
||||
Unlike other routing systems, Component Router maps URLs to components. A router takes an array of pairings like
|
||||
this:
|
||||
|
||||
//ANGULAR 1
|
||||
pre.prettyprint.lang-javascript.is-angular1.is-hidden
|
||||
code.
|
||||
//ES5
|
||||
MyController.$routeConfig = [
|
||||
{ path: '/user', component: 'user' }
|
||||
];
|
||||
|
||||
|
||||
//ANGULAR 2
|
||||
.code-box.is-angular2
|
||||
pre.prettyprint.linenums.lang-javascript(data-name="typescript")
|
||||
code.
|
||||
@Component()
|
||||
@View()
|
||||
@RouteConfig([
|
||||
{ path: '/user', component: UserComponent }
|
||||
])
|
||||
class MyComp {}
|
||||
pre.prettyprint.linenums.lang-javascript(data-name="es5")
|
||||
code.
|
||||
function MyComp() {};
|
||||
MyComp.annotations = [
|
||||
new angular.ComponentAnnotation({ ... }),
|
||||
new angular.ViewAnnotation({ ... }),
|
||||
new angular.router.RouteConfigAnnotation([
|
||||
{path: '/', component: UserComponent}
|
||||
])
|
||||
|
||||
|
||||
.l-sub-section
|
||||
h3#section-sibling-outlets Sibling Outlets
|
||||
|
||||
|
||||
p You can configure multiple outlets on the same path like this:
|
||||
|
||||
//ANGULAR 1
|
||||
.codebox.is-angular1.is-hidden
|
||||
pre.prettyprint.linenums.lang-javascript(data-name="es5")
|
||||
code.
|
||||
//ES5
|
||||
MyController.$routeConfig = [
|
||||
{ path: '/user',
|
||||
components: {
|
||||
master: 'userList',
|
||||
detail: 'user'
|
||||
} }
|
||||
];
|
||||
|
||||
pre.prettyprint.linenums.lang-html(data-name="html")
|
||||
code.
|
||||
//HTML
|
||||
<div ng-outlet="master"></div>
|
||||
<div ng-outlet="detail"></div>
|
||||
|
||||
|
||||
//ANGULAR 2
|
||||
.code-box.is-angular2
|
||||
pre.prettyprint.linenums.lang-typescript(data-name="typescript")
|
||||
code.
|
||||
//TypeScript
|
||||
@Component({})
|
||||
@View({
|
||||
template:
|
||||
`<div router-outlet="master"></div>
|
||||
<div router-outlet="detail"></div>`,
|
||||
directives: [RouterOutlet, RouterLink]
|
||||
})
|
||||
@RouteConfig({
|
||||
path: '/user', components: {
|
||||
master: UserListComp,
|
||||
detail: UserComp
|
||||
}
|
||||
})
|
||||
class MyComponent {}
|
||||
|
||||
pre.prettyprint.linenums.lang-javascript(data-name="es5")
|
||||
code.
|
||||
function MyComponent() {};
|
||||
MyComponent.annotations = [
|
||||
new angular.ComponentAnnotation({ ... }),
|
||||
new angular.ViewAnnotation({
|
||||
template:
|
||||
'<div router-outlet="master"></div>' +
|
||||
'<div router-outlet="detail"></div>',
|
||||
directives: [RouterOutlet]
|
||||
}),
|
||||
new angular.router.RouteConfigAnnotation([{
|
||||
path: '/user', components: {
|
||||
master: UserComponent,
|
||||
detail: UserComp
|
||||
}
|
||||
}])
|
||||
];
|
||||
|
||||
p You can link to any sibling just as you normally would:
|
||||
|
||||
//ANGULAR 1
|
||||
pre.prettyprint.linenums.lang-html.is-angular1.is-hidden
|
||||
code.
|
||||
//HTML
|
||||
<p>These both link to the same view:</p>
|
||||
<a ng-link="userList">link to userList</a>
|
||||
<a ng-link="user">link to user component</a>
|
||||
|
||||
//ANGULAR 2
|
||||
pre.prettyprint.linenums.lang-html.is-angular2
|
||||
code.
|
||||
//HTML
|
||||
<p>These both link to the same view:</p>
|
||||
<a router-link="userList">link to userList</a>
|
||||
<a router-link="user">link to user component</a>
|
||||
|
||||
|
||||
p Or, you can explicitly link to a outlet-component pair like this:
|
||||
|
||||
//ANGULAR 1
|
||||
pre.prettyprint.linenums.lang-html.is-angular1.is-hidden
|
||||
code.
|
||||
//HTML
|
||||
<p>These both link to the same view:</p>
|
||||
<a ng-link="master:userList">link to userList</a>
|
||||
<a ng-link="detail:user">link to user component</a>
|
||||
|
||||
//ANGULAR 2
|
||||
pre.prettyprint.linenums.lang-html.is-angular2
|
||||
code.
|
||||
//HTML
|
||||
<p>These both link to the same view:</p>
|
||||
<a router-link="master:userList">link to userList</a>
|
||||
<a router-link="detail:user">link to user component</a>
|
||||
|
||||
.l-sub-section
|
||||
h3#section-redirecting-routes Redirecting routes
|
||||
|
||||
p You can use `redirectTo` for migrating to a new URL scheme and setting up default routes.
|
||||
|
||||
p.
|
||||
For example, as specified below, when a user navigates to `/`, the URL changes to `/user` and the outlet
|
||||
at that level loads the `user` component.
|
||||
|
||||
//ANGULAR 1
|
||||
pre.prettyprint.linenums.lang-javascript.is-angular1.is-hidden(data-name="es5")
|
||||
code.
|
||||
//ES5
|
||||
MyController.$routeConfig = [
|
||||
{ path: '/', redirectTo: '/user' },
|
||||
{ path: '/user', component: 'user' }
|
||||
];
|
||||
function MyController() {}
|
||||
|
||||
//ANGULAR 2
|
||||
.code-box.is-angular2
|
||||
pre.prettyprint.linenums.lang-typescript(data-name="typescript")
|
||||
code.
|
||||
//TypeScript
|
||||
@Component({})
|
||||
@View({
|
||||
directives: [RouterOutlet]
|
||||
})
|
||||
@RouteConfig([
|
||||
{ path: '/', redirectTo: '/user' },
|
||||
{ path: '/user', component: UserComp }
|
||||
])
|
||||
class MyComp {}
|
||||
pre.prettyprint.linenums.lang-javascript(data-name="es5")
|
||||
code.
|
||||
function MyComponent() {};
|
||||
MyComponent.annotations = [
|
||||
new angular.ComponentAnnotation({ ... }),
|
||||
new angular.ViewAnnotation({
|
||||
directives: [RouterOutlet]
|
||||
}),
|
||||
new angular.router.RouteConfigAnnotation([
|
||||
{ path: '/user', component: UserComp }
|
||||
{ path: '/', redirectTo: '/user' },
|
||||
])
|
||||
];
|
||||
|
||||
|
||||
.l-sub-section
|
||||
h3#section-aliases Aliases
|
||||
|
||||
p.
|
||||
When linking to a route, you normally use the name of the component. You can also specify an alias to use
|
||||
instead.
|
||||
|
||||
p Consider the following route configuration:
|
||||
|
||||
//ANGULAR 1
|
||||
pre.prettyprint.linenums.lang-javascript.is-angular1.is-hidden(data-name="es5")
|
||||
code.
|
||||
//ES5
|
||||
MyController.$routeConfig = [
|
||||
{ path: '/', component: 'user' }
|
||||
];
|
||||
|
||||
//ANGULAR 2
|
||||
.code-box.is-angular2
|
||||
pre.prettyprint.linenums.lang-typescript(data-name="typescript")
|
||||
code.
|
||||
//TypeScript
|
||||
@Component({
|
||||
selector: 'my-comp'
|
||||
})
|
||||
@View({
|
||||
directives: [RouterOutlet]
|
||||
})
|
||||
@RouteConfig([
|
||||
{ path: '/', component: UserComp }
|
||||
])
|
||||
class MyComp {}
|
||||
pre.prettyprint.linenums.lang-javascript(data-name="es5")
|
||||
code.
|
||||
function MyComp() {};
|
||||
MyComp.annotations = [
|
||||
new angular.ComponentAnnotation({ ... }),
|
||||
new angular.ViewAnnotation({
|
||||
directives: [RouterOutlet]
|
||||
}),
|
||||
new angular.router.RouteConfigAnnotation([
|
||||
{ path: '/', component: UserComp }
|
||||
])
|
||||
];
|
||||
|
||||
|
||||
p We can link to the route in our template with the name of the component:
|
||||
//ANGULAR 1
|
||||
pre.prettyprint.linenums.lang-html.is-angular1.is-hidden
|
||||
code.
|
||||
//HTML
|
||||
<a ng-link="user">link to user component</a>
|
||||
|
||||
//ANGULAR 2
|
||||
pre.prettyprint.linenums.lang-html
|
||||
code.
|
||||
//HTML
|
||||
<a router-link="user">link to user component</a>
|
||||
|
||||
p Or, we can define an alias <code>myUser</code> like this:
|
||||
|
||||
//ANGULAR 1
|
||||
pre.prettyprint.linenums.lang-javascript.is-angular1.is-hidden(data-name="es5")
|
||||
code.
|
||||
//ES5
|
||||
MyController.$routeConfig = [
|
||||
{ path: '/', component: 'user', as: 'myUser' }
|
||||
];
|
||||
|
||||
//ANGULAR 2
|
||||
.code-box.is-angular2
|
||||
pre.prettyprint.linenums.lang-typescript(data-name="typescript")
|
||||
code.
|
||||
//TypeScript
|
||||
@Component()
|
||||
@View()
|
||||
@RouteConfig([
|
||||
{ path: '/', component: UserComp, as: 'myUser' }
|
||||
])
|
||||
class MyComp {}
|
||||
pre.prettyprint.linenums.lang-javascript(data-name="es5")
|
||||
code.
|
||||
//ES5
|
||||
//TODO: Need Angular 2 ES5 Example here
|
||||
|
||||
|
||||
p And refer instead to the alias for the component in our template, with the same end-result:
|
||||
//ANGULAR 1
|
||||
pre.prettyprint.linenums.lang-html.is-angular1.is-hidden
|
||||
code.
|
||||
//HTML
|
||||
<a ng-link="myUser">link to user component</a>
|
||||
|
||||
//ANGULAR 2
|
||||
pre.prettyprint.linenums.lang-html
|
||||
code.
|
||||
//HTML
|
||||
<a router-link="myUser">link to user component</a>
|
||||
|
||||
|
||||
p.
|
||||
This is especially useful when you have sibling components, but want to refer to an entire level of routing in
|
||||
your controller. For example:
|
||||
|
||||
//ANGULAR 1
|
||||
pre.prettyprint.linenums.lang-javascript.is-angular1.is-hidden(data-name="es5")
|
||||
code.
|
||||
//ES5
|
||||
MyController.$routeConfig = [
|
||||
{ path: '/',
|
||||
components: {
|
||||
master: 'userList',
|
||||
detail: 'user'
|
||||
},
|
||||
as: 'myUser'
|
||||
}
|
||||
];
|
||||
|
||||
//ANGULAR 2
|
||||
.code-box.is-angular2
|
||||
pre.prettyprint.linenums.lang-typescript(data-name="typescript")
|
||||
code.
|
||||
//TypeScript
|
||||
@RouteConfig([
|
||||
{ path: '/', components:
|
||||
{ master: UserListComp, detail: UserComp },
|
||||
as: 'myUser' }
|
||||
])
|
||||
pre.prettyprint.linenums.lang-javascript(data-name="es5")
|
||||
code.
|
||||
new angular.router.RouteConfigAnnotation([
|
||||
{ path: '/', components:
|
||||
{ master: UserListComp, detail: UserComp },
|
||||
as: 'myUser' }
|
||||
])
|
||||
|
||||
|
||||
//- TODO(btford): expand on this.
|
||||
.l-sub-section
|
||||
h3#dynamic-configuration Dynamic Configuration
|
||||
|
||||
p.is-angular2.
|
||||
You can configure dynamic routing by asking the DI system for a <code>Router</code>.
|
||||
|
||||
p.is-angular1.is-hidden
|
||||
You can configure dynamic routing by making a request for <code>$router</code>.
|
|
@ -22,7 +22,7 @@ include ../../../../_includes/_util-fns
|
|||
|
||||
We use the Angular `Http` client to communicate via `XMLHttpRequest (XHR)`.
|
||||
|
||||
We'll illustrate with a mini-version of the [tutorial](tutorial.html)'s "Tour of Heroes" (ToH) application.
|
||||
We'll illustrate with a mini-version of the [tutorial](../tutorial)'s "Tour of Heroes" (ToH) application.
|
||||
This one gets some heroes from the server, displays them in a list, lets us add new heroes, and save them to the server.
|
||||
|
||||
It works like this.
|
||||
|
|
|
@ -13,6 +13,17 @@ include ../../../../_includes/_util-fns
|
|||
|
||||
Our lofty goal in this chapter is make it easy for you to write Angular application tests. If that goal exceeds our reach, we can at least make testing *easier*… and easy enough that you’ll want to write tests for your application.
|
||||
|
||||
.alert.is-important
|
||||
:marked
|
||||
## Content out-of-date
|
||||
|
||||
These testing chapters were written before the Angular 2 Beta release
|
||||
and are scheduled for significant updates.
|
||||
|
||||
Much of the material remains accurate and relevant but references to
|
||||
specific features of Angular 2 and the Angular 2 testing library
|
||||
may not be correct. Please bear with us.
|
||||
:marked
|
||||
## The Testing Spectrum
|
||||
|
||||
Exploring behavior with tests is called “Functional Testing”. There are other important forms of testing too such as acceptance, security, performance, and deployment testing. We concentrate on functional testing in this section on unit testing.
|
||||
|
|
|
@ -16,7 +16,7 @@ code-example(format="linenums" language="html" escape="html").
|
|||
The code for `InitCapsPipe` in `init-caps-pipe.ts` is quite brief:
|
||||
|
||||
```
|
||||
import {Pipe} from 'angular2/angular2';
|
||||
import {Pipe} from 'angular2/core';
|
||||
|
||||
@Pipe({ name: 'initCaps' })
|
||||
export class InitCapsPipe {
|
||||
|
@ -60,14 +60,14 @@ figure.image-display
|
|||
tried to load Angular and couldn't find it.
|
||||
|
||||
code-example(format="" language="html" escape="html").
|
||||
GET http://127.0.0.1:8080/src/angular2/angular2 404 (Not Found)
|
||||
GET http://127.0.0.1:8080/src/angular2/core 404 (Not Found)
|
||||
|
||||
:marked
|
||||
We are writing an Angular application afterall and
|
||||
we were going to need Angular sooner or later. That time has come.
|
||||
The `InitCapsPiep` clearly depends on Angular as is clear in the first few lines:
|
||||
```
|
||||
import {Pipe} from 'angular2/angular2';
|
||||
import {Pipe} from 'angular2/core';
|
||||
|
||||
@Pipe({ name: 'initCaps' })
|
||||
export class InitCapsPipe {
|
||||
|
|
|
@ -1,623 +0,0 @@
|
|||
include ../../../../_includes/_util-fns
|
||||
:markdown
|
||||
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.
|
||||
|
||||
In this chapter we'll learn what Dependency Injection is, why we want it, and how to use it.
|
||||
<a name="why-di"></a>
|
||||
.l-main-section
|
||||
:markdown
|
||||
## Why Dependency Injection?
|
||||
|
||||
Let's start with the following code.
|
||||
|
||||
```
|
||||
class Engine {}
|
||||
|
||||
class Tires {}
|
||||
|
||||
class Car {
|
||||
private engine: Engine;
|
||||
private tires: Tires;
|
||||
|
||||
constructor() {
|
||||
this.engine = new Engine();
|
||||
this.tires = new Tires();
|
||||
}
|
||||
// Method using the engine and tires
|
||||
drive() {}
|
||||
}
|
||||
```
|
||||
|
||||
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 creates its own copies by "new-ing" them 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
|
||||
`this.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 definion 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 ... like 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 if 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 become difficult to test.
|
||||
|
||||
How can we make `Car` more robust, more flexible, and more testable?
|
||||
|
||||
That's super easy. We probably already know what to do. We change our `Car` constructor to this:
|
||||
<a name="ctor-injection"></a>
|
||||
```
|
||||
constructor(engine: Engine, tires: Tires) {
|
||||
this.engine = engine;
|
||||
this.tires = tires;
|
||||
}
|
||||
```
|
||||
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.
|
||||
|
||||
Now we create a car by passing the engine and tires to the constructor.
|
||||
```
|
||||
var car = new Car(new Engine(), new Tires());
|
||||
```
|
||||
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
|
||||
:markdown
|
||||
The consumer of `Car` has the problem. The consumer must update the car creation code to
|
||||
something like:
|
||||
```
|
||||
var car = new Car(new Engine(theNewParameter), new Tires());
|
||||
```
|
||||
The critical point is this: `Car` itself did not have to change.
|
||||
We'll take care of the consumer's problem soon enough.
|
||||
|
||||
:markdown
|
||||
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 car = new Car(new MockEngine(), new MockLowPressureTires());
|
||||
```
|
||||
|
||||
**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:
|
||||
```
|
||||
class SuperFactory {
|
||||
createEngine = () => new Engine();
|
||||
createTires = () => new Tires();
|
||||
createCar = () => new Car(this.createEngine(), this.createTires());
|
||||
}
|
||||
```
|
||||
It's not so bad now with only three creation methods.
|
||||
But maintaining it will be hairy as the application grows.
|
||||
This `SuperFactory` is going to become a huge spider web 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.
|
||||
```
|
||||
function main() {
|
||||
var injector = new Injector([Car, Engine, Tires, Logger]);
|
||||
var car = injector.get(Car);
|
||||
car.drive();
|
||||
}
|
||||
```
|
||||
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 InjectionFramework** 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
|
||||
:markdown
|
||||
## 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/).
|
||||
```
|
||||
import {Component} from 'angular2/angular2';
|
||||
import {Hero} from './hero';
|
||||
import {HEROES} from './mock-heroes';
|
||||
|
||||
@Component({
|
||||
selector: 'my-heroes'
|
||||
templateUrl: 'app/heroes.component.html'
|
||||
})
|
||||
export class HeroesComponent {
|
||||
|
||||
heroes: Hero[] = HEROES;
|
||||
|
||||
}
|
||||
```
|
||||
It assigns a list of mocked heroes to its `heroes` property for binding within the template.
|
||||
Pretty straight forward.
|
||||
|
||||
Those heroes are currently a fixed, in-memory collection, defined in another file and imported by the component.
|
||||
That works 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 this component's 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
|
||||
:markdown
|
||||
Write this service in its own file. See [this note](#forward-ref) to understand why.
|
||||
:markdown
|
||||
```
|
||||
import {Hero} from './hero';
|
||||
import {HEROES} from './mock-heroes';
|
||||
|
||||
class HeroService {
|
||||
|
||||
heroes: Hero[];
|
||||
|
||||
constructor() {
|
||||
this.heroes = HEROES;
|
||||
}
|
||||
|
||||
getHeroes() {
|
||||
return this.heroes;
|
||||
}
|
||||
}
|
||||
```
|
||||
Our `HeroService` exposes a `getHeroes()` method that returns
|
||||
the same mock data as before but none of its consumers need to know that.
|
||||
|
||||
A service is nothing more than a class in Angular 2.
|
||||
It remains nothing more than a class until we register it with
|
||||
the Angular injector.
|
||||
|
||||
### Configuring the Injector
|
||||
|
||||
We don't have to create the injector.
|
||||
<a name="bootstrap"></a>
|
||||
Angular creates an application-wide injector for us during the bootstrap process.
|
||||
```
|
||||
bootstrap(HeroesComponent);
|
||||
```
|
||||
|
||||
Let’s configure the injector at the same time that we bootstrap by adding
|
||||
our `HeroService` to an array in the second argument.
|
||||
We'll explain that array when we talk about [providers](#providers) later in this chapter.
|
||||
```
|
||||
bootstrap(AppComponent, [HeroService]);
|
||||
```
|
||||
That’s it! The injector now knows about the `HeroService` which is available for injection across our entire application.
|
||||
|
||||
### Preparing the `HeroesComponent` for injection
|
||||
|
||||
The `HeroesComponent` should get its heroes from this service.
|
||||
Per the dependency injection pattern, the component must "ask for" the service in its constructor [as we explained
|
||||
earlier](#ctor-injection)".
|
||||
|
||||
```
|
||||
constructor(heroService: HeroService) {
|
||||
this.heroes = heroService.getHeroes();
|
||||
}
|
||||
```
|
||||
<a name="di-metadata"></a>
|
||||
.l-sub-section
|
||||
:markdown
|
||||
Adding a parameter to the constructor isn't all that's happening here.
|
||||
|
||||
We are writing the app in TypeScript and have followed the parameter name with a type notation, `:HeroService`.
|
||||
The class is also decorated with the `@Component` decorator (scroll up to confirm that fact).
|
||||
|
||||
When the TypeScript compiler evaluates this class, it sees the decorator and adds class metadata
|
||||
into the generated JavaScript code. Within that metadata lurks the information that
|
||||
associates the `heroService` parameter with the `HeroService` class.
|
||||
|
||||
That's how the Angular injector will know to inject an instance of the `HeroService` when it
|
||||
creates a new `HeroesComponent`.
|
||||
:markdown
|
||||
### Creating the `HeroesComponent` with the injector (implicitly)
|
||||
When we introduced the idea of an injector above, we showed how to create
|
||||
a new `Car` with that injector.
|
||||
```
|
||||
var car = injector.get(Car);
|
||||
```
|
||||
Search the entire Tour of Heroes source. We won't find a single line like
|
||||
```
|
||||
var hc = injector.get(HeroesComponent);
|
||||
```
|
||||
We *could* write code like that if we wanted to. We just don't have to.
|
||||
Angular does that for us when it renders a `HeroesComponent`
|
||||
whether we ask for it in an HTML template ...
|
||||
```
|
||||
<my-heroes></heroes>
|
||||
```
|
||||
... or navigate to a `HeroesComponent` view with the [router](./router.html).
|
||||
|
||||
### Singleton services
|
||||
We might wonder what happens when we inject the `HeroService` into other components.
|
||||
Do we get the same instance every time?
|
||||
|
||||
Yes we do. Dependencies are singletons.
|
||||
We’ll discuss that later in our chapter about
|
||||
[Hierarchical Injectors](./hierarchical-dependency-injection.html).
|
||||
|
||||
### Testing the component
|
||||
We emphasized earlier that designing a class for dependency injection makes it easier to test.
|
||||
|
||||
Mission accomplished! We don't even need the Angular Dependency Injection system to test the `HeroesComponent`.
|
||||
We simply create a bew `HeroesComponent` with a mock service and poke at it:
|
||||
```
|
||||
it("should have heroes when created", () => {
|
||||
let hc = new HeroesComponent(mockService);
|
||||
expect(hc.heroes.length).toEqual(mockService.getHeroes().length);
|
||||
})
|
||||
```
|
||||
### 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.
|
||||
|
||||
Here's a rewrite of `HeroService` with a new constructor that takes a `logger` parameter.
|
||||
```
|
||||
import {Hero} from './hero';
|
||||
import {HEROES} from './mock-heroes';
|
||||
import {Logger} from './logger';
|
||||
|
||||
@Injectable()
|
||||
class HeroService {
|
||||
|
||||
heroes: Hero[];
|
||||
|
||||
constructor(private logger: Logger) {
|
||||
this.heroes = HEROES;
|
||||
}
|
||||
|
||||
getHeroes() {
|
||||
this.logger.log('Getting heroes ...')
|
||||
return this.heroes;
|
||||
}
|
||||
}
|
||||
```
|
||||
The constructor now asks for an injected instance of a `Logger` and stores it in a private property called `logger`.
|
||||
We call that property within our `getHeroes()` method when anyone asks for heroes.
|
||||
|
||||
**The `@Injectable()` decoration catches our eye!**
|
||||
|
||||
.alert.is-critical
|
||||
:markdown
|
||||
**Always include the parentheses!** Always call `@Injectable()`. It's easy to forget the parentheses.
|
||||
Our application will fail mysteriously if we do. It bears repeating: **always include the parentheses.**
|
||||
:markdown
|
||||
We haven't seen `@Injectable()` before.
|
||||
As it happens, we could have added it to `HeroService`. We didn't bother because we didn't need it then.
|
||||
|
||||
We need it now ... now that our service has an injected dependency.
|
||||
We need it because Angular requires constructor parameter metadata in order to inject a `Logger`.
|
||||
As [we mentioned earlier](#di-metadata), TypeScript *only generates metadata for classes that have a decorator*. .
|
||||
|
||||
The `HeroesComponent` has an injected dependency too. Why don't we add `@Injectable()` to the `HeroesComponent`?
|
||||
We *can* add it if we really want to. It isn't necessary because the `HeroesComponent` is already decorated with `@Component`.
|
||||
TypeScript generates metadata for *any* class with a decorator and *any* decorator will do.
|
||||
|
||||
.l-main-section
|
||||
:markdown
|
||||
<a name="providers"></a>
|
||||
## Injector Providers
|
||||
|
||||
Remember when we added the `HeroService` to an array in the [bootstrap](#bootstrap) process?
|
||||
```
|
||||
bootstrap(AppComponent, [HeroService]);
|
||||
```
|
||||
That list of classes is actually a list of **providers**.
|
||||
|
||||
"Providers" create the instances of the things that we ask the injector to inject.
|
||||
There are many ways ways to "provide" a thing that has the necessary shape and behavior to serve as a `HeroService`.
|
||||
A class is a natural provider - it's meant to be created. But it's not the only way
|
||||
to produce something injectable. We could hand the injector an object to return. We could give it a factory function to call.
|
||||
Any of these approaches might be a good choice under the right circumstances.
|
||||
|
||||
What matters is that the injector knows what to do when something asks for a `HeroService`.
|
||||
|
||||
### Provider mappings
|
||||
When we registered the `HeroService` with the injector, we were actually registering
|
||||
a mapping between the `HeroService` *token* and a provider that can create a `HeroService`.
|
||||
|
||||
When we wrote ...
|
||||
```
|
||||
import {bootstrap} from 'angular2/angular2';
|
||||
|
||||
bootstrap(AppComponent, [HeroService]);
|
||||
```
|
||||
... Angular translated that statement into a mapping instruction involving the Angular `provide` method
|
||||
```
|
||||
import {bootstrap, provide} from 'angular2/angular2';
|
||||
|
||||
bootstrap(AppComponent, [
|
||||
provide(HeroService, {useClass:HeroService})
|
||||
]);
|
||||
```
|
||||
Of course we prefer the shorthand syntax - `[HeroService]` - when the provider and the token are the same class.
|
||||
|
||||
Isn't that always the case? Not always.
|
||||
|
||||
### Alternative Class Providers
|
||||
|
||||
Occasionally we'll ask a different class to provide the service.
|
||||
|
||||
We do that regularly when testing a component that we're creating with dependency injection.
|
||||
In this example, we tell the injector
|
||||
to return a `MockHeroService` when something asks for the `HeroService`.
|
||||
```
|
||||
beforeEachProviders(() => [
|
||||
provide(HeroService, {useClass: MockHeroService});
|
||||
]);
|
||||
```
|
||||
### Value Providers
|
||||
|
||||
Sometimes it's easier to provide a ready-made object rather than ask the injector to create it from a class.
|
||||
|
||||
We do that a lot when we write tests. We might write the following test setup
|
||||
for tests that explore how the `HeroComponent` behaves when the `HeroService`
|
||||
returns an empty hero list.
|
||||
```
|
||||
beforeEachProviders(() => {
|
||||
|
||||
let emptyHeroService = { getHeroes: () => [] };
|
||||
|
||||
return [ provide(HeroService, {useValue: emptyHeroService}) ];
|
||||
});
|
||||
```
|
||||
Notice that we mapped with `useValue` instead of `useClass`.
|
||||
|
||||
### Factory Providers
|
||||
|
||||
Sometimes the best choice for a provider is neither a class nor a value.
|
||||
|
||||
Suppose our HeroService has some cool new feature that we're only offering to "special" users.
|
||||
The HeroService shouldn't know about users and
|
||||
we won't know if the current user is special until runtime anyway.
|
||||
We decide to extend our `HeroService` constructor to accept a `useCoolFeature` flag
|
||||
that toggles the feature on or off.
|
||||
We rewrite the `HeroService` again as follows.
|
||||
```
|
||||
@Injectable()
|
||||
class HeroService {
|
||||
|
||||
heroes: Hero[];
|
||||
|
||||
constructor(private logger: Logger, private useCoolFeature: boolean) {
|
||||
this.heroes = HEROES;
|
||||
}
|
||||
|
||||
getHeroes() {
|
||||
let msg = this.useCoolFeature ? 'the cool new way' : 'the old way';
|
||||
this.logger.log('Getting heroes ...' + msg)
|
||||
return this.heroes;
|
||||
}
|
||||
}
|
||||
```
|
||||
The feature flag is a simple boolean value. We'd like to inject the flag but it seems silly to write an entire class for a
|
||||
simple flag.
|
||||
|
||||
We can replace the `HeroService` provider with a factory function that creates a properly configured `HeroService` for the current user.
|
||||
We'll' build up to that result, beginning with our definition of the factory function:
|
||||
```
|
||||
let heroServiceFactory = (logger: Logger, userService: UserService) => {
|
||||
return new HeroService(logger, userService.user.isSpecial);
|
||||
}
|
||||
```
|
||||
.l-sub-section
|
||||
:markdown
|
||||
The factory takes two parameters: the logger service and a user service.
|
||||
The logger we pass straight to the constructor as we did before.
|
||||
|
||||
We'll know to use the cool new feature if the `userService.user.isSpecial` flag is true,
|
||||
a fact we can't know until runtime.
|
||||
:markdown
|
||||
We use dependency injection everywhere so of course the factory function depends on
|
||||
two injected services: `Logger` and `UserService`.
|
||||
We declare those requirements in our provider definition object:
|
||||
```
|
||||
let heroServiceDefinition = {
|
||||
useFactory: heroServiceFactory,
|
||||
deps: [Logger, UserService]
|
||||
};
|
||||
```
|
||||
.l-sub-section
|
||||
:markdown
|
||||
The `useFactory` field tells Angular that the provider is a factory function and that its implementation is the `heroServiceFactory`.
|
||||
|
||||
The `deps` property is an array of provider mapping tokens.
|
||||
The `Logger` and `UserService` classes serve as tokens for their own class provider mappings.
|
||||
:markdown
|
||||
Finally, we create the mapping and adjust the bootstrapping to include that mapping in its provider configuration.
|
||||
```
|
||||
let heroServiceMapping = provide(HeroService, heroServiceDefinition);
|
||||
|
||||
bootstrap(AppComponent, [heroServiceMapping, Logger, UserService]);
|
||||
```
|
||||
### String tokens
|
||||
|
||||
Sometimes we have an object dependency rather than a class dependency.
|
||||
|
||||
Applications often define configuration objects with lots of small facts like the title of the application or the address of a web api endpoint.
|
||||
These configuration objects aren't always instances of a class. They're just objects ... like this one:
|
||||
```
|
||||
let config = {
|
||||
apiEndpoint: 'api.heroes.com',
|
||||
title: 'The Hero Employment Agency'
|
||||
};
|
||||
```
|
||||
We'd like to make this `config` object available for injection.
|
||||
We know we can register an object with a "Value Provider". But what do we use for the token?
|
||||
|
||||
Until now, we've always had a class to use as the token for mapping.
|
||||
The `HeroService` class was our token, whether we mapped it to another class, a value, or a factory provider.
|
||||
This time we don't have a class. There is no `Config` class.
|
||||
|
||||
Fortunately, a token can be either a JavaScript type (e.g. the class function) **or a string**. We'll map our configuration object
|
||||
to a string!
|
||||
```
|
||||
bootstrap(AppComponent, [
|
||||
// other mappings //
|
||||
provide('App.config', {useValue:config})
|
||||
]);
|
||||
```
|
||||
Now let's update the `HeroesComponent` constructor so it can display the configured title.
|
||||
Right now the constructor signature is
|
||||
```
|
||||
constructor(heroService: HeroService)
|
||||
```
|
||||
We might think we can write:
|
||||
```
|
||||
// FAIL!
|
||||
constructor(heroService: HeroService, config: config)
|
||||
```
|
||||
That's not going to work. There is no type called `config` and we didn't register the `config` object under that name anyway.
|
||||
We'll need a little help from another Angular decorator called `@Inject`.
|
||||
```
|
||||
import {Inject} from 'angular2/angulare2'
|
||||
|
||||
constructor(heroService: HeroService, @Inject('app.config') config)
|
||||
|
||||
```
|
||||
|
||||
.l-main-section
|
||||
:markdown
|
||||
# Next Steps
|
||||
We learned the basics of Angular Dependency Injection in this chapter.
|
||||
|
||||
The Angular Dependency Injection is more capable than we've described.
|
||||
We can learn more about its advanced features, beginning with its support for
|
||||
a hierarchy of nested injectors in the next
|
||||
[Dependency Injection chapter](./hierarchical-dependency-injection.html)
|
||||
|
||||
.l-main-section
|
||||
<a name="forward-ref"></a>
|
||||
:markdown
|
||||
### Appendix: Why we recommend one class per file
|
||||
Developers expect one class per file. Multiple classes per file is confusing and is best avoided.
|
||||
If we define every class in its own file, there is nothing in this note to worry about.
|
||||
Move along!
|
||||
|
||||
If we scorn this advice
|
||||
and we add our `HeroService` class to the `HeroesComponent` file anyway,
|
||||
**define the `HeroesComponent` last!**
|
||||
If we put it define component before the service,
|
||||
we'll get a runtime null reference error.
|
||||
|
||||
To understand why, paste the following incorrect, ultra-simplified rendition of these two
|
||||
classes into the [TypeScript playground](http://www.typescriptlang.org/Playground).
|
||||
|
||||
```
|
||||
class HeroesComponent {
|
||||
static $providers=[HeroService]
|
||||
}
|
||||
|
||||
class HeroService { }
|
||||
|
||||
alert(HeroesComponent.$providers)
|
||||
```
|
||||
.l-sub-section
|
||||
:markdown
|
||||
The `HeroService` is incorrectly defined below the `HeroComponent`.
|
||||
|
||||
The `$providers` static property represents the metadata about the injected `HeroService`
|
||||
that TypeScript compiler would add to the component class.
|
||||
|
||||
The `alert` simulates the action of the Dependency Injector at runtime
|
||||
when it attempts to create a `HeroesComponent`.
|
||||
:markdown
|
||||
Run it. The alert appears but displays nothing.
|
||||
This is the equivalent of the null reference error thrown at runtime.
|
||||
|
||||
We understand why when we review the generated JavaScript which looks like this:
|
||||
```
|
||||
var HeroesComponent = (function () {
|
||||
function HeroesComponent() {
|
||||
}
|
||||
HeroesComponent.$providers = [HeroService];
|
||||
return HeroesComponent;
|
||||
})();
|
||||
|
||||
var HeroService = (function () {
|
||||
function HeroService() {
|
||||
}
|
||||
return HeroService;
|
||||
})();
|
||||
|
||||
alert(HeroesComponent.$providers);
|
||||
```
|
||||
|
||||
Notice that the TypeScript compiler turns classes into function expressions
|
||||
assigned to variables. The value of the captured `HeroService` variable is undefined
|
||||
when the `$providers` array is assigned. The `HeroService` variable gets its value too late
|
||||
to be captured.
|
||||
|
||||
Reverse the order of class definition so that the `HeroService`
|
||||
appears before the `HeroesComponent` that requires it.
|
||||
Run again. This time the alert displays the `HeroService` function definition.
|
||||
|
||||
If we insist on defining the `HeroService` in the same file and insist on
|
||||
defining the component first, Angular offers a way to make that work.
|
||||
The `forwardRef()` method let's us reference a class
|
||||
before it has been defined.
|
||||
Learn more about this problem and the `forwardRef()`
|
||||
in this [blog post](http://blog.thoughtram.io/angular/2015/09/03/forward-references-in-angular-2.html).
|
|
@ -1,623 +0,0 @@
|
|||
include ../../../../_includes/_util-fns
|
||||
:markdown
|
||||
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.
|
||||
|
||||
In this chapter we'll learn what Dependency Injection is, why we want it, and how to use it.
|
||||
<a name="why-di"></a>
|
||||
.l-main-section
|
||||
:markdown
|
||||
## Why Dependency Injection?
|
||||
|
||||
Let's start with the following code.
|
||||
|
||||
```
|
||||
class Engine {}
|
||||
|
||||
class Tires {}
|
||||
|
||||
class Car {
|
||||
private engine: Engine;
|
||||
private tires: Tires;
|
||||
|
||||
constructor() {
|
||||
this.engine = new Engine();
|
||||
this.tires = new Tires();
|
||||
}
|
||||
// Method using the engine and tires
|
||||
drive() {}
|
||||
}
|
||||
```
|
||||
|
||||
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 creates its own copies by "new-ing" them 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
|
||||
`this.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 definion 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 ... like 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 if 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 become difficult to test.
|
||||
|
||||
How can we make `Car` more robust, more flexible, and more testable?
|
||||
|
||||
That's super easy. We probably already know what to do. We change our `Car` constructor to this:
|
||||
<a name="ctor-injection"></a>
|
||||
```
|
||||
constructor(engine: Engine, tires: Tires) {
|
||||
this.engine = engine;
|
||||
this.tires = tires;
|
||||
}
|
||||
```
|
||||
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.
|
||||
|
||||
Now we create a car by passing the engine and tires to the constructor.
|
||||
```
|
||||
var car = new Car(new Engine(), new Tires());
|
||||
```
|
||||
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
|
||||
:markdown
|
||||
The consumer of `Car` has the problem. The consumer must update the car creation code to
|
||||
something like:
|
||||
```
|
||||
var car = new Car(new Engine(theNewParameter), new Tires());
|
||||
```
|
||||
The critical point is this: `Car` itself did not have to change.
|
||||
We'll take care of the consumer's problem soon enough.
|
||||
|
||||
:markdown
|
||||
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 car = new Car(new MockEngine(), new MockLowPressureTires());
|
||||
```
|
||||
|
||||
**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:
|
||||
```
|
||||
class SuperFactory {
|
||||
createEngine = () => new Engine();
|
||||
createTires = () => new Tires();
|
||||
createCar = () => new Car(this.createEngine(), this.createTires());
|
||||
}
|
||||
```
|
||||
It's not so bad now with only three creation methods.
|
||||
But maintaining it will be hairy as the application grows.
|
||||
This `SuperFactory` is going to become a huge spider web 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.
|
||||
```
|
||||
function main() {
|
||||
var injector = new Injector([Car, Engine, Tires, Logger]);
|
||||
var car = injector.get(Car);
|
||||
car.drive();
|
||||
}
|
||||
```
|
||||
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
|
||||
:markdown
|
||||
## 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/).
|
||||
```
|
||||
import {Component} from 'angular2/core';
|
||||
import {Hero} from './hero';
|
||||
import {HEROES} from './mock-heroes';
|
||||
|
||||
@Component({
|
||||
selector: 'my-heroes'
|
||||
templateUrl: 'app/heroes.component.html'
|
||||
})
|
||||
export class HeroesComponent {
|
||||
|
||||
heroes: Hero[] = HEROES;
|
||||
|
||||
}
|
||||
```
|
||||
It assigns a list of mocked heroes to its `heroes` property for binding within the template.
|
||||
Pretty straight forward.
|
||||
|
||||
Those heroes are currently a fixed, in-memory collection, defined in another file and imported by the component.
|
||||
That works 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 this component's 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
|
||||
:markdown
|
||||
Write this service in its own file. See [this note](#forward-ref) to understand why.
|
||||
:markdown
|
||||
```
|
||||
import {Hero} from './hero';
|
||||
import {HEROES} from './mock-heroes';
|
||||
|
||||
class HeroService {
|
||||
|
||||
heroes: Hero[];
|
||||
|
||||
constructor() {
|
||||
this.heroes = HEROES;
|
||||
}
|
||||
|
||||
getHeroes() {
|
||||
return this.heroes;
|
||||
}
|
||||
}
|
||||
```
|
||||
Our `HeroService` exposes a `getHeroes()` method that returns
|
||||
the same mock data as before but none of its consumers need to know that.
|
||||
|
||||
A service is nothing more than a class in Angular 2.
|
||||
It remains nothing more than a class until we register it with
|
||||
the Angular injector.
|
||||
|
||||
### Configuring the Injector
|
||||
|
||||
We don't have to create the injector.
|
||||
<a name="bootstrap"></a>
|
||||
Angular creates an application-wide injector for us during the bootstrap process.
|
||||
```
|
||||
bootstrap(HeroesComponent);
|
||||
```
|
||||
|
||||
Let’s configure the injector at the same time that we bootstrap by adding
|
||||
our `HeroService` to an array in the second argument.
|
||||
We'll explain that array when we talk about [providers](#providers) later in this chapter.
|
||||
```
|
||||
bootstrap(AppComponent, [HeroService]);
|
||||
```
|
||||
That’s it! The injector now knows about the `HeroService` which is available for injection across our entire application.
|
||||
|
||||
### Preparing the `HeroesComponent` for injection
|
||||
|
||||
The `HeroesComponent` should get its heroes from this service.
|
||||
Per the dependency injection pattern, the component must "ask for" the service in its constructor [as we explained
|
||||
earlier](#ctor-injection)".
|
||||
|
||||
```
|
||||
constructor(heroService: HeroService) {
|
||||
this.heroes = heroService.getHeroes();
|
||||
}
|
||||
```
|
||||
<a name="di-metadata"></a>
|
||||
.l-sub-section
|
||||
:markdown
|
||||
Adding a parameter to the constructor isn't all that's happening here.
|
||||
|
||||
We are writing the app in TypeScript and have followed the parameter name with a type notation, `:HeroService`.
|
||||
The class is also decorated with the `@Component` decorator (scroll up to confirm that fact).
|
||||
|
||||
When the TypeScript compiler evaluates this class, it sees the decorator and adds class metadata
|
||||
into the generated JavaScript code. Within that metadata lurks the information that
|
||||
associates the `heroService` parameter with the `HeroService` class.
|
||||
|
||||
That's how the Angular injector will know to inject an instance of the `HeroService` when it
|
||||
creates a new `HeroesComponent`.
|
||||
:markdown
|
||||
### Creating the `HeroesComponent` with the injector (implicitly)
|
||||
When we introduced the idea of an injector above, we showed how to create
|
||||
a new `Car` with that injector.
|
||||
```
|
||||
var car = injector.get(Car);
|
||||
```
|
||||
Search the entire Tour of Heroes source. We won't find a single line like
|
||||
```
|
||||
var hc = injector.get(HeroesComponent);
|
||||
```
|
||||
We *could* write code like that if we wanted to. We just don't have to.
|
||||
Angular does that for us when it renders a `HeroesComponent`
|
||||
whether we ask for it in an HTML template ...
|
||||
```
|
||||
<my-heroes></heroes>
|
||||
```
|
||||
... or navigate to a `HeroesComponent` view with the [router](./router.html).
|
||||
|
||||
### Singleton services
|
||||
We might wonder what happens when we inject the `HeroService` into other components.
|
||||
Do we get the same instance every time?
|
||||
|
||||
Yes we do. Dependencies are singletons.
|
||||
We’ll discuss that later in our chapter about
|
||||
[Hierarchical Injectors](./hierarchical-dependency-injection.html).
|
||||
|
||||
### Testing the component
|
||||
We emphasized earlier that designing a class for dependency injection makes it easier to test.
|
||||
|
||||
Mission accomplished! We don't even need the Angular Dependency Injection system to test the `HeroesComponent`.
|
||||
We simply create a bew `HeroesComponent` with a mock service and poke at it:
|
||||
```
|
||||
it("should have heroes when created", () => {
|
||||
let hc = new HeroesComponent(mockService);
|
||||
expect(hc.heroes.length).toEqual(mockService.getHeroes().length);
|
||||
})
|
||||
```
|
||||
### 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.
|
||||
|
||||
Here's a rewrite of `HeroService` with a new constructor that takes a `logger` parameter.
|
||||
```
|
||||
import {Hero} from './hero';
|
||||
import {HEROES} from './mock-heroes';
|
||||
import {Logger} from './logger';
|
||||
|
||||
@Injectable()
|
||||
class HeroService {
|
||||
|
||||
heroes: Hero[];
|
||||
|
||||
constructor(private logger: Logger) {
|
||||
this.heroes = HEROES;
|
||||
}
|
||||
|
||||
getHeroes() {
|
||||
this.logger.log('Getting heroes ...')
|
||||
return this.heroes;
|
||||
}
|
||||
}
|
||||
```
|
||||
The constructor now asks for an injected instance of a `Logger` and stores it in a private property called `logger`.
|
||||
We call that property within our `getHeroes()` method when anyone asks for heroes.
|
||||
|
||||
**The `@Injectable()` decoration catches our eye!**
|
||||
|
||||
.alert.is-critical
|
||||
:markdown
|
||||
**Always include the parentheses!** Always call `@Injectable()`. It's easy to forget the parentheses.
|
||||
Our application will fail mysteriously if we do. It bears repeating: **always include the parentheses.**
|
||||
:markdown
|
||||
We haven't seen `@Injectable()` before.
|
||||
As it happens, we could have added it to `HeroService`. We didn't bother because we didn't need it then.
|
||||
|
||||
We need it now ... now that our service has an injected dependency.
|
||||
We need it because Angular requires constructor parameter metadata in order to inject a `Logger`.
|
||||
As [we mentioned earlier](#di-metadata), TypeScript *only generates metadata for classes that have a decorator*. .
|
||||
|
||||
The `HeroesComponent` has an injected dependency too. Why don't we add `@Injectable()` to the `HeroesComponent`?
|
||||
We *can* add it if we really want to. It isn't necessary because the `HeroesComponent` is already decorated with `@Component`.
|
||||
TypeScript generates metadata for *any* class with a decorator and *any* decorator will do.
|
||||
|
||||
.l-main-section
|
||||
:markdown
|
||||
<a name="providers"></a>
|
||||
## Injector Providers
|
||||
|
||||
Remember when we added the `HeroService` to an array in the [bootstrap](#bootstrap) process?
|
||||
```
|
||||
bootstrap(AppComponent, [HeroService]);
|
||||
```
|
||||
That list of classes is actually a list of **providers**.
|
||||
|
||||
"Providers" create the instances of the things that we ask the injector to inject.
|
||||
There are many ways ways to "provide" a thing that has the necessary shape and behavior to serve as a `HeroService`.
|
||||
A class is a natural provider - it's meant to be created. But it's not the only way
|
||||
to produce something injectable. We could hand the injector an object to return. We could give it a factory function to call.
|
||||
Any of these approaches might be a good choice under the right circumstances.
|
||||
|
||||
What matters is that the injector knows what to do when something asks for a `HeroService`.
|
||||
|
||||
### Provider mappings
|
||||
When we registered the `HeroService` with the injector, we were actually registering
|
||||
a mapping between the `HeroService` *token* and a provider that can create a `HeroService`.
|
||||
|
||||
When we wrote ...
|
||||
```
|
||||
import {bootstrap} from 'angular2/platform/browser';
|
||||
|
||||
bootstrap(AppComponent, [HeroService]);
|
||||
```
|
||||
... Angular translated that statement into a mapping instruction involving the Angular `provide` method
|
||||
```
|
||||
import {bootstrap, provide} from 'angular2/platform/browser';
|
||||
|
||||
bootstrap(AppComponent, [
|
||||
provide(HeroService, {useClass:HeroService})
|
||||
]);
|
||||
```
|
||||
Of course we prefer the shorthand syntax - `[HeroService]` - when the provider and the token are the same class.
|
||||
|
||||
Isn't that always the case? Not always.
|
||||
|
||||
### Alternative Class Providers
|
||||
|
||||
Occasionally we'll ask a different class to provide the service.
|
||||
|
||||
We do that regularly when testing a component that we're creating with dependency injection.
|
||||
In this example, we tell the injector
|
||||
to return a `MockHeroService` when something asks for the `HeroService`.
|
||||
```
|
||||
beforeEachProviders(() => [
|
||||
provide(HeroService, {useClass: MockHeroService});
|
||||
]);
|
||||
```
|
||||
### Value Providers
|
||||
|
||||
Sometimes it's easier to provide a ready-made object rather than ask the injector to create it from a class.
|
||||
|
||||
We do that a lot when we write tests. We might write the following test setup
|
||||
for tests that explore how the `HeroComponent` behaves when the `HeroService`
|
||||
returns an empty hero list.
|
||||
```
|
||||
beforeEachProviders(() => {
|
||||
|
||||
let emptyHeroService = { getHeroes: () => [] };
|
||||
|
||||
return [ provide(HeroService, {useValue: emptyHeroService}) ];
|
||||
});
|
||||
```
|
||||
Notice that we mapped with `useValue` instead of `useClass`.
|
||||
|
||||
### Factory Providers
|
||||
|
||||
Sometimes the best choice for a provider is neither a class nor a value.
|
||||
|
||||
Suppose our HeroService has some cool new feature that we're only offering to "special" users.
|
||||
The HeroService shouldn't know about users and
|
||||
we won't know if the current user is special until runtime anyway.
|
||||
We decide to extend our `HeroService` constructor to accept a `useCoolFeature` flag
|
||||
that toggles the feature on or off.
|
||||
We rewrite the `HeroService` again as follows.
|
||||
```
|
||||
@Injectable()
|
||||
class HeroService {
|
||||
|
||||
heroes: Hero[];
|
||||
|
||||
constructor(private logger: Logger, private useCoolFeature: boolean) {
|
||||
this.heroes = HEROES;
|
||||
}
|
||||
|
||||
getHeroes() {
|
||||
let msg = this.useCoolFeature ? 'the cool new way' : 'the old way';
|
||||
this.logger.log('Getting heroes ...' + msg)
|
||||
return this.heroes;
|
||||
}
|
||||
}
|
||||
```
|
||||
The feature flag is a simple boolean value. We'd like to inject the flag but it seems silly to write an entire class for a
|
||||
simple flag.
|
||||
|
||||
We can replace the `HeroService` provider with a factory function that creates a properly configured `HeroService` for the current user.
|
||||
We'll' build up to that result, beginning with our definition of the factory function:
|
||||
```
|
||||
let heroServiceFactory = (logger: Logger, userService: UserService) => {
|
||||
return new HeroService(logger, userService.user.isSpecial);
|
||||
}
|
||||
```
|
||||
.l-sub-section
|
||||
:markdown
|
||||
The factory takes two parameters: the logger service and a user service.
|
||||
The logger we pass straight to the constructor as we did before.
|
||||
|
||||
We'll know to use the cool new feature if the `userService.user.isSpecial` flag is true,
|
||||
a fact we can't know until runtime.
|
||||
:markdown
|
||||
We use dependency injection everywhere so of course the factory function depends on
|
||||
two injected services: `Logger` and `UserService`.
|
||||
We declare those requirements in our provider definition object:
|
||||
```
|
||||
let heroServiceDefinition = {
|
||||
useFactory: heroServiceFactory,
|
||||
deps: [Logger, UserService]
|
||||
};
|
||||
```
|
||||
.l-sub-section
|
||||
:markdown
|
||||
The `useFactory` field tells Angular that the provider is a factory function and that its implementation is the `heroServiceFactory`.
|
||||
|
||||
The `deps` property is an array of provider mapping tokens.
|
||||
The `Logger` and `UserService` classes serve as tokens for their own class provider mappings.
|
||||
:markdown
|
||||
Finally, we create the mapping and adjust the bootstrapping to include that mapping in its provider configuration.
|
||||
```
|
||||
let heroServiceMapping = provide(HeroService, heroServiceDefinition);
|
||||
|
||||
bootstrap(AppComponent, [heroServiceMapping, Logger, UserService]);
|
||||
```
|
||||
### String tokens
|
||||
|
||||
Sometimes we have an object dependency rather than a class dependency.
|
||||
|
||||
Applications often define configuration objects with lots of small facts like the title of the application or the address of a web api endpoint.
|
||||
These configuration objects aren't always instances of a class. They're just objects ... like this one:
|
||||
```
|
||||
let config = {
|
||||
apiEndpoint: 'api.heroes.com',
|
||||
title: 'The Hero Employment Agency'
|
||||
};
|
||||
```
|
||||
We'd like to make this `config` object available for injection.
|
||||
We know we can register an object with a "Value Provider". But what do we use for the token?
|
||||
|
||||
Until now, we've always had a class to use as the token for mapping.
|
||||
The `HeroService` class was our token, whether we mapped it to another class, a value, or a factory provider.
|
||||
This time we don't have a class. There is no `Config` class.
|
||||
|
||||
Fortunately, a token can be either a JavaScript type (e.g. the class function) **or a string**. We'll map our configuration object
|
||||
to a string!
|
||||
```
|
||||
bootstrap(AppComponent, [
|
||||
// other mappings //
|
||||
provide('App.config', {useValue:config})
|
||||
]);
|
||||
```
|
||||
Now let's update the `HeroesComponent` constructor so it can display the configured title.
|
||||
Right now the constructor signature is
|
||||
```
|
||||
constructor(heroService: HeroService)
|
||||
```
|
||||
We might think we can write:
|
||||
```
|
||||
// FAIL!
|
||||
constructor(heroService: HeroService, config: config)
|
||||
```
|
||||
That's not going to work. There is no type called `config` and we didn't register the `config` object under that name anyway.
|
||||
We'll need a little help from another Angular decorator called `@Inject`.
|
||||
```
|
||||
import {Inject} from 'angular2/core'
|
||||
|
||||
constructor(heroService: HeroService, @Inject('app.config') config)
|
||||
|
||||
```
|
||||
|
||||
.l-main-section
|
||||
:markdown
|
||||
# Next Steps
|
||||
We learned the basics of Angular Dependency Injection in this chapter.
|
||||
|
||||
The Angular Dependency Injection is more capable than we've described.
|
||||
We can learn more about its advanced features, beginning with its support for
|
||||
a hierarchy of nested injectors in the next
|
||||
[Dependency Injection chapter](./hierarchical-dependency-injection.html)
|
||||
|
||||
.l-main-section
|
||||
<a name="forward-ref"></a>
|
||||
:markdown
|
||||
### Appendix: Why we recommend one class per file
|
||||
Developers expect one class per file. Multiple classes per file is confusing and is best avoided.
|
||||
If we define every class in its own file, there is nothing in this note to worry about.
|
||||
Move along!
|
||||
|
||||
If we scorn this advice
|
||||
and we add our `HeroService` class to the `HeroesComponent` file anyway,
|
||||
**define the `HeroesComponent` last!**
|
||||
If we put it define component before the service,
|
||||
we'll get a runtime null reference error.
|
||||
|
||||
To understand why, paste the following incorrect, ultra-simplified rendition of these two
|
||||
classes into the [TypeScript playground](http://www.typescriptlang.org/Playground).
|
||||
|
||||
```
|
||||
class HeroesComponent {
|
||||
static $providers=[HeroService]
|
||||
}
|
||||
|
||||
class HeroService { }
|
||||
|
||||
alert(HeroesComponent.$providers)
|
||||
```
|
||||
.l-sub-section
|
||||
:markdown
|
||||
The `HeroService` is incorrectly defined below the `HeroComponent`.
|
||||
|
||||
The `$providers` static property represents the metadata about the injected `HeroService`
|
||||
that TypeScript compiler would add to the component class.
|
||||
|
||||
The `alert` simulates the action of the Dependency Injector at runtime
|
||||
when it attempts to create a `HeroesComponent`.
|
||||
:markdown
|
||||
Run it. The alert appears but displays nothing.
|
||||
This is the equivalent of the null reference error thrown at runtime.
|
||||
|
||||
We understand why when we review the generated JavaScript which looks like this:
|
||||
```
|
||||
var HeroesComponent = (function () {
|
||||
function HeroesComponent() {
|
||||
}
|
||||
HeroesComponent.$providers = [HeroService];
|
||||
return HeroesComponent;
|
||||
})();
|
||||
|
||||
var HeroService = (function () {
|
||||
function HeroService() {
|
||||
}
|
||||
return HeroService;
|
||||
})();
|
||||
|
||||
alert(HeroesComponent.$providers);
|
||||
```
|
||||
|
||||
Notice that the TypeScript compiler turns classes into function expressions
|
||||
assigned to variables. The value of the captured `HeroService` variable is undefined
|
||||
when the `$providers` array is assigned. The `HeroService` variable gets its value too late
|
||||
to be captured.
|
||||
|
||||
Reverse the order of class definition so that the `HeroService`
|
||||
appears before the `HeroesComponent` that requires it.
|
||||
Run again. This time the alert displays the `HeroService` function definition.
|
||||
|
||||
If we insist on defining the `HeroService` in the same file and insist on
|
||||
defining the component first, Angular offers a way to make that work.
|
||||
The `forwardRef()` method let's us reference a class
|
||||
before it has been defined.
|
||||
Learn more about this problem and the `forwardRef()`
|
||||
in this [blog post](http://blog.thoughtram.io/angular/2015/09/03/forward-references-in-angular-2.html).
|
|
@ -1,479 +0,0 @@
|
|||
include ../../../../_includes/_util-fns
|
||||
|
||||
:marked
|
||||
# Routing Around the App
|
||||
Our Tour of Heroes is a single view, but we have new requirements to create other views, such as a dashboard, and navigate between them. We’ll add a dashboard component and use Angular’s router to handle navigation between views. We have another requirement to allow selecting a hero from either the dashboard or the heroes view and route directly to the hero details. We’ll need to learn about and use route parameters to tackle this.
|
||||
|
||||
When we’re done, users will be able to navigate the app like this:
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/toh/nav-diagram.png' alt="View navigations")
|
||||
:marked
|
||||
Finally, we’ll want to filter and format data in our app using Angular’s Pipes.
|
||||
## Reviewing Where We Left Off
|
||||
Let’s verify that we have the following structure after adding our hero service and hero detail component in the previous chapter:
|
||||
|
||||
.filetree
|
||||
.file angular2-tour-of-heroes
|
||||
.children
|
||||
.file node_modules
|
||||
.file app
|
||||
.children
|
||||
.file app.component.ts
|
||||
.file hero.ts
|
||||
.file hero-detail.component.ts
|
||||
.file hero.service.ts
|
||||
.file mock-heroes.ts
|
||||
.file main.ts
|
||||
.file index.html
|
||||
.file package.json
|
||||
.file tsconfig.json
|
||||
|
||||
:marked
|
||||
### Keep the app transpiling and running
|
||||
We want to start the TypeScript compiler, have it watch for changes, and start our server. We'll do this by typing
|
||||
|
||||
code-example(format="." language="bash").
|
||||
npm start
|
||||
|
||||
:marked
|
||||
## Flying Overhead
|
||||
Before we dash into routing for our Tour of Heroes, let’s fly over what we’re going to need to do. Since we’ll want to be routing to entirely different views, we’ll want to separate a few of the components and their template content. We’re also going to be adding two entirely new components. The first for our new requirement, the dashboard. The second component will be to host our menus with routing links and configuration.
|
||||
Here is our checklist of what we’ll need to tackle
|
||||
1. Move the heroes from AppComponent to the more aptly named HeroesComponent
|
||||
1. Create a new AppComponent that will host the menu links and routing
|
||||
1. Create the DashboardComponent to show our top heroes
|
||||
1. Create a class to handle routing configuration
|
||||
We’ll tackle these by separating the components, creating our new components, and adding routing so we can navigate around our Tour of Heroes.
|
||||
|
||||
## Separating the Components
|
||||
We’ll want a component to host the menu links for the heroes and the dashboard. This will be the first component that our app loads. But currently our app loads `AppComponent` first, which has our list of heroes. It’s time to separate our components so we have a component that hosts our menu links and a component that lists our heroes. We’ll call these `AppComponent` and `HeroesComponent`.
|
||||
|
||||
### Creating the HeroesComponent
|
||||
Since we have an `AppComponent` that lists heroes, let’s start by renaming `app.component.ts` to `hero.component.ts`. Then we’ll rename the component from `AppComponent` to `HeroComponent` and we’ll rename the selector to `my-heroes`.
|
||||
code-example.
|
||||
selector: 'my-heroes',
|
||||
:marked
|
||||
Finally, we’ll export the `HeroesComponent` as we will want to use it from another module when we define our routing.
|
||||
code-example.
|
||||
export class HeroesComponent {
|
||||
:marked
|
||||
## Creating the New AppComponent
|
||||
Our app needs a menu and a place to show the dashboard and heroes views. This is effectively the shell for our app.
|
||||
|
||||
We’ll create a new file named `app.component.ts` and create our new `AppComponent` inside of the file. This will be the first component we load in our app. It will host the menu links, when we create them.
|
||||
|
||||
We assign our `AppComponent` a selector of `my-app`.
|
||||
+makeExample('toh-4/ts-snippets/app/app.component.pt4.ts')
|
||||
:marked
|
||||
`AppComponent` is the entry point of our app. This makes it the ideal place to bootstrap our app, which is why we pass in the `AppComponent` and the shared `HeroService` that all many of our components will use.
|
||||
|
||||
We export our `AppComponent` as we’ll want to refer to it from our bootstrapping process.
|
||||
|
||||
## Bootstrapping the Tour of Heroes
|
||||
The start-up of an app is also known as bootstrapping. We are currently bootstrapping in our `HeroesComponent`, which no longer makes sense. So let’s change that and separate this startup logic.
|
||||
|
||||
Let’s move the bootstrapping logic into a new file. We’ll create a new file named `main.ts` in the `app` folder.
|
||||
|
||||
Let’s add the following lines to `main.ts`:
|
||||
+makeExample('toh-4/ts-snippets/app/bootstrap.pt4.ts')
|
||||
:marked
|
||||
The bootstrap function accepts as its first parameter, the first component that the app will use.
|
||||
|
||||
Now let’s do a little cleanup work. Let’s remove the bootstrap logic and remove `bootstrap` from the import statement in the `heroes.component.ts`.
|
||||
|
||||
We had already exported our `AppComponent`, which is why we can now import it and bootstrap it here. We pass in the shared `HeroService` that many of our components will use, so it will be ready when we need it.
|
||||
|
||||
Now let’s tell our module loader to start by loading our `bootstrap` module. We’ll do this in our`index.html` file
|
||||
+makeExample('toh-4/ts-snippets/app/index.pt4.html','bootstrap')
|
||||
:marked
|
||||
### Viewing our Progress
|
||||
Let’s add a title for our app which we’ll bind to a `title` property on our component. We’ll set the title to “Tour of Heroes”.
|
||||
+makeExample('toh-4/ts-snippets/app/index.pt4.html','title')
|
||||
:marked
|
||||
Our title now belongs in the `AppComponent`, but it also still exists in `HeroesComponent`. So let’s tidy up by removing the `title` from the `HeroesComponent` class and template.
|
||||
|
||||
When we view our app in the browser we should now only see our title of “Tour of Heroes”.
|
||||
But where is the rest of our app? We haven’t shown it yet!
|
||||
|
||||
Our app’s entry point is the `bootstrap` module which loads the `AppComponent`. `AppComponent` in turn only shows a title.
|
||||
|
||||
Our next step is to configure the menu links and routes that will show our views.
|
||||
|
||||
## Adding the Router to our App
|
||||
The Angular router is a separate and distinct module that we can include as needed. Well, our Tour of Heroes app needs routing, so let’s add it!
|
||||
|
||||
### Including the Router
|
||||
We add a script tag referencing the router code. We’ll make sure this comes after the angular script reference.
|
||||
|
||||
Then let’s set our base href to `/src/` since that is where our source code is located. Our `index.html`’s head section should nw look like this:
|
||||
+makeExample('toh-4/ts-snippets/app/index.pt4.html','head')
|
||||
:marked
|
||||
Now we’ll be loading the Angular router!
|
||||
|
||||
### Configuring Routes
|
||||
We want to display a menu that has links to a dashboard and to our list of heroes. Let’s configure the first route to show our `HeroesComponent`.
|
||||
|
||||
Our app will have a few routes and we may want to use them in more than one module. Let’s create a file named `route.config.ts` to host our routes.
|
||||
|
||||
#### Defining Routes
|
||||
We want to show our heroes list. So let’s define our first route to show our `HeroesComponent` template.
|
||||
+makeExample('toh-4/ts-snippets/app/route.config.pt4.ts','first-route')
|
||||
:marked
|
||||
Our route needs a path. This is what will show up in the address bar of the browser. Our path for our `HeroesComponent` will be `/`.
|
||||
|
||||
The `component` property identifies the component we will load when we go to this route. In this case we want to load the `HeroesComponent`.
|
||||
|
||||
The `as` property is what the route is known as. In other words, it is the name of the route.
|
||||
We set these three properties in an object and export that object in an array named `APP_ROUTES`. Right now we have one route. But as we add more routes, this technique of hosting them in a `route.config.ts` file and exporting an array of them will make it easier to manage our routes.
|
||||
|
||||
#### The RouteConfig Decorator
|
||||
Now that we have defined a route, we need to tell Angular where to find it. Let’s go to our `AppComponent` and add the `RouteConfig` decorator the class. We’ll need to import the `RouteConfig` decorator from Angular’s router module, too.
|
||||
|
||||
Now we import the `APP_ROUTES`array of routes that we just created. We’ll pass these into the `RouteConfig` decorator.
|
||||
+makeExample('toh-4/ts-snippets/app/index.pt4.html','routes-title')
|
||||
:marked
|
||||
Our app now has its first route, but we need a place to show the heroes view.
|
||||
|
||||
<!-- Learn more about RouteConfig in the chapter [Router] -->
|
||||
|
||||
### Showing the View with the Router-Outlet
|
||||
Angular knows we have a route when we navigate to `/heroes`. But where does the view show in the HTML? We haven’t told our app how to do that yet!
|
||||
|
||||
Let’s add the `<router-outlet>` directive to the template of `AppComponent`. The `<router-outlet>` is a placeholder that the router uses to place the views. When we go to the `/heroes` route, the router will show the `HeroesComponent` template where we place the `<router-outlet>`.
|
||||
+makeExample('toh-4/ts-snippets/app/index.pt4.html','router-outlet')
|
||||
:marked
|
||||
When need to declare to the `AppComponent` that we are using the `router-outlet` directive. To do this we’ll import a special `ROUTER_DIRECTIVES` array of router specific directives.
|
||||
```
|
||||
import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';
|
||||
```
|
||||
Then we’ll declare them to the component in the `@Component` decorator’s `directives` property.
|
||||
```
|
||||
directives: [ROUTER_DIRECTIVES]
|
||||
```
|
||||
Let’s go view our app in the browser and see where we are. Uh oh, we see an error in the developer console.
|
||||
code-example(language="html").
|
||||
EXCEPTION: No provider for Router! (RouterOutlet -> Router)
|
||||
:marked
|
||||
Angular is warning us that we are using the router and the `router-outlet` but we did not inject the router’s provider. We can fix this by injecting this in the bootstrap module. First we’ll import the `ROUTER_PROVIDERS` from the router module.
|
||||
```
|
||||
import {ROUTER_PROVIDERS} from 'angular2/router';
|
||||
```
|
||||
Then we’ll pass the `ROUTER_PROVIDERS` to the `bootstrap` method.
|
||||
```
|
||||
bootstrap(AppComponent, [ROUTER_PROVIDERS, HeroService]);
|
||||
```
|
||||
Now when we view our app in the browser we see our heroes list!
|
||||
|
||||
## Creating Navigation Links
|
||||
Okay, our Tour of Heroes is not quite where we want it yet. We’ve created the `AppComponent` which hosts the routing and we move the heroes list to the `HeroesComponent`. But to fulfill our requirements we need to create a dashboard and add a way to navigate between the different views. Let’s continue by adding the new dashboard component and the navigation links.
|
||||
|
||||
### Empty Dashboard
|
||||
Routing makes a lot more sense once we have multiple views. We have a heroes view to show but now we need to create our dashboard view. Let’s create the `DashboardComponent`so we can finish creating the navigation between the components.
|
||||
|
||||
Let’s create a super simple dashboard component.
|
||||
+makeExample('toh-4/ts-snippets/app/index.pt4.html','simple-dashboard-component')
|
||||
:marked
|
||||
We’ll come back to the dashboard once we complete routing. For now this will do nicely to help us make sure we can navigate between the `HeroesComponent` and the `DashboardComponent`.
|
||||
|
||||
### Configuring the Dashboard’s Routes
|
||||
Now that we have a component for our dashboard, let’s go configure a route that will take us there.
|
||||
|
||||
We’ll open `route.config.ts` and add another route for the dashboard.
|
||||
|
||||
Our dashboard should be the first thing we see when load our app. So the dashboard route will be the default route of `/` while our heroes will now be accessible via the path `/heroes`.
|
||||
|
||||
We’ll also import the `DashboarComponent` so we can route to it with the dashboard route. And we’ll add the dashboard route to the `APP_ROUTES` export.
|
||||
|
||||
Our `route.config.ts` should now look like the following code:
|
||||
+makeExample('toh-4/ts-snippets/app/route.config.pt4.ts','dashboard-route')
|
||||
:marked
|
||||
Now we two components we can route between and we have defined the routes for both components. Next up, we’ll add navigation links to route between them.
|
||||
|
||||
### Navigation Links
|
||||
Let’s add the navigation links to the`AppComponent`’s template. The Angular router uses a special `router-link` directive to navigate to the routes we defined. We can think of these as links that will navigate to another component using the router.
|
||||
|
||||
We also add a title for our app which we’ll bind to a `title` property on our component.
|
||||
+makeExample('toh-4/ts-snippets/app/index.pt4.html','router-link')
|
||||
:marked
|
||||
The `router-link` is not set to url. We bound the `router-link` to the routes that we specified in the `as` property of our `@RouteConfig`. The `as` is the key that we use to reference the routes.
|
||||
|
||||
#### Reusable Routing Config
|
||||
We just hard-coded the routes that the `router-link` properties are bound. We can do better. We recall that we previously created route configuration in `route.config.ts`. We import its `Routes`.
|
||||
+makeExample('toh-4/ts-snippets/app/index.pt4.html','import-app-routes')
|
||||
:marked
|
||||
And then we use it to initialize a `routes` property on our `AppComponent`.
|
||||
+makeExample('toh-4/ts-snippets/app/app.component.pt4.ts','initialize-routes-property')
|
||||
:marked
|
||||
Then we can simply reference the routes as shown below using our route configuration variables, such as `routes.heroes.as`.
|
||||
+makeExample('toh-4/ts-snippets/app/index.pt4.html','router-link')
|
||||
:marked
|
||||
When we view our app in the browser we are brought directly to our dashboard. We can navigate between the dashboard and the heroes til our hearts are content.
|
||||
|
||||
## Adding the Top Heroes to Our Dashboard
|
||||
Our dashboard view is a bit bland as it only contains a title. Let’s spice it up by adding the top 4 heroes at a glance.
|
||||
<!-- Ward sweep section below -->
|
||||
### Top Heroes Template Content
|
||||
Let’s add the template to our dashboard to show the top four heroes. We’ll use the `ngFor` directive to iterate over a list of heroes (which we have not retrieved yet) and display them. We’ll use `<div>` elements as we’re going to custom style them.
|
||||
+makeExample('toh-4/ts-snippets/app/index.pt4.html','ng-for')
|
||||
:marked
|
||||
We’ve been down this road before. We are using the `ngFor` directive, so we have to declare it in the component. Let’s do that now by first importing `CORE_DIRECTIVES`. (Remember that `CORE_DIRECTIVES` is a convenience array containing the most common directives such as `ngFor`.)
|
||||
```
|
||||
import {Component, CORE_DIRECTIVES} from 'angular2/angular2';
|
||||
```
|
||||
Then we declare the `CORE_DIRECTIVES` to the component.
|
||||
```
|
||||
directives: [CORE_DIRECTIVES]
|
||||
```
|
||||
### Using the Shared HeroService
|
||||
We just iterated over a list of heroes, but we don’t have any heroes in the `DashboardComponent`. We do have a `HeroService` that provides heroes. In fact, we already used this service in the `HeroComponent`. Let’s re-use this same service for the `DashboardComponent` to get a list of heroes.
|
||||
|
||||
We’ll create a `heroes` property in our `DashboardComponent`.
|
||||
```
|
||||
public heroes: Hero[];
|
||||
```
|
||||
And we import the `Hero`
|
||||
```
|
||||
import {Hero} from './hero';
|
||||
```
|
||||
#### Injecting a Service
|
||||
We’ll be needing the `HeroService`, so let’s import it and inject it into our `DashboardComponent`. Here we import it.
|
||||
```
|
||||
import {HeroService} from './hero.service';
|
||||
```
|
||||
And here we inject it into our component’s constructor.
|
||||
```
|
||||
constructor(private _heroService: HeroService) { }
|
||||
```
|
||||
#### Getting the Heroes on Initialization
|
||||
We want our heroes to be loaded when the component is initialized, just like we did in the `HeroesComponent`. We’ll tackle this the same way, by using the onInit Lifecycle hook.
|
||||
|
||||
We can implement the `OnInit` interface and code the `onInit` method.
|
||||
```
|
||||
export class DashboardComponent implements OnInit {
|
||||
```
|
||||
Here we implement the `onInit` method to get our heroes, again just like we did for the `HeroesComponent`.
|
||||
+makeExample('toh-4/ts-snippets/app/app.component.pt4.ts','oninit')
|
||||
:marked
|
||||
Notice we did not have to know how to get the heroes, we just needed to know the method to call from the `HeroService`. This is an advantage of using shared services.
|
||||
|
||||
When we view our app in the browser we see the dashboard light up with all of our heroes. This isn’t exactly what we want so let’s trim that down to the top four heroes.
|
||||
|
||||
### Slicing with Pipes
|
||||
Our requirement is to show the top four heroes. If only we had something that would automatically get a subset of the data. Well, we do! They are called Pipes.
|
||||
|
||||
Angular has various built-in pipes that make formatting and filtering data easy. We’ll take advantage of a pipe named `slice` to get a slice of our heroes array.
|
||||
```
|
||||
<div *ngFor="#hero of heroes | slice:0:4">
|
||||
```
|
||||
After the `ngFor` we added a pipe character and then the `slice` pipe. We tell the `slice` pipe to start at the 0th item in the array and get four items.
|
||||
.l-sub-section
|
||||
:marked
|
||||
Learn more about Pipes in the chapter [Pipes](../guide/pipes.html)
|
||||
:marked
|
||||
When we view our app we now see the first four heroes are displayed.
|
||||
|
||||
### Heroes with Style
|
||||
Our creative designers have added a requirement that the dashboard should show the heroes in a row of rectangles. We’ve written some CSS to achieve this along with some simple media queries to achieve responsive design.
|
||||
|
||||
We could put the CSS in the component, but there are over 30 lines of CSS and it would get crowded fast. Most editors make it easier to code CSS in a *.css file too. Fortunately, we can separate the styles into their own file and reference them.
|
||||
|
||||
#### Adding the Dashboard’s CSS File
|
||||
Let’s create a file to hold the `DashboardComponent`’s CSS. We’ll name it `dashboard.component.css` and put it in the `app` folder.
|
||||
|
||||
Now let’s add the following CSS to the file.
|
||||
+makeExample('toh-4/ts-snippets/app/index.pt4.html','css')
|
||||
:marked
|
||||
We need to reference the file from our component so the styles will be applied properly. Let’s add this reference to the component’s `styleUrls` property.
|
||||
```
|
||||
styleUrls: ['app/dashboard.component.css'],
|
||||
```
|
||||
The `styleUrls` property is an array, which we might guess suggests that we can add multiple styles from different locations. And we would be right! In this case we have one file, but we could add more for our component if needed.
|
||||
|
||||
#### Template Urls
|
||||
While we are at it, let’s move our HTML for the `DashboardComponent` to its own file. We’ll create a file named `dashboard.component.html` in the `app` folder and move the HTML there.
|
||||
We need to reference the the template, so let’s change our component’s `template` property to `templateUrl` and set it to the location of the file.
|
||||
```
|
||||
template: 'app/dashboard.component.html',
|
||||
```
|
||||
Notice we are now using single quotes and not the back-ticks since we only have the need for a single line string.
|
||||
|
||||
#### Applying the Styles to Our Template
|
||||
|
||||
Now that we have some style, let’s take advantage of it by applying it to our template. Our template should now look like this:
|
||||
+makeExample('toh-4/ts-snippets/app/index.pt4.html','template-styled')
|
||||
:marked
|
||||
When we view our app in the browser it now shows our dashboard with our four top heroes in a row.
|
||||
|
||||
## Styling the Navigation Links
|
||||
Our creative design team requested that the navigation links also be styled. Let’s make the navigation links look more like selectable buttons.
|
||||
|
||||
### Defining Styles for Navigation Links
|
||||
Let’s add the following styles to our `AppComponent`’s `styles` property. We’ll define some classes named `router-link` that style the default, active, visited and hover selectors. The active selector changes the color of the link to make it easy to identify which link is selected.
|
||||
+makeExample('toh-4/ts-snippets/app/app.component.pt4.ts','styles')
|
||||
:marked
|
||||
This time we define the styles in our component because there are only a few of them.
|
||||
|
||||
### Applying Styles to the Navigation Links
|
||||
Now that we have styles for our navigation links, let’s apply them in the `AppComponent` template. When we are done, our component will look like the following:
|
||||
+makeExample('toh-4/ts-snippets/app/index.pt4.html','styled-nav-links')
|
||||
:marked
|
||||
#### Where Did the Active Route Come From?
|
||||
The Angular Router makes it easy for us to style our active navigation link. The `router-link-active` class is automatically added to the Router’s active route for us. So all we have to do is define the style for it.
|
||||
|
||||
Sweet! When we view our app in the browser we see the navigation links are now styled, as are our top heroes!
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/toh/dashboard-top-heroes.png' alt="View navigations")
|
||||
:marked
|
||||
## Add Route to Hero Details
|
||||
We can navigate between the dashboard and the heroes view, but we have a requirement from our users to be able to select a hero from either of those views and go directly to the selected hero’s details. Let’s configure a route to go directly to the `HeroDetailComponent` passing the hero’s id as a parameter.
|
||||
|
||||
### Configuring a Route with a Parameter
|
||||
We’ve already added a few routes to the `routes.config.ts` file, so it’s natural that we’d start there to add the route to go to the `HeroDetailComponent`. Let’s start by adding the import statement for the component.
|
||||
+makeExample('toh-4/ts-snippets/app/route.config.pt4.ts','route-parameter-import')
|
||||
:marked
|
||||
Now we add a route for the details to the `Routes` object.
|
||||
+makeExample('toh-4/ts-snippets/app/route.config.pt4.ts','route-parameter-detail')
|
||||
:marked
|
||||
The route will lead to the `HeroDetailComponent`, passing along the value for the hero’s id. The routing configuration identifies parameters as parts of the path that are prefixed with a `:` such as `:id`.
|
||||
|
||||
### Receiving a Parameter
|
||||
We want to navigate to the `HeroDetailComponent`, so let’s modify it to accept the `id` parameter. We import the `RouteParams` so we can access the parameter.
|
||||
We assign our `AppComponent` a selector of `my-app`.
|
||||
+makeExample('toh-4/ts-snippets/app/app.component.pt4.ts','import-params')
|
||||
:marked
|
||||
Now we inject the `RouteParams` into the `HeroDetailComponent` constructor.
|
||||
+makeExample('toh-4/ts-snippets/app/app.component.pt4.ts','inject-routeparams')
|
||||
:marked
|
||||
We want to immediately access the parameter, so let’s implement the `OnInit` interface and its `onInit` method.
|
||||
+makeExample('toh-4/ts-snippets/app/app.component.pt4.ts','access-params')
|
||||
:marked
|
||||
And let’s not forget to import `OnInit`.
|
||||
+makeExample('toh-4/ts-snippets/app/app.component.pt4.ts','import-onit')
|
||||
:marked
|
||||
Using the `onInit`method, we can grab the parameter as soon as the component initializes. We’ll access the parameter by name and later we’ll get the hero by its `id`.
|
||||
+makeExample('toh-4/ts-snippets/app/app.component.pt4.ts','onit-id-param')
|
||||
:marked
|
||||
Our `HeroDetailComponent` is already used in the `HeroesComponent`. When we select a hero from the list we are passing the hero object from the list to the `HeroDetailComponent`. We want this component to support that functionality or be able to accept the hero’s id. Let’s revise the logic in the `onInit` to handle this.
|
||||
+makeExample('toh-4/ts-snippets/app/app.component.pt4.ts','onit-hero-id')
|
||||
:marked
|
||||
Our component will first check if it has a hero. If it doesn’t it will then check for the routing parameter so it can get the hero.
|
||||
.l-sub-section
|
||||
:marked
|
||||
Learn more about RouteParams in the chapter [Router](../guide/router.html)
|
||||
:marked
|
||||
Getting the Hero
|
||||
When we pass the id to the `HeroDetailComponent` we need to go get the hero from our `HeroService`. Let’s import the `HeroService` so we can use it to get our hero.
|
||||
+makeExample('toh-4/ts-snippets/app/bootstrap.pt4.ts','import-hero-service')
|
||||
:marked
|
||||
And then we inject the `HeroService` into the constructor.
|
||||
+makeExample('toh-4/ts-snippets/app/app.component.pt4.ts','inject-hero-service')
|
||||
:marked
|
||||
We then stub out the call to the `HeroService` to get the hero by the hero’s id. But wait a second, we don’t have a way to get the hero by id … yet.
|
||||
|
||||
Our `HeroService` is the right place to get a single hero. We’ll create a method named `getHero` that will accept a parameter, find the hero, and return the hero in a promise.
|
||||
|
||||
We add this method to the `HeroService`.
|
||||
+makeExample('toh-4/ts-snippets/app/hero.service.pt4.ts','get-hero-method')
|
||||
:marked
|
||||
Then we go back to our `HeroDetailComponent` and we can call the `getHero` method.
|
||||
+makeExample('toh-4/ts-snippets/app/app.component.pt4.ts','onit-hero-method')
|
||||
:marked
|
||||
We grab the hero and set it to the local `hero` property. Now we have everything in place to receive the parameter.
|
||||
|
||||
### Select a Hero on the Dashboard
|
||||
When a user selects a hero in the dashboard, we want to route to the details. Let’s open our dashboard’s template and add a click event to each hero in the template.
|
||||
+makeExample('toh-4/ts-snippets/app/index.pt4.html','select-hero-click-event')
|
||||
:marked
|
||||
The click event will call the `gotoDetail` method in the `DashboardComponent`. We don’t have that method yet, so let’s create it. We’ll want to use the router to navigate to the details route we created. So we have to import the router, and while we are at it, we’ll import the `Routes` object we created that describe our routes.
|
||||
+makeExample('toh-4/ts-snippets/app/bootstrap.pt4.ts','import-router')
|
||||
:marked
|
||||
Now we can write our method to navigate to the route and pass the parameter. We’ll use the router’s `navigate` method and pass an array that has 2 parameters. The first is the name of the route (the `as` property in the `RouteConfig`). The second is an object with the parameters and values.
|
||||
+makeExample('toh-4/ts-snippets/app/route.config.pt4.ts','router-navigate-method')
|
||||
:marked
|
||||
Now when we view our app in the browser and select a hero from the dashboard, we go directly to the hero’s details!
|
||||
.l-sub-section
|
||||
:marked
|
||||
Learn more about RouteParams in the chapter [Router](../guide/router.html)
|
||||
:marked
|
||||
### Select a Hero on the HeroesComponent
|
||||
When a user selects a hero in the dashboard, we go to the details. But we also want this to happen from the `HeroesComponent`. Let’s add the same changes to the `HeroesComponent` that we made to the dashboard.
|
||||
+makeExample('toh-4/ts-snippets/app/app.component.pt4.ts','select-hero')
|
||||
:marked
|
||||
The requirement here is to show the hero when selected and allow the user to the details via a button. So when a user selects a hero we want to show the hero’s name and provide a button to navigate to the details.
|
||||
|
||||
Let’s open the `HeroesComponent`, remove the `my-hero-detail` component, and change the template to display the hero’s name instead.
|
||||
+makeExample('toh-4/ts-snippets/app/index.pt4.html','display-hero-name')
|
||||
:marked
|
||||
We also added a button with a click event that will call our `gotoDetail` method in our `HeroesComponent`.
|
||||
|
||||
Notice we also used the `uppercase` pipe to format the selected hero’s name. Pipes are extremely helpful at formatting and filtering.
|
||||
.l-sub-section
|
||||
:marked
|
||||
Learn more about Pipes in the chapter [Pipes](../guide/pipes.html)
|
||||
:marked
|
||||
When we view the app in our browser we can now navigate from the dashboard or the heroes component directly to the selected hero’s details!
|
||||
|
||||
### Cleaning Up Templates and Styles
|
||||
We’ve added a lot of HTML and CSS in our template and styles, respectively, in the `HeroesComponent`. Let’s move each of these to their own files.
|
||||
|
||||
We move the HTML for the `HeroesComponent` template to `heroes.component.html`. Then we reference the file in the component’s `templateUrl` property.
|
||||
|
||||
Now our `HeroesComponent` looks much cleaner and easier to maintain since our template and styles are in another file.
|
||||
+makeExample('toh-4/ts-snippets/app/app.component.pt4.ts','reference-heroes-component')
|
||||
:marked
|
||||
We’ll also move the HTML out of the `HeroDetailComponent` and into its own file named `hero-detail.component.html`. Then we reference the file from the `templateUrl` property.
|
||||
+makeExample('toh-4/ts-snippets/app/app.component.pt4.ts','reference-hero-detail-component')
|
||||
:marked
|
||||
### Adding Styles to the App
|
||||
When we add styles to a component we are making it easier to package everything a component needs together. The HTML, the CSS, and the code are all together in one convenient place. However we can also add styles at an app level outside of a component.
|
||||
|
||||
Our designers just gave us a few more basic styles to apply to our entire app. Let’s add some CSS in a file `styles.css` to the `src` folder to style the app’s basic elements.
|
||||
+makeExample('toh-4/ts-snippets/app/index.pt4.html','basic-styles')
|
||||
:marked
|
||||
And let’s reference the stylesheet from the `index.html`.
|
||||
+makeExample('toh-4/ts-snippets/app/index.pt4.html','stylesheet')
|
||||
:marked
|
||||
When we view our app in the browser we can see everything still works as expected!
|
||||
|
||||
### Reviewing the App Structure
|
||||
Let’s verify that we have the following structure after all of our good refactoring in this chapter:
|
||||
|
||||
.filetree
|
||||
.file angular2-tour-of-heroes
|
||||
.children
|
||||
.file node_modules
|
||||
.file app
|
||||
.children
|
||||
.file app.component.ts
|
||||
.file dashboard.component.css
|
||||
.file dashboard.component.html
|
||||
.file dashboard.component.ts
|
||||
.file hero.ts
|
||||
.file hero-detail.component.html
|
||||
.file hero-detail.component.ts
|
||||
.file hero.service.ts
|
||||
.file heroes.component.css
|
||||
.file heroes.component.html
|
||||
.file heroes.component.ts
|
||||
.file main.ts
|
||||
.file mock-heroes.ts
|
||||
.file index.html
|
||||
.file package.json
|
||||
.file styles.css
|
||||
.file tsconfig.json
|
||||
:marked
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Recap
|
||||
|
||||
### The Road We’ve Travelled
|
||||
Let’s take stock in what we’ve built.
|
||||
- We added the router to navigate between different components and their templates
|
||||
- We learned how to create router links to represent navigation menu items
|
||||
- We extended a component to either accept a hero as input or accept a router parameter to get the hero
|
||||
- We extended our shared service by adding a new method to it
|
||||
- We added the `slice` pipe to filter the top heroes, and the `uppercase` pipe to format data
|
||||
|
||||
### The Road Ahead
|
||||
Our Tour of Heroes has grown to reuse services, share components, route between components and their templates, and filter and format data with pipes. We have many of the foundations to build an application. In the next chapter we’ll explore how to replace our mock data with real data using http.
|
Loading…
Reference in New Issue