Angular 9 Fundamentals
Angular 9 Fundamentals
Angular 9 Fundamentals
FUNDAMENTALS!
Agenda
Hello Angular
Component Fundamentals
Template Driven Forms
Angular Services
Server Communication
Component Driven Architecture
Angular Routing
Unit Testing Fundamentals
Getting Started
https://github.com/onehungrymind/angular9-fundamentals-workshop
The Big Picture
aka How to impress
your Angular friends
at a dinner party
Why Angular?
Angular follows
common and familiar
enterprise patterns and
conventions
Angular is a “batteries
included” framework
Angular ships with
tooling to accelerate
the developer workflow
Angular has a rich and
vibrant ecosystem
Angular has a proven
track record
The Angular 1.x Big Picture
module
config
routes
service directive
The Simplified Angular Big Picture
module
routes
component
service
The Angular Big Picture
module
routes
component
service
ES6 Modules
@Component({
selector: 'app-items',
templateUrl: './items.component.html',
styleUrls: ['./items.component.css']
})
export class ItemsComponent implements OnInit { }
ES6 Modules
import { Component, OnInit } from '@angular/core';
import { ItemsService, Item } from '../shared';
@Component({
selector: 'app-items',
templateUrl: './items.component.html',
styleUrls: ['./items.component.css']
})
export class ItemsComponent implements OnInit { }
ES6 Modules
@NgModule
@NgModule
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from '@angular/core';
import { environment } from './environments/environment';
import { AppModule } from './app/';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);
Bootstrapping
The Angular Big Picture
module
routes
components
services
Routing
• Routes are defined in a route definition table that in its simplest form
contains a path and component reference
• Components are loaded into the router-outlet directive
• We can navigate to routes using the routerLink directive
• The router uses history.pushState which means we need to set a
base-ref tag to our index.html file
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ItemsComponent } from './items/items.component';
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: []
})
export class AppRoutingModule {
}
Routing
Components
module
routes
components
services
Components
module
routes
component
template class
services
Component Classes
constructor(
private itemsService: ItemsService
) {}
ngOnInit() {
this.getItems();
}
getItems() {
this.itemsService.loadItems()
.subscribe((items: Item[]) => this.items = items);
}
}
Components
Templates
External Template
@Component({
selector: 'app-items-list',
template: `
<div *ngFor="let item of items" (click)="selected.emit(item)">
<div>
<div><h2>{{item.name}}</h2></div>
<div>{{item.description}}</div>
</div>
</div>
`,
styleUrls: ['./items-list.component.css']
})
export class ItemsListComponent {
@Input() items: Item[];
@Output() selected = new EventEmitter();
@Output() deleted = new EventEmitter();
}
Inline Templates
Components
module
routes
component
template class
services
Metadata
component
<template> @metadata() class { }
Metadata
Metadata
@Component({
selector: 'app-items-list',
templateUrl: './items-list.component.html',
styleUrls: ['./items-list.component.css']
})
export class ItemsListComponent {
@Input() items: Item[];
@Output() selected = new EventEmitter();
@Output() deleted = new EventEmitter();
}
Inline Metadata
Data Binding
component
template {{value}} class
[property] = "value"
(event) = “handler()”
[(ngModel)] = "property"
Data Binding
<template>
(event binding) @metadata [property binding]
class { }
<h1>{{title}}</h1>
<p>{{body}}</p>
<hr />
<experiment *ngFor="let e of experiments" [experiment]="e"></experiment>
<hr />
<div>
<h2 class="text-error">Experiments: {{message}}</h2>
<form class="form-inline">
<input type="text" [(ngModel)]="message" placeholder="Message">
<button type=“submit" (click)="updateMessage(message)">Update Message</
button>
</form>
</div>
Data Binding
BUT! What about
directives?
Directives
Directives
import { Directive, ElementRef } from '@angular/core';
Directives
Services
module
routes
components
services
Services
@Injectable()
export class ItemsService {
constructor(private http: HttpClient) {}
loadItems() {
return this.http.get(BASE_URL);
}
}
Services
BONUS! TypeScript Time!
export class ItemsComponent implements OnInit {
items: Item[];
selectedItem: Item;
constructor(
private itemsService: ItemsService
) {}
ngOnInit() {
this.itemsService.loadItems()
.subscribe((items: Item[]) => this.items = items);
}
}
Basic Component
export class ItemsComponent implements OnInit {
items: Item[];
selectedItem: Item;
constructor(
private itemsService: ItemsService
) {}
ngOnInit() {
this.itemsService.loadItems()
.subscribe((items: Item[]) => this.items = items);
}
}
Strong Types
export interface Item {
id: number;
img?: string;
name: string;
description?: string;
}
Interface
export class ItemsComponent implements OnInit {
items: Item[];
selectedItem: Item;
constructor(
private itemsService: ItemsService
) {}
ngOnInit() {
this.itemsService.loadItems()
.subscribe((items: Item[]) => this.items = items);
}
}
Field Assignment
export class ItemsComponent implements OnInit {
items: Item[];
selectedItem: Item;
constructor(
private itemsService: ItemsService
) {}
ngOnInit() {
this.itemsService.loadItems()
.subscribe((items: Item[]) => this.items = items);
}
}
Constructor Assignment
export class ItemsComponent implements OnInit {
items: Item[];
selectedItem: Item;
constructor(
private itemsService: ItemsService
) {}
ngOnInit() {
this.itemsService.loadItems()
.subscribe((items: Item[]) => this.items = items);
}
}
Implements Interface
The Angular CLI
CLI
➜ ~ npm install -g angular-cli
➜ ~ ng new my-dream-app
➜ ~ cd my-dream-app
➜ ~ ng serve
Angular CLI !== Crutch
Includes
Generating a project
ng generate component my-new-component
ng g component my-new-component # using the
alias
Generating a component
ng generate service my-new-service
ng g service my-new-service # using the alias
Generating a service
ng build
Generating a build
ng test
ng e2e
Running tests
ng lint
Linting
Component
Fundamentals
Anatomy of a Component
<template>
(event binding) @metadata [property binding]
class { }
Class !== Inheritance
Class Definition
Class
Import
Import
Class Decoration
Decorate
@NgModule({
declarations: [
AppComponent,
ItemsComponent,
ItemsListComponent,
ItemDetailComponent,
],
imports: [
BrowserModule,
FormsModule,
HttpClientModule,
AppRoutingModule
],
providers: [ItemsService],
bootstrap: [AppComponent]
})
export class AppModule { }
Exposing a Component
export class ItemsComponent {
items: Item[];
selectedItem: Item;
resetItem() {
const emptyItem: Item = {id: null, name: '', description: ''};
this.selectedItem = emptyItem;
}
selectItem(item: Item) {
this.selectedItem = item;
}
}
constructor(
private itemsService: ItemsService
) {}
ngOnInit() {
this.itemsService.loadItems()
.subscribe((items: Item[]) => this.items = items);
}
}
Injecting a Dependency
Lifecycle Hooks
constructor(
private itemsService: ItemsService
) {}
ngOnInit() {
this.itemsService.loadItems()
.subscribe((items: Item[]) => this.items = items);
}
}
Lifecycle Hooks
Template
Fundamentals
Templates
<template>
(event binding) @metadata [property binding]
class { }
Data Binding
component
template {{value}} class
[property] = "value"
(event) = “handler()”
[(ngModel)] = "property"
Property Binding
Property Bindings
Event Binding
Event Bindings
Two-way Binding
Two-way Binding
Structural Directives
Structural Directives
<span [ngSwitch]="toeChoice">
<!-- with *NgSwitch -->
<span *ngSwitchCase="'Eenie'">Eenie</span>
<span *ngSwitchCase="'Meanie'">Meanie</span>
<span *ngSwitchCase="'Miney'">Miney</span>
<span *ngSwitchCase="'Moe'">Moe</span>
<span *ngSwitchDefault>other</span>
<!-- with <template> -->
<template [ngSwitchCase]="'Eenie'"><span>Eenie</span></template>
<template [ngSwitchCase]="'Meanie'"><span>Meanie</span></template>
<template [ngSwitchCase]="'Miney'"><span>Miney</span></template>
<template [ngSwitchCase]="'Moe'"><span>Moe</span></template>
<template ngSwitchDefault><span>other</span></template>
</span>
Template Tag
Local Template Variable
FormsModule
ngModel
ngModel
Form Controls
ngForm
<pre>{{formRef.value | json}}</pre>
<pre>{{formRef.valid | json}}</pre>
<!--
{
"name": "First Item",
"description": "Item Description"
}
true
-->
ngForm
<form #formRef="ngForm">
<fieldset ngModelGroup="user">
<label>First Name</label>
<input [(ngModel)]="user.firstName" name="firstName"
required placeholder="Enter your first name" type="text">
<label>Last Name</label>
<input [(ngModel)]="user.lastName" name="lastName"
required placeholder="Enter your last name" type="text">
</fieldset>
</form>
ngModelGroup
<div ngModelGroup="user">
<label>First Name</label>
<input [(ngModel)]="firstName" name="firstName"
required placeholder="Enter your first name" type="text">
<label>Last Name</label>
<input [(ngModel)]="lastName" name="lastName"
required placeholder="Enter your last name" type="text">
</div>
<pre>{{formRef.value | json}}</pre>
<!--
{
"user": {
"firstName": "Test",
"lastName": "Test"
}
}
-->
ngModelGroup
Validation Styles
input.ng-valid {
border-bottom: 1px solid green;
}
Validation Styles
Angular Services
Everything is
just a class
class { } class { }
@metadata() @metadata()
component service
class { } class { }
@metadata() @metadata()
directive pipe
Just a class!
@Injectable()
export class ItemsService {
constructor(private http: HttpClient) {}
loadItems() { }
loadItem(id) { }
saveItem(item: Item) { }
createItem(item: Item) { }
updateItem(item: Item) { }
deleteItem(item: Item) { }
}
Defining a Service
@NgModule({
declarations: [
AppComponent,
ItemsComponent,
ItemsListComponent,
ItemDetailComponent,
],
imports: [
BrowserModule,
FormsModule,
HttpClientModule,
AppRoutingModule
],
providers: [ItemsService],
bootstrap: [AppComponent]
})
export class AppModule { }
Exposing a Service
export class ItemsComponent implements OnInit {
items: Item[];
selectedItem: Item;
constructor(
private itemsService: ItemsService
) {}
ngOnInit() {
this.itemsService.loadItems()
.subscribe((items: Item[]) => this.items = items);
}
}
Consuming a Service
Server
Communication
The HTTP Module
HttpClientModule
The HTTP Module Methods
createItem(item: Item) {
return this.http.post(`${BASE_URL}`, item);
}
updateItem(item: Item) {
return this.http.patch(`${BASE_URL}${item.id}`, item);
}
deleteItem(item: Item) {
return this.http.delete(`${BASE_URL}${item.id}`);
}
HTTP Methods
Observable.subscribe
http.get
export class ItemsComponent {
items: Item[];
selectedItem: Item;
constructor(
private itemsService: ItemsService
) {}
getItems() {
this.itemsService.loadItems()
.subscribe((items: Item[]) => this.items = items);
}
}
Observable.subscribe
Headers
Growing Application
Realistic Application
Growing Growing
View Controller
Uh oh!
Named Named
Route Route
Named
Route
Directive
Component
<template>
(event binding) @metadata [property binding]
class { }
What if we could
define custom
properties and events
to bind to?
Custom Data Binding
<template>
(event binding) @metadata [property binding]
class { }
Component Contract
<template>
@Output @metadata @Input
class { }
Parent and Child
<template>
@metadata
(event) class { } [property]
Parent
Parent and Child
<template>
@metadata
(event) class { } [property]
Parent
Parent and Child
<template>
@metadata
(event) class { } [property]
Parent
@Input
@Input
@Component({
selector: 'app',
template: `
<my-component [greeting]="greeting"></my-component>
<my-component></my-component>
`
})
export class App {
greeting = 'Hello child!';
}
Parent Component
@Output
@Output
@Component({
selector: 'app',
template: `
<div>
<h1>{{greeting}}</h1>
<my-component (greeter)="greet($event)"></my-component>
</div>
`
})
export class App {
private greeting;
greet(event) {
this.greeting = event;
}
}
Parent Component
Component Contracts
Component Contract
Nice Neat Containers
Container and Presentational Components
Presentational Component
export class ItemsComponent implements OnInit {
items: Array<Item>;
selectedItem: Item;
constructor(private itemsService: ItemsService) { }
ngOnInit() { }
resetItem() { }
selectItem(item: Item) { }
saveItem(item: Item) { }
replaceItem(item: Item) { }
pushItem(item: Item) { }
deleteItem(item: Item) { }
}
Container Component
State flows down
GET ALL THE DATA!
CONTAINER
PRESENTATION PRESENTATION
Events flows up
PROCESS THIS EVENT!
CONTAINER
PRESENTATION PRESENTATION
You basically
understand redux
Angular Routing
Routing
• Routes are defined in a route definition table that in its simplest form
contains a path and component reference
• Components are loaded into the router-outlet component
• We can navigate to routes using the routerLink directive
• The router uses history.pushState which means we need to set a
base-ref tag to our index.html file
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ItemsComponent } from './items/items.component';
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: []
})
export class Ng2RestAppRoutingModule { }
Routing
Components
module
routes
components
services
Components
module
routes
component
template class
services
Testing
Fundamentals
TESTING IS
HARD!
WRITING
SOFTWARE
IS HARD!
The biggest problem in the
development and maintenance
of large-scale software systems
is complexity — large systems
are hard to understand.
Out of the Tarpit - Ben Mosely Peter Marks
We believe that the major contributor to this
complexity in many systems is the handling
of state and the burden that this adds when
trying to analyse and reason about the
system. Other closely related contributors
are code volume, and explicit concern with
the flow of control through the system.
Out of the Tarpit - Ben Mosely Peter Marks
Complexity
and purgatory
price;
mode;
widgets: Widget[];
reCalculateTotal(widget: Widget) {
switch (this.mode) {
case 'create':
const newWidget = Object.assign({}, widget, {id: UUID.UUID()});
this.widgets = [...this.widgets, newWidget];
break;
case 'update':
this.widgets = this.widgets.map(wdgt => (widget.id === wdgt.id) ? Object.assign({}, widget) : wdgt);
break;
case 'delete':
this.widgets = this.widgets.filter(wdgt => widget.id !== wdgt.id);
break;
default:
break;
}
testService.mode = 'create';
testService.reCalculateTotal(testWidget);
testService.mode = 'update';
testService.reCalculateTotal(testWidget);
testService.mode = 'delete';
testService.reCalculateTotal(testWidget);
const testService = new RefactorService();
const testWidget = { id: 100, name: '', price: 100, description: ''};
const testWidgets = [{ id: 100, name: '', price: 200, description: ''}];
testService.widgets = testWidgets;
testService.mode = 'create';
testService.reCalculateTotal(testWidget);
testService.mode = 'update';
testService.reCalculateTotal(testWidget);
testService.mode = 'delete';
testService.reCalculateTotal(testWidget);
price;
mode;
widgets: Widget[];
reCalculateTotal(widget: Widget) {
switch (this.mode) {
case 'create':
const newWidget = Object.assign({}, widget, {id: UUID.UUID()});
this.widgets = [...this.widgets, newWidget];
break;
case 'update':
this.widgets = this.widgets.map(wdgt => (widget.id === wdgt.id) ? Object.assign({}, widget) : wdgt);
break;
case 'delete':
this.widgets = this.widgets.filter(wdgt => widget.id !== wdgt.id);
break;
default:
break;
}
reCalculateTotal(widget: Widget) {
switch (this.mode) {
case 'create':
const newWidget = Object.assign({}, widget, {id: UUID.UUID()});
this.widgets = [...this.widgets, newWidget];
break;
case 'update':
this.widgets = this.widgets.map(wdgt => (widget.id === wdgt.id) ? Object.assign({}, widget) : wdgt);
break;
case 'delete':
this.widgets = this.widgets.filter(wdgt => widget.id !== wdgt.id);
break;
default:
break;
}
Class
Basic Structure
Utilities vs Isolated
Two Approaches
The Testing Big Picture
Karma
Jasmine
Testing Utilities
Code
Your First Test
Karma
• Karma is the test runner that is used to execute Angular unit tests
• You can manually install and configure Karma
• Karma is installed and configured by default when you create a
project with the Angular CLI
• Karma is configured via the karma.conf.js file
• Tests (specs) are identified with a .spec.ts naming convention
Debugging with Karma
@Component({
selector: 'app-simple',
template: '<h1>Hello {{subject}}!</h1>'
})
export class SimpleComponent implements OnInit {
subject: string = 'world';
constructor() { }
ngOnInit() { }
}
The Component
1. Configure Module
TestBed
describe('SimpleComponent', () => {
let component: SimpleComponent;
let fixture: any;
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ SimpleComponent ]
});
});
});
Configure Module
1. Configure Module
2. Create Fixture
TestBed.createComponent
describe('SimpleComponent', () => {
let component: SimpleComponent;
let fixture: ComponentFixture<SimpleComponent>;
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ SimpleComponent ]
})
.createComponent(SimpleComponent);
});
});
The Fixture
1. Configure Module
2. Create Fixture
3. Get Component Instance
ComponentFixture
describe('SimpleComponent', () => {
let component: SimpleComponent;
let fixture: ComponentFixture<SimpleComponent>;
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ SimpleComponent ]
})
.createComponent(SimpleComponent);
component = fixture.componentInstance;
});
});
describe('SimpleComponent', () => {
let component: SimpleComponent;
let fixture: ComponentFixture<SimpleComponent>;
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ SimpleComponent ]
})
.createComponent(SimpleComponent);
component = fixture.componentInstance;
});
describe('SimpleComponent', () => {
let component: SimpleComponent;
let fixture: ComponentFixture<SimpleComponent>;
let de: DebugElement;
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ SimpleComponent ]
})
.createComponent(SimpleComponent);
component = fixture.componentInstance;
de = fixture.debugElement;
fixture.detectChanges();
});
detectChanges
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
describe('$COMPONENT$', () => {
let component: $COMPONENT$$END$;
let fixture: ComponentFixture<$COMPONENT$>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ $COMPONENT$ ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent($COMPONENT$);
component = fixture.componentInstance;
fixture.detectChanges();
});
• With an external template, Angular needs to read the file before it can
create a component instance. This is problematic because
TestBed.createComponent is synchronous.
• The first thing we do is break our initial beforeEach into an
asynchronous beforeEach call and a synchronous beforeEach call
• We then use the async testing utility to load our external templates
• And then call TestBed.compileComponents to compile our components
• WebPack users can skip this slide
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ TemplateComponent ]
})
.compileComponents();
}));
async
beforeEach(() => {
fixture = TestBed.createComponent(TemplateComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
fixture
Component with a Service Dependency
Component
export class GreetingService {
subject: {name: string} = { name: 'world' };
}
Service
describe('ServiceComponent', () => {
let component: ServiceComponent;
let fixture: ComponentFixture<ServiceComponent>;
let de: DebugElement;
let greetingServiceStub;
let greetingService;
});
Local Members
beforeEach(() => {
greetingServiceStub = {
subject: {name: 'world'},
};
fixture = TestBed.configureTestingModule({
declarations: [ ServiceComponent ],
providers: [{ provide: GreetingService, useValue: greetingServiceStub }]
})
.createComponent(ServiceComponent);
component = fixture.componentInstance;
de = fixture.debugElement;
fixture.detectChanges();
greetingService = de.injector.get(GreetingService);
});
Test Double
beforeEach(() => {
greetingServiceStub = {
subject: {name: 'world'},
};
fixture = TestBed.configureTestingModule({
declarations: [ ServiceComponent ],
providers: [{ provide: GreetingService, useValue: greetingServiceStub }]
})
.createComponent(ServiceComponent);
component = fixture.componentInstance;
de = fixture.debugElement;
fixture.detectChanges();
greetingService = de.injector.get(GreetingService);
});
Test Double
beforeEach(() => {
greetingServiceStub = {
subject: {name: 'world'},
};
fixture = TestBed.configureTestingModule({
declarations: [ ServiceComponent ],
providers: [{ provide: GreetingService, useValue: greetingServiceStub }]
})
.createComponent(ServiceComponent);
component = fixture.componentInstance;
de = fixture.debugElement;
fixture.detectChanges();
greetingService = de.injector.get(GreetingService);
});
Test Double
it('updates component subject when service subject is changed', () => {
greetingService.subject.name = 'cosmos';
fixture.detectChanges();
expect(component.subject.name).toBe('cosmos');
const h1 = de.query(By.css('h1')).nativeElement;
expect(h1.innerText).toBe('Hello cosmos!');
});
Actual Test
Service with HttpClient
describe('RemoteService', () => {
let injector: TestBed;
let service: RemoteService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [RemoteService]
});
injector = getTestBed();
service = injector.get(RemoteService);
httpMock = injector.get(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
});
it('should fetch all widgets', () => {
const mockWidgets: Widget[] = [
{id: 1, name: 'mock', description: 'mock', price: 100},
{id: 2, name: 'mock', description: 'mock', price: 100},
{id: 3, name: 'mock', description: 'mock', price: 100}
];
class RemoteServiceStub {
all() { return of(noop())}
create() { return of(noop()) }
update() { return of(noop()) }
delete() { return of(noop()) }
}
let component: RemoteComponent;
let fixture: ComponentFixture<RemoteComponent>;
let debugElement: DebugElement;
let service: RemoteService;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
RemoteComponent
],
providers: [
{provide: RemoteService, useClass: RemoteServiceStub}
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(RemoteComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
service = debugElement.injector.get(RemoteService);
fixture.detectChanges();
});
it('should call remoteService.all on getWidgets', () => {
spyOn(service, 'all').and.callThrough();
component.getWidgets();
expect(service.all).toHaveBeenCalled();
});
component.createWidget(mockWidget);
expect(service.create).toHaveBeenCalledWith(mockWidget);
});
I😍YOU!
@simpulton
Thanks!