Open dherges opened 5 years ago
Herleitung: property binding, event binding, banana-in-the-box-syntax als Reminder
Refresher/Reminder: event bubbling from children to parent -> triggers change detection
Mit zone.js und NgZone: jedes DOM event fragt eine Change Detection an
Spezialfall Touch Gesten: eine Geste besteht aus mehreren Events, eine Geste sollte eine Change Detection anfragen. Jedoch nicht jedes Event.
Fallstricke:
@Output() fooChange = new EventEmitter();
/** WATCH OUT: this method is not running in NgZone! */
onTouchMove() {
this.fooChange.next(); // <-- target of event binding would also run outside NgZone
}
===>
/** WATCH OUT: this method is not running in NgZone! */
onTouchMove() {
this.ngZone.run(() => {
this.fooChange.next(); // <-- target of event binding will execute in NgZone (as expected)
});
}
ngOnDestroy() {
this.removeListener();
}
constructor(
private _cdr: ChangeDetectorRef,
public _sliderRef: ElementRef,
private ngZone: NgZone,
private renderer: Renderer2
) {
const nativeElement = this._sliderRef.nativeElement;
this.ngZone.runOutsideAngular(() => {
this.hammerSliderEl = new Hammer(nativeElement);
this.hammerSliderEl.on('panmove', guardOutsideAngular(event => this.onPanMove(event)));
});
}
ngOnDestroy() {
if (this.hammerSliderEl) {
// Gracefully clean up DOM Event listeners that were registed by hammer.js
this.hammerSliderEl.off('panmove');
this.hammerSliderEl.stop(true);
this.hammerSliderEl.destroy();
}
}
onPanMove($event) {
if (/* … magic expression … */) {
/* … only internal changes, no rendering needed … */)
} else {
this._cdr.detectChanges();
}
}
// Run in ngZone because it should update slider values outside component
this.unregisterListeners = [
this.renderer.listen(nativeElement, 'touchstart', event => this._onTouchStart(event)),
this.renderer.listen(nativeElement, 'mousedown', event => this._onClick(event))
];
if (this.unregisterListeners) {
this.unregisterListeners.forEach(fn => fn());
}
Idea: in Shadow DOM environments, let's go for CSS Variables! The component gives hooks how it can be styled, the container app decides what it looks like.
https://developers.google.com/web/fundamentals/web-components/shadowdom#stylefromoutside
@function my-theme-color($key: "primary”) {
// Returns the simple CSS variable expression `var(--primary)`
@return var(--#{$key});
}
@function my-theme-color($key: "primary”, $level: 0, $opacity: 0) {
// TODO: return HSLA calculation w/ CSS variable expression
}
Herleitung:
/deep/
, >>>
and ::ng-deep
-> deprecatedIdee: CSS Variablen als "style hooks"
https://developers.google.com/web/fundamentals/web-components/shadowdom#stylefromoutside
@Component({
selector: 'my-themable-component',
template: `<div class="theme-hook"></div>`,
styles: [`
.theme-hook {
background-color: var(--color-palette-background-light);
color: var(--color-palette-text-dark);
}
`]
})
export class MyThemableComponent {
/* .. */
}
Global Stylesheet my-theme.css
:
:root {
--color-palette-background-light: #fff;
--color-palette-text-dark: #333;
}
Ein Stylesheet um Mitternacht my-theme-dark.css
:
:root {
--color-palette-background-light: #222;
--color-palette-text-dark: #bbb;
}
Herleitung: darken()
, lighten()
, saturate()
, and so on are color manipulation functions, e.g. in SCSS
Output is a calculated #<rr><gg><bb>
HEX color code
Calculation is done at compilation (i.e.: expression does not update at runtime)
:host-context()
im komponenten style
Nachteile:
Exkurs: Der "Farbkreis", menschliche Farb-Wahrnehmung und der HSL-Farbraum
Demo an Hand Color Picker Tool
@Component({
selector: 'my-farbkreis',
template: `
<div class="eine-farbe">Dies ist eine Farbe</div>
<div class="komplementaer-farbe">Dies ist ihre Komplementär-Farbe</div>
`,
styles: [`
.eine-farbe {
background-color: var(--color-palette-base);
}
.komplementaer-farbe {
background-color: hsl(
var(--color-palette-base-hue) + 180,
var(--color-palette-base-saturation),
var(--color-palette-base-lightness)
);
}
`]
})
export class FarbkreisComponent {}
Vorteil(e):
Nachteil(e):
Use CDK Portal API to attach content to another physical location in the DOM.
Become notified about changes in ViewChildren, ContentChildren. Implement a decorator to subscribe changes
observable.
<ng-template>
Idee: Creating elements from a <template>
(Shadow DOM)
Angular Equivalent: <ng-template>
Anwendung:
<ng-template #myTemplate let-button>
<span>Hello {{ button.label }}</span>
</ng-template>
<my-button-toolbar buttonTemplate="myTemplate" buttons="buttons"></my-button-toolbar>
Implementierung:
@Component({
selector: 'my-button-toolbar',
template: `
<button myButton *ngFor="let button of buttons">
<ng-container *ngTemplateOutlet="buttonTemplate; context: button"></ng-container>
</button>
`
})
export class ButtonToolbarComponent {
@Input()
public buttons: ButtonInterface[];
@Input()
public buttonTemplate: TemplateRef<any>;
}
Anwendung:
<my-button-toolbar buttons="buttons">
<ng-template buttonTemplate let-button>
<span>Hello {{ button.label }}</span>
</ng-template>
</my-button-toolbar>
Implementierung:
@Component({
selector: 'my-button-toolbar',
template: `
<button myButton *ngFor="let button of buttons">
<ng-container *ngTemplateOutlet="buttonTemplate.templateRef; context: button"></ng-container>
</button>
`
})
export class ButtonToolbarComponent {
@Input()
public buttons: ButtonInterface[];
@ContentChild(ButtonTemplateDirective)
public buttonTemplate: ButtonTemplateDirective;
}
@Directive({ selector: '[buttonTemplate]' })
export class ButtonTemplateDirective {
constructor(
public templateRef: TemplateRef<any>
) {}
}
Use browser Fullscreen API to switch into fullscreen display.
Metapher: Automobil
Building Blocks for a car: mirror, wheel, exhaust, ...
The product: Golf VI, Golf VII, Golf VII Variant, Passat, Polo, ...
Can we re-use the same mirror in different car models?
Maybe, yes.
https://unsplash.com/photos/A53o1drQS2k
Context: the car model
https://unsplash.com/photos/JFQE8Ed2pIg
Variations: the exhaust needs to work on different car models, thus we need variations of an exhaust
Would you build a dedicated exhaust for each car model? Definetely not. We'd build an exhaust that fits into and adapts to the context.
https://unsplash.com/photos/qWwpHwip31M
Actually, the component is not the exhaust, but the moulding blank ("Rohling") of an exhaust.
Or: the component is a set of building instructions for the exhaust.
Other familiar case studies: web shop and shopping cart:
The questions: one shopping cart to rule them all?
Ideal goal: high cohesion, low coupling
Button and a Button Toolbar: the button needs to work as part of a toolbar (high cohesion between toolbar and button). The button also needs to work on its own (loose coupling between toolbar and button).
<button myButton>Go ahead</button>
<button myButton="primary">Go ahead</button>
<button-toolbar>
<button myButton>List</button>
<button myButton>Grid</button>
</button-toolbar>
What if...
<toolbar>
<toolbar-button>List</toolbar-button>
<toolbar-button>Grid</toolbar-button>
</toolbar>
<button myButton="primary-bordered">Go ahead</button>
<button myButton="primary" [myBordered]="true">Go ahead</button>
<button myButton class="btn-primary btn-bordered">Go ahead</button>
<primary-btn>Go ahead</primary-btn>
<secondary-btn>Go back</secondary-btn>
=> lots of possibilties. there may be a golden gun, but there's no silver bullet
Von...
@Directive({
selector: 'button'
})
export class ButtonDirective {}
...zu
@Directive({
selector: 'button[myButton]'
})
export class ButtonDirective {
@Input() myButton: 'primary' | 'secondary' = 'primary';
}
...zu
@Directive({
selector: '[myButton]'
})
export class ButtonDirective {
@Input() myButton: 'primary' | 'secondary' = 'primary';
}
<my-button>
vs. <button myButton>
Take Aways:
Komponente
<ng-content>
Kind-Inhalte projezierenDirektive
Mentales Model: Komponente = Direktive + Template
Mit Selektoren
import { ButtonDirective } from '@my/components/button';
@Component({
selector: 'my-button-toolbar',
template: `<ng-content select="button[myButton]"></ng-content>`
})
export class ButtonToolbarComponent {
@ContentChildren(ButtonDirective)
public buttons$: QueryList<ButtonDirective>;
}
<my-button-toolbar>
<button myButton *ngFor="let button of allButtons">{{ button.label }}</button>
</my-button-toolbar>
<hr>
<button myButton class="meta-button" (click)="onAddAnotherButton()">Button in Toolbar hinzufügen</button>
Lösungs-Idee: QueryList.changes: Observable<any>
@Component({
selector: 'my-button-toolbar',
template: `<ng-content select="button[myButton]"></ng-content>`
})
export class ButtonToolbarComponent implements AfterContentInit, OnDestroy {
@ContentChildren(ButtonDirective)
public buttons$: QueryList<ButtonDirective>;
private buttonContent: Subscription;
ngAfterContentInit() {
this.buttonContent = this.button$.changes.subscribe(
childButtons => { /* react to dynamic content change */ }
);
}
ngOnDestroy() {
if (thus.buttonContent) {
this.buttonContent.unscubscribe();
}
}
}
Syntax Sugar for a ngOnContentChanges()
lifecycle hook?
@Component({
selector: 'my-button-toolbar',
template: `<ng-content select="button[myButton]"></ng-content>`
})
export class ButtonToolbarComponent implements ContentChanges{
@ContentChildren(ButtonDirective)
public buttons$: QueryList<ButtonDirective>;
ngOnContentChanges(change) {
const buttonChange = change['button$'];
/* react to dynamic content change... */
}
}
Living collection in comments below