Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Angular: Utilities, Patterns, Memo #165

Open
dherges opened this issue Dec 14, 2018 · 5 comments
Open

Angular: Utilities, Patterns, Memo #165

dherges opened this issue Dec 14, 2018 · 5 comments

Comments

@dherges
Copy link
Contributor

dherges commented Dec 14, 2018

Living collection in comments below

@dherges dherges changed the title angular utilities oder patterns oder sonstwas.... Angular: Utilities, Patterns, Memo May 4, 2019
@dherges
Copy link
Contributor Author

dherges commented May 4, 2019

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
Copy link
Contributor Author

dherges commented May 4, 2019

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:

  • ViewEncapsulation / Shadow DOM vs. Globale Stylesheets
  • /deep/ , >>> and ::ng-deep -> deprecated

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:

  • komponente muss für jedes theme angepasst werden
  • calculation ist immer noch statisch (zur Kompile Zeit)

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

  • Farbwert wird "zur Laufzeit" als CSS Expression berechnet
  • Komponente stellt Style Hook bereit, Anwendung definiert beliebige Anzahl an Themes (Farb-Paletten)
  • CSS Variablen als "API contract": elegante Lösung

Nachteil(e):

  • Farbwert wird "zur Laufzeit" berechnet: komplexe Ausdrücke, Wertebereich HSL darf nicht verletzt werden, Einheiten beachten

@dherges
Copy link
Contributor Author

dherges commented May 9, 2019

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
Copy link
Contributor Author

dherges commented May 16, 2019

@dherges
Copy link
Contributor Author

dherges commented May 18, 2019

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:

  • shopping cart preview icon
  • shopping cart dropdown
  • full-page shopping cart to increase/decrease quantities
  • read-only shopping cart prior to payment confirmation.

Cohesion, Coupling

The questions: one shopping cart to rule them all?

  • x axis: number of contexts
  • y axis: number of variations
  • lower left: one component
  • upper right: many components
  • the difficult parts: anything in between

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

  • a button inside toolbar should always be a "primary" button?
  • each button can be borderless or bordered (e.g. two variations: style and border)?
<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

  • custom element
  • native Events wie click, select, change müssen ggf. "nach-implementiert" werden
  • bringt ein HTML Template und Component Stylesheets mit
  • kann optional mit <ng-content> Kind-Inhalte projezieren
  • hat optional ViewChildren und optional ContentChildren

Direktive

  • kann an beliebiges Element attached werden
  • Native Events sind "out-of-the-box" verfügbar.
  • implizite Content Projection
  • hat optional ContentChildren, aber keine ViewChildren

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

1 participant