sparkles-dev / sparkles

A web tech stack built on open source.
https://sparkles.dev
MIT License
2 stars 0 forks source link

Angular: Utilities, Patterns, Memo #165

Open dherges opened 5 years ago

dherges commented 5 years ago

Living collection in comments below

dherges commented 5 years ago

Touch Events and NgZone

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());
    }
dherges commented 5 years ago

An Approach for Theming With Shadow DOM And Web Components

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:

Idee: 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;
}
Advanced: Color Manipulation Functions

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)

Lösungs-Idee: "context-aware styles"

:host-context() im komponenten style

Nachteile:

Eine Neue Lösungs-Idee: CSS Color Manipulation mit dem HSL/A Farbraum

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):

dherges commented 5 years ago

DOM Outlet

Use CDK Portal API to attach content to another physical location in the DOM.

Content Projection: a custom lifecycle hook

Become notified about changes in ViewChildren, ContentChildren. Implement a decorator to subscribe changes observable.

draft code

Alternative to Content Projection: <ng-template>

Idee: Creating elements from a <template> (Shadow DOM)

Angular Equivalent: <ng-template>

Flavour 1: ng-template als Input property

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>;
}
Flavour 2: ng-template als content children

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>
  ) {}
}

Fullscreen API

Use browser Fullscreen API to switch into fullscreen display.

dherges commented 5 years ago

WebRTC

https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices

QR Codes

https://github.com/zxing-js/ngx-scanner

dherges commented 5 years ago

Design Systems on Angular

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.

Context

https://unsplash.com/photos/A53o1drQS2k

Context: the car model

Variations

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.

Component

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:

Cohesion, Coupling

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).

A Button...

<button myButton>Go ahead</button>
<button myButton="primary">Go ahead</button>

...and a Button Toolbar

<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>

A Dumb Idea?

<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

Open/Closed Principle

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

Component vs. Directive

<my-button> vs. <button myButton>

Take Aways:

Komponente

Direktive

Mentales Model: Komponente = Direktive + Template

Content Projection

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>;
}
Dynamischer Content
<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();
    }
  }
}
ngOnContentChanges()

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... */
  }
}