22. Advanced Angular
ViewEncapsulation.Emulated (styles are scoped to component and do not leak outwards, default):
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-highlight',
template: `
<mat-card>
<div class="highlight">
the will to win is nothing without the will to prepare
</div>
</mat-card>
`,
styles: [`
mat-card {
width: 600px;
}
.highlight {
border: 3px solid red;
background-color: yellow;
text-align: center;
margin-bottom: 20px;
}
`],
encapsulation: ViewEncapsulation.Emulated
})
export class HighlightComponent { }
ViewEncapsulation.None (styles apply to all elements on the page, upward and downward, like global styles):
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-highlight',
template: `
<mat-card>
<div class="highlight">
the will to win is nothing without the will to prepare
</div>
</mat-card>
`,
styles: [`
mat-card {
width: 600px;
}
.highlight {
border: 3px solid red;
background-color: yellow;
text-align: center;
margin-bottom: 20px;
}
`],
encapsulation: ViewEncapsulation.None
})
export class HighlightComponent { }
ViewEncapsulation.Native Shadow DOM, everything inside #shadow-root element is encapsulated and isolated from the rest of the page:
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-highlight',
template: `
<mat-card>
<div class="highlight">
the will to win is nothing without the will to prepare
</div>
</mat-card>
`,
styles: [`
mat-card {
width: 600px;
}
.highlight {
border: 3px solid red;
background-color: yellow;
text-align: center;
margin-bottom: 20px;
}
`],
encapsulation: ViewEncapsulation.Native
})
export class HighlightComponent { }
import { Directive, ElementRef, Input, HostListener } from '@angular/core';
@Directive({
selector: '[appPopup]',
exportAs: 'appPopup'
})
export class PopupDirective {
@Input() message: string;
constructor(private el: ElementRef) {
console.log(el);
}
@HostListener('click') displayMessage(): void {
alert(this.message);
}
}
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<mat-toolbar color="primary">
<button (click)="sidenav.toggle()" mat-mini-fab><mat-icon>menu</mat-icon></button>
<span class="fill-remaining-space"></span>
<span>
Angular 5 Tour Of Technologies
</span>
</mat-toolbar>
<mat-sidenav-container>
<mat-sidenav #sidenav mode="side" class="app-sidenav">
<p>Sidenav Content here</p>
</mat-sidenav>
<div class="app-content">
<div>
<p appPopup #popup1="appPopup" message="clicked p tag">this should be bound to our popup directive</p>
<mat-icon appPopup #popup2="appPopup" message="clicked icon tag">favorite</mat-icon>
<button mat-raised-button color="warn" (click)="popup1.displayMessage()">
Display popup for p element
</button>
<button mat-raised-button color="primary" (click)="popup2.displayMessage()">
Display popup for icon element
</button>
</div>
<app-princess></app-princess>
</div>
</mat-sidenav-container>
`,
styles: [`
:host {
display: flex;
flex-direction: column;
flex: 1;
}
`]
})
export class AppComponent {
title = 'Tour of Technologies';
}
MessageComponent Content Projection:
import { Component, OnInit, Input, HostBinding, ViewEncapsulation } from '@angular/core';
@Component({
selector: '[appMessage]',
template: `
<div class="header">
{{ header }}
</div>
<mat-card class="size">
<ng-content></ng-content>
</mat-card>
`,
styles: [`
.size {
width: 600px;
}
`],
encapsulation: ViewEncapsulation.Emulated
})
export class MessageComponent implements OnInit {
@Input() header: string;
@HostBinding('attr.class') cssClass = 'mat-display-1';
ngOnInit() {
console.log('header', this.header);
}
}
Host Component:
<div class="app-content">
<div appMessage header="My Message">
I come to bring you love, peace and Open Source Software.
</div>
<app-princess></app-princess>
</div>
Let's view the results in our Chromium Web Browser:
OnInit/OnDestroy Component:
import { Component, OnInit, OnDestroy } from '@angular/core';
@Component({
selector: 'app-life-cycle-hooks',
template: `
<div>
<mat-icon>home</mat-icon> Init/Destroy
</div>
`,
styles: [``]
})
export class LifeCycleHooksComponent implements OnInit, OnDestroy {
constructor() { }
ngOnInit() {
console.log('OnInit invoked');
}
ngOnDestroy() {
console.log('OnDestroy invoked');
}
}
Host Component:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<mat-toolbar color="primary">
<button (click)="sidenav.toggle()" mat-mini-fab><mat-icon>menu</mat-icon></button>
<span class="fill-remaining-space"></span>
<span>
Angular 5 Tour Of Technologies
</span>
</mat-toolbar>
<mat-sidenav-container>
<mat-sidenav #sidenav mode="side" class="app-sidenav">
<p>Sidenav Content here</p>
</mat-sidenav>
<div class="app-content">
<mat-card>
<h4>OnInit and OnDestroy</h4>
<button mat-raised-button color="primary" (click)="toggle()">Toggle</button>
<app-life-cycle-hooks *ngIf="display"></app-life-cycle-hooks>
</mat-card>
<app-princess></app-princess>
</div>
</mat-sidenav-container>
`,
styles: [`
:host {
display: flex;
flex-direction: column;
flex: 1;
}
`]
})
export class AppComponent {
title = 'Tour of Technologies';
display = false;
toggle(): void {
this.display = !this.display;
}
}
OnChanges:
import { Component, OnChanges, Input, SimpleChange } from '@angular/core';
@Component({
selector: 'app-life-cycle-hooks',
template: `
<mat-card>
<mat-card-header>
<mat-card-title>{{ name }}</mat-card-title>
<mat-card-subtitle>{{ comment }}</mat-card-subtitle>
</mat-card-header>
</mat-card>
`,
styles: [``]
})
export class LifeCycleHooksComponent implements OnChanges {
@Input() name: string;
@Input() comment: string;
constructor() { }
ngOnChanges(changes: {[propName: string]: SimpleChange}) {
console.log('Changes', changes);
}
}
Host Component:
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<mat-toolbar color="primary">
<button (click)="sidenav.toggle()" mat-mini-fab><mat-icon>menu</mat-icon></button>
<span class="fill-remaining-space"></span>
<span>
Angular 5 Tour Of Technologies
</span>
</mat-toolbar>
<mat-sidenav-container>
<mat-sidenav #sidenav mode="side" class="app-sidenav">
<p>Sidenav Content here</p>
</mat-sidenav>
<div class="app-content">
<h4>OnChanges</h4>
<form>
<mat-form-field>
<input #namefld
matInput
(keyup)="setValues(namefld, commentfld)"
placeholder="Enter your name"
value="{{ name }}">
</mat-form-field>
<mat-form-field>
<textarea #commentfld
matInput
(keyup)="setValues(namefld, commentfld)"
placeholder="Leave a comment">{{ comment }}
</textarea>
</mat-form-field>
</form>
<app-life-cycle-hooks [name]="name" [comment]="comment">
</app-life-cycle-hooks>
<app-princess></app-princess>
</div>
</mat-sidenav-container>
`,
styles: [`
:host {
display: flex;
flex-direction: column;
flex: 1;
}
`]
})
export class AppComponent implements OnInit {
title = 'Tour of Technologies';
name = '';
comment = '';
ngOnInit() {
this.name = 'Nils-Holger Nägele';
this.comment = 'I love learning ...';
}
setValues(namefld, commentfld): void {
this.name = namefld.value;
this.comment = commentfld.value;
}
}
appIf:
import { Directive, Input, ViewContainerRef, TemplateRef } from '@angular/core';
@Directive({
selector: '[appIf]'
})
export class IfDirective {
@Input() set appIf(condition) {
if (condition) {
this.viewContainer.createEmbeddedView(this.template);
} else {
this.viewContainer.clear();
}
}
constructor(private viewContainer: ViewContainerRef,
private template: TemplateRef<any>) { }
}
Host Component:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<mat-toolbar color="primary">
<button (click)="sidenav.toggle()" mat-mini-fab><mat-icon>menu</mat-icon></button>
<span class="fill-remaining-space"></span>
<span>
Angular 5 Tour Of Technologies
</span>
</mat-toolbar>
<mat-sidenav-container>
<mat-sidenav #sidenav mode="side" class="app-sidenav">
<p>Sidenav Content here</p>
</mat-sidenav>
<div class="app-content">
<mat-card>
<button mat-raised-button color="warn" (click)="toggle()">
Toggle
</button>
<div *appIf="display">
this message is displayed if condition evaluates to true.
</div>
</mat-card>
<app-princess></app-princess>
</div>
</mat-sidenav-container>
`,
styles: [`
:host {
display: flex;
flex-direction: column;
flex: 1;
}
mat-card {
width: 400px;
}
`]
})
export class AppComponent {
title = 'Tour of Technologies';
display = true;
toggle() {
this.display = !this.display;
}
}
appFor:
import { Directive, IterableDiffer, IterableDiffers, ViewRef, ViewContainerRef,
TemplateRef, ChangeDetectorRef, DoCheck, Input} from '@angular/core';
@Directive({
selector: '[appFor]'
})
export class ForDirective implements DoCheck {
private items: any;
private differ: IterableDiffer<any>;
private views: Map<any, ViewRef> = new Map<any, ViewRef>();
constructor(private viewContainer: ViewContainerRef,
private template: TemplateRef<any>,
private differs: IterableDiffers) { }
@Input() set appForOf(items) {
this.items = items;
if (this.items && !this.differ) {
this.differ = this.differs.find(items).create();
}
}
ngDoCheck(): void {
if (this.differ) {
const changes = this.differ.diff(this.items);
if (changes) {
changes.forEachAddedItem(change => {
const view = this.viewContainer.createEmbeddedView(this.template,
{ $implicit: change.item });
this.views.set(change.item, view);
});
changes.forEachRemovedItem(change => {
const view = this.views.get(change.item);
const idx = this.viewContainer.indexOf(view);
this.viewContainer.remove(idx);
this.views.delete(change.item);
});
}
}
}
}
Host Component:
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<mat-toolbar color="primary">
<button (click)="sidenav.toggle()" mat-mini-fab><mat-icon>menu</mat-icon></button>
<span class="fill-remaining-space"></span>
<span>
Angular 5 Tour Of Technologies
</span>
</mat-toolbar>
<mat-sidenav-container>
<mat-sidenav #sidenav mode="side" class="app-sidenav">
<p>Sidenav Content here</p>
</mat-sidenav>
<div class="app-content">
<mat-card>
<mat-list>
<mat-list-item *appFor="let t of technologies">
{{ t.name }} is a {{ t.category }}
<button mat-raised-button color="warn" (click)="remove(t)">
x
</button>
</mat-list-item>
</mat-list>
<form>
<mat-form-field>
<input matInput #name placeholder="Name">
</mat-form-field>
<mat-form-field>
<textarea matInput #category placeholder="Category"></textarea>
</mat-form-field>
<button mat-raised-button color="primary" (click)="add(name, category)">
Add
</button>
</form>
</mat-card>
<app-princess></app-princess>
</div>
</mat-sidenav-container>
`,
styles: [`
:host {
display: flex;
flex-direction: column;
flex: 1;
}
mat-card {
width: 400px;
}
`]
})
export class AppComponent implements OnInit {
title = 'Tour of Technologies';
technologies: any[];
ngOnInit() {
this.technologies = [
{ name: 'Angular 5', category: 'Single Page Application Framework' },
{ name: 'Angular CLI 1.6', category: 'Tool to build, scaffold and maintain' },
{ name: 'Angular Material', category: 'CSS Framework' },
{ name: 'Angular Universal', category: 'Server Side Rendering' }
];
}
remove(technology) {
const idx = this.technologies.indexOf(technology);
this.technologies.splice(idx, 1);
return false;
}
add(name, category) {
this.technologies.push({ name: name.value, category: category.value });
name.value = '';
category.value = '';
}
}
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
export class Overlay {
private el: HTMLElement;
constructor() {
const elem = document.createElement('div');
elem.className = 'tooltip';
this.el = elem;
}
close() {
this.el.hidden = true;
}
open(elem, text) {
this.el.innerHTML = text;
this.el.hidden = false;
const rect = elem.nativeElement.getBoundingClientRect();
this.el.style.left = rect.left + 'px';
this.el.style.right = rect.top + 'px';
}
attach(target) {
target.appendChild(this.el);
}
detach() {
this.el.parentNode.removeChild(this.el);
}
}
@Directive({
selector: '[appToolTip]'
})
export class ToolTipDirective {
@Input() appToolTip: string;
constructor(private el: ElementRef, private overlay: Overlay) {
this.overlay.attach(el.nativeElement);
}
@HostListener('mouseenter')
onMouseEnter() {
this.overlay.open(this.el, this.appToolTip);
}
@HostListener('mouseleave')
onMouseLeave() {
this.overlay.close();
}
}
HostComponent:
import { Component } from '@angular/core';
import { Overlay } from './tool-tip.directive';
@Component({
selector: 'app-root',
template: `
<mat-toolbar color="primary">
<button (click)="sidenav.toggle()" mat-mini-fab><mat-icon>menu</mat-icon></button>
<span class="fill-remaining-space"></span>
<span>
{{ title }}
</span>
</mat-toolbar>
<mat-sidenav-container>
<mat-sidenav #sidenav mode="side" class="app-sidenav">
<p>Sidenav Content here</p>
</mat-sidenav>
<div class="app-content">
<mat-card>
<div appToolTip="hello baby">My Number is 42</div>
</mat-card>
<app-princess></app-princess>
</div>
</mat-sidenav-container>
`,
providers: [Overlay],
styles: [`
:host {
display: flex;
flex-direction: column;
flex: 1;
}
mat-card {
width: 400px;
}
div[appToolTip] {
width: 150px;
height: 50px;
cursor: pointer;
color: red;
}
.tooltip {
position: absolute;
background: white;
}
`]
})
export class AppComponent {
title = 'Angular 5 Tour Of Technologies';
}
ZippyComponent:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-zippy-header',
template: `
<header>{{ header }}</header>
`,
styles: [`
header {
cursor: pointer;
border-bottom: 1px solid #ccc;
font-size: 1.2em;
background-color: #eee;
}
`]
})
export class ZippyHeaderComponent {
@Input() header: string;
}
@Component({
selector: 'app-zippy',
template: `
<section>
<app-zippy-header
(click)="visible = !visible"
[header]="header">
</app-zippy-header>
<div *ngIf="visible">
<ng-content select="content"></ng-content>
</div>
</section>
`,
styles: [`
section {
width: 300px;
border: 1px solid #ccc;
}
`]
})
export class ZippyComponent {
@Input() header: string;
visible = true;
}
Host Component:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<mat-toolbar color="primary">
<button (click)="sidenav.toggle()" mat-mini-fab><mat-icon>menu</mat-icon></button>
<span class="fill-remaining-space"></span>
<span>
{{ title }}
</span>
</mat-toolbar>
<mat-sidenav-container>
<mat-sidenav #sidenav mode="side" class="app-sidenav">
<p>For My Polymer Princess Carmen</p>
</mat-sidenav>
<div class="app-content">
<mat-card>
<app-zippy header="Header">
<content>
The world is your oyster. Live life to the fullest.
Enjoy every second. Carpe Diem. Peace and Love.
</content>
</app-zippy>
</mat-card>
<app-princess></app-princess>
</div>
</mat-sidenav-container>
`,
styles: [`
:host {
display: flex;
flex-direction: column;
flex: 1;
}
mat-card {
width: 400px;
height: 200px;
}
`]
})
export class AppComponent {
title = 'Angular 5 Tour Of Technologies';
}
Custom Component:
(function() {
const template = document.createElement('template');
template.innerHTML = `
<style>
:host {
all: initial;
display: block;
contain: content;
text-align: center;
background: linear-gradient(to left, hotpink, transparent);
max-width: 500px;
margin: 0 auto;
border-radius: 8px;
transition: transform .2s ease-out;
}
:host([hidden]) {
display: none;
}
button,
span {
font-size: 3rem;
font-family: monospace;
padding: 0 .5rem;
}
button {
cursor: pointer;
background: pink;
color: black;
border: 0;
border-radius: 6px;
box-shadow: 0 0 5px rgba(173, 61, 85, .5);
}
button:active {
background: #ad3d55;
color: white;
}
</style>
<div>
<button type="button" increment>+</button>
<span></span>
<button type="button" decrement>-</button>
</div>
`;
class MyCounter extends HTMLElement {
constructor() {
super();
this.increment = this.increment.bind(this);
this.decrement = this.decrement.bind(this);
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
this.incrementBtn = this.shadowRoot.querySelector('[increment]');
this.decrementBtn = this.shadowRoot.querySelector('[decrement]');
this.displayVal = this.shadowRoot.querySelector('span');
this.maxReached = new CustomEvent('maxReached');
this.minReached = new CustomEvent('minReached');
}
connectedCallback() {
this.incrementBtn.addEventListener('click', this.increment);
this.decrementBtn.addEventListener('click', this.decrement);
if (!this.hasAttribute('value')) {
this.setAttribute('value', 1);
}
}
increment() {
const step = +this.step || 1;
const newValue = +this.value + step;
if (this.max) {
if (newValue > +this.max) {
this.value = +this.max;
this.dispatchEvent(this.maxReached);
} else {
this.value = +newValue;
}
} else {
this.value = +newValue;
}
}
decrement() {
const step = +this.step || 1;
const newValue = +this.value - step;
if (this.min) {
if (newValue < +this.min) {
this.value = +this.min;
this.dispatchEvent(this.minReached);
} else {
this.value = +newValue;
}
} else {
this.value = +newValue;
}
}
static get observedAttributes() {
return ['value'];
}
attributeChangedCallback(name, oldValue, newValue) {
this.displayVal.innerText = this.value;
}
get value() {
return this.getAttribute('value');
}
get step() {
return this.getAttribute('step');
}
get min() {
return this.getAttribute('min');
}
get max() {
return this.getAttribute('max');
}
set value(newValue) {
this.setAttribute('value', newValue);
}
set step(newValue) {
this.setAttribute('step', newValue);
}
set min(newValue) {
this.setAttribute('min', newValue);
}
set max(newValue) {
this.setAttribute('max', newValue);
}
disconnectedCallback() {
this.incrementBtn.removeEventListener('click', this.increment);
this.decrementBtn.removeEventListener('click', this.decrement);
}
}
window.customElements.define('my-counter', MyCounter);
})();
HostComponent:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<mat-toolbar color="primary">
<button (click)="sidenav.toggle()" mat-mini-fab><mat-icon>menu</mat-icon></button>
<span class="fill-remaining-space"></span>
<span>
{{ title }}
</span>
</mat-toolbar>
<mat-sidenav-container>
<mat-sidenav #sidenav mode="side" class="app-sidenav">
<p>For My Polymer Princess Carmen</p>
</mat-sidenav>
<div class="app-content">
<mat-grid-list cols="2">
<mat-grid-tile>
<mat-card>
<h3>Hello {{ name }}</h3>
<my-counter min="0" value="8" max="1024" step="2"></my-counter>
</mat-card>
</mat-grid-tile>
<mat-grid-tile>
<app-princess></app-princess>
</mat-grid-tile>
</mat-grid-list>
</div>
</mat-sidenav-container>
`,
styles: [`
:host {
display: flex;
flex-direction: column;
flex: 1;
}
mat-card {
width: 400px;
}
`]
})
export class AppComponent {
title = 'Angular 5 Tour Of Technologies';
name = 'Nils-Holger Nägele';
}
Let's view the results in Chromium:
TodoComponent:
import { Component, ViewEncapsulation } from '@angular/core';
interface Todo {
completed: boolean;
label: string;
}
@Component({
selector: 'app-todo',
template: `
<mat-list>
<h2>Good Morning {{ name }}!</h2>
<mat-form-field>
<input #newTodo matInput placeholder="Enter a new todo">
</mat-form-field>
<button mat-raised-button color="accent"
(click)="add(newTodo.value); newTodo.value = ''">
Add
</button>
<h3>What needs to be done?</h3>
<mat-list-item
*ngFor="let todo of todos;
let idx = index;" [class.completed]="todo.completed">
<mat-checkbox [checked]="todo.completed" (change)="toggle(idx)">
{{ todo.label }}
</mat-checkbox>
</mat-list-item>
</mat-list>
`,
styles: [`
.completed {
text-decoration: line-through;
}
`],
encapsulation: ViewEncapsulation.Emulated
})
export class TodoComponent {
todos: Todo[] = [
{ label: 'Create new Angular 5 Apps', completed: false },
{ label: 'Write new Angular 5 Tutorials', completed: false },
{ label: 'Write Angular 4 Love Affair Gitbook', completed: false },
{ label: 'Read + Code Angular Documentation', completed: false },
{ label: 'Make her a bunch', completed: false },
{ label: 'Save The World', completed: false }
];
name = 'Nils-Holger';
add(label) {
if (!label) { return; }
this.todos.push({ label: label, completed: false });
}
remove(idx) {
this.todos.splice(idx, 1);
}
toggle(idx) {
const todo = this.todos[idx];
todo.completed = !todo.completed;
}
}
AppComponent:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<mat-toolbar color="primary">
<button (click)="sidenav.toggle()" mat-mini-fab><mat-icon>menu</mat-icon></button>
<span class="fill-remaining-space"></span>
<span>
{{ title }}
</span>
</mat-toolbar>
<mat-sidenav-container>
<mat-sidenav #sidenav mode="side" class="app-sidenav">
<p>For My Polymer Princess Carmen</p>
</mat-sidenav>
<div class="app-content">
<mat-grid-list cols="2">
<mat-grid-tile>
<mat-card>
<app-todo></app-todo>
</mat-card>
</mat-grid-tile>
<mat-grid-tile>
<app-princess></app-princess>
</mat-grid-tile>
</mat-grid-list>
</div>
</mat-sidenav-container>
`,
styles: [`
:host {
display: flex;
flex-direction: column;
flex: 1;
}
mat-card {
width: 400px;
}
`]
})
export class AppComponent {
title = 'Angular 5 Tour Of Technologies';
}
Let's have a look at the results in Chromium:
import { Component, ViewEncapsulation, Input, Output, EventEmitter } from '@angular/core';
interface Todo {
completed: boolean;
label: string;
}
@Component({
selector: 'app-todo-input',
template: `
<mat-form-field>
<input #newTodo matInput [placeholder]="placeholder">
</mat-form-field>
<button mat-raised-button color="accent"
(click)="emit(newTodo.value); newTodo.value = ''">
{{ buttonLabel }}
</button>
`,
encapsulation: ViewEncapsulation.Emulated
})
export class TodoInputComponent {
@Input() placeholder: string;
@Input() buttonLabel: string;
@Output() add = new EventEmitter<string>();
emit(label) {
this.add.emit(label);
}
}
@Component({
selector: 'app-todo-list',
template: `
<mat-list>
<mat-list-item
*ngFor="let todo of todos;
let idx = index;" [class.completed]="todo.completed">
<mat-checkbox [checked]="todo.completed" (change)="toggle(idx)">
{{ todo.label }}
</mat-checkbox>
</mat-list-item>
</mat-list>
`,
styles: [`
.completed {
text-decoration: line-through;
}
`],
encapsulation: ViewEncapsulation.Emulated
})
export class TodoListComponent {
@Input() todos: Todo[];
@Output() toggleCompletion = new EventEmitter<Todo>();
toggle(idx: number) {
const todo = this.todos[idx];
this.toggleCompletion.emit(todo);
}
}
@Component({
selector: 'app-todo',
template: `
<h2>Good Morning {{ name }}!</h2>
<app-todo-input
[placeholder]="'Enter a new todo'"
[buttonLabel]="'add'"
(add)="add($event)">
</app-todo-input>
<h3>What needs to be done?</h3>
<app-todo-list [todos]="todos" (toggleCompletion)="toggle($event)">
</app-todo-list>
`,
encapsulation: ViewEncapsulation.Emulated
})
export class TodoComponent {
todos: Todo[] = [
{ label: 'Create new Angular 5 Apps', completed: false },
{ label: 'Write new Angular 5 Tutorials', completed: false },
{ label: 'Write Angular 4 Love Affair Gitbook', completed: false },
{ label: 'Read + Code Angular Documentation', completed: false },
{ label: 'Make her a bunch', completed: false },
{ label: 'Save The World', completed: false }
];
name = 'Nils-Holger';
add(label) {
if (!label) { return; }
this.todos.push({ label: label, completed: false });
}
toggle(todo: Todo) {
todo.completed = !todo.completed;
}
}
Content Projection:
import { Component } from '@angular/core';
@Component({
selector: 'app-fancy-button',
template: `
<button mat-raised-button color="accent"><ng-content></ng-content></button>
`
})
export class FancyButtonComponent { }
HostComponent:
<mat-card>
<app-fancy-button>Click <i>me</i> now!</app-fancy-button>
</mat-card>
Let's view the results in Chromium:
Content projection multiple chunks:
import { Component } from '@angular/core';
@Component({
selector: 'app-panel',
template: `
<mat-accordion>
<mat-expansion-panel (opened)="panelOpenState = true"
(closed)="panelOpenState = false">
<mat-expansion-panel-header>
<mat-panel-title>
<ng-content select=".panel-title"></ng-content>
</mat-panel-title>
<mat-panel-description>
Currently I am {{ panelOpenState ? 'open' : 'closed' }}
</mat-panel-description>
</mat-expansion-panel-header>
<ng-content select=".panel-content"></ng-content>
</mat-expansion-panel>
</mat-accordion>
`
})
export class PanelComponent {
panelOpenState = false;
}
Host Component:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<mat-toolbar color="primary">
<button (click)="sidenav.toggle()" mat-mini-fab><mat-icon>menu</mat-icon></button>
<span class="fill-remaining-space"></span>
<span>
{{ title }}
</span>
</mat-toolbar>
<mat-sidenav-container>
<mat-sidenav #sidenav mode="side" class="app-sidenav">
<p>For My Polymer Princess Carmen</p>
</mat-sidenav>
<div class="app-content">
<mat-grid-list cols="2">
<mat-grid-tile>
<mat-card>
<app-panel>
<span class="panel-title">Self Aware Panel</span>
<p class="panel-content">I have met my hero, he is me.</p>
</app-panel>
</mat-card>
</mat-grid-tile>
<mat-grid-tile>
<app-princess></app-princess>
</mat-grid-tile>
</mat-grid-list>
</div>
</mat-sidenav-container>
`,
styles: [`
:host {
display: flex;
flex-direction: column;
flex: 1;
}
mat-card {
width: 400px;
}
`]
})
export class AppComponent {
title = 'Angular 5 Tour Of Technologies';
}
Let's view the results in our Chromium Web Browser:
Immutable Data Structures, Change Detection Strategies:
Immutable:
import { Component, ViewEncapsulation, Input,
Output, EventEmitter, ChangeDetectionStrategy,
DoCheck } from '@angular/core';
import * as Immutable from 'immutable';
interface Todo {
completed: boolean;
label: string;
}
@Component({
selector: 'app-todo-input',
template: `
<mat-form-field>
<input #newTodo matInput [placeholder]="placeholder">
</mat-form-field>
<button mat-raised-button color="accent"
(click)="emit(newTodo.value); newTodo.value = '';">
{{ buttonLabel }}
</button>
`,
encapsulation: ViewEncapsulation.Emulated
})
export class TodoInputComponent implements DoCheck {
@Input() placeholder: string;
@Input() buttonLabel: string;
@Output() add = new EventEmitter<string>();
emit(label) {
this.add.emit(label);
}
ngDoCheck() {
console.log('Change detection runs in TodoInputComponent');
}
}
@Component({
selector: 'app-todo-list',
template: `
<mat-list>
<mat-list-item
*ngFor="let todo of todos; let idx = index;"
[class.completed]="todo.get('completed')">
<mat-checkbox [checked]="todo.get('completed')" (change)="toggle(idx)">
{{ todo.get('label') }}
</mat-checkbox>
</mat-list-item>
</mat-list>
`,
styles: [`
.completed {
text-decoration: line-through;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.Emulated
})
export class TodoListComponent implements DoCheck {
@Input() todos;
@Output() toggleCompletion = new EventEmitter<number>();
toggle(idx: number) {
this.toggleCompletion.emit(idx);
}
ngDoCheck() {
console.log('Change detection runs in TodoListComponent');
}
}
@Component({
selector: 'app-todo',
template: `
<h2>Good Morning {{ name }}!</h2>
<app-todo-input
[placeholder]="'Enter a new todo'"
[buttonLabel]="'add'"
(add)="add($event)">
</app-todo-input>
<h3>What needs to be done?</h3>
<app-todo-list [todos]="todos" (toggleCompletion)="toggle($event)">
</app-todo-list>
`,
encapsulation: ViewEncapsulation.Emulated
})
export class TodoComponent implements DoCheck {
todos = Immutable.fromJS([
{ label: 'Create new Angular 5 Apps', completed: false },
{ label: 'Write new Angular 5 Tutorials', completed: false },
{ label: 'Write Angular 4 Love Affair Gitbook', completed: false },
{ label: 'Read + Code Angular Documentation', completed: false },
{ label: 'Make her a bunch', completed: false },
{ label: 'Save The World', completed: false }
]);
name = 'Nils-Holger';
add(label) {
if (!label) { return; }
this.todos = this.todos.push(Immutable.fromJS({ label: label, completed: false }));
}
toggle(idx: number) {
this.todos = this.todos.update(idx, todo => {
return Immutable.fromJS({
label: todo.label,
completed: !todo.completed
});
});
}
ngDoCheck() {
console.log('Change detection runs in TodoComponent');
}
}
Injector Basics:
import { NgModule, Component, Inject, InjectionToken, Injectable } from '@angular/core';
import { CommonModule } from '@angular/common';
const BUFFER_SIZE = new InjectionToken<number>('buffer-size');
class Buffer {
constructor(@Inject(BUFFER_SIZE) private size: number) {
console.log(this.size);
}
}
@Injectable()
class SocketService {
constructor(private buffer: Buffer) { }
}
@Component({
selector: 'app-injector-basics',
template: `Open your browser's console`
})
class InjectorBasicsComponent {
constructor(private socketService: SocketService) {
console.log(socketService);
}
}
@NgModule({
imports: [ CommonModule],
declarations: [ InjectorBasicsComponent ],
exports: [ InjectorBasicsComponent ],
providers: [{ provide: BUFFER_SIZE, useValue: 42 }, Buffer, SocketService]
})
export class InjectorBasicsModule { }
forwardRef:
import { NgModule, Component, Inject,
InjectionToken, Injectable, forwardRef } from '@angular/core';
import { CommonModule } from '@angular/common';
const BUFFER_SIZE = new InjectionToken<number>('buffer-size');
@Injectable()
class SocketService {
constructor(@Inject(forwardRef(() => Buffer)) private buffer: Buffer) { }
}
class Buffer {
constructor(@Inject(BUFFER_SIZE) private size: number) {
console.log(this.size);
}
}
Developer Class:
export class Developer {
public id: number;
public githubHandle: string;
public avatarUrl: string;
public realName: string;
public email: string;
public technology: string;
public popular: boolean;
}
DeveloperCollection Class:
import { Developer } from './developer';
export class DeveloperCollection {
private developers: Developer[] = [];
getUserByGitHubHandle(username: string): Developer {
return this.developers.filter(u => u.githubHandle === username)
.pop();
}
getUserById(id: number): Developer {
return this.developers.filter(u => u.id === id).pop();
}
addDeveloper(dev: Developer): void {
this.developers.push(dev);
}
getAll(): Developer[] {
return this.developers;
}
}
Route Configurations:
const routeModule = RouterModule.forRoot([
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
},
{
path: 'home',
component: HomeComponent
},
{
path: 'dev-add',
component: AddDeveloperComponent
},
{
path: 'add-dev',
redirectTo: 'dev-add'
}
]);
routerLink && router-outlet:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<mat-toolbar color="primary">
<button (click)="sidenav.toggle()" mat-mini-fab><mat-icon>menu</mat-icon></button>
<span class="fill-remaining-space"></span>
<span>
{{ title }}
</span>
</mat-toolbar>
<mat-sidenav-container>
<mat-sidenav #sidenav mode="side" class="app-sidenav">
<p>For My Polymer Princess Carmen</p>
</mat-sidenav>
<div class="app-content">
<mat-grid-list cols="2" rowHeight="200px">
<mat-grid-tile>
<nav mat-tab-nav-bar>
<a mat-tab-link [routerLink]="['home']">Home</a>
<a mat-tab-link [routerLink]="['dev-add']">Add Developer</a>
</nav>
</mat-grid-tile>
<mat-grid-tile rowspan="2">
<app-princess></app-princess>
</mat-grid-tile>
<mat-grid-tile colspan="1">
<router-outlet></router-outlet>
</mat-grid-tile>
</mat-grid-list>
</div>
</mat-sidenav-container>
`,
styles: [`
:host {
display: flex;
flex-direction: column;
flex: 1;
}
`]
})
export class AppComponent {
title = 'Angular 5 Tour Of Technologies';
}
EmailValidator:
import { Directive } from '@angular/core';
import { NG_VALIDATORS } from '@angular/forms';
function validateEmail(emailControl) {
if (!emailControl.value || /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(emailControl.value)) {
return null;
} else {
return { 'invalidEmail': true };
}
}
@Directive({
selector: '[appEmailValidator]',
providers: [{
provide: NG_VALIDATORS,
multi: true,
useValue: validateEmail
}]
})
export class EmailValidatorDirective { }