The primary concern with the demos in these notes is learning Angular. Though there is some styling in the examples, it is minimal and not really the point of the material being covered.
- Intro to TypeScript
- Installing Angular
- Building Blocks of Angular
Variables
var num: number = 5;
var name: string = "Still";
var something: any = 123;
var list: Array<number> = [1,2,3];
var jumbledList: Array<any> = [1, "something", name];
JavaScript Equivalent
var num = 5;
var name = "Still";
var something = 123;
var list = [1,2,3];
Functions
function square(num: number): number {
return num * num;
}
JavaScript Equivalent
function square(num) {
return num * num;
}
Square receives a number in (num: number)
and returns a number in : number {
.
Classes
class Person {
constructor(public name: string) {
}
}
var person = new Person("Some Person");
JavaScript Equivalent
var Person = (function () {
function Person(name) {
this.name = name;
}
return Person;
}());
Test out TypeScript in the browser at TypeScript Playground
TypeScript
class Demo {
constructor(public id: number, public name: string, public scores: Array<number>) {
this.Id = id;
this.Name = name;
this.Scores = scores;
}
public Id: number;
public Name: string;
public Scores: Array<number>;
public WriteToDocument = function () {
let pId = document.createElement('p');
let pName = document.createElement('p');
pId.innerText = "Id: " + this.Id.toString();
pName.innerText = "Name: " + this.Name;
document.body.appendChild(pId);
document.body.appendChild(pName);
let headerScore = document.createElement('h3');
headerScore.innerText = "Scores";
document.body.appendChild(headerScore);
for (let score of this.Scores) {
let pScore = document.createElement('p');
pScore.innerText = score;
document.body.appendChild(pScore);
}
document.body.appendChild(document.createElement('hr'));
}
}
var d1: Demo = new Demo(1, "Jaime", [91, 94, 96, 87]);
d1.WriteToDocument();
JavaScript Equivalent
var Demo = (function () {
function Demo(id, name, scores) {
this.id = id;
this.name = name;
this.scores = scores;
this.WriteToDocument = function () {
var pId = document.createElement('p');
var pName = document.createElement('p');
pId.innerText = "Id: " + this.Id.toString();
pName.innerText = "Name: " + this.Name;
document.body.appendChild(pId);
document.body.appendChild(pName);
var headerScore = document.createElement('h3');
headerScore.innerText = "Scores";
document.body.appendChild(headerScore);
for (var _i = 0, _a = this.Scores; _i < _a.length; _i++) {
var score = _a[_i];
var pScore = document.createElement('p');
pScore.innerText = score;
document.body.appendChild(pScore);
}
document.body.appendChild(document.createElement('hr'));
};
this.Id = id;
this.Name = name;
this.Scores = scores;
}
return Demo;
}());
var d1 = new Demo(1, "Jaime", [91, 94, 96, 87]);
d1.WriteToDocument();
Installation is according to the Angular Setup Guide. Make sure that Git is installed and you are running the commands from the Git CMD window.
Scaffold an Angular app into the specified project directory, delete non-essential files, install dependent node modules, and open in Visual Studio Code
git clone https://github.com/angular/quickstart.git <project-dir>
cd <project-dir>
for /f %i in (non-essential-files.txt) do del %i /F /S /Q
npm install
code .
File | Purpose |
---|---|
src/app/... |
Angular application files go here |
e2e/... |
End-to-End tests for app, written in Jasmine and run by protractor e2e test runner |
node_modules |
npm packages installed with the npm install command |
.editorconfig |
Tooling configuration file. Ignore until you have a compelling reason to do otherwise |
src/index.html |
The app host page. Loads a few essential scripts in a prescribed order. Then, boots the application, placing the root AppComponent in the custom <my-app> body tag |
src/styles.css |
Global app styles. Initialized with an <h1> style for the QuickStart demo |
src/systemjs.config.js |
Tells the SystemJS module loader where to find modules referenced in JavaScript import statements such as import { Component } from '@angular/core'; . Do not modify unless fully versed in SystemJS configuration |
src/systemjs.config.extras.js |
Optional extra SystemJS configuration. A way to add SystemJS mappings, such as for application barrels, without changing the original system.config.js |
src/tsconfig.json |
Tells the TypeScript compiler how to transpile TypeScript source files into JavaScript files that run in all modern browsers |
tslint.json |
The npm installed TypeScript linter inspects TypeScript code and complains when you violate one of its rules. The file defines linting rules favored by the Angular style guide and by the authors of the Angular documentation |
bs-config.json |
Configuration file for lite-server . This is a lightweight, development only node server that serves a web app, opens it in the browser, refreshes when html or javascript change, injects CSS changes using sockets, and has a fallback page when a route is not found |
Apart from the initial configuration files and dependencies listed above, the following setup is needed to get the project running.
The index.html
file needs to be configured to load the appropriate scripts and provide an entry point for Angular via the component specified in the bootstrap
property of the angular module (in this case, <my-app>
)
src/index.html
<!DOCTYPE html>
<html>
<head>
<title>Angular QuickStart</title>
<base href="/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('main.js').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<my-app>Loading AppComponent content here ...</my-app>
</body>
</html>
A main.ts
file is needed to bootstrap the main app module
See Bootstrapping in
main.ts
for a deep dive on module bootstrapping.
src/main.ts
import { platformBrowserDynamic } from 'angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
app.module.ts
defines the root module for the application. It describes how the application parts fit together. Every application needs at least one Angular module, the root module that you bootstrap to launch the application.
Notice here the bootstrap
property specifies AppComponent
(defined next)
src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
imports: [ BrowserModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
app.component.ts
(see bootstrap
property for NgModule
) is the main application view, called the root component, that hosts all other app views. Only the root module should set the bootstrap property.
src/app/app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `<h1>Hello {{name}}</h1>`
})
export class AppComponent { name = 'Angular'; }
To de-clutter the transpiled .js
and .js.map
files from the project, install the gulp
and gulp-clean
modules as devDependencies
from the terminal
npm install gulp --save-dev
npm install gulp-clean --save-dev
In the root of the project, create gulpfile.js
gulpfile.js
var
gulp = require('gulp'),
clean = require('gulp-clean');
gulp.task('clean', function () {
return gulp.src(['src/app/*.js', 'src/app/*.js.map', 'src/main.js', 'src/main.js.map'])
.pipe(clean({force: true}));
});
Optionally, you can setup the clean
task as an npm script in package.json
package.json
"scripts": {
"clean": "gulp clean"
}
Any time after running the application that you want to remove the transpiled files, you can use either of the below commands
npm run clean
gulp clean
Directives
- Component - Templates (HTML), Styles (CSS), & Logic (TypeScript / JavaScript)
- Attribute - Styling HTML
- Structural - Manipulating HTML
Data Flow
- Interpolation - Variable printing in Templates
- Event Binding - Trigger Events
- 2-Way Binding - Variables updated in real time
Providers
- Services
- Reusable Logic
- Data Storing and Manipulation
- Libraries
Component Directives are reusable building blocks for an application.
Component
- HTML
- CSS
- TypeScript / JavaScript
Basic Example
@Component({
selector: 'demo-block',
template: '<h3>Demo Block Component</h3>',
styles: ['h3 { color: gray; }']
})
export class DemoBlockComponent {
console.log("Hello Angular");
}
<demo-block></demo-block>
Using External HTML & CSS Assets
@Component({
selector: 'demo-block',
templateUrl: 'demo-block.component.html',
styleUrls: ['demo-block.component.css']
})
export class DemoBlockComponent {
console.log("Hello Angular")
}
<demo-block></demo-block>
For non-root components, the
moduleId
property must be specified in the dependency injection metadata
src/app/tasks.component.ts
import { Component } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'tasks',
templateUrl: 'tasks.component.html'
})
export class TasksComponent { }
src/app/tasks.component.html
<h4>This is the Tasks Component</h4>
After the task is created, it then needs to be registered with the module that hosts the component
app.module.ts
import { TasksComponent } from './tasks.component';
// additional imports
@NgModule({
declarations: [ AppComponent, TasksComponent],
// Additional injection metadata
})
export class AppModule { }
At this point, the component can then be used in other components that have access to the module it is defined in
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<h1>Hello {{name}}</h1>
<tasks></tasks>
`
})
export class AppComponent { name = 'Angular'; }
Attribute Directives can change the appearance or behavior of an element
Modify tasks.component.ts
to include a stylesheet reference and a toggle property in the class definition
tasks.component.ts
import { Component } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'tasks',
templateUrl: 'tasks.component.html',
styleUrls: [ 'tasks.component.css' ]
})
export class TasksComponent {
toggle: boolean = true;
}
Modify tasks.component.html
to use the following built-in attribute directives:
[class]
[ngClass]
tasks.component.html
<p [class.red]="toggle">[class.red]="toggle" syntax</p>
<p [ngClass]="{ red: !toggle, blue: toggle }">[ngClass]="{ red: !toggle, blue: toggle }" syntax</p>
Add the css class that defines the styles for the component
tasks.component.css
p {
font-family: Arial, Helvetica, sans-serif;
}
.red {
color: red;
}
.blue {
color: blue;
}
Run the app and see how the component renders with attribute directives on these two elements
See Attribute Directives for details on building your own
Structural Directives are responsible for HTML layout. They shape or reshape the DOM's structure, typically by adding, removing, or manipulating elements.
Three Common Structural Directives
Directive | Purpose |
---|---|
NgIf |
You can add or remove an element from the DOM by applying an NgIf directive to that element. Bind the directive to an expression that returns a boolean value. When the expression is truthy, the element will be added to the dom. When the expression is falsy, the element is removed from the DOM, destroying that component and all of its sub-components. |
NgFor |
NgFor is a repeater directive - a way to present a list of items. You define a block of HTML that defines how a single item should be displayed. You tell Angular to use that block as a template for rendering each item in the list. |
NgSwitch |
NgSwitch is like the JavaScript switch statement. It can display one element from among several possible elements, based on a switch condition. Angular puts only the selected element into the DOM. NgSwitch is actually a set of three, cooperating directives: NgSwitch , NgSwitchCase , and NgSwitchDefault . |
Example
<div *ngIf="hero">{{hero.name}}</div>
<ul>
<li *ngFor="let herof of heroes">{{hero.name}}</li>
</ul>
<div [ngSwitch="hero?.emotion">
<happy-hero *ngSwitchCase="'happy'" [hero]="hero"></happy-hero>
<sad-hero *ngSwitchCase="'sad'" [hero]="hero"></sad-hero>
<confused-hero *ngSwitchCase="'confused'" [hero]="hero"></confused-hero>
<unknown-hero *ngSwitchDefault [hero]="hero"></unknown-hero>
</div>
When you see a directive spelled in both PascalCase and camelCase, the PascalCase refers to the directive class, and the camelCase refers to the directive's attribute name.
In the next section, there are some setup steps and features added that go beyond the scope of this section. The focus here is on the structural directives, not things like adding module references or setting up click events.
Import the Angular FormsModule
in app.module.ts
and add it to the imports
injection metadata array
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { TasksComponent } from './tasks.component';
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [ BrowserModule, FormsModule ],
declarations: [ AppComponent, TasksComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
Update tasks.component.css
for elements used to add structure to the component layout
tasks.component.css
p, h3 {
font-family: Arial, Helvetica, sans-serif;
}
.red {
color: red;
}
.blue {
color: blue;
}
hr.gradient {
border: 0;
height: 1px;
background-image: -webkit-linear-gradient(left, #f0f0f0, #8c8b8b, #f0f0f0);
background-image: -moz-linear-gradient(left, #f0f0f0, #8c8b8b, #f0f0f0);
background-image: -o-linear-gradient(left, #f0f0f0, #8c8b8b, #f0f0f0);
background-image: linear-gradient(left, #f0f0f0, #8c8b8b, #f0f0f0);
}
In tasks.components.ts
, add an Array<string>
property named tasks
, an Array<string>
property named categories
, and a string
property named category
tasks.components.ts
import { Component } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'tasks',
templateUrl: 'tasks.component.html',
styleUrls: [ 'tasks.component.css' ]
})
export class TasksComponent {
toggle: boolean = true;
tasks: Array<string> = [
'Finish Angular notes',
'Finish Bootstrap tutorial',
'Write Web App for Azure'
];
categories: Array<string> = [
'business',
'leisure',
'research'
];
category: string = '';
setToggle() {
this.toggle = !this.toggle;
}
}
The following sections are added to tasks.component.html
:
- A toggle button that modifies the value of the toggle property in the component class. Above it is a label that indicates the current value.
- A category select element that provides options for the values specified in the
categories
array. Binds to thecategory
property, and provides a label above to indicate the current value of this property. - Add an <h3> for Attribute Directives and separate it from the Structural Directives section with a horizontal rule.
- A Structural Directives section that demonstrates the following:
- Toggling an element with
NgIf
- Displaying a list of tasks from an array using
NgFor
- Displaying the details of a category based on the selected value using
NgSwitch
- Toggling an element with
tasks.component.html
<p>Toggle: {{toggle}}</p>
<button (click)="setToggle()">Toggle</button>
<p>Category: {{category}}</p>
<select [(ngModel)]="category">
<option *ngFor="let cat of categories" [value]="cat">{{cat}}</option>
</select>
<h3>Attribute Directives</h3>
<p [class.red]="toggle">[class.red]="toggle" syntax</p>
<p [ngClass]="{ red: !toggle, blue: toggle }">[ngClass]="{ red: !toggle, blue: toggle }" syntax</p>
<hr class="gradient">
<h3>Structural Directives</h3>
<p><strong>NgIf</strong></p>
<p *ngIf="toggle">Shows when toggle is true</p>
<p><strong>NgFor Tasks</strong></p>
<ul>
<li *ngFor="let task of tasks">
<p>{{task}}</p>
</li>
</ul>
<p><strong>NgSwitch</strong></p>
<div [ngSwitch]="category">
<div *ngSwitchCase="'business'">
<p>{{category}} - when actively working on a project</p>
</div>
<div *ngSwitchCase="'leisure'">
<p>{{category}} - when doing something to mitigate burnout</p>
</div>
<div *ngSwitchCase="'research'">
<p>{{category}} - when filling gaps in knowledge</p>
</div>
<div *ngSwitchDefault>
<p>category not currently selected</p>
</div>
</div>
Run the application, and see how the structural directives affect the layout of the component view
Click below image to see demonstration
Interpolation - A form of property data binding in which a template expression between double-curly braces renders as text. That text may be concatenated with neighboring text before it is assigned to an element property or displayed between element tags.
<label>My current hero is {{hero.name}}</label>
Also see Template Syntax - Interpolation
Property Binding - Write a template property binding to set a property of a view element. The binding sets the property to the value of a template expression. The most common property binding sets an element property to a component property value.
When setting an element property to a non-string data value, you must use property binding over interpolation
<!-- Binding the src property of an image lement to a component's heroImageUrl property -->
<img [src]="heroImageUrl" />
<!-- Disabling a button when the component says that it isUnchanged -->
<button [disabled]="isUnchanged">Cancel is disabled</button>
<!-- Setting a property of a directive -->
<div [ngClass]="classes">[ngClass] binding to the classes property</div>
<!-- Setting the model property of a custom component, enabling parent and child components to communicate -->
<hero-detail [hero]="currentHero"></hero-detail>
Event Binding - Event binding allows data to flow from an element to a component. The only way to know about a user action is to listen for certain events such as keystrokes, mouse movements, clicks, and touches. You declare your interest in user actions through Angular event binding. Event binding syntax consists of a target event within parenthesis on the left of an equal sign, and a quoted template statement on the right.
<button (click)="onSave()">Save</button>
<!-- canonical form of event binding -->
<button on-click="onSave()">Save</button>
A name between parenthesis - for instance, (click)
above - identifies the target event.
In an event binding, Angular sets up an event handler for the target event. When the event is raised, the handler executes the template statement. The template statement typically involves a receiver, which performs an action in response to the event, such as storing a value from the HTML control into a model. The binding conveys information about the event, including data values, through an event object name $event
. The shape of the event object is determined by the target event. If the target event is a native DOM element event, then $event
is a DOM event object, with properties such as target
and target.value
.
<input [value]="currentHero.name" (input)="currentHero.name=$event.target.value" />
Two-way Data Binding - Allows you to both display a property and update that property when the user makes changes. ON the lement side that takes a combination of setting a specific element property and listening for an element change event. Angular offers a special two-way data binding syntax for this purpose, [(x)]
. The [(x)]
syntax combines the brackets of property binding, [x]
, with the parenthesis of event binding, (x)
.
The [(x)]
syntax is easy to demonstrate when the element has a settable property called x
and a corresponding event named xChange
. Here's a SizerComponent
that fits the pattern. It has a size
value property and a companion sizeChange
event.
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'my-sizer',
template: `
<div>
<button (click)="dec()" title="smaller">-</button>
<button (click)="inc()" title="bigger">+</button>
<label [style.font-size.px]="size">Fontsize: {{size}}px</label>
</div>`
})
export class SizerComponent {
@Input() size: number | string;
@Output() sizeChange = new EventEmitter<number>();
dec() { this.resize(-1); }
inc() { this.resize(+1); }
resize(delta: number) {
this.size = Math.min(40, Math.max(8, +this.size + delta));
this.sizeChange.emit(this.size);
}
}
The initial size
is an input value from a property binding. Clicking the buttons increases or decreases the size
, within min/max values constraints, and then raises (emits) the sizeChange
event with the adjusted size.
Here's an example in which the AppComponent.fontSizePx
is two-way bound ot the SizerComponent
:
<my-sizer [(size)]="fontSizePx"></my-sizer>
<div [style.font-size.px]="fontSizePx">Resizable Text</div>
The AppComponent.fontSizePx
establishes the initial SizerComponent.size
value. Clicking the buttons updates the AppComponent.fontSizePx
via the two-way binding. The revised AppComponent.fontSizePx
value flows through to the style binding, making the displayed text bigger or smaller.
The two-way binding syntax is really just syntactic sugar for a property binding and an event binding. Angular desugars the SizerComponent
binding into this:
<my-sizer [size]="fontSizePx" (sizeChange)="fontSizePx=$event"></my-sizer>
In tasks.components.ts
, add a number
property named num
set to the value 7
, and a string
property named color
set to the value black
. Also, a setNum()
function is added for binding to a button click.
tasks.components.ts
import { Component } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'tasks',
templateUrl: 'tasks.component.html',
styleUrls: [ 'tasks.component.css' ]
})
export class TasksComponent {
toggle: boolean = true;
tasks: Array<string> = [
'Finish Angular notes',
'Finish Bootstrap tutorial',
'Write Web App for Azure'
];
categories: Array<string> = [
'business',
'leisure',
'research'
];
category: string = '';
num: number = 7;
color: string = 'black';
setToggle() {
this.toggle = !this.toggle;
}
setNum() {
this.num++;
if (this.num > 10) {
this.num = 1;
}
}
}
In tasks.component.html
, a Data Binding section is created that demonstrates the following features:
- Interpolation - The
num
variable is interpolated in the paragraph element -Number: {{num}}
- Event Binding - The
num
variable is modified by thesetNum()
function by binding to a buttonclick
event -(click)="setNum()"
- Two-way Binding - The
color
variable is configured for two way binding on the input element -[(ngModel)]="color"
- Property Binding - The
style.color
property is set to the value of the color variable using property binding interpolation -[style.color]="color"
tasks.component.html
<!-- previous demonstrations -->
<hr class="gradient">
<h3>Data Binding</h3>
<p><strong>Interpolation</strong></p>
<p>Number: {{num}}</p>
<p><strong>Event Binding</strong></p>
<button (click)="setNum()">Update Num</button>
<p><strong>Two-way Binding</strong></p>
<input [(ngModel)]="color">
<p [style.color]="color">The color of this text is determined by the value specified in the input!</p>
Run the app and check out the data binding interactions that were added
Click below image to see demonstration
Service is a broad category encompassing any value, function, or feature that your application needs. Almost anything can be a service. A service is typically a class with a narrow, well-defined purpose. It should do something specific and do it well.
There is nothing specifically Angular about services. Angular has no definition of a service. There is no service base class, and no place to register a service. Yet services are fundamental to any Angular application. Components are big consumers of services.
Here's an example of a service class that logs to the browser console:
export class Logger {
log(msg: any) { console.log(msg); }
error(msg: any) { console.error(msg); }
warn(msg: any) { console.warn(msg); }
}
Services are made available to components through dependency injection. Dependency injection is a way to supply a new instance of a class with the fully-formed dependencies it requires. Most dependencies are services. Angular uses dependency injection to provide new components with the services they need. Angular can tell which services a component needs by looking at the types of its constructor paramters. For instance:
constructor(private service: HeroService) { }
When Angular creates a component, it first asks an injector for the services that the component requires. An injector maintains a container of service instances that it has previosuly created. If a requested service instance is not in the container, the injector makes one and adds it to the container before returning the service to Angular. When all requested services have been resolved and returned, Angular can call the component's constructor with those services as arguments. This is dependency injection.
Before you can inject services, a provider of a service must be registered with the injector. A provider is something that can create or return a service, typically the service class itself. Providers can be registered in modules or in components. In general, add providers to the root module so that the same instance of a service is availble everywhere.
providers: [
BackendService,
HeroService,
Logger
]
See Dependency Injection and Lifecycle Hooks for an advanced look at dependency injection
Create the src/app/tasks.service.ts
file. It needs to be marked with the @Injectable
attribute, which needs to be imported from @angular/core
. The service will contain a task array property named tasks
.
tasks.service.ts
import { Injectable } from '@angular/core';
@Injectable()
export class TasksService {
tasks = [
'First Task',
'Second Task',
'Third Task'
];
constructor() { }
}
To make the service injectable across the entire module, register the TaskService
with the providers
array in the metadata region of app.module.ts
. The TaskService
class needs to be imported from the tasks.service
file.
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { TasksComponent } from './tasks.component';
import { FormsModule } from '@angular/forms';
import { TasksService } from './tasks.service';
@NgModule({
imports: [ BrowserModule, FormsModule ],
declarations: [ AppComponent, TasksComponent ],
bootstrap: [ AppComponent ],
providers: [ TasksService ]
})
export class AppModule { }
In tasks.components.ts
, import the TasksService
class, then create a constructor in the TasksComponent
class with the TasksService
injected as the object property taskService
.
import { TasksService } from './tasks.service';
// Metadata Configuration
export class TasksComponent {
// Previous component logic
constructor(public taskService : TasksService) { }
}
Add the Services section to tasks.component.html
<!-- previous demonstration -->
<hr class="gradient">
<h3>Services</h3>
<p><strong>Tasks</strong></p>
<!-- use an Angular pipe to interpolate the data as JSON instead of a string -->
<p>{{taskService.tasks | json}}</p>
Run the application to see how the service data is used by the component
The Angular Router enables navigation from one view to the next as users perform application tasks. The router can interpret a browser URL as an instruction to navigate to a client-generated view. It can pass optional parameters along to the supporting view component that help it decide what specific content to present. You can bind the router to links on a page and it will navigate to the appropriate application view when the user clicks a link. You can navigate imperatively when the user clicks a button, selects from a drop box, or in response to some other stimulus from any source. And the router logs activity in the browser's history journal so the back and forward buttons work as well.
<base href>
Most routing applications should add a <base>
element to the index.html
as the first child in the <head>
tag to tell the router how to compose navigation URLs.
If the app
folder is the application root, as it is in the current setup for these notes, set the href
value exactly as below:
<base href="/">
Router Imports
The Angular Router is an optional service that presents a particular component view for a given URL. It is not part of the Angular core. It is in its own library package, @angular/router
. Import what you need from it as you would from any other Angular package.
import { RouterModule, Routes } from '@angular/router/';
Configuration
A routed Angular application has one, singleton instance of the Router
service. When the browser's URL changes, that router looks for a corresponding Route
from which it can determine the component to display.
A router has no routes until you configure it. The following example creates four route definitions, configures the router via the RouterModule.forRoot
method, and adds the result to the AppModule imports
array.
const appRoutes: Routes = [
{ path: 'crisis-center', component: CrisisListComponent },
{ path: 'hero/:id', component: HeroDetailComponent },
{
path: 'heroes',
component: HeroListComponent,
data: { title: 'Heroes List' }
},
{
path: '',
redirectTo: '/heroes',
pathMatch: 'full'
},
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(appRoutes)
// other imports here
],
// Other metadata
})
export class AppModule { }
The appRoutes
array of routes describes how to navigate. Pass it to the RouterModule.forRoot
method in the module imports
to configure the router.
Each Route
maps a URL path
to a component. There are no leading slashes in the path. The router parses and builds the final URL for you, allowing you to use both relative and "absolute" paths when navigating between application views.
The :id
in the second route is a token for a route parameter. In a URL such as /hero/42
, "42" is the value of the id
parameter. The corresponding HeroDetailComponent
will use that value to find and present the hero whose id
is 42.
The data
property in the third route is a place to store arbitrary data associated with this specific route. The data property is accessible within each activated route. Use it to store items such as page titles, breadcrumb text, and other read-only, static data.
The empty path
in the fourth route represents the default path for the application, the place to go when the path in the URL is empty, as it typically is at the start. This default route redirects to thte route for the /heroes
URL and, therefore, will display the HeroesListComponent
.
The **
path in the last route is a wildcard. The router will select this route if the requested URL doesn't match any paths for routes defined earlier in the configuration. This is useful for displaying a "404 - Not Found" page or redirecting to another route.
The order of routes in the configuration matters and this is by design. The router uses a first-match wins strategy when matching routes, so more specific routes should be placed above less specific routes. In the configuration above, routes with a static path are listed first, followed by an empty path route, that matches the default route. The wildcard comes last because it matches every URL and should be selected only if no other routes are matched first.
Router Outlet
Given this configuration, when the browser URL for this application becomes /heroes
, the router matches that URL to the route path /heroes
and displays the HeroListComponent
after a RouterOutlet
that you've placed in the host view's HTML.
<router-outlet></router-outlet>
<!-- Routed views go here -->
Router links
Now you have routes configured and a place to render them, but how do you navigate? The URL could arrive directly from the browser address bar. But most of the time you navigate as a result of some user action such as the click of an anchor tag.
<h1>Angular Router</h1>
<nav>
<a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
<a routerLink="/heroes" routerLinkActive="active">Heroes</a>
</nav>
<router-outlet></router-outlet>
The RouterLink
directives on the anchor tags give the router control over those elements. The navigation paths are fixed, so you can assign a string to the routerLink
(a "one-time" binding).
Had the navigation path been more dynamic, you could ahve bound to a template expression that returned an array of route link parameters (the link parameters array). The router resolves that array into a complete URL.
The RouterLinkActive
directive on each anchor tag helps visually distinguish the anchor for the currently selected "active" route. The router adds the active
CSS class to the element when the associated RouterLink becomes active. You can add this directive to the anchor or to its parent element.
Router state
After the end of each successful navigation lifecycle, the router builds a tree of ActivatedRoute
objects that make up the current state of the router. You can access the current RouterState
from anywhere in the application using the Router
service and the routerState
property.
Each ActivatedRoute
in the RouterState
provides methods to traverse up and down the route tree to get information from the parent, child, and sibling routes.
Summary
The application has a configured router. The shell component has a RouterOutlet
wher it can display views produced by the router. It has RouterLink
s that users can click to navigate via the router.
Here are the key Router
terms and their meanings:
Router Part | Meaning |
---|---|
Router |
Displays the application component for the active URL. Manages navigation from one component to the next. |
RouterModule |
A separate Angular module that provides the necessary service providers and directives for navigating through application views. |
Routes |
Defines an array of Routes, each mapping a URL path to a component. |
Route |
Defines how the router should navigate to a component based on a URL pattern. Most routes consist of a path and a component type. |
RouterOutlet |
The directive (<router-outlet> ) that marks where the router should display a view. |
RouterLink |
The directive for binding a clickable HTML element to a route. Clicking an element with a routerLink directive that is bound to a string or a link parameters array triggers a navigation. |
RouterLinkActive |
The directive for adding/removing classes from an HTML element when an associated routerLink contained on or inside the element becomes active/inactive. |
ActivatedRoute |
A service that is provided to each route component that contains route specific information such as route parameters, static data, resolve data, global query params, and the global fragment. |
RouterState |
The current state of the router including a tree of the currently activated routes together with convenience methods for traversing the route tree. |
Link parameters array | An array that the router interprets as a routing instructing. You can bind that array to a RouterLink or pass the array asn an argument to the Router.navigate method. |
Routing component | An Angular component with a RouterOutlet that displays views based on router navigations. |
See Routing and Navigation for more information.
The demos in this overview are getting out of hand. It's time to separate each demo into its own component, then use routing to render each demo separately. To accomplish this, the following steps will be taken:
- Ensure that
<base href="/">
is set in the<head>
element ofindex.html
- Rename
tasks.component.css
and use this for each component. Slight modifications to CSS contents - Build components for each demo segment specified in
tasks.component.html
andtasks.component.ts
- Delete
tasks.component.ts
andtasks.component.html
- Import the Angular Router components, as well as the newly created Components, and configure routing. Also, remove the TasksComponent import and declaration
- Update
app.component.ts
to use anapp.component.html
template and setup the master view / router outlet
src/index.html
<!DOCTYPE html>
<html>
<head>
<title>Angular QuickStart</title>
<base href="/">
<!-- remainder of header -->
</head>
<body>
<my-app>Loading AppComponent content here...</my-app>
</body>
</html>
src/app/app.component.css
p, h2, h3, h4, h5, h6, a, input, label, li, textarea {
font-family: Arial, Helvetica, sans-serif;
}
.red {
color: red;
}
.blue {
color: blue;
}
hr.gradient {
border: 0;
height: 1px;
background-image: -webkit-linear-gradient(left, #f0f0f0, #8c8b8b, #f0f0f0);
background-image: -moz-linear-gradient(left, #f0f0f0, #8c8b8b, #f0f0f0);
background-image: -o-linear-gradient(left, #f0f0f0, #8c8b8b, #f0f0f0);
background-image: linear-gradient(left, #f0f0f0, #8c8b8b, #f0f0f0);
}
a {
text-decoration: none;
padding: 8px;
background-color: #eee;
color: dodgerblue;
}
a:hover {
background-color: #ccc;
}
src/app/attribute.component.ts
import { Component } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'attribute-demo',
templateUrl: 'attribute.component.html',
styleUrls: [ 'app.component.css' ]
})
export class AttributeComponent {
toggle: boolean = true;
setToggle() {
this.toggle = !this.toggle;
}
}
src/app/attribute.component.html
<h3>Attribute Directives</h3>
<hr class="gradient">
<p>Toggle: {{toggle}}</p>
<button (click)="setToggle()">Toggle</button>
<p [class.red]="!toggle">[class.red]="!toggle" syntax</p>
<p [ngClass]="{ red: !toggle, blue: toggle }">[ngClass]="{ red: !toggle, blue: toggle }" syntax</p>
src/app/structural.component.ts
import { Component } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'structural-demo',
templateUrl: 'structural.component.html',
styleUrls: [ 'app.component.css' ]
})
export class StructuralComponent {
toggle: boolean = true;
tasks: Array<string> = [
'Finish Angular notes',
'Finish Bootstrap tutorial',
'Write Web App for Azure'
];
categories: Array<string> = [
'business',
'leisure',
'research'
];
category: string = '';
setToggle() {
this.toggle = !this.toggle;
}
}
src/app/structural.component.html
<h3>Structural Directives</h3>
<hr class="gradient">
<p>Toggle: {{toggle}}</p>
<button (click)="setToggle()">Toggle</button>
<p>Category: {{category}}</p>
<select [(ngModel)]="category">
<option *ngFor="let cat of categories" [value]="cat">{{cat}}</option>
</select>
<hr class="gradient">
<h4>NgIf</h4>
<p *ngIf="toggle">Shows when toggle is true</p>
<p *ngIf="!toggle">Shows when toggle is false</p>
<h4>NgFor Tasks</h4>
<ul>
<li *ngFor="let task of tasks">
<p>{{task}}</p>
</li>
</ul>
<h4>NgSwitch</h4>
<div [ngSwitch]="category">
<div *ngSwitchCase="'business'">
<p>{{category}} - when actively working on a project</p>
</div>
<div *ngSwitchCase="'leisure'">
<p>{{category}} - when doing something to mitigate burnout</p>
</div>
<div *ngSwitchCase="'research'">
<p>{{category}} - when filling gaps in knowledge</p>
</div>
<div *ngSwitchDefault>
<p>category not currently selected</p>
</div>
</div>
src/app/binding.component.ts
import { Component } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'binding-demo',
templateUrl: 'binding.component.html',
styleUrls: [ 'app.component.css' ]
})
export class BindingComponent {
num: number = 7;
color: string = 'black';
setNum() {
this.num++;
if (this.num > 10) {
this.num = 1;
}
}
}
src/app/binding.component.html
<h3>Data Binding</h3>
<hr class="gradient">
<h4>Interpolation</h4>
<p>Number: {{num}}</p>
<h4>Event Binding</h4>
<button (click)="setNum()">Update Num</button>
<h4>Two-way Binding</h4>
<input [(ngModel)]="color">
<p [style.color]="color">The color of this text is determined by the value specified in the input!</p>
src/app/services.component.ts
import { Component } from '@angular/core';
import { TasksService } from './tasks.service';
@Component({
moduleId: module.id,
selector: 'services-demo',
templateUrl: 'services.component.html',
styleUrls: [ 'app.component.css' ]
})
export class ServicesComponent {
constructor(public taskService: TasksService) { }
}
src/app/services.component.html
<h3>Services</h3>
<hr class="gradient">
<h4>Tasks as JSON</h4>
<p>{{taskService.tasks | json}}</p>
With the new components created, tasks.component.ts
and tasks.component.html
can be deleted, and routing can be configured in app.module.ts
. The new components are imported and declared in the declarations
metadata array. Routes are stored in a const
variable named appRoutes
which is a Routes
collection. The routes are configured in the RouterModule.forRoot()
function in the imports
metadata array.
src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';
import { AppComponent } from './app.component';
import { AttributeComponent } from './attribute.component';
import { StructuralComponent } from './structural.component';
import { BindingComponent } from './binding.component';
import { ServicesComponent } from './services.component';
import { TasksService } from './tasks.service';
const appRoutes: Routes = [
{
path: 'attribute-directives', component: AttributeComponent
},
{
path: 'structural-directives', component: StructuralComponent
},
{
path: 'binding', component: BindingComponent
},
{
path: 'services', component: ServicesComponent
},
{
path: '', redirectTo: '/attribute-directives', pathMatch: 'full'
},
{
path: '**', redirectTo: '/attribute-directives'
}
];
@NgModule({
imports: [
BrowserModule,
FormsModule,
RouterModule.forRoot(appRoutes)
],
declarations: [
AppComponent,
AttributeComponent,
StructuralComponent,
BindingComponent,
ServicesComponent
],
bootstrap: [ AppComponent ],
providers: [ TasksService ]
})
export class AppModule { }
The root component, AppComponent
, now needs to be configured by defining the necessary routerLink
s, and providing the <router-outlet>
for the view components to render to.
src/app/app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: 'app/app.component.html',
styleUrls: [ 'app/app.component.css' ]
})
export class AppComponent { }
src/app/app.component.html
<h2>Angular Overview</h2>
<nav>
<a routerLink="/attribute-directives" routerLinkActive="active">Attibute Directives</a>
<a routerLink="/structural-directives">Structural Directives</a>
<a routerLink="/binding">Data Binding</a>
<a routerLink="/services">Services</a>
</nav>
<router-outlet></router-outlet>
Run the application to see the how each demo component is rendered using routing
Click below image to see demonstration
Use an Http Client to talk to a remote server. Http is the primary protocol for browser / server communication.
First, configure the application to use server communication facilities. The Angular Http
client communicates with the server using a familiar HTTP request/response protocol. The Http
client is one of a family of services in the Angular HTTP library.
When importing from the
@angular/http
module, SystemJS knows how to load services from the Angular HTTP library because thesystem.config.js
file maps to that module name.
Before you can use the Http
client, you need to register it as a service provider with the dependency injection system in the root module.
src/app/app.module.ts
- Configuring HTTP Example
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } form '@angular/forms';
import { HttpModule, JsonpModule } from '@angular/http';
import { AppComponent } form './app.component';
@NgModule({
import: [
BrowserModule,
FormsModule,
HttpModule,
JsonpModule
],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
A sample component template to demonstrate making HTTP calls
hero-list.component.html
- Example Template
<h1>Tour of Heroes {{mode}}</h1>
<h3>Heroes:</h3>
<ul>
<li *ngFor="let hero of heroes">{{hero.name}}</li>
</ul>
<label>New hero name: <input #newHeroName /></label>
<button (click)="addHero(newHeroName.value); newHeroName.value=''">Add Hero</button>
<p class="error" *ngIf="errorMessage">{{errorMessage}}</p>
#newHeroName
is a template reference variable, which is often a reference to a DOM element within a template. It can also be a reference to an Angular component or directive, or a web component. You can refer to a template reference variable anywhere in the template. In most cases, Angular sets the reference variable's value to the element on which it was declared. A directive can change that behavior and set the value to something else, such as itself. NgForm
does that: #heroForm="ngForm"
. The reference variable accesses the value of the input box in the (click)
event binding. When the user clicks the button, that value passes to the component's addHero
method and then the event binding clears it to make it ready for a new hero name.
hero-list.component.ts
- Example Component
export class HeroListcomponent implements OnInit {
errorMessage: string;
heroes: Hero[];
mode = 'Observable';
constructor (private heroService: HeroService) {}
ngOnInit() { this.getHeroes(); }
getHeroes() {
this.heroService.getHeroes()
.subscribe(
heroes => this.heroes = heroes,
error => this.errorMessage = <any>error);
}
addHero (name: string) {
if (!name) { return; }
this.heroService.addHero(name)
.subscribe(hero => this.heroes.push(hero),
error => this.errorMessage = <any>error);
}
}
Angular injects a HeroService
into the constructor and the component calls that service to fetch and save data. The component does not talk directly to the Angular Http
client. The component doesn't know or care how it gets the data. It delegates to the HeroService
.
This is a golden rule: always delegate data access to a supporting service class.
Although at runtime the component requests heroes immediately after creation, you don't call the service's get
method in the component's constructor. Instead, call it inside the ngOnInit
lifecycle hook and rely on Angular to call ngOnInit
when it instantiates the component.
The service's getHeroes()
and addHero()
methods return an Observable
of hero data that the Angular Http
client fetched from the server. Think of an Observable
as a stream of events published by some source. To listen for events in this stream, subscribe to the Observable
. These subscriptions specify the actions to take when the web request produces a success event (with the hero data in the event payload) or a fail event (with the error in the payload).
hero.service.ts
- Example Service that interfaces with HTTP
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import { Hero } form './hero';
@Injectable()
export class HeroService {
private heroesUrl = 'app/heroes';
constructor (private http: Http) { }
getHeroes(): Observable<Hero[]> {
return this.http.get(this.heroesUrl)
.map(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body.data || { };
}
private handleError (error: Response | any) {
let errMsg: string;
if (error instanceOf Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
err.Msg = error.message ? error.message : error.toString();
}
console.error(errMessage);
return Observable.throw(errMsg);
}
}
If you are familiar with asynchronous methods in modern JavaScript, you might expect the get
method to return a promise. You'd expect to chain a call to then()
and extract the heroes. Instead, you're calling a map()
method. Clearly this is not a promise.
In fact, the http.get
method returns an Observable of HTTP Responses (Observable<Response>
) from the RxJS library and map
is one of the RxJS operators.
RxJS is a third party library, endorsed by Angular, that implements the asynchronous observable pattern. To make RxJS observables usable, you must import the RxJS operators individually. The RxJS library is large. Size matters when building a production application and deploying it to mobile devices. You should include only necessary features. Each code file should add the operators it needs by importing from an RxJS library. The getHeroes
method needs the map
and catch
operators, so it imports them as follows:
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
See Http Client for full details on working with the Http
and Jsonp
modules.
For specific details on setting up the InMemoryWebApi see Http Client - In Memory Web API
Create a task.ts
file in the app
folder. This will represent the data objects that will be worked with in this segment
task.ts
export class Task {
public title: string;
public completed: boolean;
public created_at: string;
public updated_at: string;
}
Create a task-data.ts
class in the app
folder. This will represent the mock database interface that the Http
module will call
src/app/task-data.ts
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { Task } from './task';
export class TaskData implements InMemoryDbService {
createDb() {
let tasks: Task[] = [
{
title: "First Task",
completed: false,
created_at: "",
updated_at: ""
},
{
title: "Second Task",
completed: false,
created_at: "",
updated_at: ""
},
{
title: "Third Task",
completed: false,
created_at: "",
updated_at: ""
},
{
title: "Fourth Task",
completed: false,
created_at: "",
updated_at: ""
}
];
return {tasks};
}
}
Create an observable-task.service.ts
file in the app
folder. This is the service that will execute the api calls using Observable
and Http
. Note that when adding the task
object to the post
call, it needs to be stringified. Otherwise, it will wrap the object in a task
property, and will not be in the same format as the rest of the data when a subsequent getTasks()
is called
observable-task.service.ts
import { Injectable } from '@angular/core';
import { Http, Response, RequestOptions, Headers } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import { Task } from './task';
@Injectable()
export class ObservableTaskService {
private tasksUrl = 'app/tasks';
constructor(private http: Http) { }
getTasks(): Observable<Task[]> {
return this.http.get(this.tasksUrl)
.map(this.extractData)
.catch(this.handleError);
}
addTask(task: Task): Observable<Task> {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this.tasksUrl, JSON.stringify(task), options)
.map(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body.data || { };
}
private handleError(error: Response | any) {
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
}
Create an observable.component.ts
file in the app
folder. This component uses the ObservableTaskService
to retrieve and add data without knowing the implementation details of the Http
module
import { Component, OnInit } from '@angular/core';
import { ObservableTaskService } from './observable-task.service';
import { Task } from './task';
@Component({
moduleId: module.id,
selector: 'observable-demo',
templateUrl: 'observable.component.html',
styleUrls: ['app.component.css']
})
export class ObservableComponent implements OnInit {
errorMessage: string;
tasks: Task[];
task: Task = new Task();
constructor(private taskService: ObservableTaskService) { }
ngOnInit() {
this.getTasks();
}
getTasks() {
this.taskService.getTasks()
.subscribe(
tasks => {
this.tasks = tasks
},
error => this.errorMessage = <any>error
);
}
addTask() {
if (!this.task.title) { return; }
this.taskService.addTask(this.task)
.subscribe(
task => {
this.getTasks();
this.task = new Task();
},
error => this.errorMessage = <any>error
);
}
}
Create an observable.component.html
file in the app
folder. This serves as the view template for the component
observable.component.html
<h3>Http and Observables</h3>
<hr class="gradient">
<h4>Tasks</h4>
<ul>
<li *ngFor="let tsk of tasks">{{tsk.title}} - {{tsk.completed}}</li>
</ul>
<label>Task Title: <input [(ngModel)]="task.title" placeholder="title"/></label>
<label>Task Completed: <input type="checkbox" [(ngModel)]="task.completed" /></label>
<button (click)="addTask()">Add Task</button>
<p class="red" *ngIf="errorMessage">{{errorMessage}}</p>
Update the app.module.ts
file to import the HttpModule
, InMemoryWebApiModule
, ObservableComponent
, ObservableTaskService
, and TaskData
classes. Update the appRoutes
collection to include an observable
path for ObservableComponent
. Configure the InMemoryWebApiModule
in the imports
metadata property. Add ObservableComponent
to the declarations
metadata array. Add ObservableTaskService
to the providers
metadata array
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';
import { HttpModule } from '@angular/http';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { AppComponent } from './app.component';
import { AttributeComponent } from './attribute.component';
import { StructuralComponent } from './structural.component';
import { BindingComponent } from './binding.component';
import { ServicesComponent } from './services.component';
import { ObservableComponent } from './observable.component';
import { TasksService } from './tasks.service';
import { ObservableTaskService } from './observable-task.service';
import { TaskData } from './task-data';
const appRoutes: Routes = [
{
path: 'attribute-directives', component: AttributeComponent
},
{
path: 'structural-directives', component: StructuralComponent
},
{
path: 'binding', component: BindingComponent
},
{
path: 'services', component: ServicesComponent
},
{
path: 'observable', component: ObservableComponent
},
{
path: '', redirectTo: '/attribute-directives', pathMatch: 'full'
},
{
path: '**', redirectTo: '/attribute-directives'
}
];
@NgModule({
imports: [
BrowserModule,
FormsModule,
HttpModule,
RouterModule.forRoot(appRoutes),
InMemoryWebApiModule.forRoot(TaskData)
],
declarations: [
AppComponent,
AttributeComponent,
StructuralComponent,
BindingComponent,
ServicesComponent,
ObservableComponent
],
bootstrap: [AppComponent],
providers: [
TasksService,
ObservableTaskService
]
})
export class AppModule { }
Update the app.component.html
template to include a route for /observable
app.component.html
<h2>Angular Overview</h2>
<nav>
<a routerLink="/attribute-directives" routerLinkActive="active">Attibute Directives</a>
<a routerLink="/structural-directives">Structural Directives</a>
<a routerLink="/binding">Data Binding</a>
<a routerLink="/services">Services</a>
<a routerLink="/observable">Observables and Http</a>
</nav>
<router-outlet></router-outlet>
Run the application and navigate to /observable
to see Http
interactions with the InMemoryWebApiModule
Click below image to see demonstration
Although the Angular http
client API returns an Observable<Response>
, you can turn it into a Promise<Response>
.
See this StackOverflow thread regarding differences between
Promise
andObservable
.
Create the promise-task.service.ts
. Notice how the only imported RxJS item is the toPromise
operator.
promise-task.service.ts
import { Injectable } from '@angular/core';
import { Http, Response, RequestOptions, Headers } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { Task } from './task';
@Injectable()
export class PromiseTaskService {
private tasksUrl = 'app/tasks';
constructor(private http: Http) { }
getTasks(): Promise<Task[]> {
return this.http.get(this.tasksUrl)
.toPromise()
.then(this.extractData)
.catch(this.handleError);
}
addTask(task: Task): Promise<Task> {
let headers = new Headers({ 'Content-Type': 'application/json'});
let options = new RequestOptions({ headers: headers });
return this.http.post(this.tasksUrl, JSON.stringify(task), options)
.toPromise()
.then(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body.data || { };
}
private handleError(error: Response | any) {
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Promise.reject(errMsg);
}
}
Create promise.component.ts
. The only difference between this and observable.component.ts
is the use of Promise
rather than Observable
.
promise.component.ts
import { Component, OnInit } from '@angular/core';
import { PromiseTaskService } from './promise-task.service';
import { Task } from './task';
@Component({
moduleId: module.id,
selector: 'promise-demo',
templateUrl: 'promise.component.html',
styleUrls: ['app.component.css']
})
export class PromiseComponent implements OnInit {
errorMessage: string;
tasks: Task[];
task: Task = new Task();
constructor(private taskService: PromiseTaskService) { }
ngOnInit() {
this.getTasks();
}
getTasks() {
this.taskService.getTasks()
.then(
tasks => this.tasks = tasks,
error => this.errorMessage = <any>error);
}
addTask() {
if (!this.task.title) { return; }
this.taskService.addTask(this.task)
.then(
task => {
this.getTasks();
this.task = new Task();
},
error => this.errorMessage = <any>error
);
}
}
Create the promise.component.html
view template
promise.component.html
<h3>Http and Promises</h3>
<hr class="gradient">
<h4>Tasks</h4>
<ul>
<li *ngFor="let tsk of tasks">{{tsk.title}} - {{tsk.completed}}</li>
</ul>
<label>Task Title: <input [(ngModel)]="task.title" placeholder="title"/></label>
<label>Task Completed: <input type="checkbox" [(ngModel)]="task.completed" /></label>
<button (click)="addTask()">Add Task</button>
<p class="red" *ngIf="errorMessage">{{errorMessage}}</p>
In app.module.ts
, import the component and service classes, add a promise
path that points to PromiseComponent
in the appRoutes
collection, add PromiseComponent
to the declarations
metadata array, and add PromiseTaskService
to the providers
metadata array
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';
import { HttpModule } from '@angular/http';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { AppComponent } from './app.component';
import { AttributeComponent } from './attribute.component';
import { StructuralComponent } from './structural.component';
import { BindingComponent } from './binding.component';
import { ServicesComponent } from './services.component';
import { ObservableComponent } from './observable.component';
import { PromiseComponent } from './promise.component';
import { TasksService } from './tasks.service';
import { ObservableTaskService } from './observable-task.service';
import { PromiseTaskService } from './promise-task.service';
import { TaskData } from './task-data';
const appRoutes: Routes = [
{
path: 'attribute-directives', component: AttributeComponent
},
{
path: 'structural-directives', component: StructuralComponent
},
{
path: 'binding', component: BindingComponent
},
{
path: 'services', component: ServicesComponent
},
{
path: 'observable', component: ObservableComponent
},
{
path: 'promise', component: PromiseComponent
},
{
path: '', redirectTo: '/attribute-directives', pathMatch: 'full'
},
{
path: '**', redirectTo: '/attribute-directives'
}
];
@NgModule({
imports: [
BrowserModule,
FormsModule,
HttpModule,
RouterModule.forRoot(appRoutes),
InMemoryWebApiModule.forRoot(TaskData)
],
declarations: [
AppComponent,
AttributeComponent,
StructuralComponent,
BindingComponent,
ServicesComponent,
ObservableComponent,
PromiseComponent
],
bootstrap: [AppComponent],
providers: [
TasksService,
ObservableTaskService,
PromiseTaskService
]
})
export class AppModule { }
In app.component.html
, create a link for the new component
app.component.html
<h2>Angular Overview</h2>
<nav>
<a routerLink="/attribute-directives" routerLinkActive="active">Attibute Directives</a>
<a routerLink="/structural-directives">Structural Directives</a>
<a routerLink="/binding">Data Binding</a>
<a routerLink="/services">Services</a>
<a routerLink="/observable">Observables and Http</a>
<a routerLink="/promise">Promises and Http</a>
</nav>
<router-outlet></router-outlet>
Run the application, and you'll see that although the implementation of the services behind both the promise
and observable
components is different, they are both still able to send and retrieve data
Click below image to see demonstration
If the API resource that you are trying to access is on a remote server with a different origin (origin is the combination of URI scheme, hostname, and port number), then XMLHttpRequests
using the Http
service are not possible.
Modern browsers do allow
XHR
requests to servers from a different origin if the server supports the CORS protocol. If the server requires user credentials, you'll enable them in the request headers.
Some servers do not support CORS, but do support an older, read-only alternative called JSONP.
See this StackOverflow thread for a detailed explanation.
Create wikipedia.service.ts
. The URLSearchParams
class allows you to condition the query string in the way that the public API expects to receive data
wikipedia.service.ts
import { Injectable } from '@angular/core';
import { Jsonp, URLSearchParams } from '@angular/http';
import 'rxjs/add/operator/map';
@Injectable()
export class WikipediaService {
constructor(private jsonp: Jsonp) { }
search (term: string) {
let wikiUrl = 'http://en.wikipedia.org/w/api.php';
let params = new URLSearchParams();
params.set('search', term);
params.set('action', 'opensearch');
params.set('format', 'json');
params.set('callback', 'JSONP_CALLBACK');
return this.jsonp
.get(wikiUrl, { search: params })
.map(response => <string[]>response.json()[1]);
}
}
Create wiki.component.ts
. To prevent the search()
function from being called everytime the input value changes, the items
observable is setup as a Subject
with debounceTime
, distinctUntilChanged
and switchMap
configured to delay the actual search
service function from executing until the value has stopped changing for at least .3 seconds.
Subject
- Represents an object that is both an observable sequence as well as an observer. Each notification is broadcasted to all subscribed observers. This class inherits both from the Rx.Observable
and Rx.Observer
classes.
debounceTime
- Discard emitted values that take less than the specified time between output.
distinctUntilChanged
- Only emit when the current value is different than the last.
switchMap
- Map to observable, complete previous inner observable, emit values.
Note that the
WikipediaService
is added to theproviders
array in the component rather than the module. If the service is only used by a single component, you don't need to expose it to the entire module. You can instead merely expose it to the component itself.
wiki.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/switchMap';
import { Subject } from 'rxjs/Subject';
import { WikipediaService } from './wikipedia.service';
@Component({
moduleId: module.id,
selector: 'wiki-demo',
templateUrl: 'wiki.component.html',
styleUrls: ['app.component.css'],
providers: [WikipediaService]
})
export class WikiComponent implements OnInit {
items: Observable<string[]>;
private searchTermStream = new Subject<string>();
constructor(private wikipediaService: WikipediaService) { }
search(term: string) { this.searchTermStream.next(term); }
ngOnInit() {
this.items = this.searchTermStream
.debounceTime(300)
.distinctUntilChanged()
.switchMap((term: string) => this.wikipediaService.search(term));
}
}
Create wiki.component.html
template view for the component
wiki.component.html
<h3>Jsonp</h3>
<hr class="gradient">
<p>
Search Wikipedia when typing stops
</p>
<input #term (keyup)="search(term.value)" />
<ul>
<li *ngFor="let item of items | async">{{item}}</li>
</ul>
In app.module.ts
, import the WikiComponent
class, add a jsonp
route that points to WikiComponent
in the appRoutes
collection, and add WikiComponent
to the declarations
metadata array
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';
import { HttpModule, JsonpModule } from '@angular/http';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { AppComponent } from './app.component';
import { AttributeComponent } from './attribute.component';
import { StructuralComponent } from './structural.component';
import { BindingComponent } from './binding.component';
import { ServicesComponent } from './services.component';
import { ObservableComponent } from './observable.component';
import { PromiseComponent } from './promise.component';
import { WikiComponent } from './wiki.component';
import { TasksService } from './tasks.service';
import { ObservableTaskService } from './observable-task.service';
import { PromiseTaskService } from './promise-task.service';
import { TaskData } from './task-data';
const appRoutes: Routes = [
{
path: 'attribute-directives', component: AttributeComponent
},
{
path: 'structural-directives', component: StructuralComponent
},
{
path: 'binding', component: BindingComponent
},
{
path: 'services', component: ServicesComponent
},
{
path: 'observable', component: ObservableComponent
},
{
path: 'promise', component: PromiseComponent
},
{
path: 'jsonp', component: WikiComponent
},
{
path: '', redirectTo: '/attribute-directives', pathMatch: 'full'
},
{
path: '**', redirectTo: '/attribute-directives'
}
];
@NgModule({
imports: [
BrowserModule,
FormsModule,
HttpModule,
JsonpModule,
RouterModule.forRoot(appRoutes),
InMemoryWebApiModule.forRoot(TaskData)
],
declarations: [
AppComponent,
AttributeComponent,
StructuralComponent,
BindingComponent,
ServicesComponent,
ObservableComponent,
PromiseComponent,
WikiComponent
],
bootstrap: [AppComponent],
providers: [
TasksService,
ObservableTaskService,
PromiseTaskService
]
})
export class AppModule { }
In app.component.html
, add a link to the /jsonp
route
app.component.html
<h2>Angular Overview</h2>
<nav>
<a routerLink="/attribute-directives" routerLinkActive="active">Attibute Directives</a>
<a routerLink="/structural-directives">Structural Directives</a>
<a routerLink="/binding">Data Binding</a>
<a routerLink="/services">Services</a>
<a routerLink="/observable">Observables and Http</a>
<a routerLink="/promise">Promises and Http</a>
<a routerLink="/jsonp">Jsonp</a>
</nav>
<router-outlet></router-outlet>
Click below image to see demonstration
See Component Interaction for an in depth look at communicating between components
Before jumping into this sample, the app
folder is beginning to get a bit crowded. So, I've taken the time to move components for each section into their own folders and update imports and URLs where appropriate. The file structure for the app folder is now as follows:
- app
- attribute
attribute.component.html
attribute.component.ts
- binding
binding.component.html
binding.component.ts
- observable
observable-task.service.ts
observable.component.html
observable.component.ts
- promise
promise-task.service.ts
promise.component.html
promise.component.ts
- services
services.component.html
services.component.ts
tasks.service.ts
- structural
structural.component.html
structural.component.ts
- wiki
wiki.component.html
wiki.component.ts
wikipedia.service.ts
app.component.ts
app.component.html
app.component.css
app.module.ts
task-data.ts
task.ts
- attribute
The updates that need to be made are as follows:
- All of the
.component.ts
and.service.ts
files contained in the new subfolders that import a class stored in theapp
folder need to change the reference fromfrom ./{class]
tofrom ../{class}
- All of the
.component.ts
files contained in the new subfolders that referenceapp.component.css
in thestyleUrls
array need to change the reference to../app.component.css
import
statements inapp.module.ts
that reference a service or component stored in a new subfolder need to change fromfrom ./*.component
and./*.service
to./{folder}/*.component
and./{folder}/*.service
- The
clean
task ingulpfile.js
has been updated to reflect the new directory structure. It is now as follows:
gulpfile.js
var
gulp = require('gulp'),
clean = require('gulp-clean');
gulp.task('clean', function () {
return gulp.src(['src/app/**/*.js', 'src/app/**/*.map', 'src/main.js', 'src/main.js.map'])
.pipe(clean({force: true}));
});
The following example will demonstrate the following capabilities:
- Using components within a component
- Passing data from parent to child with input binding
- Parent listens to child event
- Parent and child communicate via a service
Create a car.ts
file in the app
folder
car.ts
export class Car {
public make: string;
public model: string;
public description: string;
public url: string;
public source: string;
}
Create a comment.ts
file in the app
folder
comment.ts
export class Comment {
public name: string;
public comment: string;
}
Update the task-data.ts
file to provide an API route to cars
task-data.ts
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { Task } from './task';
import { Car } from './car';
export class TaskData implements InMemoryDbService {
createDb() {
let tasks: Array<Task> = [
{
title: 'First Task',
completed: false,
created_at: '',
updated_at: ''
},
{
title: 'Second Task',
completed: false,
created_at: '',
updated_at: ''
},
{
title: 'Third Task',
completed: false,
created_at: '',
updated_at: ''
},
{
title: 'Fourth Task',
completed: false,
created_at: '',
updated_at: ''
}
];
let cars: Array<Car> = [
{
make: 'McLaren',
model: 'P1',
description: `
The McLaren P1 is a British limited-production plug-in hybrid sports car produced by McLaren.
The concept car was capable of reaching speeds of 218 mph (351 km/h) with the limiter on.
The P1 features a 3.8-litre twin-turbo V8 petrol engine. The twin turbos boost the petrol engine
at 1.4 bar to deliver 727 bhp (737 PS; 542 kW) and 531 lb·ft (720 N·m) of torque at 7,500 rpm, combined
with an in-house developed electric motor producing 177 bhp (179 PS; 132 kW) and 192 lb·ft (260 N·m).
With both engine and the electric motor, the P1 has a total power and torque output of 904 bhp (917 PS; 674 kW)
and 723 lb·ft (980 N·m) of torque respectively.
`,
// tslint:disable-next-line:max-line-length
url: 'http://www.topgear.com/sites/default/files/styles/16x9_1280w/public/cars-car/image/2015/02/buyers_guide_-_mclaren_p1_2014_-_front_quarter.jpg?itok=gSl3mI9a',
source: 'https://en.wikipedia.org/wiki/McLaren_P1'
},
{
make: 'Porsche',
model: '918 Spyder',
description: `
The Porsche 918 Spyder is a mid-engined plug-in hybrid sports car by Porsche.
The Spyder is powered by a naturally aspirated 4.6-litre V8 engine, developing 608 metric horsepower (447 kW),
with two electric motors delivering an additional 279 metric horsepower (205 kW) for a combined output of 887
metric horsepower (652 kW). The 918 Spyder's 6.8 kWh lithium-ion battery pack delivers an all-electric range
of 19 km (12 mi) under EPA's five-cycle tests. The car has a top speed of around 340 km/h (210 mph).
`,
url: 'http://www.topgear.com/sites/default/files/styles/16x9_1280w/public/news/image/2015/04/Large%20Image_9581.jpg?itok=aQqGWV34',
source: 'https://en.wikipedia.org/wiki/Porsche_918_Spyder'
},
{
make: 'Lamborghini',
model: 'Veneno',
description: `
The Lamborghini Veneno is a limited production supercar based on the Lamborghini Aventador and was built to celebrate
Lamborghini’s 50th anniversary. When introduced in 2013 at a price of US$4,500,000, it was the most expensive production
car in the world. he prototype, Car Zero, is finished in grey and includes an Italian flag vinyl on both sides of the car.
The engine is a development of the Aventador's 6.5 L V12 and produces 750 PS (552 kW; 740 bhp).
`,
url: 'http://www.topgear.com/sites/default/files/styles/16x9_1280w/public/news/image/2015/04/Large%20Image_9665.jpg?itok=Bqxc5GrG',
source: 'https://en.wikipedia.org/wiki/Lamborghini_Aventador#Veneno'
},
{
make: 'Ferrari',
model: 'LaFerrari',
description: `
LaFerrari is the first mild hybrid from Ferrari, providing the highest power output of any Ferrari whilst decreasing fuel
consumption by 40 percent. LaFerrari's internal combustion engine is a mid-rear mounted Ferrari F140 65° V12 with a 6.3-litre
(6262 cc) capacity producing 800 PS (588 kW, 789 bhp) @ 9000 rpm and 700 N·m (520 lbf·ft) of torque @ 6,750 rpm, supplemented
by a 163 PS (120 kW; 161 bhp) KERS unit (called HY-KERS), which will provide short bursts of extra power.[25] The KERS system
adds extra power to the combustion engine's output level for a total of 963 PS (708 kW; 950 bhp) and a combined torque of
900 N·m (664 lb·ft).
`,
// tslint:disable-next-line:max-line-length
url: 'http://www.topgear.com/sites/default/files/styles/16x9_1280w/public/cars-road-test/image/2015/04/Large%20Image_138.jpg?itok=mGvfDq3x',
source: 'https://en.wikipedia.org/wiki/LaFerrari'
}
];
return {tasks, cars};
}
}
In the app
folder, create a folder named car
, and create a car.service.ts
file
car.service.ts
import { Injectable } from '@angular/core';
import { Http, Response, RequestOptions, Headers } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import { Car } from '../car';
import { Comment } from '../comment';
@Injectable()
export class CarService {
private carsUrl = 'app/cars';
comments = Array<Comment>();
constructor(private http: Http) { }
getCars(): Observable<Car[]> {
return this.http.get(this.carsUrl)
.map(this.extractData)
.catch(this.handleError);
}
addCar(car: Car): Observable<Car> {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this.carsUrl, JSON.stringify(car), options)
.map(this.extractData)
.catch(this.handleError);
}
addComment(comment: Comment) {
this.comments.push(comment);
}
private extractData(res: Response) {
let body = res.json();
return body.data || { };
}
private handleError(error: Response | any) {
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
}
For some simple styling for these components, add a car.component.css
file
car.component.css
.car-card {
width: 800px;
background-color: #eee;
padding-top: 5px;
background-color: #ddd;
}
.car-card > p {
padding: 4px;
margin-left: 16px;
margin-right: 16px;
margin-top: 0px;
color: #333;
}
.car-card > img {
width: 768px;
margin-left: 16px;
margin-bottom: 0px;
padding-bottom: 0px;
}
.car-card > h4 {
margin-left: 15px;
}
.car-card > h4 > a {
background-color: #1e90ff;
color: #eee;
}
.car-card > h4 > a:hover {
background-color: #166cbf;
}
The first component that will be created is a CarDetailsComponent
. This component will be responsible for rendering the details of the Car
class. It contains an @Input()
binding to a car
property of type Car
. This component's car
property will be received by the parent component by iterating over an array of cars using NgFor
car-details.component.ts
import { Component, Input } from '@angular/core';
import { Car } from '../car';
@Component({
moduleId: module.id,
selector: 'car-details',
templateUrl: 'car-details.component.html',
styleUrls: ['../app.component.css', 'car.component.css']
})
export class CarDetailsComponent {
@Input() car: Car;
}
Create a car-details.component.html
template view for the component. Notice how the href
and src
properties are set using attribute binding
car-details.component.html
<div class="car-card">
<h4><a [href]="car.source" target="_blank">{{car.make}} {{car.model}}</a></h4>
<img [src]="car.url" />
<p>
{{car.description}}
</p>
</div>
Create a car-creator.component.ts
file. This component defines an EventEmitter<Car>
that the parent component will subscribe to. When the addCar()
function is executed, the parent component will receive the car
property from this component and execute the logic linked to the event
car-creator.component.ts
import { Component, EventEmitter, Output } from '@angular/core';
import { Car } from '../car';
@Component({
moduleId: module.id,
selector: 'car-creator',
templateUrl: 'car-creator.component.html',
styleUrls: ['../app.component.css', 'car.component.css']
})
export class CarCreatorComponent {
car: Car = new Car();
@Output() onAddCar = new EventEmitter<Car>();
addCar() {
if (
!this.car.make ||
!this.car.model ||
!this.car.description ||
!this.car.source ||
!this.car.url
) { return; }
this.onAddCar.emit(this.car);
this.car = new Car();
}
}
Create the car-creator.component.html
template view for the component
car-creator.component.html
<h4>Add Car</h4>
<div>
<label>Make: <input [(ngModel)]="car.make" placeholder="make"></label>
<label>Model: <input [(ngModel)]="car.model" placeholder="model"></label>
<label>Source: <input [(ngModel)]="car.source" placeholder="www.car.com"></label>
<label>Image URL: <input [(ngModel)]="car.url" placeholder="www.car.com/image.png"></label>
</div>
<div>
<p>Description</p>
<textarea rows="5" cols="100" [(ngModel)]="car.description"></textarea>
</div>
<button (click)="addCar()">Add Car</button>
Create a car-comments.component.ts
file. The component has a comment
property of type Comment
that is bound to the input elements in its view template. When the addComment()
function executes, the comment
is passed into the carService.addComment()
function, which in turn pushes the comment into the service's comments
array. The parent component displays the comments from the service using an NgFor
directive
car-comments.components.ts
import { Component } from '@angular/core';
import { CarService } from './car.service';
import { Comment } from '../comment';
@Component({
moduleId: module.id,
selector: 'car-comments',
templateUrl: 'car-comments.component.html',
styleUrls: ['../app.component.css', 'car.component.css']
})
export class CarCommentsComponent {
comment: Comment = new Comment();
constructor(private carService: CarService) { }
addComment() {
if (!this.comment.name || !this.comment.comment) {
return;
}
this.carService.addComment(this.comment);
this.comment = new Comment();
}
}
Create the car-comments.component.html
view template
car-comments.component.html
<h4>Add Comment</h4>
<div>
<p>Name</p>
<input [(ngModel)]="comment.name" />
</div>
<div>
<p>Comment</p>
<textarea rows="5" cols="100" [(ngModel)]="comment.comment"></textarea>
</div>
<button (click)="addComment()">Add Comment</button>
Now that the child elements have been created, the main component can be created. Add a cars.component.ts
file.
When the component initializes, the cars
array is set using the getCars()
function. This in turn executes an API call through the carService.getCars()
function.
Also when initialized, the comments
array is set to the carService.comments
array. Anytime the carService.comments
array is updated by the CarCommentsComponent
, the comments
array will reflect the update.
Finally, the addCar()
function receives is executed whenever the CarCreatorComponent
emits its onAddCar
event. The car
property is received from the event, and passed into the carService.addCar()
function
cars.component.ts
import { Component, OnInit } from '@angular/core';
import { CarService } from './car.service';
import { CarDetailsComponent } from './car-details.component';
import { Car } from '../car';
import { Comment } from '../comment';
@Component({
moduleId: module.id,
selector: 'cars',
templateUrl: 'cars.component.html',
styleUrls: ['../app.component.css', 'car.component.css'],
providers: [CarService],
entryComponents: [CarDetailsComponent]
})
export class CarsComponent implements OnInit {
errorMessage: string;
cars: Car[];
car: Car = new Car();
comments: Array<Comment>;
constructor(private carService: CarService) { }
ngOnInit() {
this.getCars();
this.comments = this.carService.comments;
}
getCars() {
this.carService.getCars()
.subscribe(
cars => {
this.cars = cars;
},
error => this.errorMessage = <any>error
);
}
addCar(car: Car) {
this.carService.addCar(car)
.subscribe(
() => {
this.getCars();
},
error => this.errorMessage = <any>error
);
}
}
Create the cars.component.html
view template for the component. Notice how the onAddCar
event in the <car-creator>
component is set to the main component's addCar()
function. Also, the car
property for the <car-details>
component is bound to the value of the current car iteration of the NgFor
loop
cars.component.html
<div>
<car-creator (onAddCar)="addCar($event)"></car-creator>
<h4>Cars</h4>
<car-details *ngFor="let c of cars" [car]="c"></car-details>
<car-comments></car-comments>
<div *ngIf="comments.length > 0">
<h4>Comments</h4>
<div *ngFor="let cmnt of comments">
<p><strong>{{cmnt.name}}</strong></p>
<p>{{cmnt.comment}}</p>
</div>
</div>
</div>
In app.module.ts
, import the components, create a complex-component
route in the appRoutes
array, and add the components to the declarations
metadata array
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';
import { HttpModule, JsonpModule } from '@angular/http';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { AppComponent } from './app.component';
import { AttributeComponent } from './attribute/attribute.component';
import { StructuralComponent } from './structural/structural.component';
import { BindingComponent } from './binding/binding.component';
import { ServicesComponent } from './services/services.component';
import { ObservableComponent } from './observable/observable.component';
import { PromiseComponent } from './promise/promise.component';
import { CarsComponent } from './car/cars.component';
import { CarDetailsComponent } from './car/car-details.component';
import { CarCreatorComponent } from './car/car-creator.component';
import { CarCommentsComponent } from './car/car-comments.component';
import { WikiComponent } from './wiki/wiki.component';
import { TasksService } from './services/tasks.service';
import { ObservableTaskService } from './observable/observable-task.service';
import { PromiseTaskService } from './promise/promise-task.service';
import { TaskData } from './task-data';
const appRoutes: Routes = [
{
path: 'attribute-directives', component: AttributeComponent
},
{
path: 'structural-directives', component: StructuralComponent
},
{
path: 'binding', component: BindingComponent
},
{
path: 'services', component: ServicesComponent
},
{
path: 'observable', component: ObservableComponent
},
{
path: 'promise', component: PromiseComponent
},
{
path: 'jsonp', component: WikiComponent
},
{
path: 'complex-components', component: CarsComponent
},
{
path: '', redirectTo: '/attribute-directives', pathMatch: 'full'
},
{
path: '**', redirectTo: '/attribute-directives'
}
];
@NgModule({
imports: [
BrowserModule,
FormsModule,
HttpModule,
JsonpModule,
RouterModule.forRoot(appRoutes),
InMemoryWebApiModule.forRoot(TaskData)
],
declarations: [
AppComponent,
AttributeComponent,
StructuralComponent,
BindingComponent,
ServicesComponent,
ObservableComponent,
PromiseComponent,
WikiComponent,
CarsComponent,
CarDetailsComponent,
CarCreatorComponent,
CarCommentsComponent
],
bootstrap: [AppComponent],
providers: [
TasksService,
ObservableTaskService,
PromiseTaskService
]
})
export class AppModule { }
Finally, add a link to /complex-components
in app.component.html
app.component.html
<h2>Angular Overview</h2>
<nav>
<a routerLink="/attribute-directives" routerLinkActive="active">Attibute Directives</a>
<a routerLink="/structural-directives">Structural Directives</a>
<a routerLink="/binding">Data Binding</a>
<a routerLink="/services">Services</a>
<a routerLink="/observable">Observables and Http</a>
<a routerLink="/promise">Promises and Http</a>
<a routerLink="/jsonp">Jsonp</a>
<a routerLink="/complex-components">Complex Components</a>
</nav>
<router-outlet></router-outlet>
That was a lot of work! Run the application and see how the each component functions in the context of the parent component.