Rachèl Heimbach
Principal Consultant @ OpenValue
Solution Architect @ Rabobank
Frank Merema
Lead Frontend developer @ FrontValue
HTML
CSS
JavaScript
Web Components is a suite of different technologies allowing you to create reusable custom elements — with their functionality encapsulated away from the rest of your code — and utilize them in your web apps.
and more...
HTML
SCSS
TypeScript
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
let n = 1; // number
n = '123';
Redundant
let n: number = 1;
let n: number;
n = 1;
export class ProductsService {
private products: Product[];
constructor() {}
getProducts() {
return this.products;
}
public setProducts(products: Product[]) {
this.products = products;
}
}
interface Product {
name: string;
price: number;
}
Redundant
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"strictPropertyInitialization": false,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2017",
"module": "es2020",
"lib": [
"es2018",
"dom"
]
},
export class DataService<T> {
private data: T;
constructor() {}
get(): T {
return this.data;
}
set(data: T): void {
this.data = data;
}
}
const numberService = new DataService<number>();
numberService.set(1);
const textService = new DataService<string>();
textService.set('Text');
... is a command-line interface tool that you use to initialize, develop, scaffold, and maintain Angular applications directly from a command shell.
A list of products where each product consists of a title, category, image, rating, description and price.
A list of products where each product consists of a title, category, image, rating, description and price.
Products
Product
<section class="product">
<a>
<img
class="square-image"
src="https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg"
alt="Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops"
/>
</a>
<div class="info">
<p class="category">men's clothing</p>
<a class="title">Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops</a>
<p class="rating">
<span class="fa fa-star"></span>
<span class="fa fa-star"></span>
<span class="fa fa-star"></span>
<span class="fa fa-star"></span>
<span class="fa fa-star-o"></span>
<span class="count">(120)</span>
</p>
<p class="description">
Your perfect pack for everyday use and walks in the forest. Stash your
laptop (up to 15 inches) in the padded sleeve, your everyday
</p>
<p class="price">109.95</p>
</div>
</section>
(plain html)
<section class="product">
<a>
<ov-square-image
src="https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg"
alt="Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops"
></ov-square-image>
</a>
<div class="info">
<p class="category">men's clothing</p>
<a class="title">Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops</a>
<ov-rating rating="4" count="120"></ov-rating>
<p class="description">
Your perfect pack for everyday use and walks in the forest. Stash your
laptop (up to 15 inches) in the padded sleeve, your everyday
</p>
<p class="price">109.95</p>
</div>
</section>
(webcomponents)
A list of products where each product consists of a title, category, image, rating, description and price.
Products
Product
Product
SquareImage
Rating
A very simple Angular app that renders a list of products.
Products
Product
Product
SquareImage
Rating
App
Root of the component tree.
import { Component } from '@angular/core';
@Component({
selector: 'ov-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
}
...
App
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<ov-root></ov-root>
</body>
</html>
index.html
# Install deps
npm i
# Start app
npm start
# Run docs (assignments)
npm run docs
# Run tests
npm test
A list of products where each product consists of a title, category, image, rating, description and price.
// ng generate interface shared/product
export interface Product {
id: number;
title: string;
price: number;
description: string;
category: string;
image: string;
rating: Rating;
}
// ng generate interface shared/rating
export interface Rating {
rate: number;
count: number;
}
// ng generate component product
import { Component, Input, OnInit } from '@angular/core';
import { Product } from '../shared/product';
@Component({
selector: 'ov-product',
templateUrl: './product.component.html',
styleUrls: ['./product.component.scss'],
})
export class ProductComponent implements OnInit {
@Input() product: Product;
constructor() {}
ngOnInit(): void {}
}
...
Product
Product
product
<ov-product [product]="product"></ov-product>
<!-- Binds property value to the result of expression firstName. -->
<input [value]="firstName">
<!-- Binds attribute role to the result of expression myAriaRole. -->
<div [attr.role]="myAriaRole">
<!-- Binds the presence of the CSS class extra-sparkle on the element
to the truthiness of the expression isDelightful. -->
<div [class.extra-sparkle]="isDelightful">
<!-- Bind product to ProductComponent -->
<ov-product [product]="product">
<section class="product">
<a>
<ov-square-image
[src]="product.image"
[alt]="product.title"
></ov-square-image>
</a>
<div class="info">
<p class="category">{{ product.category }}</p>
<a class="title">{{ product.title }}</a>
<p class="rating">
<ov-rating
[rate]="product.rating.rate"
[count]="product.rating.count"
></ov-rating>
</p>
<p class="description">{{ product.description }}</p>
<p class="price">{{ product.price }}</p>
</div>
</section>
...
Product
Product
SquareImage
Rating
[product]
[src], [alt]
[rate], [count]
Components
*ngFor - ViewEncapsulation
// ng generate component search
import { Component, Input, OnInit
EventEmitter } from '@angular/core';
@Component({
selector: 'ov-search',
templateUrl: './search.component.html',
styleUrls: ['./search.component.scss'],
})
export class SearchComponent implements OnInit {
@Input() query: string;
@Output() search = new EventEmitter<string>();
constructor() {}
ngOnInit(): void {}
onChange(input: HTMLInputElement) {
this.search.emit(input.value);
}
}
...
Search
search($event)
<ov-search
[query]="searchValue"
(search)="onSearch($event)"
></ov-search>
<!--
Calls method readRainbow when a click event is triggered
on this button element (or its children) and passes in the
event object.
-->
<button (click)="readRainbow($event)">
<!--
Sets up two-way data binding. Equivalent to:
<my-cmp [title]="name" (titleChange)="name=$event">
-->
<my-cmp [(title)]="name">
It describes how to compile a component's template and how to create an injector at runtime
It identifies the module's own components, directives, and pipes
CommonModule
FormsModule
...
RouterModule
BrowserModule
HttpClientModule
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
App
AppModule
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Contains core dependencies that are always needed on boot.
Products
Product
SquareImage
Rating
App
AppModule
Products
Product
SquareImage
Rating
App
AppModule
ProductsModule
SharedModule
Products
Product
SquareImage
App
AppModule
ProductsModule
SharedModule
Rating
RatingModule
?
Module architecture
Modules
E2E
(UI)
Integration
Unit
Critical user flow testing
Render checks + Edge case testing
Isolated testing
Configures and initializes environment for unit testing and provides methods for creating components and services in unit tests.
TestBed is the primary api for writing unit tests for Angular applications and libraries.
@Component({
selector: 'app-banner',
template: '<h1>{{title}}</h1>'
})
export class BannerComponent {
title = 'Test Tour of Heroes';
}
describe('BannerComponent', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
});
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance; // BannerComponent test instance
});
it('should display original title', () => {
const h1 = fixture.nativeElement.querySelector('h1');
expect(h1.textContent).toContain(component.title);
});
});
Module
Fixture
export declare class ComponentFixture<T> {
/**
* The instance of the root component class.
*/
componentInstance: T;
/**
* The native element at the root of the component.
*/
nativeElement: any;
/**
* The DebugElement associated with the root element of this component.
*/
debugElement: DebugElement;
// ...
}
@Component({
selector: 'app-banner',
template: '<h1>{{title}}</h1>'
})
export class BannerComponent {
title = 'Test Tour of Heroes';
}
describe('BannerComponent', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
});
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance; // BannerComponent test instance
});
it('should display original title', () => {
const h1 = fixture.nativeElement.querySelector('h1');
expect(h1.textContent).toContain(component.title);
});
});
Smart
Feature
Service access
Controls state
App specific
Dumb children
Deep test
Smart
Dumb
Dumb
Dumb
Dumb
Dumb
Piece of UI
No service access
No state
Portable/reusable
Dumb children
Shallow test
Service
Data
Data
Event
Event
Dumb
Dumb
Dumb
Data
Event
Event
Data
Product
SquareImage
Rating
product
rate, count
src, alt
let component: ProductsComponent;
let fixture: ComponentFixture<ProductsComponent>;
const PRODUCTS_MOCK: Product[] = [...];
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ProductsComponent, MockComponent(ProductComponent)],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ProductsComponent);
component = fixture.componentInstance;
component.products = PRODUCTS_MOCK;
fixture.detectChanges();
});
it('should render 3 products', () => {
const productComponents = fixture.debugElement.queryAll(
By.directive(ProductComponent)
);
expect(productComponents.length).toBe(3);
productComponents.forEach(({ componentInstance }, index) => {
expect(componentInstance.product).toEqual(PRODUCTS_MOCK[index]);
});
});
Mock
Get mock instances
Assign mock data
Call change detection
Assert binding
Unit testing
Homework
The Router enables navigation by interpreting a browser URL as an instruction to change the view.
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'products', component: ProductsComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
App
RouterOutlet
Home
/
Products
/products
const routes: Routes = [
{ path: 'products', component: ProductsComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)]
})
export class ProductsModule { }
<nav>
<a routerLink="/"
routerLinkActive="active"
[routerLinkActiveOptions]="{exact:true}">
Home
</a>
<a routerLink="/products"
routerLinkActive="active"
[routerLinkActiveOptions]="{exact:true}">
Products
</a>
</nav>
<router-outlet></router-outlet>
app.component.html
.active {
background-color: grey;
cursor: auto;
}
app.component.scss
const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'products',
loadChildren: () =>
import('./products-overview/products-overview.module')
.then(m => m.ProductsOverviewModule)
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
App
RouterOutlet
Home
/
Products
/products
const routes: Routes = [
{ path: '', component: ProductsComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)]
})
export class ProductsOverviewModule { }
Products
Product
SquareImage
Rating
App
AppModule
ProductsModule
RouterOutlet
Home
...
HomeModule
/
/products
SharedModule
...
Routing
It provides dependencies to a class upon instantiation. Use it to increase flexibility and modularity in your applications.
Dependencies are services or objects that a class needs to perform its function.
Components shouldn't fetch or save data directly [...] They should focus on presenting data and delegate data access to a service.
@Injectable()
export class ProductService {
get(): Product[] {
// ...
}
}
Decorator that marks a class as available to be provided and injected as a dependency.
@Injectable()
An injector uses the provider to create a new instance of a dependency for a class that requires it.
A provider object defines how to obtain an injectable dependency associated with a DI token.
import { InjectionToken } from '@angular/core';
export const API_URL = new InjectionToken<string>('api url');
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [
{
provide: API_URL,
useValue: '/api',
},
],
bootstrap: [AppComponent],
})
export class AppModule {}
1. Define an injection token
2. Provide the token with a value/class/factory
export class AppComponent implements OnInit {
constructor(
@Inject(API_URL)
private apiUrl: string
) {}
ngOnInit() {
console.log(this.apiUrl); // /api
}
}
3. Inject the token where needed
1. Define a service
@Injectable()
export class ProductService {
get(): Product[] {
// ...
}
}
2. Provide it to a module
@NgModule({
declarations: [FeatureComponent],
providers: [ProductService]
})
export class FeatureModule {}
export class AppComponent implements OnInit {
constructor(
private productService: ProductService
) {}
// ...
}
3. Inject the service where needed
The injector scope where all sync providers are put by default.
ProductService
@NgModule({
declarations: [FeatureComponent],
providers: [ProductService]
})
export class FeatureModule {}
Root injector
1. Define a service
@Injectable({
provideIn: 'root'
})
export class ProductService {
get(): Product[] {
// ...
}
}
2. Inject the service where needed
export class AppComponent implements OnInit {
constructor(
private productService: ProductService
) {}
// ...
}
Benefits:
ProductService
Root injector
ProductsPage
...
Products
Product
All over the place pattern
Pros:
Cons:
ProductService
Root injector
ProductsPage
...
Products
Product
Page controller pattern
Pros:
Cons:
ProductService
Root injector
ProductsPage
...
Products
Product
Smart component pattern
Pros:
Cons:
$scope
function Controller($scope) {
// ...
$scope.watch(productService.products, () => { ... });
// ...
}
Observables are lazy Push collections of multiple values. They fill the missing spot in the following table:
SINGLE | MULTIPLE | |
---|---|---|
Pull | Function | Iterator |
Push | Promise | Observable |
An Observer is a consumer of values delivered by an Observable.
Observers are simply a set of callbacks, one for each type of notification delivered by the Observable: next, error, and complete.
import { Observable } from 'rxjs';
const number$ = Observable.of(1);
number$
.subscribe(
n => console.log(n),
e => console.error(e),
() => console.log('Completed')
);
// 1
// Completed
import { Observable } from 'rxjs';
const numbers$ = Observable.from([1, 2, 3]);
numbers$
.subscribe(
n => console.log(n),
e => console.error(e),
() => console.log('Completed')
);
// 1
// 2
// 3
// Completed
A Subject is a special type of Observable which shares a single execution path among observers.
You can think of this as a single speaker talking at a microphone in a room full of people. Their message (the subject) is being delivered to many (multicast) people (the observers) at once.
import { Subject } from 'rxjs';
const numbersSubject = new Subject();
numbersSubject.subscribe(...);
numbersSubject.next(1);
// 1
numbersSubject.next(2);
// 2
numbersSubject.complete();
// Completed
import { Subject } from 'rxjs';
const numbersSubject = new Subject();
numbersSubject.subscribe(...);
numbersSubject.subscribe(...);
numbersSubject.next(1);
// 1
// 1
numbersSubject.next(2);
// 2
// 2
numbersSubject.complete();
// Completed
// Completed
RxJS is mostly useful for its operators, even though the Observable is the foundation.
Operators are the essential pieces that allow complex asynchronous code to be easily composed in a declarative manner.
import { Observable } from 'rxjs';
const number$ = Observable.create(
observer => {
observer.next(1);
observer.next(2);
observer.complete();
}
);
number$.subscribe(...);
// 1
// 2
// Completed
number$.subscribe(...);
// 1
// 2
// Completed
import { Observable } from 'rxjs';
const numbersSubject = new Subject();
const number$ = Observable.create(
observer =>
numbersSubject.subscribe(observer)
);
number$.subscribe(...);
numbersSubject.next(1);
// 1
number$.subscribe(...);
numbersSubject.next(2);
// 2
// 2
A Subscription is an object that represents a disposable resource, usually the execution of an Observable.
A Subscription has one important method, unsubscribe, that takes no argument and just disposes the resource held by the subscription.
import { Observable } from 'rxjs';
import { delay } from 'rxjs/operators';
const number$ = Observable.from([1, 2, 3])
.pipe(
delay(1000)
);
const subscription = number$.subscribe(...);
setTimeout(() => {
subscription.unsubscribe();
console.log('Unsubscribed');
}, 2001);
// 1
// 2
// Unsubscribed
import { Observable } from 'rxjs';
const numbersSubject = new Subject();
const number$ = Observable.create(
observer =>
numbersSubject.subscribe(observer)
);
const subscription = number$.subscribe(...);
number$.subscribe(...);
numbersSubject.next(1);
// 1
// 1
subscription.unsubscribe();
numbersSubject.next(2);
// 2
@Injectable({
providedIn: 'root'
})
export class ProductsService {
constructor(private httpClient: HttpClient) {}
get(): Observable<Product[]> {
return this.httpClient
.get<Product[]>('/api/products');
}
}
@Component({
...
})
export class ProductsComponent {
products: Product[];
constructor(private productsService: ProductsService) {}
ngOnInit() {
this.productsService.get()
.subscribe(
products => this.products = products,
error => console.error(error)
);
}
}
ProductsService
Products
data
@Injectable({
providedIn: 'root'
})
export class ProductsService {
products$: Observable<Product[]>;
private productsSubject = new Subject<Product[]>();
constructor(private httpClient: HttpClient) {
this.products$ = this.productsSubject.asObservable();
}
get(): void {
this.httpClient.get<Product[]>('/products')
.subscribe(
products => this.productsSubject.next(products),
error => console.log(error)
);
}
}
ProductsService
Products
data
@Component({
selector: 'ov-products',
templateUrl: './products.component.html',
styleUrls: ['./products.component.scss']
})
export class ProductsComponent implements OnInit {
// ...
constructor(private productsService: ProductsService) {
}
ngOnInit(): void {
this.productsService.products$
.subscribe(products => {
console.log(products);
});
this.productsService.get();
}
// ...
}
@Component({
selector: 'ov-products',
templateUrl: './products.component.html',
styleUrls: ['./products.component.scss']
})
export class ProductsComponent implements OnInit, OnDestroy {
// ...
private subscription: Subscription;
constructor(private productsService: ProductsService) {
}
ngOnInit(): void {
this.subscription = this.productsService.products$
.subscribe(products => {
console.log(products);
});
this.productsService.get();
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
// ...
}
@Component({
selector: 'ov-products',
templateUrl: './products.component.html',
styleUrls: ['./products.component.scss']
})
export class ProductsComponent implements OnInit, OnDestroy {
products$: Observable<Product[]>;
constructor(private productsService: ProductsService) {
this.products$ = productsService.products$;
}
// ...
}
<ng-container *ngIf="products$ | async as products else loading">
{{ products | json }}
</ng-container>
<ng-template #loading>
Loading...
</ng-template>
@Component({
selector: 'ov-products',
templateUrl: './products.component.html',
styleUrls: ['./products.component.scss']
})
export class ProductsComponent implements OnInit {
products$: Observable<Product[]>;
constructor(private productsService: ProductsService) {
this.products$ = productsService.products$;
}
ngOnInit(): void {
this.productsService.get();
}
onSearch(query: string) {
this.productsService.get({
filter: query
});
}
// ...
}
@Component({
selector: 'ov-search',
templateUrl: './search.component.html',
styleUrls: ['./search.component.scss']
})
export class SearchComponent implements OnInit, OnDestroy {
@Output()
search = new EventEmitter<string>();
searchFormControl: FormControl;
private subscription: Subscription;
ngOnInit(): void {
this.searchFormControl = new FormControl('')
this.subscription = this.searchFormControl.valueChanges
.pipe(
debounceTime(300)
)
.subscribe(value => this.search.emit(value))
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
// ...
}
<input matInput type="text" [formControl]="searchFormControl">
@Component({
selector: 'ov-search',
templateUrl: './search.component.html',
styleUrls: ['./search.component.scss']
})
export class SearchComponent implements OnInit, OnDestroy {
@Output()
search = new EventEmitter<string>();
searchFormControl: FormControl;
private subscription: Subscription;
ngOnInit(): void {
this.searchFormControl = new FormControl('')
this.subscription = this.searchFormControl.valueChanges
.pipe(
debounceTime(300),
distinctUntilChanged()
)
.subscribe(value => this.search.emit(value))
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
// ...
}