Scalable User INterfaces with Angular

By Gion Kunz

Build scalable UI Architectures using Angular Components

Why 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

and so on...

commonalities

Artificial

Nature

Microcosm

Macrocosm

Fundamental

Significant

We Tend to Develop on Two Levels only

Pages

UI Elements

We need to build user interfaces like this...

...not like that

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

<div class="task-list__l-container">
  <div class="task-list__l-box-c">
    <div class="task-list__tasks">
      <div *ngFor="let task of tasks" class="task">
        <div class="task__l-box-a">
          <div class="checkbox">
            <label class="checkbox__label">
              <input class="checkbox__input" type="checkbox"
                     [checked]="task.done"
                     (change)="task.done = $event.target.checked">
              <span class="checkbox__text"></span>
            </label>
          </div>
        </div>
        <div class="task__l-box-b">
          <div class="task__title">{{task.title}}</div>
        </div>
      </div>
    </div>
  </div>
</div>
<div class="task-list__l-container">
  <div class="task-list__l-box-c">
    <div class="task-list__tasks">
      <ngc-task *ngFor="let task of tasks" 
                [task]="task"
                class="task"></ngc-task>
    </div>
  </div>
</div>

Task List

<div class="task__l-box-a">
  <ngc-checkbox [(checked)]="task.done"></ngc-checkbox>
</div>
<div class="task__l-box-b">
  <div class="task__title">{{task.title}}</div>
</div>

Task

<label class="checkbox__label">
  <input class="checkbox__input" type="checkbox"
         [checked]="checked"
         (change)="onCheckedChange($event.target.checked)">
  <span class="checkbox__text">{{label}}</span>
</label>

Checkbox

Keep it simple!

  • Allows to Focus

  • Fitness / Ready for Change

  • Flexibility

  • Reduce atomistic Complexity

Composition

Intrinsic vs. Extrinsic Composition

Intrinsic

Extrinsic

What do you put into the empty jar?

What do you put into the empty jar?

Content Projection

@Component({
  selector: 'jar',
  template: '<ng-content></ng-content>'
})
export class Jar {}
@Component({
  selector: 'app',
  template: '<jar><p>Content</p></jar>'
})
export class App {}

Jar

App

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

@Component({
  selector: 'ngc-app',
  template: `
    <ngc-collapsible title="Click to reveal content">
      <p>I'm the content of the collapsible</p>
    </ngc-collapsible>
  `
})
export class App {}

App

import {Component, Input} from '@angular/core';

@Component({
  selector: 'ngc-collapsible',
  template: `
    <div (click)="toggle()">
      {{title}}
    </div>
    <div *ngIf="active"
         class="content">
      <ng-content></ng-content>
    </div>
  `
})
export class Collapsible {
  @Input() title: string;
  active: boolean;
  toggle() {
    this.active = !this.active;
  }
}

Collapsible

Prefer Composition over Bloating

Prefer Composition over Bloating

<profile-image [user]="user"></profile-image>
<div class="user-name">{{user.name}}</div>
<div class="title" *ngIf="title">{{title}}</div>
<div class="preview">{{message | truncate:50}}</div>
<div class="tag" *ngFor="let tag of tags">{{tag}}</div>
<div class="time" *ngIf="time">{{time}}</div>

Message Item

<section *ngIf="!expanded">
  <profile-image [user]="user"></profile-image>
  <div class="user-name">{{user.name}}</div>
  <div class="title" *ngIf="title">{{title}}</div>
  <div class="preview">{{message | truncate:50}}</div>
  <div class="tag" *ngFor="let tag of tags">{{tag}}</div>
  <div class="time" *ngIf="time">{{time}}</div>
</section>
<section *ngIf="expanded">
  <header>
    <profile-image [user]="user"></profile-image>
    <div class="to">{{to}}</div>
    <div class="time" *ngIf="time">{{time}}</div>
    <button (click)="viewDetails()">View Details</button>
    <button (click)="reply()">Reply</button>
    <button (click)="forward()">Forward</button>
  </header>
  <div class="message">{{message}}</div>
  <button (click)="showQuoted()">Show quoted text</button>
</section>

Bloating the Message Item

<message-item *ngIf="!expanded"></message-item>
<message-details *ngIf="expanded"></message-details>

Expandable Message Item

DATA

Inversion of Control

Data

Action

Container Component

Data

Action

import {Component, Input, Output, EventEmitter} from '@angular/core';

@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<any>();
  
  remove() {
    this.onRemove.emit();
  }
}

List Item

@Component({
  selector: 'ngc-list',
  template: `
    <ngc-list-item *ngFor="let item of list; let i = index"
                   [item]="item" (onRemove)="removeItem(i)">
    </ngc-list-item>
  `
})
export class ListComponent {
  @Input() list: string[];
  @Output() onRemoveItem = new EventEmitter<number>();
  
  removeItem(index: number) {
    this.onRemoveItem.emit(index);
  }
}

List

@Component({
  selector: 'ngc-app',
  template: `
    <ngc-list [list]="list"
              (onRemoveItem)="removeListItem($event)">
    </ngc-list>
  `
})
export class MainComponent {
  list: string[] = ['Apples', 'Bananas', 'Strawberries'];
  
  removeListItem(index: number) {
    this.list = this.list.filter((e, i) => i !== index);
  }
}

App

Centralize your Data!

  • Re-use Components in different context

  • Clear responsibilities

  • Loose coupling with Input / Output

Change Detection

Asynchronous Zones using Zone.js

document.querySelector('button')
  .addEventListener('click', () => {
    setTimeout(() => {
      fetch('./data.json')
        .then((response) => response.text())
        .then((text) => {
          console.log(text);
        });
    }, 1000);
  });

-> Root Zone

-> Sub Zone 1

-> Sub Zone 2

-> Sub Zone 3

<- Sub Zone 3 End

<- Sub Zone 2 End

<- Sub Zone 1 End

Using Zones to Detect Changes

document.querySelector('button')
  .addEventListener('click', () => {
    setTimeout(() => {
      fetch('./data.json')
        .then((response) => response.text())
        .then((text) => {
          console.log(text));
        }
    }, 1000);
  });

-> Root Zone

-> Sub Zone 1

-> Sub Zone 2

-> Sub Zone 3

<- Sub Zone 3 End

<- Sub Zone 2 End

<- Sub Zone 1 End

Detect Changes

By Default Angular checks every binding of every Component on any event

Click Event

Default Change Detection

Pure Components

Pure components change, only if their input changes

@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<any>();
  
  remove() {
    this.onRemove.emit();
  }
}

Pure? Yes!

@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<any>();
  
  remove() {
    this.onRemove.emit();
  }
}

"OnPush" Change Detection Strategy

Click Event

Change Detection With Pure Components

Build Pure Components!

  • Performance Benefit

  • Simple to reason about

  • Easy to Reuse (no side effects)

Platform

Angular

UNIVERSAL

Angular runs where JavaScript runs!

Core Platforms

  • Browser

  • Web Worker

  • Server

Custom Platform

class MyRenderer implements Renderer {
  createElement(name: string): any {}
  createComment(value: string): any {}
  createText(value: string): any {}
  appendChild(parent: any, newChild: any): any {}
  removeChild(parent: any, oldChild: any): void {}
  setAttribute(node: any, name: string, value: string): void {}
  removeAttribute(node: any, name: string): void {}
  addClass(el: any, name: string): void {}
  removeClass(el: any, name: string): void {}
  ...
}

Implement Renderer

Let's Create a Canvas Graph Rendering Platform!

import {
  GraphBrowserModule, 
  platformGraphDynamic
} from './graph-platform';

@NgModule({
  imports: [GraphBrowserModule],
  declarations: [GrowingListComponent],
  bootstrap: [GrowingListComponent]
})
export class GrowingListModule {}

platformGraphDynamic().bootstrapModule(GrowingListModule);

Bootstrapping using Graph Platform

Use the platform abstraction

@Component({
  selector: 'app',
  template: 'Hello World'
})
export class AppComponent {
  constructor(@Inject(ElementRef) elementRef) {
    elementRef.nativeElement
      .setAttribute('data-message', 'Hey!');
  }
}
@Component({
  selector: 'app',
  template: 'Hello World'
})
export class AppComponent {
  constructor(@Inject(ElementRef) elementRef,
              @Inject(Renderer) renderer) {
    renderer.setAttribute(
      elementRef.nativeElement, 
      'data-message', 
      'Hey!'
    );
  }
}

KEEP
CALM

CREATE
COMPONENTS

AND

Angular Workshop

FRIDAY, JULY 23 / MONDAY, JULY 26

10% Discount Code "VOXXED"

2nd Edition coming in Aug / Sep 2017

Thank You!

 Gion Kunz

 Front End Developer

@GionKunz

gion.kunz@oddeven.ch

https://oddeven.ch

Develop Scalable User Interfaces - 50min

By oddEVEN

Develop Scalable User Interfaces - 50min

  • 1,800