Declarative Angular

When I first heard about compliant mechanisms, I was quite impressed. Although they surround us in everyday life - in the form of backpack fasteners, mouse buttons or shampoo caps - we rarely think about the concept of such devices.

, compliant- . (rigid body) , ompliant- , .

, , , , , , , โ€” . : , . , , , .

โ€” , , , . Angular-, .

Compliant-

Angular : -, , observable-. , . , . , , , .

, , . โ€” . , , .

. , SVG . , . SVG, :

@Component({
  selector: "svg[lineChart]",
  templateUrl: "./line-chart.template.html",
  styleUrls: ["./line-chart.style.less"],
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {    
    preserveAspectRatio: "none"
  }
})
export class LineChartComponent {}

. viewBox , :

  @HostBinding('attr.viewBox')
  get viewBox(): string {
    return `${this.x} ${this.y} ${this.width} ${this.height}`;
  }

, . path-:

<svg:path
  fill="none"
  stroke="currentColor"
  vector-effect="non-scaling-stroke"
  stroke-width="2"
  [attr.d]="d"
/>

d. . , . , :

  get d(): string {
    return this.data.reduce(
      (d, point, index) =>
        index ? `${d} ${draw(this.data, index, this.smoothing)}` : `M ${point}`,
      ""
    );
  }

! , , โ€” . , .

Media-

- . , - . . : , /. , :

  @Input()
  currentTime = 0;
 
  @Input()
  paused = true;
 
  @Input()
  @HostBinding("volume")
  volume = 1;
 
  @Output()
  readonly currentTimeChange = new EventEmitter<number>();
 
  @Output()
  readonly pausedChange = new EventEmitter<boolean>();
 
  @Output()
  readonly volumeChange = new EventEmitter<number>();
 
  @HostListener("volumechange")
  onVolumeChange() {
    this.volume = this.elementRef.nativeElement.volume;
    this.volumeChange.emit(this.volume);
  }

@HostBinding volume? . currentTime: . - . :

  @Input()
  set currentTime(currentTime: number) {
    if (currentTime !== this.currentTime) {
      this.elementRef.nativeElement.currentTime = currentTime;
    }
  }
 
  get currentTime(): number {
    return this.elementRef.nativeElement.currentTime;
  }
 
  @HostListener("timeupdate")
  @HostListener("seeking")
  @HostListener("seeked")
  onCurrentTimeChange() {
    this.currentTimeChange.emit(this.currentTime);
  }

ยซ/ยป:

  @Input()
  set paused(paused: boolean) {
    if (paused) {
      this.elementRef.nativeElement.pause();
    } else {
      this.elementRef.nativeElement.play();
    }
  }
 
  get paused(): boolean {
    return this.elementRef.nativeElement.paused;
  }

, :

<video
  #video
  media
  class="video"
  [(currentTime)]="currentTime"
  [(paused)]="paused"
  (click)="toggleState()"
>
  <ng-content></ng-content>
</video>
<div class="controls">
  <button
    class="button"
    type="button"
    title="Play/Pause"
    (click)="toggleState()"
  >
    {{icon}}
  </button>
  <input
    class="progress"
    type="range"
    [max]="video.duration"
    [(ngModel)]="currentTime"
  />
</div>

ng-content . :

  currentTime = 0; 
  paused = true;
 
  get icon(): string {
    return this.paused ? "\u23F5" : "\u23F8";
  }
 
  toggleState() {
    this.paused = !this.paused;
  }

-

, , . - โ€” , . ! , .

preventDefault. ng-event-plugins, .

. , . input, :

<combo-box [items]="items">
  <input type="text" [(ngModel)]="value">
</combo-box>

label , input . ! , . , :

<label>
  <ng-content></ng-content>
  <div class="toggle" (mousedown.prevent)="toggle()"></div>
</label>
<div *ngIf="open" class="list" (mousedown.prevent)="noop()">
  <div 
    *ngFor="let item of filteredItems; let index = index"
    class="item"
    [class.item_active]="isActive(index)"
    (click)="onClick(item)"
    (mouseenter)="onMouseEnter(index)"
  >
    {{item}}
  </div>
</div>

mousedown , . โ€” - . , .

, , . . NaN , number. :

  get open(): boolean {
    return !isNaN(this.index);
  }

. NgControl @ContentChild, . :

  @ContentChild(NgControl)
  private readonly control: NgControl;
 
  get value(): string {
    return String(this.control.value);
  }
 
  get filteredItems(): readonly string[] {
    return this.items.filter(item => 
      item.toLowerCase().includes(this.value.toLowerCase())
    );
  }

:

  get clampedIndex(): number {
    return limit(this.index, this.filteredItems.length - 1);
  }

// ...
 
function limit(value: number, max: number): number {
  return Math.max(Math.min(value || 0, max), 0);
}

, .

, . , :

  onClick(item: string) {
    this.selectItem(item);
  }
 
  onMouseEnter(index: number) {
    this.index = index;
  }
 
  @HostListener('keydown.esc')
  @HostListener('focusout')
  close() {
    this.index = NaN;
  }
 
  toggle() {
    this.index = this.open ? NaN : 0;
  }
 
  private selectItem(value: string) {
    this.control.control.setValue(value);
    this.close();
  }

: , Enter โ€” . :

  @HostListener('keydown.arrowDown.prevent', ['1'])
  @HostListener('keydown.arrowUp.prevent', ['-1'])
  onArrow(delta: number) {
    this.index = this.open 
      ? limit(
        this.clampedIndex + delta, 
        this.filteredItems.length - 1
      ) 
      : 0;
  }
 
  @HostListener('keydown.enter.prevent')
  onEnter() {
    this.selectItem(
      this.open
        ? this.filteredItems[this.clampedIndex]
        : this.value
    )
  }
 
  @HostListener('input')
  onInput() {
    this.index = this.clampedIndex;
  }

, -, . . , . ยซ ยป. : , . .

. ARIA-, aria-activedescendant. - . - .

, -47 ? , , . : , . โ€” . , โ€” . 

, : , ? , OnPush- . , , , Default , , Angular touched-.

, , . , viewBox, 1 , 300 android-. , . .

, . 100 15 โ€” . , , . 100 3 300 โ€” . โ€” JavaScript. . , , compliant- , .

, . . . , :

export function Pure<T>(
  _target: Object,
  propertyKey: string,
  { enumerable, value }: TypedPropertyDescriptor<T>
): TypedPropertyDescriptor<T> {
  const original = value;
 
  return {
    enumerable,
    get(): T {
      let previousArgs: ReadonlyArray<unknown> = [];
      let previousResult: any;
 
      const patched = (...args: Array<unknown>) => {
        if (
          previousArgs.length === args.length &&
          args.every((arg, index) => arg === previousArgs[index])
        ) {
          return previousResult;
        }
 
        previousArgs = args;
        previousResult = original(...args);
 
        return previousResult;
      };
 
      Object.defineProperty(this, propertyKey, {
        value: patched
      });
 
      return patched as any;
    }
  };
}

ยซ + ยป:

  get filteredItems(): readonly string[] {
    return this.filter(this.items, this.value);
  }
 
  @Pure
  private filter(items: readonly string[], value: string): readonly string[] {
    return items.filter(item => 
      item.toLowerCase().includes(value.toLowerCase())
    );
  }

. , ngOnChanges: stackblitz.com/edit/compliant-components-performance-ivy.

โ€” 1000 , . , . . , , @HostBinding, . , . , 1000, . @Pure- . 100 :

โ€” :

, android- 10%. : ยซ 10% !ยป . , , 60 . 1,5 , โ€” Angular. 

DOM . . ? , DOM, . , . 

, OnPush, . 

. , , . , , โ€” . , . , !

, , :

UI- Tinkoff. , . open-source, GitHub npm. Pure- Angular. , !




All Articles