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!

5 Simple Rules for Components

  • Should have Single Responsibility

  • Should be Simple

  • Should be Small

  • Should be Encapsulated

  • Should use Composition

<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

Selective Projection

@Component({
  selector: 'selective-jar',
  template: '<ng-content select="p"></ng-content>'
})
export class SelectiveJar {}
@Component({
  selector: 'app',
  template: `
    <selective-jar>
      <p>Content</p>
      <div>Ignored</div>
    </selective-jar>
  `
})
export class App {}

Selective 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

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

@Component({
  selector: 'ngc-app',
  template: `
    <ngc-tabs>
      <ngc-tab name="Tab One">Content 1</ngc-tab>
      <ngc-tab name="Tab Two">Content 2</ngc-tab>
      <ngc-tab name="Tab Three">Content 3</ngc-tab>
    </ngc-tabs>
  `
})
export class App {}

App

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

@Component({
  selector: 'ngc-tab',
  template: `
    <div *ngIf="active">
      <ng-content></ng-content>
    </div>
  `
})
export class Tab {
  @Input() name: string;

  active: boolean = false;
}

Tab

import {Component, ContentChildren, QueryList} from '@angular/core';
import {Tab} from './tab';

@Component({
  selector: 'ngc-tabs',
  template: `
    <div class="tab-buttons">
        <button *ngFor="let tab of tabs" [class.active]="tab.active"
                (click)="activateTab(tab)">{{tab.name}}</button>
    </div>
    <ng-content select="ngc-tab"></ng-content>
  `
})
export class Tabs {
  @ContentChildren(Tab) tabs: QueryList<Tab>;

  ngAfterContentInit() {
    this.activateTab(this.tabs.first);
  }

  activateTab(tab) {
    this.tabs.toArray().forEach((t) => t.active = false);
    tab.active = true;
  }
}

Tabs

Composition vs. Aggregation

Can a Tab component exist without a Tabs component?

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

My UI Framework Whishlist

  • Encapsulated components with clear interfaces

  • Larger components by composition

  • Components interact with each other within their hierarchy

DATA

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

Inversion of Control

Data

Update

Container Component

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

2x

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

Data

Update

Container Component

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 Dynamic

  • Web Worker

  • Server

Custom Platform

class MyRendererFactory implements RendererFactory {
  createRenderer(
    element: any, 
    type: RendererType|null 
  ): Renderer {}
}

Implement a RendererFactory

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: Element, name: string): void {}
  addClass(el: Element, name: string): void {}
  removeClass(el: Element, name: string): void {}
  ...
}

Implement Renderer

export const MY_RENDER_PROVIDERS: Provider[] = [
  MyRendererFactory,
  {provide: RendererFactory, useClass: MyRendererFactory}
];

@NgModule({
  exports: [BrowserModule],
  providers: [
    MY_RENDER_PROVIDERS
  ],
})
export class MyPlatformModule {}

export const myPlatformDynamic =
    createPlatformFactory(platformCoreDynamic, 'MyPlatform');

Create Platform

@Component({
  selector: 'growing-list',
  template: `
    <h1>Growing list</h1>
    <ul>
      <li *ngFor="let item of items">{{item}}</li>
    </ul>
  `
})
export class GrowingListComponent {
  items: string[] = ['Item 1', 'Item 2', 'Item 3'];
  constructor() {
    setInterval(() => 
      this.items.push(`Item ${this.items.length + 1}`), 2000
    );
  }
}

Growing List

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!'
    );
  }
}

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

By oddEVEN

Develop Scalable User Interfaces

  • 1,596