oddEVEN
We provide marketing technology, front end and software development consulting to bring your digital marketing and ecommerce projects to the next level.
By Gion Kunz
Build scalable UI Architectures using Angular Components
Herbert A. Simon, The Sciences of the Artificial (1969)
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...
Consists of ~1000 parts
Assembled of individual parts in one go
Fell apart completely, if he needed to answer calls
Consists of ~1000 parts
Assembled of intermediate stable components
Fell apart into components only
...complex systems evolve from simple systems much more rapidly when there are stable intermediate forms...
Herbert A. Simon
and so on...
Artificial
Nature
Microcosm
Macrocosm
Fundamental
Significant
Pages
UI Elements
We need to build user interfaces like this...
...not like that
Don't create components for reusability, create components for simplicity!
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>
<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>
<label class="checkbox__label">
<input class="checkbox__input" type="checkbox"
[checked]="checked"
(change)="onCheckedChange($event.target.checked)">
<span class="checkbox__text">{{label}}</span>
</label>
Allows to Focus
Fitness / Ready for Change
Flexibility
Reduce atomistic Complexity
Intrinsic
Extrinsic
@Component({
selector: 'jar',
template: '<ng-content></ng-content>'
})
export class Jar {}
@Component({
selector: 'app',
template: '<jar><p>Content</p></jar>'
})
export class App {}
@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 {}
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 {}
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;
}
}
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 {}
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;
}
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;
}
}
Can a Tab component exist without a Tabs component?
<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 *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>
<message-item *ngIf="!expanded"></message-item>
<message-details *ngIf="expanded"></message-details>
Encapsulated components with clear interfaces
Larger components by composition
Components interact with each other within their hierarchy
@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({
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);
}
}
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();
}
}
@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);
}
}
@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);
}
}
Re-use Components in different context
Clear responsibilities
Loose coupling with Input / Output
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
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
Click Event
2x
@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();
}
}
Data
Update
Container Component
Click Event
Performance Benefit
Simple to reason about
Easy to Reuse (no side effects)
Browser Dynamic
Web Worker
Server
class MyRendererFactory implements RendererFactory {
createRenderer(
element: any,
type: RendererType|null
): Renderer {}
}
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 {}
...
}
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');
@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
);
}
}
import {
GraphBrowserModule,
platformGraphDynamic
} from './graph-platform';
@NgModule({
imports: [GraphBrowserModule],
declarations: [GrowingListComponent],
bootstrap: [GrowingListComponent]
})
export class GrowingListModule {}
platformGraphDynamic().bootstrapModule(GrowingListModule);
@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!'
);
}
}
By oddEVEN