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