From 1da18c754a6f1a84d2ccd1419197ae7c0627cc89 Mon Sep 17 00:00:00 2001 From: divdavem Date: Wed, 25 Oct 2023 18:14:13 +0200 Subject: [PATCH] refactor: useDirectiveForHost utility in Angular (#199) --- angular/headless/src/lib/use.directive.ts | 76 ++++++++++--------- .../src/lib/accordion/accordion.component.ts | 34 +-------- .../lib/src/lib/slider/slider.component.ts | 45 ++++------- 3 files changed, 61 insertions(+), 94 deletions(-) diff --git a/angular/headless/src/lib/use.directive.ts b/angular/headless/src/lib/use.directive.ts index 9fae0e53ae..fa73939b6b 100644 --- a/angular/headless/src/lib/use.directive.ts +++ b/angular/headless/src/lib/use.directive.ts @@ -1,5 +1,5 @@ -import type {OnChanges, OnDestroy, SimpleChanges} from '@angular/core'; -import {Directive, ElementRef, inject, Input} from '@angular/core'; +import type {OnChanges} from '@angular/core'; +import {DestroyRef, Directive, ElementRef, inject, Input} from '@angular/core'; import type {Directive as AgnosUIDirective} from '@agnos-ui/core'; // All calls of the directive in this class are done asynchronously (with await 0) @@ -7,51 +7,59 @@ import type {Directive as AgnosUIDirective} from '@agnos-ui/core'; // or the corresponding issue with signals (https://github.com/angular/angular/issues/50320) // This is relevant especially if calling the directive changes variables used in a template. -@Directive({ - standalone: true, - selector: '[auUse]', -}) -export class UseDirective implements OnChanges, OnDestroy { - @Input('auUse') - use: AgnosUIDirective | undefined; +export const useDirectiveForHost = (use?: AgnosUIDirective, params?: T) => { + const ref = inject(ElementRef); - @Input('auUseParams') - params: T | undefined; - - #ref = inject(ElementRef); + let instance = use?.(ref.nativeElement, params as T); - #directive: AgnosUIDirective | undefined; - #directiveInstance?: ReturnType>; - - async #destroyDirectiveInstance() { - const directiveInstance = this.#directiveInstance; - this.#directiveInstance = undefined; - if (directiveInstance?.destroy) { + async function destroyDirectiveInstance() { + const oldInstance = instance; + instance = undefined; + use = undefined; + if (oldInstance?.destroy) { await 0; - directiveInstance.destroy?.(); + oldInstance.destroy?.(); } } - async ngOnChanges(changes: SimpleChanges): Promise { - if (this.use !== this.#directive) { - this.#destroyDirectiveInstance(); - const directive = this.use; - this.#directive = directive; - if (directive) { + inject(DestroyRef).onDestroy(destroyDirectiveInstance); + + async function update(newUse?: AgnosUIDirective, newParams?: T) { + if (newUse !== use) { + destroyDirectiveInstance(); + use = newUse; + params = newParams; + if (newUse) { await 0; // checks that the directive did not change while waiting: - if (directive === this.#directive && !this.#directiveInstance) { - this.#directiveInstance = directive(this.#ref.nativeElement, this.params as T); + if (use === newUse && !instance) { + instance = use(ref.nativeElement, params as T); } } - } else if (changes['params']) { + } else if (newParams != params) { + params = newParams; await 0; - this.#directiveInstance?.update?.(this.params as T); + instance?.update?.(params as T); } } - ngOnDestroy(): void { - this.#destroyDirectiveInstance(); - this.#directive = undefined; + return {update}; +}; + +@Directive({ + standalone: true, + selector: '[auUse]', +}) +export class UseDirective implements OnChanges { + @Input('auUse') + use: AgnosUIDirective | undefined; + + @Input('auUseParams') + params: T | undefined; + + #useDirective = useDirectiveForHost(); + + ngOnChanges() { + this.#useDirective.update(this.use, this.params); } } diff --git a/angular/lib/src/lib/accordion/accordion.component.ts b/angular/lib/src/lib/accordion/accordion.component.ts index c44a2bb5cd..5282fad734 100644 --- a/angular/lib/src/lib/accordion/accordion.component.ts +++ b/angular/lib/src/lib/accordion/accordion.component.ts @@ -16,6 +16,7 @@ import { patchSimpleChanges, toAngularSignal, toSlotContextWidget, + useDirectiveForHost, } from '@agnos-ui/angular-headless'; import {NgIf} from '@angular/common'; import type {AfterContentChecked, AfterViewInit, OnChanges, Signal, SimpleChanges} from '@angular/core'; @@ -109,11 +110,6 @@ const defaultConfig: Partial = { '[class]': '"accordion-item " + state().itemClass', '[id]': 'state().itemId', }, - hostDirectives: [ - { - directive: UseDirective, - }, - ], imports: [SlotDirective, UseDirective], template: ` `, }) @@ -201,19 +197,10 @@ export class AccordionItemComponent implements OnChanges, AfterContentChecked, A }); readonly widget = toSlotContextWidget(this._widget); readonly api = this._widget.api; - useDirective = inject(UseDirective); state: Signal = toAngularSignal(this._widget.state$); constructor() { - this.useDirective.use = this._widget.directives.accordionItemDirective; - this.useDirective.ngOnChanges({ - useDirective: { - previousValue: undefined, - currentValue: this.useDirective.use, - firstChange: true, - isFirstChange: () => true, - }, - }); + useDirectiveForHost(this._widget.directives.accordionItemDirective); } ngAfterContentChecked(): void { @@ -239,15 +226,8 @@ export class AccordionItemComponent implements OnChanges, AfterContentChecked, A host: { '[class]': '"accordion " + state$().className', }, - hostDirectives: [ - { - directive: UseDirective, - }, - ], }) export class AccordionDirective implements OnChanges { - useDirective = inject(UseDirective); - /** * If `true`, only one item at the time can stay open. */ @@ -381,15 +361,7 @@ export class AccordionDirective implements OnChanges { state$: Signal = toAngularSignal(this._widget.state$); constructor() { - this.useDirective.use = this._widget.directives.accordionDirective; - this.useDirective.ngOnChanges({ - useDirective: { - previousValue: undefined, - currentValue: this.useDirective.use, - firstChange: true, - isFirstChange: () => true, - }, - }); + useDirectiveForHost(this._widget.directives.accordionDirective); } ngOnChanges(changes: SimpleChanges): void { diff --git a/angular/lib/src/lib/slider/slider.component.ts b/angular/lib/src/lib/slider/slider.component.ts index 7f04430d17..825024af25 100644 --- a/angular/lib/src/lib/slider/slider.component.ts +++ b/angular/lib/src/lib/slider/slider.component.ts @@ -1,20 +1,16 @@ import type {SliderState} from '@agnos-ui/angular-headless'; -import {callWidgetFactory, createSlider, patchSimpleChanges, toAngularSignal, toSlotContextWidget, UseDirective} from '@agnos-ui/angular-headless'; -import type {Directive} from '@agnos-ui/core'; -import {NgFor, NgIf} from '@angular/common'; -import type {AfterViewInit, OnChanges, OnDestroy, Signal, SimpleChanges} from '@angular/core'; import { - ChangeDetectionStrategy, - Component, - ElementRef, - EventEmitter, - forwardRef, - inject, - Input, - NgZone, - Output, - ViewEncapsulation, -} from '@angular/core'; + UseDirective, + callWidgetFactory, + createSlider, + patchSimpleChanges, + toAngularSignal, + toSlotContextWidget, + useDirectiveForHost, +} from '@agnos-ui/angular-headless'; +import {NgFor, NgIf} from '@angular/common'; +import type {OnChanges, Signal, SimpleChanges} from '@angular/core'; +import {ChangeDetectionStrategy, Component, EventEmitter, Input, NgZone, Output, ViewEncapsulation, forwardRef, inject} from '@angular/core'; import {NG_VALUE_ACCESSOR} from '@angular/forms'; import {take} from 'rxjs'; @@ -218,12 +214,7 @@ import {take} from 'rxjs'; `, ], }) -export class SliderComponent implements OnChanges, AfterViewInit, OnDestroy { - /** - * auSlider element reference - */ - private _elementRef = inject(ElementRef); - private _sliderDirective: ReturnType; +export class SliderComponent implements OnChanges { private _zone = inject(NgZone); readonly _widget = callWidgetFactory({ @@ -299,6 +290,10 @@ export class SliderComponent implements OnChanges, AfterViewInit, OnDestroy { @Output('auValuesChange') valuesChange = new EventEmitter(); + constructor() { + useDirectiveForHost(this._widget.directives.sliderDirective); + } + /** * Control value accessor methods */ @@ -336,14 +331,6 @@ export class SliderComponent implements OnChanges, AfterViewInit, OnDestroy { patchSimpleChanges(this._widget.patch, changes); } - ngAfterViewInit() { - this._sliderDirective = this._widget.directives.sliderDirective(this._elementRef.nativeElement); - } - - ngOnDestroy() { - this._sliderDirective?.destroy?.(); - } - handleBlur() { this.onTouched(); }