Angular Advanced

 Gion Kunz

 Front End Developer

gion.kunz@oddeven.ch

https://oddeven.ch

Timeline

Time Topic
08:30 Start
10:00 Break 15 min
12:00 Lunch Break
13:00 Continue
14:30 Break 15 min
16:00 Break 15 min
17:30 Finish
  • Project Setup

  • Components

  • Data

  • Routing

  • Testing

  • Performance

Workshop Content

Preparation

Using the Workshop VM

You're all set to go :-)

Using your own operating system

Project Setup

Where to Start?

  • Angular CLI

  • Angular Seed (by @mgechev)

  • Angular 2 Webpack Starter

  • Roll Your Own with JSPM

Angular Cli

# Install Angular CLI
npm install -g @angular/cli

# Create new project
ng new oddeven-ng2-advanced --prefix oe
cd oddeven-ng2-advanced

# Create components with generator
ng generate component my-new-component

# Start dev server
ng serve --hmr

# Create production build
npm run build

Angular2 Webpack Starter

# Clone Angular 2 Webpack Starter
git clone https://github.com/angularclass/angular2-webpack-starter.git

# Change directory
cd angular2-webpack-starter

# Install the repo with npm
npm install

# Start the server
npm start

Why Roll Your own?

2404 Unknown Lines of Configuration in
33 Unknown Files

Building up the Understanding is Crucial

Roll Your Own with JSPM

# Install JSPM
npm install -g jspm

# Create new folder
mkdir angular-2-advanced
cd angular-2-advanced

# Initialize NPM project and install local JSPM
npm init
npm install --save-dev jspm

# Initialize JSPM and install the repo with npm
jspm init
jspm install npm:@angular/core npm:@angular/common ⏎
npm:@angular/compiler npm:@angular/platform-browser ⏎
npm:@angular/platform-browser-dynamic ⏎
npm:rxjs npm:zone.js npm:reflect-metadata

Components

The parable of the two watchmakers

Herbert A. Simon, The Sciences of the Artificial (1969)

Hora

Tempus

Once upon a time, there were two watchmakers, named Hora and Tempus, who manufactured very fine watches...

Hora's Shop

Tempus' Shop

They both received a lot of orders and made lots of money...

Hora's Shop

Tempus' Shop

However, Hora prospered while Tempus went broke and lost his shop...

Why did Hora SUCCEED?

Watch manufacturing process

Tempus' Manufacture

  • Consists of ~1000 parts

  • Assembled of individual parts in one go

  • Fell apart completely, if he needed to answer calls

Hora's Manufacture

  • Consists of ~1000 parts

  • Assembled of intermediate stable components

  • Fell apart into components only

Conclusion

...complex systems evolve from simple systems much more rapidly when there are stable intermediate forms...

 

Herbert A. Simon

commonalities

Artificial

Nature

Microcosm

Macrocosm

Fundamental

Significant

We Tend to Develop on Two Levels only

Pages

UI Elements

Hierarchic Systems

The Holarchy

Arthur Koestler, The Ghost in the Machine (1967)

Holon

Are User Interfaces Holarchic Systems?

Holon

Holon

We need to build user interfaces like this...

...not like that

5 Simple Rules for Components

  • Should have Single Responsibility

  • Should be Simple

  • Should be Small

  • Should be Encapsulated

  • Should use Composition

Don't create components for reusability, create components for simplicity!

SEMANTICS

Getting the semantics right, is the first step to a good application design.

Semantic Levels in User Interfaces

  • UI Semantics

  • Domain Semantics

3 Simple Rules to Follow

  • Start with the smallest visible element

  • Try to apply UI Semantics

  • Try to apply Domain semantics

User Interface DISSECTION Excercise

Data

<application>
  <navigation>
    <user-profile></user-profile>
    <navigation-group></navigation-group>
    <navigation-group></navigation-group>
    <navigation-group></navigation-group>
  </navigation>
  <main-content></main-content>
</application>

Components Hierarchy vs. Semantic Hierarchy

Semantic Boundaries

Application

Navigation

User Profile

User Image

Hard Boundary

Soft
Boundary

Soft semantic boundaries tend to increase / decrease scope depth.

Hard semantic boundaries tend to require different scope.

Application

Navigation

User Profile

User Image

Separation of Concerns

@Component({
  selector: 'user-profile',
  template: `
    <p class="name">{{name}}</p>
  `
})
class UserProfile {
  constructor() {
    fetch('https://jsonplaceholder.typicode.com/users/1')
      .then((response) => response.json())
      .then((user) => this.name = user.name);
  }
}

Component with mixed concerns

Components connected to data lose Their composability

@Component({
  selector: 'user-profile',
  template: `
    <p class="name">{{name}}</p>
  `
})
class UserProfile {
  @Input() name;
}
//-------------------------------------------------------
@Component({
  selector: 'user-profile-container',
  template: `
    <user-profile [name]="user.name"></user-profile>
  `
})
class UserProfileContainer {
  constructor() {
    fetch('https://jsonplaceholder.typicode.com/users/1')
      .then((response) => response.json())
      .then((user) => this.user = user);
  }
}

Separate using Container Components

Application

Container Component

User Profile

User Image

Navigation

Inversion of Control

Data

Update

Small Excercise

Fix the "Clear List" button

Fluxarchitecture

Action

Dispatcher

Store

View

Redux

Flux

Elm

ngrx store

Redux

rxjs

Action

Reducer

Store

View

Current State and Action

Dispatch Action

New State

Architecture of Redux / ngrx

1

3

2

4

Refactoring Excercise 

Rebuilding our TODO list using ngrx store

Routing

A Router serves three main purposes

  • Navigablity & Bookmarkability

  • UI Composition

  • Data

import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
import {MainComponent} from './main.component.ts';
import {Child1Component} from './child1/child1.component.ts';
import {Child2Component} from './child2/child2.component.ts';

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      {path: 'child1', component: Child1Component},
      {path: 'child2', component: Child2Component}
    ])
  ],
  declarations: [MainComponent, Child1Component, Child2Component],
  bootstrap: [MainComponent]
})
export class MainModule {}

Include Router in Main Module

RouterModule.forRoot([{
  path: 'child1',
  component: Child1Component
}, {
  path: '',
  pathMatch: 'full',
  redirectTo: '/child1'
}, {
  path: '**',
  component: PageNotFoundComponent
}];

Redirects and Fallback Component

The router selects the route with a first match wins strategy.

<router-outlet></router-outlet>

The router outlet acts as a container for routed components to be loaded into.

Router Outlet 

<h1>Welcome!</h1>
<nav>
  <a routerLink="/child1" 
     routerLinkActive="active">child1</a>
  <a routerLink="/child2" 
     routerLinkActive="active">child2</a>
</nav>
<router-outlet></router-outlet>

Creating Navigation Links and Styling Active Links

Route Parameters

RouterModule.forRoot([{
  path: 'child/:id',
  component: ChildComponent
}])

Route Configuration with Parameters

import {ActivatedRoute} from '@angular/router';

@Component({
  selector: 'ngc-child',
  template: 'Child:<p>Param: {{params.id}}</p>'
})
export class ChildComponent {
  constructor(@Inject(ActivatedRoute) route: ActivatedRoute) {
    this.params = route.snapshot.params;
  }
}

Accessing Parameters in Routed Components using Snapshot

By default, the router re-uses a component instance when it re-navigates to the same component.

import {Component, Inject} from '@angular/core';
import {ActivatedRoute} from '@angular/router';

@Component({
  selector: 'ngc-child',
  template: 'Child:<p>Param: {{(params | async).id}}</p>'
})
export class ChildComponent {
  constructor(@Inject(ActivatedRoute) route: ActivatedRoute) {
    this.params = route.params;
  }
}

Using Observable Params

Child Routes

RouterModule.forRoot([{
  path: 'child', 
  component: ChildComponent,
  children: [{
    path: 'grand-child',
    component: GrandChildComponent
  }]
}])

Routes can be Nested for configuring Child Routes

constructor(@Inject(Router) private router: Router) {}

Navigate using Router API

...
this.router.navigate(['/child1']);

Auxiliary Routes

Named Outlets

<router-outlet></router-outlet>
<footer>
  <router-outlet name="footer"></router-outlet>
</footer>

Registering Auxiliary Routes

RouterModule.forRoot([
  {path: 'child1', component: Child1Component},
  {path: 'child2', component: Child2Component},
  {path: 'child-aux', component ChildAuxComponent, outlet: 'footer'}
])

Navigating Auxiliary Routes

<a [routerLink]="[{outlets: {primary: ['child1'], footer: ['child-aux']}}]"></a>
<a [routerLink]="[{outlets: {footer: ['child-aux']}}]"></a>
<a [routerLink]="['/child', cId, 'grand-child', gcId]"></a>

Using Router DSL to construct URL segments /child/:id/grand-child/:id

Using Router DSL to load Auxiliary route into footer Outlet

Using Router DSL to load Auxiliary route and child into main / primary outlet

<a [routerLink]="[{outlets: {footer: null}}]"></a>

Clear Auxiliary outlet

Lazy Loading

Creating Decoupled Child Route Module

import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {RouterModule} from '@angular/router';
import {Child1Component} from './child1.component';

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild([
      {path: '', component: Child1Component}
    ])
  ],
  declarations: [Child1Component]
})
export default class Child1Module {}

Configuring Routing on Root Module

import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
import {MainComponent} from './main.component';

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      {path: 'child1', loadChildren: './src/child1/child1.module'}
    ])
  ],
  declarations: [MainComponent],
  bootstrap: [MainComponent]
})
export class MainModule {}

TESTING

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

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
}

Component Fixture

class DebugElement {
    nativeElement: any
    query(predicate: Predicate<DebugElement>): DebugElement
    queryAll(predicate: Predicate<DebugElement>): DebugElement[]
    children: DebugElement[]
    triggerEventHandler(eventName: string, eventObj: any)
}

Debug Element

import {DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser';

const countElement = fixture.debugElement
  .query(By.css('.count'))
  .nativeElement;

console.log(countElement.textContent);

Querying for Elements

import {DebugElement} from '@angular/core';

const buttonDebugElement: DebugElement = 
  fixture.debugElement.query(By.css('button'));

buttonDebugElement.triggerEventHandler('click', null);

Triggering Events

import {CounterService} from './counter.service';

export class CounterServiceMock() {
  increment() {}
}

TestBed.configureTestingModule({
  declarations: [MainComponent],
  providers: [{
    provide: CounterService, 
    useClass: CounterServiceMock
  }]
});

Mocking Services using provide()

Tesing
Excercise 

Adding unit tests to our TODO app

Performance

Performance Optimization

  • Optimize Change Detection

  • Ahead of Time Compilation

  • Server Side Rendering (Angular Universal)

Thank You!

 Gion Kunz

 Front End Developer

@GionKunz

gion.kunz@oddeven.ch

https://oddeven.ch