Angular
Basics
Rachèl Heimbach
Principal Consultant @ OpenValue
Solution Architect @ Rabobank
Frank Merema
Lead Frontend developer @ FrontValue
Frontend development
HTML
CSS
JavaScript
Web components
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.
Webcomponent tech.
- Custom elements
- Shadow DOM
- HTML templates
Architecture
Component-based frameworks
and more...
Angular
HTML
SCSS
TypeScript
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
TypeScript
- Type Assignment
- Type Inference
- Combined
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;
}
}
- Interfaces
- Access modifiers
interface Product {
name: string;
price: number;
}
Redundant
TypeScript Playground
tsconfig.json
"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"
]
},
Generics<T>
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');
Decorators
Angular
... is a command-line interface tool that you use to initialize, develop, scaffold, and maintain Angular applications directly from a command shell.
Angular CLI
Component based
Dependency Injection
Modular
Component based
Dependency Injection
Modular
Web components
Products list
A list of products where each product consists of a title, category, image, rating, description and price.
Hierarchy
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>
Product
(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>
Product
(webcomponents)
Hierarchy
A list of products where each product consists of a title, category, image, rating, description and price.
Products
Product
Product
SquareImage
Rating
Components
Angular App
A very simple Angular app that renders a list of products.
Products
Product
Product
SquareImage
Rating
App
AppComponent
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
Checkout repo
# Install deps
npm i
# Start app
npm start
# Run docs (assignments)
npm run docs
# Run tests
npm test
Let's create a component
Product data model
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;
}
Product TS
// 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>
[binding]
<!-- 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">
Product HTML
<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]
Assignment 1.1-1.3
Components
Directives
ViewEncapsulation
Assignment 1.4-1.5
*ngFor - ViewEncapsulation
Search TS
// 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>
(event)
<!--
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">
Lifecycle hooks
- ngOnChanges: When an input/output binding value changes.
- ngOnInit: After the first ngOnChanges.
- ngDoCheck: Developer's custom change detection.
- ngAfterContentInit: After component content initialized.
- ngAfterContentChecked: After every check of component content.
- ngAfterViewInit: After a component's views are initialized.
- ngAfterViewChecked: After every check of a component's views.
- ngOnDestroy: Just before the directive is destroyed.
Component based
Dependency Injection
Modular
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
What is a module?
Angular modules
CommonModule
FormsModule
...
RouterModule
BrowserModule
HttpClientModule
Main.ts
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));
AppModule
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
?
Angular Material
Module architecture
Assignment 2
Modules
TestBed
Component testing
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.
What is TestBed?
TestBed example
@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
TestBed 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;
// ...
}
TestBed example
@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
Smart vs Dumb
Shallow test
- Mock children
- Test UI based on input
- Test child bindings
- Test child event handlers
- Test event emitting
Dumb
Dumb
Dumb
Data
Event
Event
Data
Product test
- Mock children
- Test UI based on input
- Test child bindings
- Test child event handler
- Test event emitting
Product
SquareImage
Rating
product
rate, count
src, alt
NgMocks
Products test
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
Assignment 3
Unit testing
Assignment 4
Homework
Routing
The Router enables navigation by interpreting a browser URL as an instruction to change the view.
Router?
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 { }
RouterModule
<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>
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 { }
Lazy loading
Products
Product
SquareImage
Rating
App
AppModule
ProductsModule
RouterOutlet
Home
...
HomeModule
/
/products
SharedModule
...
Assignment 5
Routing
Component based
Dependency Injection
Modular
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.
Angular DI
Components shouldn't fetch or save data directly [...] They should focus on presenting data and delegate data access to a service.
Service
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.
What is a provider?
Custom provider
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
@Injectable() (old)
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
Root injector
The injector scope where all sync providers are put by default.
ProductService
@NgModule({
declarations: [FeatureComponent],
providers: [ProductService]
})
export class FeatureModule {}
Root injector
@Injectable() (today)
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:
- Lazy loadable -> smaller bundles
Example architectures
ProductService
Root injector
ProductsPage
...
Products
Product
All over the place pattern
Pros:
- Easy
Cons:
- Complex UI
- Harder to debug
ProductService
Root injector
ProductsPage
...
Products
Product
Page controller pattern
Pros:
- Complexity page level
- Simple UI components
- Clear separation between UI and App logic
Cons:
- "Prop drilling"
ProductService
Root injector
ProductsPage
...
Products
Product
Smart component pattern
Pros:
- Standalone features
- Few smart components
- Many dumb components
Cons:
- UI is not completely separated from business logic
History lesson
$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:
Observable
SINGLE | MULTIPLE | |
---|---|---|
Pull | Function | Iterator |
Push | Promise | Observable |
An Observer is a consumer of values delivered by an Observable.
Observer
Observers are simply a set of callbacks, one for each type of notification delivered by the Observable: next, error, and complete.
Single
Multiple
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.
Subject
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.
Single sub
import { Subject } from 'rxjs';
const numbersSubject = new Subject();
numbersSubject.subscribe(...);
numbersSubject.next(1);
// 1
numbersSubject.next(2);
// 2
numbersSubject.complete();
// Completed
Multiple subs
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
Operators are the essential pieces that allow complex asynchronous code to be easily composed in a declarative manner.
Cold
Hot
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.
Subscription
A Subscription has one important method, unsubscribe, that takes no argument and just disposes the resource held by the subscription.
Subscription
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
Data fetching
@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
Data fetching
@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
Memory leaks
@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();
}
// ...
}
Subscriptions
@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();
}
// ...
}
Async Pipe
@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>
Data fetching
@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
});
}
// ...
}
ReactiveForms
Data fetching
@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">
Data fetching
@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();
}
// ...
}
OV Angular
By rachnerd
OV Angular
- 440