Gion Kunz
Let's create together.
by Gion Kunz
Complete local setup and create first project using Angular CLI.
# Install Angular CLI npm install -g @angular/cli # Create new project ng new my-first-app cd my-first-app # Create components with generator ng generate component my-component # Start dev server ng serve # Create production build ng build
import {Component} from '@angular/core';
@Component({
selector: 'my-component',
template: '<p>Hello World!</p>'
})
export class MyComponent {}
Module A
Module B
import
1
2
3
4
5
6
7
8
9
Components
Components
runtime
1
2
3
4
5
6
7
8
9
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
@NgModule({
imports: [BrowserModule],
declarations: [Comp1, Comp2, Comp3, ...],
exports: [Comp1, Comp2, Comp3, ...],
bootstrap: [Component]
})
export class AppModule {}
import {platformBrowserDynamic}
from '@angular/platform-browser-dynamic';
import {AppModule} from './app-module';
platformBrowserDynamic().bootstrapModule(AppModule);
@Component({
selector: 'app'
template: '<div>{{message}}</div>'
})
class AppComponent {
message: string = 'Hello World!';
}
@Component({
selector: 'counter'
template: '<input type="text" [value]="num">'
})
class AppComponent {
num: number = 0;
constructor() {
setInterval(() => this.num++, 1000);
}
}
<p [title]="title"></p>
<p [attr.role]="role"></p>
<p [class.is-active]="isActive()"></p>
<p [style.display]="!isActive() ? 'none' : null"></p>
Regular DOM property
Attribute binding (setAttribute)
CSS class binding (classList)
CSS style binding
@Component({
selector: 'app'
template: `
<button (click)="onClick()">Click me</button>
`
})
class AppComponent {
onClick() {
alert('Cool!');
}
}
<input (input)="handleInput($event.target.value)">
@Component({
selector: 'app'
template: `
<input (keydown.enter)="submit($event.target.value)">
`
})
class AppComponent {
submit(value: string) {
console.log(value);
}
}
<ul>
<li *ngFor="let item of items">
{{item}}
</li>
</ul>
<p>Sheeps: {{sheepCount}}</p>
<p *ngIf="sheepCount > 1000">
You should be asleep now!
</p>
Create simple click counter component.
Create a new counter component with Angular CLI
Include a count state within the counter component
Create the necessary template to render the current count
Provide a button with an event binding to increase the count state in the component on every click by 1
Create a clickable component which will increase a counter when clicked.
import {Input} from '@angular/core';
@Component({
selector: 'app-message',
template: '{{message}}'
})
export class MessageComponent {
@Input() message: string;
}
@Component({
selector: 'app-root',
template: '<app-message [message]="message"></app-message>'
})
export class AppComponent {
message: string = 'Hello World!';
}
import {Output, EventEmitter} from '@angular/core';
@Component({
selector: 'timer',
template: '<button (click)="startTimer()">Start</button>'
})
export class TimerComponent {
@Output() timeout = new EventEmitter();
startTimer() {
setTimeout(
() => this.timeout.emit('I timed out!'),
5000
);
}
}
@Component({
selector: 'app',
template: '<timer (timeout)="onTimeout($event)"></timer>'
})
export class AppComponent {
onTimeout(message) {
console.log(message);
}
}
A small UI Composition Exercise.
git clone https://github.com/syncrea/angular-workshop.git
cd angular-workshop
git checkout lab-4 npm install
Create a TodoItem component by extracting the relevant portion from TodoList
Render the component tree correctly TodoList > TodoItem
Pass input correctly and delegate output as required
Refactor an existing ToDo application which consist of only 1 TodoList component into 2 separate components, TodoList and TodoItem.
@Component({
selector: 'user-profile',
template: `
<p class="name">{{name}}</p>
`
})
class UserProfile {
constructor() {
fetch('/api/users/1')
.then((response) => response.json())
.then((user) => this.name = user.name);
}
}
@Component({
selector: 'user-profile-container',
template: `
<user-profile [name]="user.name">
</user-profile>
`
})
class UserProfileContainer {
constructor() {
fetch('/api/users/1')
.then((response) => response.json())
.then((user) => this.user = user);
}
}
@Component({
selector: 'user-profile',
template: `
<p class="name">{{name}}</p>
`
})
class UserProfile {
@Input() name;
}
Container Component
Data
Data
Action
Action
Let's introduce a container component within our ToDo app.
git stash save -u my-lab-4 git checkout lab-5
Create a TodoListContainer component
Move all data and data manipulation into the newly created container component
Render the component tree correctly TodoListContainer > TodoList > TodoItem
Pass input correctly and delegate output as required
Let's modify our ToDo list application to include a container component which holds and manipulates our Data.
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
import {MainComponent} from './main.component.ts';
import {ChildlComponent} from './child1/child1.component.ts';
import {Child2Component} from './child2/child2.component.ts';
@NgModule({
imports: [
BrowserModule,
RouterModule.forRoot([
{path: 'childl', component: ChildlComponent},
{path: 'child2', component: Child2Component}
])
],
declarations: [MainComponent, ChildlComponent, Child2Component],
bootstrap: [MainComponent]
})
export class MainModule {}
RouterModule.forRoot([{
path: 'child1',
component: Child1Component
},{
path: '',
pathMatch: 'full',
redirectTo: '/child1'
},{
path: '**',
component: PageNotFoundComponent
}];
<h1>Website</h1>
<main class="content">
<router-outlet></router-outlet>
</main>
<h1>Welcome!</h1>
<nav>
<a routerLink="/child1"
routerLinkActive="active">Child1</a>
<a routerLink="/child2"
routerLinkActive="active">Child2</a>
</nav>
<router-outlet></router-outlet>
RouterModule.forRoot([{
path: 'child/:id',
component: ChildComponent
}])
import {ActivatedRoute} from '@angular/router';
@Component({
selector: 'ngc-child',
template: 'Child:<p>Param: {{params.id}}</p>'
})
export class ChildComponent {
constructor(route: ActivatedRoute) {
this.params = route.snapshot.params;
}
}
@Component({
selector: 'ngc-child',
template: `
Child:
<p>Param: {{params.id}}</p>`
})
export class ChildComponent {
params: any;
constructor(route: ActivatedRoute) {
route.params.subscribe((params) => {
this.params = params;
});
}
}
@Component({
selector: 'app-details-container',
template: '<app-details [item]="item"></app-details>'
})
export class DetailsContainerComponent {
item: Item;
constructor(route: ActivatedRoute, itemService: ItemService) {
route.params.subscribe(
(params) => this.item = itemService.getItemById(params.id)
);
}
}
Using the router to create a details view of ToDo items.
git stash save -u my-lab-5 git checkout lab-6
Replace the static reference to <app-todo-list-container> in the app.component.html template with a <router-outlet> element to enable the router
Include a second route configuration in your main module which includes a parameter :no and routes to the TodoDetailsContainer component
Implement the showDetails method on the TodoListContainer component and navigate to the newly created details route
We want to use the Angular Router to create a navigable detail view in our ToDo application.
Click Event
@Component({
selector: 'ngc-list-item',
template: `
<div class="content">{{item}}</div>
<button (click)="remove()" class="remove">Remove</button>`
})
export class ListItemComponent {
@Input() item: string;
@Output() onRemove = new EventEmitter<never>();
remove() {
this.onRemove.emit();
}
}
@Component({
selector: 'ngc-list-item',
template: `
<div class="content">{{item}}</div>
<button (click)="remove()" class="remove">Remove</button>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListItemComponent {
@Input() item: string;
@Output() onRemove = new EventEmitter<never>();
remove() {
this.onRemove.emit();
}
}
Click Event
const person = {name: 'Peter'};
person.name = 'Peach';
const numbers = [1, 2, 3, 4];
numbers[1] = 20;
person = {name: 'Peach'};
numbers = numbers.slice();
numbers[1] = 20;
let numbers = [1, 2, 3, 4];
// Mapping to new array
numbers = numbers.map(number => number * 10);
// Filtering an array
numbers = numbers.filter(number => number % 2 === 0);
let numbers = [1, 2, 3, 4];
// Appending
numbers = [...numbers, 5];
// Prepending
numbers = [0, ...numbers];
// Convert mutable op to immutable
numbers = [...numbers].sort();
// Replace one or many properties
let person = {name: 'Peter', age: 30};
person = {...person, name: 'Peach'};
// Deep immutable operations
let course = {
title: 'Angular Workshop',
person: {name: 'Peter', age: 30}
};
course = {
...course,
person: {...course.person, name: 'Peach'}
};
Work with Pure Components and optimize change detection.
git stash save -u my-lab-6 git checkout lab-7
Change all components to use ChangeDetectionStrategy.OnPush
Switch all data manipulation within the TodoService to use immutable operations
We want to switch our ToDo List application to use Pure component using “OnPush” change detection and immutable data.
import {
TestBed,
ComponentFixture} from '@angular/core/testing';
import {MainComponent} from './main.component';
import {CounterService} from './counter.service';
describe('MainComponent', () => {
let fixture: ComponentFixture<MainComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MainComponent],
providers: [CounterService]
});
fixture = TestBed.createComponent(MainComponent);
});
});
Use TestBed to create dynamic test module
Include Components and dependencies within the test module
class ComponentFixture<T> {
debugElement: DebugElement
componentInstance: T
nativeElement: any
elementRef: ElementRef
changeDetectorRef: ChangeDetectorRef
componentRef: ComponentRef<T>
detectChanges(): void
isStable(): boolean
whenStable(): Promise<any>
destroy(): void
}
Wrapper around instantiated component
Access debug element, native host element and change detector
class DebugElement {
nativeElement: any
query(predicate: Predicate<DebugElement>): DebugElement
queryAll(predicate: Predicate<DebugElement>): DebugElement[]
children: DebugElement[]
triggerEventHandler(eventName: string, eventObj: any)
}
Wrapper around component host element
Allows to travers through DebugElement tree (DOM like)
Can be used to tigger events
import {DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser';
const countElement = fixture.debugElement
.query(By.css('.count'))
.nativeElement;
console.log(countElement.textContent);
import {DebugElement} from '@angular/core';
const buttonDebugElement: DebugElement =
fixture.debugElement.query(By.css('button'));
buttonDebugElement.triggerEventHandler('click', null);
import {CounterService} from './counter.service';
export class CounterServiceMock() {
increment() {}
}
TestBed.configureTestingModule({
declarations: [MainComponent],
providers: [{
provide: CounterService,
useClass: CounterServiceMock
}]
});
Testing our ToDo App.
git stash save -u my-lab-7 git checkout lab-8
Create a TestBed setup where you include the TodoListContainer, TodoList and TodoItem components into the declarations
Provide your own mock TodoService which will be used by the TodoListContainer
Use the ComponentFixture debug element to query for a TodoItem and tigger a click event on the checkbox element
Assert on the state within your mock TodoService
We want to include a test for our Todo Application where we verify that when we tick off a Todo Item, it will be updated within our state.
Item
Error
Completed
import {from} from 'rxjs';
import {filter, map} from 'rxjs/operators';
from([1, 2, 3])
.pipe(
map(num => num * num),
filter(num => num > 3)
)
.subscribe(item => console.log(item));
Items are emitted through a stream of items
Each item flows through the stream individually
The subscription will cause the stream start flowing
import {from, timer} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';
from([1, 2, 3])
.pipe(
switchMap(num => timer(1000).pipe(map(() => num * num)))
)
.subscribe(item => console.log(item));
Route Parameters
HttpClient Service
Reactive Forms
Component Output (EventEmitter)
@Component({
selector: 'my-component',
template: '{{data}}'
})
export class MyComponent {
asyncSubscription: Subscription;
data: number;
constructor() {
this.asyncSubscription = of(1)
.pipe(delay(2000))
.subscribe(data => this.data = data);
}
ngOnDestroy() {
this.asyncSubscription.unsubscribe();
}
}
Subscriptions should be cleaned up using unsubscribe()
Within components, the ngOnDestroy lifecycle hook is a good spot for this task
Feels cumbersome and inconvenient
@Component({
selector: 'my-component',
template: '{{asyncData | async}}'
})
export class MyComponent {
asyncData: Observable<number>;
constructor() {
this.asyncData = of(1)
.pipe(delay(2000));
}
}
The async Pipe is creating the subscription in the view
Always the latest item in the stream will be rendered
When the component view is destroyed, the subscriptions is automatically unsubscribed
@Component({
selector: 'my-component',
template: `
<ul>
<li *ngFor="let item of items | asnyc">{{item}}</li>
</ul>`
})
export class MyComponent {
items: Observable<Item[]>;
constructor(http: HttpClient) {
this.items = this.http
.get<Item[]>('/api/items');
}
}
class HttpClient {
delete<T>(url: string, options?: {...}): Observable<T>
get<T>(url: string, options?: {...}): Observable<T>
head<T>(url: string, options?: {...}): Observable<T>
jsonp<T>(url: string, callbackParam?: string): Observable<T>
options<T>(url: string, options?: {...}): Observable<any>
patch<T>(url: string, body: any | null, options?: {...}): Observable<T>
post<T>(url: string, body: any | null, options?: {...}): Observable<T>
put<T>(url: string, body: any | null, options?: {...}): Observable<T>
}
Use HttpClient service to load data asynchronously.
git stash save -u my-lab-8 git checkout lab-9
Inject HttpClient into your TodoService
Create the necessary calls within TodoService to the backend
GET /api/todos
PUT /api/todos
GET /api/todos/:id
POST /api/todos/:id
Let’s connect our Todo Application to a fake RESTful backend using the HttpClient service.
By Gion Kunz
Slides for the syncrea Angular Workshop