1-What is Angular?
Angular is a platform that makes it easy to build applications with the web. Angular combines declarative templates, dependency injection, end to end tooling, and integrated best practices to solve development challenges. Angular empowers developers to build applications that live on the web, mobile, or the desktop
The Angular CLI is a command line interface tool that can create a project, add files, and perform a variety of ongoing development tasks such as testing, bundling, and deployment.
Step 1. Set up the Development Environment
Verify that you are running at least node6.9.x
and npm 3.x.x
by runningnode -v
and npm -v
in a terminal/console window.
Older versions produce errors, but newer versions are fine. Otherwise, determine java8 by running java -versionThen install the Angular CLI globally.
npm install -g @angular/cli
If having error in running angular example then uninstall and reinstall latest angular-clinpm uninstall -g angular-cli
npm cache clear
npm install -g angular-cli gulp typescript
npm install -g @angular/cli@latest
Step 2. Create a new project
Open a terminal window.Generate a new project and skeleton application by running the following commands
ng new my-app
Step 3: Serve the application
Go to the project directory and launch the servercd my-app
ng serve --open
The
ng serve
command launches the server, watches your files,
and rebuilds the app as you make changes to those files.Using the
--open
(or just -o
) option will automatically open your browser
onhttp://localhost:4200/
Step 4: Edit your first Angular component
The CLI created the first Angular component for you. This is the root component and it is namedapp-root
.
You can find it in ./src/app/app.component.ts
.Open the component file and change the
title
property from Welcome to app!! to Welcome to My First Angular App!!:src/app/app.component.ts
export class AppComponent {
title = 'My First Angular App';
}
The browser reloads automatically with the revised title. That's nice, but it could look better.
Open src/app/app.component.css and give the component some style.
src/app/app.component.css
h1 {
color: #369;
font-family: Arial, Helvetica, sans-serif;
font-size: 250%;
}
The following are all in
src/
File | Purpose |
---|---|
app/app.component.ts |
Defines the same
AppComponent as the one in the QuickStart playground. It is the root component of what will become a tree of nested components as the application evolves. |
app/app.module.ts |
Defines
AppModule , the root module that tells Angular how to assemble the application. Right now it declares only the AppComponent . Soon there will be more components to declare. |
main.ts |
Compiles the application with the JIT compiler and bootstraps the application's main module (
AppModule ) to run in the browser. The JIT compiler is a reasonable choice during the development of most projects and it's the only viable choice for a sample running in a live-codingenvironment like Plunker. You'll learn about alternative compiling and deployment options later in the documentation. |
@Component
and @View
are ways of adding meta-data to the class that Angular will use to find, and load our component into the DOM. For example, selector:'app-root'
is the CSS selected that angular will use to locate our component.Bootstrapping our application by telling Angular which component to use as the root.
2-Heroes app structure
When you're done with this tutorial, the app will look like this
live example
/ download example
.
App UI: https://angular.io/tutorial
src
folder
Your app lives in the The src
folder.
All Angular components, templates, styles, images, and anything else your app needs go here.
Any files outside of this folder are meant to support building your app.
File
|
Purpose
|
---|---|
app/app.component.{ts,html,css,spec.ts} |
Defines the AppComponent along with an HTML template, CSS stylesheet, and a unit test. It is the root component of what will become a tree of nested components as the application evolves.
|
app/app.module.ts |
Defines AppModule, the root module that tells Angular how to assemble the application. Right now it declares only the AppComponent. Soon there will be more components to declare.
|
assets/* |
A folder where you can put images and anything else to be copied wholesale when you build your application.
|
environments/* |
This folder contains one file for each of your destination environments, each exporting simple configuration variables to use in your application. The files are replaced on-the-fly when you build your app. You might use a different API endpoint for development than you do for production or maybe different analytics tokens. You might even use some mock services. Either way, the CLI has you covered.
|
favicon.ico |
Every site wants to look good on the bookmark bar. Get started with your very own Angular icon.
|
index.html |
The main HTML page that is served when someone visits your site. Most of the time you'll never need to edit it. The CLI automatically adds all js and css files when building your app so you never need to add any <script> or <link> tags here manually.
|
main.ts |
The main entry point for your app. Compiles the application with the JIT compiler and bootstraps the application's root module (AppModule) to run in the browser. You can also use the AOT compiler without changing any code by passing in --aot to ng build or ng serve.
|
polyfills.ts |
Different browsers have different levels of support of the web standards. Polyfills help normalize those differences. You should be pretty safe with core-js and zone.js, but be sure to check out the Browser Support guide for more information.
|
styles.css |
Your global styles go here. Most of the time you'll want to have local styles in your components for easier maintenance, but styles that affect all of your app need to be in a central place.
|
test.ts |
This is the main entry point for your unit tests. It has some custom configuration that might be unfamiliar, but it's not something you'll need to edit.
|
tsconfig.{app|spec}.json |
TypeScript compiler configuration for the Angular app (tsconfig.app.json) and for the unit tests (tsconfig.spec.json).
|
The root folder
src/
folder is just one of the items inside the project's root folder.
Other files help you build, test, maintain, document, and deploy the app.
These files go in the root folder next to src/
.
File
|
Purpose
|
---|---|
e2e/ |
Inside e2e/ live the end-to-end tests. They shouldn't be inside src/ because e2e tests are really a separate app that just so happens to test your main app. That's also why they have their own tsconfig.e2e.json.
|
node_modules/ |
Node.js creates this folder and puts all third party modules listed in package.json inside of it.
|
.angular-cli.json |
Configuration for Angular CLI. In this file you can set several defaults and also configure what files are included when your project is built. Check out the official documentation if you want to know more
|
.editorconfig |
Simple configuration for your editor to make sure everyone that uses your project has the same basic configuration. Most editors support an .editorconfig file. See http://editorconfig.org for more information.
|
.gitignore |
Git configuration to make sure autogenerated files are not commited to source control.
|
karma.conf.js |
Unit test configuration for the Karma test runner, used when running ng test.
|
package.json |
npm configuration listing the third party packages your project uses. You can also add your own custom scripts here.
|
protractor.conf.js |
End-to-end test configuration for Protractor, used when running ng e2e.
|
README.md |
Basic documentation for your project, pre-filled with CLI command information. Make sure to enhance it with project documentation so that anyone checking out the repo can build your app!
|
tsconfig.json |
TypeScript compiler configuration for your IDE to pick up and give you helpful tooling.
|
tslint.json |
- The Tour of Heroes app uses the double curly braces of interpolation (a type of one-way data binding) to display the app title and properties of a
Hero
object. - You wrote a multi-line template using ES2015's template literals to make the template readable.
- You added a two-way data binding to the
<input>
element using the built-inngModel
directive. This binding both displays the hero's name and allows users to change it. - The
ngModel
directive propagates changes to every other binding of thehero.name
.
In this page, you'll expand the Tour of Heroes app to display a list of heroes, and
allow users to select a hero and display the hero's details.
The Tour of Heroes app displays a list of selectable heroes.
You added the ability to select a hero and show the hero's details.
Learned how to use the built-in directives
When you're done with this page, the app should look like this
live example
/ download example
.
The (*) prefix to ngFor is a critical part of this syntax. It indicates that the <li> element and its children constitute a master template.
The ngFor directive iterates over the component's heroes array and renders an instance of this template for each hero in that array.
The let hero part of the expression identifies hero as the template input variable, which holds the current hero item for each iteration. You can reference this variable within the template to access the current hero's properties.
Read more about ngFor and template input variables in the Showing an array property with *ngFor section of the Displaying Data page and the ngFor section of the Template Syntax page.
Learn more about event binding at the
User Input page and the
Event binding section of the
Template Syntax page.
Read more about
In this page, you'll take the first step in that direction by carving out the hero details into a separate, reusable component. When you're done, the app should look like this
live example
/ download example
.
>>>src/app/hero-detail.component.ts (excerpt)
export class HeroDetailComponent {
@Input() hero: Hero;
}
What changed? As before, whenever a user clicks on a hero name,
the hero detail appears below the hero list.
But now the
Refactoring the original
Instead of copying and pasting the same code over and over, you'll create a single reusable data service and inject it into the components that need it. Using a separate service keeps components lean and focused on supporting the view, and makes it easy to unit-test components with a mock service.
Because data services are invariably asynchronous, you'll finish the page with a Promise-based version of the data service.
Here's what you achieved in this page:
live example
/ download example
.
Although the
src/app/app.component.ts (constructor)
You might be tempted to call the
To have Angular call
Each interface has a single method. When the component implements that method, Angular calls it at the appropriate time.
title = 'Tour of Heroes';
heroes: Hero[];
selectedHero: Hero;
constructor(private heroService: HeroService) { }
ngOnInit(): void {
// TODO Auto-generated method stub
this.getHeroes();
}
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
getHeroes(): void {
this.heroService.getHeroes().then(heroes => this.heroes = heroes);
}
}
7-Routing
There are new requirements for the Tour of Heroes app:
When you’re done, users will be able to navigate the app like thislive example
/ download example.
base href is essential
For more information, see the Set the base href
section of the Routing and Navigation page.
The route definition has the following parts:
The
Since the link is not dynamic, a routing instruction is defined with a one-time binding to the route path. Looking back at the route configuration, you can confirm that
The browser's address bar shows
Click the Heroes navigation link. The address bar updates to
The AppComponent is now attached to a router and displays routed views. For this reason, and to distinguish it from other kinds of components, this component type is called a router component.
src/app/dashboard.component.ts (v1)
import { Component } from '@angular/core';
@Component({
selector: 'my-dashboard',
template: '<h3>My Dashboard</h3>'
})
export class DashboardComponent { }
Configure the dashboard route
To teach app.module.ts to navigate to the dashboard, import the dashboard component and add the following route definition to the Routes array of definitions.
src/app/app.module.ts (Dashboard route)
{
path: 'dashboard',
component: DashboardComponent
},
src/app/app.component.ts (template-v3)
template: `
<h1>{{title}}</h1>
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
The
You can add the hero's
Use the following route definition.
src/app/app.module.ts (hero detail)
{
path: 'detail/:id',
component: HeroDetailComponent
},
The colon (:) in the path indicates that :id is a placeholder for a specific hero id when navigating to the HeroDetailComponent.
Revise the HeroDetailComponent
import 'rxjs/add/operator/switchMap';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Location } from '@angular/common';
import { Hero } from './hero';
import { HeroService } from './hero.service';
@Component({
selector: 'hero-detail',
templateUrl: './hero-detail.component.html',
})
export class HeroDetailComponent implements OnInit {
hero: Hero;
constructor(
private heroService: HeroService,
private route: ActivatedRoute,
private location: Location
) {}
ngOnInit(): void {
this.route.paramMap
.switchMap((params: ParamMap) => this.heroService.getHero(+params.get('id')))
.subscribe(hero => this.hero = hero);
}
goBack(): void {
this.location.back();
}
}
The
If a user re-navigates to this component while a
The hero
Users have several ways to navigate to the
To navigate somewhere else, users can click one of the two links in the
goBack(): void {
this.location.back();
}
You'll wire this method with an event binding to a Back button that you'll add to the component template.
<button (click)="goBack()">Back</button>
Although the dashboard heroes are presented as button-like blocks, they should behave like anchor tags. When hovering over a hero block, the target URL should display in the browser status bar and the user should be able to copy the link or open the hero detail view in a new tab.
To achieve this effect, reopen
This time, you're binding to an expression containing a link parameters array. The array has two elements: the path of the destination route and a route parameter set to the value of the current hero's
src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { HeroesComponent } from './heroes.component';
import { HeroDetailComponent } from './hero-detail.component';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'detail/:id', component: HeroDetailComponent },
{ path: 'heroes', component: HeroesComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}
The following points are typical of routing modules:
src/app/heroes.component.ts (current template)
template: `
<h1>{{title}}</h1>
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<hero-detail [hero]="selectedHero"></hero-detail>
`,
Delete the
Delete the last line of the template with the
You'll no longer show the full
However, when users select a hero from the list, they won't go to the detail page. Instead, they'll see a mini detail on this page and have to click a button to navigate to the full detail page.
Add the mini detail
Add the following HTML fragment at the bottom of the template where the
<div *ngIf="selectedHero">
<h2>
{{selectedHero.name | uppercase}} is my hero
</h2>
<button (click)="gotoDetail()">View Details</button>
</div>
Pipes are a good way to format strings, currency amounts, dates and other display data. Angular ships with several pipes and you can write your own.
Move content out of the component file
You still have to update the component class to support navigation to the
The component file is big. It's difficult to find the component logic amidst the noise of HTML and CSS.
Before making any more changes, migrate the template and styles to their own files.
First, move the template contents from
The two new files should look like this:
src/app/heroes.component.ts (revised metadata)
@Component({
selector: 'my-heroes',
templateUrl: './heroes.component.html',
styleUrls: [ './heroes.component.css' ]
})
export class HeroesComponent implements OnInit {..}
The
src/app/heroes.component.ts (gotoDetail)
gotoDetail(): void {
this.router.navigate(['/detail', this.selectedHero.id]);
}
Note that you're passing a two-element link parameters array—a path and the route parameter—to the router
Refesh the browser, and here are resuls:
Summary
Here's what you achieved in this page:
In this page, you'll make the following improvements.
When you're done with this page, the app should look like this
live example
/ download example
.
You're ready to import from
To allow access to these services from anywhere in the app, add
Notice that you also supply
Until you have a web server that can handle requests for hero data, the HTTP client will fetch and save data from a mock service, the in-memory web API.
Update
// Imports for loading & configuring the in-memory web api
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
if IDE warns about lacking 'angular-in-memory-web-api', you can install it into your system:
Rather than require a real API server, this example simulates communication with the remote server by adding the
InMemoryWebApiModule
to the module
The
src/app/in-memory-data.service.ts
import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const heroes = [
{ id: 0, name: 'Zero' },
{ id: 11, name: 'Mr. Nice' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];
return {heroes};
}
}
src/app/hero.service.ts (updated getHeroes and new class members)
/**
* New typescript file
*/
import { Injectable } from '@angular/core';
import 'rxjs/add/operator/toPromise';
import { Hero } from './hero';
import { Headers, Http } from '@angular/http';
@Injectable()
export class HeroService {
private heroesUrl = 'api/heroes'; // URL to web api
constructor(private http: Http) { }
/*
getHeroes(): Promise<Hero[]> {
return Promise.resolve(HEROES);
}
*/
getHeroes(): Promise<Hero[]> {
return this.http.get(this.heroesUrl)
.toPromise()
.then(response => response.json().data as Hero[])
.catch(this.handleError);
}
private handleError(error: any): Promise<any> {
console.error('An error occurred', error); // for demo purposes only
return Promise.reject(error.message || error);
}
getHeroesSlowly(): Promise<Hero[]> {
return new Promise(resolve => {
// Simulate server latency with 2 second delay
setTimeout(() => resolve(this.getHeroes()), 2000);
});
}
getHero(id: number): Promise<Hero> {
return this.getHeroes()
.then(heroes => heroes.find(hero => hero.id === id));
}
}
For now, you've converted the
There are many operators like
The caller is unaware that you fetched the heroes from the (mock) server. It receives a Promise of heroes just as it did before.
Update the
getHero(id: number): Promise<Hero> {
const url = `${this.heroesUrl}/${id}`;
return this.http.get(url)
.toPromise()
.then(response => response.json().data as Hero)
.catch(this.handleError);
}
This request is almost the same as
Also, the
src/app/hero.service.ts (update)
private headers = new Headers({'Content-Type': 'application/json'});
update(hero: Hero): Promise<Hero> {
const url = `${this.heroesUrl}/${hero.id}`;
return this.http
.put(url, JSON.stringify(hero), {headers: this.headers})
.toPromise()
.then(() => hero)
.catch(this.handleError);
To identify which hero the server should update, the hero
Refresh the browser, change a hero name, save your change, and click the browser Back button. Changes should now persist.
Insert the following into the heroes component HTML, just after the heading:
src/app/heroes.component.html (add)
<div>
<label>Hero name:</label> <input #heroName />
<button (click)="add(heroName.value); heroName.value=''">
Add
</button>
</div>
In response to a click event, call the component's click handler and then clear the input field so that it's ready for another name.
src/app/heroes.component.ts (add)
add(name: string): void {
name = name.trim();
if (!name) { return; }
this.heroService.create(name)
.then(hero => {
this.heroes.push(hero);
this.selectedHero = null;
});
}
When the given name is non-blank, the handler delegates creation of the named hero to the hero service, and then adds the new hero to the array.
Add the following button element to the heroes component HTML, after the hero name in the repeated
<button class="delete"
(click)="delete(hero); $event.stopPropagation()">x</button>
The
src/app/heroes.component.html (li-element)
<li *ngFor="let hero of heroes" (click)="onSelect(hero)"
[class.selected]="hero === selectedHero">
<span class="badge">{{hero.id}}</span>
<span>{{hero.name}}</span>
<button class="delete"
(click)="delete(hero); $event.stopPropagation()">x</button>
</li>
In addition to calling the component's
The
Start by creating
The component template is simple—just a text box and a list of matching search results.
hero-search.component.html
<div id="search-component">
<h4>Hero Search</h4>
<input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
<div>
<div *ngFor="let hero of heroes | async"
(click)="gotoDetail(hero)" class="search-result" >
{{hero.name}}
</div>
</div>
</div>
As the user types in the search box, a keyup event binding calls the component's
As expected, the
But as you'll soon see, the
Create the
private searchTerms = new Subject<string>();
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
A
Each call to
ngIf
and ngFor
in a component's template.
<li *ngFor="let hero of heroes">
The (*) prefix to ngFor is a critical part of this syntax. It indicates that the <li> element and its children constitute a master template.
The ngFor directive iterates over the component's heroes array and renders an instance of this template for each hero in that array.
The let hero part of the expression identifies hero as the template input variable, which holds the current hero item for each iteration. You can reference this variable within the template to access the current hero's properties.
Read more about ngFor and template input variables in the Showing an array property with *ngFor section of the Displaying Data page and the ngFor section of the Template Syntax page.
Read more about
ngIf
and ngFor
in the
Structural Directives page and the
Built-in directives section of the
Template Syntax page.5-Multiple Components
You'll need to break it up into sub-components, each focused on a specific task or workflow. Eventually, theAppComponent
could become a simple shell that hosts those sub-components.In this page, you'll take the first step in that direction by carving out the hero details into a separate, reusable component. When you're done, the app should look like this
<hero-detail [hero]="selectedHero"></hero-detail>
Putting square brackets around the hero
property, to the left of the equal sign (=),
makes it the target of a property binding expression.
You must declare a target binding property to be an input property.
Otherwise, Angular rejects the binding and throws an error.>>>src/app/hero-detail.component.ts (excerpt)
export class HeroDetailComponent {
@Input() hero: Hero;
}
HeroDetailView
is presenting those details.Refactoring the original
AppComponent
into two components yields benefits, both now and in the future:-
You simplified the
AppComponent
by reducing its responsibilities.
-
You can evolve the
HeroDetailComponent
into a rich hero editor without touching the parentAppComponent
.
-
You can evolve the
AppComponent
without touching the hero detail view.
-
You can re-use the
HeroDetailComponent
in the template of some future parent component.
Instead of copying and pasting the same code over and over, you'll create a single reusable data service and inject it into the components that need it. Using a separate service keeps components lean and focused on supporting the view, and makes it easy to unit-test components with a mock service.
Because data services are invariably asynchronous, you'll finish the page with a Promise-based version of the data service.
Here's what you achieved in this page:
- You created a service class that can be shared by many components.
- You used the
ngOnInit
lifecycle hook to get the hero data when theAppComponent
activates. - You defined the
HeroService
as a provider for theAppComponent
. - You created mock hero data and imported them into the service.
- You designed the service to return a Promise and the component to get the data from the Promise.
The naming convention for service files is the service name in lowercase followed by
.service
.
For a multi-word service name, use lower dash-case.
For example, the filename for SpecialSuperHeroService
is special-super-hero.service.ts
.Injectable services
Notice that you imported the AngularInjectable
function and applied that function as an @Injectable()
decorator.
Don't forget the parentheses. Omitting them leads to an error that's difficult to diagnose.
The @Injectable()
decorator tells TypeScript to emit metadata about the service.
The metadata specifies that Angular may need to inject other dependencies into this service.Although the
HeroService
doesn't have any dependencies at the moment,
applying the @Injectable()
decorator from the start ensures
consistency and future-proofing.Inject the HeroService
Instead of using the new line, you'll add two lines.- Add a constructor that also defines a private property.
- Add to the component's
providers
metadata.
src/app/app.component.ts (constructor)
constructor(private heroService: HeroService) { }
src/app/app.component.ts (providers)
providers: [HeroService]
The ngOnInit lifecycle hook
AppComponent
should fetch and display hero data with no issues.You might be tempted to call the
getHeroes()
method in a constructor, but
a constructor should not contain complex logic,
especially a constructor that calls a server, such as a data access method.
The constructor is for simple initializations, like wiring constructor parameters to properties.To have Angular call
getHeroes()
, you can implement the Angular ngOnInit lifecycle hook.
Angular offers interfaces for tapping into critical moments in the component lifecycle:
at creation, after each change, and at its eventual destruction.Each interface has a single method. When the component implements that method, Angular calls it at the appropriate time.
Read more about lifecycle hooks in the Lifecycle Hooks page.
export class AppComponent implements OnInit {title = 'Tour of Heroes';
heroes: Hero[];
selectedHero: Hero;
constructor(private heroService: HeroService) { }
ngOnInit(): void {
// TODO Auto-generated method stub
this.getHeroes();
}
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
getHeroes(): void {
this.heroService.getHeroes().then(heroes => this.heroes = heroes);
}
}
7-Routing
There are new requirements for the Tour of Heroes app:
- Add a Dashboard view.
- Add the ability to navigate between the Heroes and Dashboard views.
- When users click a hero name in either view, navigate to a detail view of the selected hero.
- When users click a deep link in an email, open the detail view for a particular hero.
When you’re done, users will be able to navigate the app like this
- Path: The router matches this route's path to the URL in the browser address bar (for example:
heroes
). - Component: The component that the router should create when navigating to this route (
HeroesComponent
).
Routes
in the Routing & Navigation page.The
forRoot()
method is called because a configured router is provided at the app's root.
The forRoot()
method supplies the Router service providers and directives needed for routing, and
performs the initial navigation based on the current browser URL.Router outlet
If you paste the path,/heroes
, into the browser address bar at the end of the URL,
the router should match it to the heroes
route and display the HeroesComponent
.
However, you have to tell the router where to display the component.
To do this, you can add a <router-outlet>
element at the end of the template.
RouterOutlet
is one of the directives provided by the RouterModule
.
The router displays each component immediately below the <router-outlet>
as users navigate through the app.Router links
Users shouldn't have to paste a route URL into the address bar. Instead, add an anchor tag to the template that, when clicked, triggers navigation to theHeroesComponent
.Since the link is not dynamic, a routing instruction is defined with a one-time binding to the route path. Looking back at the route configuration, you can confirm that
'/heroes'
is the path of the route to the HeroesComponent
.The browser's address bar shows
/
.
The route path to HeroesComponent
is /heroes
, not /
.
Soon you'll add a route that matches the path /
.Click the Heroes navigation link. The address bar updates to
/heroes
and the list of heroes displays.The AppComponent is now attached to a router and displays routed views. For this reason, and to distinguish it from other kinds of components, this component type is called a router component.
Add a dashboard
Routing only makes sense when multiple views exist. To add another view, create a placeholderDashboardComponent
, which users can navigate to and from.src/app/dashboard.component.ts (v1)
import { Component } from '@angular/core';
@Component({
selector: 'my-dashboard',
template: '<h3>My Dashboard</h3>'
})
export class DashboardComponent { }
Configure the dashboard route
To teach app.module.ts to navigate to the dashboard, import the dashboard component and add the following route definition to the Routes array of definitions.
src/app/app.module.ts (Dashboard route)
{
path: 'dashboard',
component: DashboardComponent
},
Add navigation to the template
Add a dashboard navigation link to the template, just above the Heroes link.src/app/app.component.ts (template-v3)
template: `
<h1>{{title}}</h1>
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
The
<nav>
tags don't do anything yet, but they'll be useful later when you style the links.Navigating to hero details
While the details of a selected hero displays at the bottom of theHeroesComponent
,
users should be able to navigate to the HeroDetailComponent
in the following additional ways:- From the dashboard to a selected hero.
- From the heroes list to a selected hero.
- From a "deep link" URL pasted into the browser address bar.
You can add the hero's
id
to the URL. When routing to the hero whose id
is 11,
you could expect to see a URL such as this:
/detail/11
Configure a route with a parameterUse the following route definition.
src/app/app.module.ts (hero detail)
{
path: 'detail/:id',
component: HeroDetailComponent
},
The colon (:) in the path indicates that :id is a placeholder for a specific hero id when navigating to the HeroDetailComponent.
Revise the HeroDetailComponent
import 'rxjs/add/operator/switchMap';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Location } from '@angular/common';
import { Hero } from './hero';
import { HeroService } from './hero.service';
@Component({
selector: 'hero-detail',
templateUrl: './hero-detail.component.html',
})
export class HeroDetailComponent implements OnInit {
hero: Hero;
constructor(
private heroService: HeroService,
private route: ActivatedRoute,
private location: Location
) {}
ngOnInit(): void {
this.route.paramMap
.switchMap((params: ParamMap) => this.heroService.getHero(+params.get('id')))
.subscribe(hero => this.hero = hero);
}
goBack(): void {
this.location.back();
}
}
The
switchMap
operator maps the id
in the Observable route parameters
to a new Observable
, the result of the HeroService.getHero()
method.If a user re-navigates to this component while a
getHero
request is still processing,
switchMap
cancels the old request and then calls HeroService.getHero()
again.The hero
id
is a number. Route parameters are always strings.
So the route parameter value is converted to a number with the JavaScript (+) operator.Users have several ways to navigate to the
HeroDetailComponent
.To navigate somewhere else, users can click one of the two links in the
AppComponent
or click the browser's back button.
Now add a third option, a goBack()
method that navigates backward one step in the browser's history stack
using the Location
service you injected previously.goBack(): void {
this.location.back();
}
You'll wire this method with an event binding to a Back button that you'll add to the component template.
<button (click)="goBack()">Back</button>
Select a dashboard hero
When a user selects a hero in the dashboard, the app should navigate to theHeroDetailComponent
to view and edit the selected hero.Although the dashboard heroes are presented as button-like blocks, they should behave like anchor tags. When hovering over a hero block, the target URL should display in the browser status bar and the user should be able to copy the link or open the hero detail view in a new tab.
To achieve this effect, reopen
dashboard.component.html
and replace the repeated <div *ngFor...>
tags
with <a>
tags. Change the opening <a>
tag to the following:
<a *ngFor="let hero of heroes" [routerLink]="['/detail', hero.id]" class="col-1-4">
Notice the [routerLink]
binding.
As described in the Router links section of this page,
top-level navigation in the AppComponent
template has router links set to fixed paths of the
destination routes, "/dashboard" and "/heroes".This time, you're binding to an expression containing a link parameters array. The array has two elements: the path of the destination route and a route parameter set to the value of the current hero's
id
.Refactor routes to a Routing Module
It's a good idea to refactor the routing configuration into its own class. The currentRouterModule.forRoot()
produces an Angular ModuleWithProviders
,
a class dedicated to routing should be a routing module.
For more information, see the Milestone #2: The Routing Module
section of the Routing & Navigation page.src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { HeroesComponent } from './heroes.component';
import { HeroDetailComponent } from './hero-detail.component';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'detail/:id', component: HeroDetailComponent },
{ path: 'heroes', component: HeroesComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}
- The Routing Module pulls the routes into a variable. The variable clarifies the routing module pattern in case you export the module in the future.
- The Routing Module adds
RouterModule.forRoot(routes)
toimports
. - The Routing Module adds
RouterModule
toexports
so that the components in the companion module have access to Router declarables, such asRouterLink
andRouterOutlet
. - There are no
declarations
. Declarations are the responsibility of the companion module. - If you have guard services, the Routing Module adds module
providers
. (There are none in this example.
Select a hero in the HeroesComponent
In theHeroesComponent
,
the current template exhibits a "master/detail" style with the list of heroes
at the top and details of the selected hero below.src/app/heroes.component.ts (current template)
template: `
<h1>{{title}}</h1>
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<hero-detail [hero]="selectedHero"></hero-detail>
`,
Delete the
<h1>
at the top.Delete the last line of the template with the
<hero-detail>
tags.You'll no longer show the full
HeroDetailComponent
here.
Instead, you'll display the hero detail on its own page and route to it as you did in the dashboard.However, when users select a hero from the list, they won't go to the detail page. Instead, they'll see a mini detail on this page and have to click a button to navigate to the full detail page.
Add the mini detail
Add the following HTML fragment at the bottom of the template where the
<hero-detail>
used to be:<div *ngIf="selectedHero">
<h2>
{{selectedHero.name | uppercase}} is my hero
</h2>
<button (click)="gotoDetail()">View Details</button>
</div>
Pipes are a good way to format strings, currency amounts, dates and other display data. Angular ships with several pipes and you can write your own.
Move content out of the component file
You still have to update the component class to support navigation to the
HeroDetailComponent
when users click the View Details button.The component file is big. It's difficult to find the component logic amidst the noise of HTML and CSS.
Before making any more changes, migrate the template and styles to their own files.
First, move the template contents from
heroes.component.ts
into a new heroes.component.html
file.
Don't copy the backticks. As for heroes.component.ts
, you'll
come back to it in a minute. Next, move the
styles contents into a new heroes.component.css
file.The two new files should look like this:
src/app/heroes.component.ts (revised metadata)
@Component({
selector: 'my-heroes',
templateUrl: './heroes.component.html',
styleUrls: [ './heroes.component.css' ]
})
export class HeroesComponent implements OnInit {..}
The
styleUrls
property is an array of style file names (with paths).
You could list multiple style files from different locations if you needed them.src/app/heroes.component.ts (gotoDetail)
gotoDetail(): void {
this.router.navigate(['/detail', this.selectedHero.id]);
}
Note that you're passing a two-element link parameters array—a path and the route parameter—to the router
navigate()
method, just as you did in the [routerLink]
binding
back in the DashboardComponent
Refesh the browser, and here are resuls:
Here's what you achieved in this page:
- You added the Angular router to navigate among different components.
- You learned how to create router links to represent navigation menu items.
- You used router link parameters to navigate to the details of the user-selected hero.
- You shared the
HeroService
among multiple components. - You moved HTML and CSS out of the component file and into their own files.
- You added the
uppercase
pipe to format data.
In this page, you'll make the following improvements.
- Get the hero data from a server.
- Let users add, edit, and delete hero names.
- Save the changes to the server.
When you're done with this page, the app should look like this
Providing HTTP Services
TheHttpModule
is not a core NgModule.
HttpModule
is Angular's optional approach to web access. It exists as a separate add-on module called @angular/http
and is shipped in a separate script file as part of the Angular npm package.You're ready to import from
@angular/http
because systemjs.config
configured SystemJS to load that library when you need it.Register for HTTP services
The app will depend on the Angularhttp
service, which itself depends on other supporting services.
The HttpModule
from the @angular/http
library holds providers for a complete set of HTTP services.To allow access to these services from anywhere in the app, add
HttpModule
to the imports
list of the AppModule
.
import { HttpModule } from '@angular/http';
Notice that you also supply
HttpModule
as part of the imports array in root NgModule AppModule
.Simulate the web API
We recommend registering app-wide services in the rootAppModule
providers.Until you have a web server that can handle requests for hero data, the HTTP client will fetch and save data from a mock service, the in-memory web API.
Update
src/app/app.module.ts
with this version, which uses the mock service:// Imports for loading & configuring the in-memory web api
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
if IDE warns about lacking 'angular-in-memory-web-api', you can install it into your system:
npm install angular-in-memory-web-api --save
imports
, effectively replacing the Http
client's XHR backend service with an in-memory alternative.
InMemoryWebApiModule.forRoot(InMemoryDataService),
forRoot()
configuration method takes an InMemoryDataService
class
that primes the in-memory database.
Add the file in-memory-data.service.ts
in app
with the following content:src/app/in-memory-data.service.ts
import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const heroes = [
{ id: 0, name: 'Zero' },
{ id: 11, name: 'Mr. Nice' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];
return {heroes};
}
}
This file replaces
The in-memory web API is only useful in the early stages of development and for demonstrations such as this Tour of Heroes.
Don't worry about the details of this backend substitution; you can
skip it when you have a real web API server.mock-heroes.ts
, which is now safe to delete.
Added hero "Zero" to confirm that the data service can handle a hero with id==0
.Heroes and HTTP
Now convertgetHeroes()
to use HTTP.src/app/hero.service.ts (updated getHeroes and new class members)
/**
* New typescript file
*/
import { Injectable } from '@angular/core';
import 'rxjs/add/operator/toPromise';
import { Hero } from './hero';
import { Headers, Http } from '@angular/http';
@Injectable()
export class HeroService {
private heroesUrl = 'api/heroes'; // URL to web api
constructor(private http: Http) { }
/*
getHeroes(): Promise<Hero[]> {
return Promise.resolve(HEROES);
}
*/
getHeroes(): Promise<Hero[]> {
return this.http.get(this.heroesUrl)
.toPromise()
.then(response => response.json().data as Hero[])
.catch(this.handleError);
}
private handleError(error: any): Promise<any> {
console.error('An error occurred', error); // for demo purposes only
return Promise.reject(error.message || error);
}
getHeroesSlowly(): Promise<Hero[]> {
return new Promise(resolve => {
// Simulate server latency with 2 second delay
setTimeout(() => resolve(this.getHeroes()), 2000);
});
}
getHero(id: number): Promise<Hero> {
return this.getHeroes()
.then(heroes => heroes.find(hero => hero.id === id));
}
}
HTTP Promise
The Angularhttp.get
returns an RxJS Observable
.
Observables are a powerful way to manage asynchronous data flows.
You'll read about Observables later in this page.For now, you've converted the
Observable
to a Promise
using the toPromise
operator.
.toPromise()
The Angular Observable
doesn't have a toPromise
operator out of the box.There are many operators like
toPromise
that extend Observable
with useful capabilities.
To use those capabilities, you have to add the operators themselves.
That's as easy as importing them from the RxJS library like this:
import 'rxjs/add/operator/toPromise';
Extracting the data in the then callback
In the Promise'sthen()
callback, you call the json
method of the HTTP Response
to extract the
data within the response.
.then(response => response.json().data as Hero[])
The response JSON has a single data
property, which
holds the array of heroes that the caller wants.
So you grab that array and return it as the resolved Promise value.The caller is unaware that you fetched the heroes from the (mock) server. It receives a Promise of heroes just as it did before.
Get hero by id
When theHeroDetailComponent
asks the HeroService
to fetch a hero,
the HeroService
currently fetches all heroes and
filters for the one with the matching id
.
That's fine for a simulation, but it's wasteful to ask a real server for all heroes when you only want one.
Most web APIs support a get-by-id request in the form api/hero/:id
(such as api/hero/11
).Update the
HeroService.getHero()
method to make a get-by-id request:getHero(id: number): Promise<Hero> {
const url = `${this.heroesUrl}/${id}`;
return this.http.get(url)
.toPromise()
.then(response => response.json().data as Hero)
.catch(this.handleError);
}
This request is almost the same as
getHeroes()
.
The hero id in the URL identifies which hero the server should update.Also, the
data
in the response is a single hero object rather than an array.Add a hero service update() method
The overall structure of theupdate()
method is similar to that of
getHeroes()
, but it uses an HTTP put()
to persist server-side changes.src/app/hero.service.ts (update)
private headers = new Headers({'Content-Type': 'application/json'});
update(hero: Hero): Promise<Hero> {
const url = `${this.heroesUrl}/${hero.id}`;
return this.http
.put(url, JSON.stringify(hero), {headers: this.headers})
.toPromise()
.then(() => hero)
.catch(this.handleError);
To identify which hero the server should update, the hero
id
is encoded in
the URL. The put()
body is the JSON string encoding of the hero, obtained by
calling JSON.stringify
. The body content type
(application/json
) is identified in the request header.Refresh the browser, change a hero name, save your change, and click the browser Back button. Changes should now persist.
Add the ability to add heroes
To add a hero, the app needs the hero's name. You can use aninput
element paired with an add button.Insert the following into the heroes component HTML, just after the heading:
src/app/heroes.component.html (add)
<div>
<label>Hero name:</label> <input #heroName />
<button (click)="add(heroName.value); heroName.value=''">
Add
</button>
</div>
In response to a click event, call the component's click handler and then clear the input field so that it's ready for another name.
src/app/heroes.component.ts (add)
add(name: string): void {
name = name.trim();
if (!name) { return; }
this.heroService.create(name)
.then(hero => {
this.heroes.push(hero);
this.selectedHero = null;
});
}
When the given name is non-blank, the handler delegates creation of the named hero to the hero service, and then adds the new hero to the array.
Add the ability to delete a hero
Each hero in the heroes view should have a delete button.Add the following button element to the heroes component HTML, after the hero name in the repeated
<li>
element.<button class="delete"
(click)="delete(hero); $event.stopPropagation()">x</button>
The
<li>
element should now look like this:src/app/heroes.component.html (li-element)
<li *ngFor="let hero of heroes" (click)="onSelect(hero)"
[class.selected]="hero === selectedHero">
<span class="badge">{{hero.id}}</span>
<span>{{hero.name}}</span>
<button class="delete"
(click)="delete(hero); $event.stopPropagation()">x</button>
</li>
In addition to calling the component's
delete()
method, the delete button's
click handler code stops the propagation of the click event—you
don't want the <li>
click handler to be triggered because doing so would
select the hero that the user will delete.Observables
EachHttp
service method returns an Observable
of HTTP Response
objects.The
HeroService
converts that Observable
into a Promise
and returns the promise to the caller.
This section shows you how, when, and why to return the Observable
directly.You're going to add a hero search feature to the Tour of Heroes. As the user types a name into a search box, you'll make repeated HTTP requests for heroes filtered by that name. Add the ability to search by name
Start by creating
HeroSearchService
that sends search queries to the server's web API.Create a HeroSearchComponent
HeroSearchComponent
that calls the new HeroSearchService
.The component template is simple—just a text box and a list of matching search results.
hero-search.component.html
<div id="search-component">
<h4>Hero Search</h4>
<input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
<div>
<div *ngFor="let hero of heroes | async"
(click)="gotoDetail(hero)" class="search-result" >
{{hero.name}}
</div>
</div>
</div>
search()
method with the new search box value.As expected, the
*ngFor
repeats hero objects from the component's heroes
property.But as you'll soon see, the
heroes
property is now an Observable of hero arrays, rather than just a hero array.
The *ngFor
can't do anything with an Observable
until you route it through the async
pipe (AsyncPipe
).
The async
pipe subscribes to the Observable
and produces the array of heroes to *ngFor
.Create the
HeroSearchComponent
class and metadata.Focus on the Search terms
searchTerms
:private searchTerms = new Subject<string>();
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
A
Subject
is a producer of an observable event stream;
searchTerms
produces an Observable
of strings, the filter criteria for the name search.Each call to
search()
puts a new string into this subject's observable stream by calling next()
.debounceTime(300)
waits until the flow of new string events pauses for 300 milliseconds before passing along the latest string. You'll never make requests more frequently than 300ms.distinctUntilChanged
ensures that a request is sent only if the filter text changed.switchMap()
calls the search service for each search term that makes it throughdebounce
anddistinctUntilChanged
. It cancels and discards previous search observables, returning only the latest search service observable.
With the switchMap operator
(formerly known as
If the search text is empty, the
Note that until the service supports that feature, canceling the
flatMapLatest
),
every qualifying key event can trigger an http()
method call.
Even with a 300ms pause between requests, you could have multiple HTTP requests in flight
and they may not return in the order sent.switchMap()
preserves the original request order while returning only the observable from the most recent http
method call.
Results from prior calls are canceled and discarded.If the search text is empty, the
http()
method call is also short circuited
and an observable containing an empty array is returned.Note that until the service supports that feature, canceling the
HeroSearchService
Observable
doesn't actually abort a pending HTTP request.
For now, unwanted results are discarded.Add the search component to the dashboard
Add the hero search HTML element to the bottom of theDashboardComponent
template.Summary
You're at the end of your journey, and you've accomplished a lot.- You added the necessary dependencies to use HTTP in the app.
- You refactored
HeroService
to load heroes from a web API. - You extended
HeroService
to supportpost()
,put()
, anddelete()
methods. - You updated the components to allow adding, editing, and deleting of heroes.
- You configured an in-memory web API.
- You learned how to use Observables.
0 comments:
Post a Comment