Skip to content

Commit

Permalink
refactor: useDirectiveForHost utility in Angular (#199)
Browse files Browse the repository at this point in the history
  • Loading branch information
divdavem authored Oct 25, 2023
1 parent 24d49aa commit 1da18c7
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 94 deletions.
76 changes: 42 additions & 34 deletions angular/headless/src/lib/use.directive.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,65 @@
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)
// in order to avoid ExpressionChangedAfterItHasBeenCheckedError
// 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<T> implements OnChanges, OnDestroy {
@Input('auUse')
use: AgnosUIDirective<T> | undefined;
export const useDirectiveForHost = <T>(use?: AgnosUIDirective<T>, params?: T) => {
const ref = inject(ElementRef);

@Input('auUseParams')
params: T | undefined;

#ref = inject(ElementRef);
let instance = use?.(ref.nativeElement, params as T);

#directive: AgnosUIDirective<T> | undefined;
#directiveInstance?: ReturnType<AgnosUIDirective<T>>;

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<void> {
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<T>, 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<T> implements OnChanges {
@Input('auUse')
use: AgnosUIDirective<T> | undefined;

@Input('auUseParams')
params: T | undefined;

#useDirective = useDirectiveForHost<T>();

ngOnChanges() {
this.#useDirective.update(this.use, this.params);
}
}
34 changes: 3 additions & 31 deletions angular/lib/src/lib/accordion/accordion.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -109,11 +110,6 @@ const defaultConfig: Partial<AccordionItemProps> = {
'[class]': '"accordion-item " + state().itemClass',
'[id]': 'state().itemId',
},
hostDirectives: [
{
directive: UseDirective,
},
],
imports: [SlotDirective, UseDirective],
template: ` <ng-template [auSlotProps]="{state: state(), widget}" [auSlot]="state().slotItemStructure"></ng-template> `,
})
Expand Down Expand Up @@ -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<AccordionItemState> = 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 {
Expand All @@ -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.
*/
Expand Down Expand Up @@ -381,15 +361,7 @@ export class AccordionDirective implements OnChanges {
state$: Signal<AccordionState> = 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 {
Expand Down
45 changes: 16 additions & 29 deletions angular/lib/src/lib/slider/slider.component.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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<Directive>;
export class SliderComponent implements OnChanges {
private _zone = inject(NgZone);

readonly _widget = callWidgetFactory({
Expand Down Expand Up @@ -299,6 +290,10 @@ export class SliderComponent implements OnChanges, AfterViewInit, OnDestroy {
@Output('auValuesChange')
valuesChange = new EventEmitter<number[]>();

constructor() {
useDirectiveForHost(this._widget.directives.sliderDirective);
}

/**
* Control value accessor methods
*/
Expand Down Expand Up @@ -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();
}
Expand Down

0 comments on commit 1da18c7

Please sign in to comment.