Verified Commit 96204197 authored by Merzough Münker's avatar Merzough Münker
Browse files

feat(form-system): support observable-based control options in directives

- Enhanced `OptionsFromMethodDirective` to support observables for control options.
- Added unsubscription logic to `OnDestroy` lifecycle to prevent memory leaks.
- Updated related directives to handle observable-based options seamlessly.
parent 0a174cab
Loading
Loading
Loading
Loading
+27 −15
Original line number Diff line number Diff line
@@ -10,22 +10,32 @@ import {
  OnDestroy,
  ProviderToken,
} from '@angular/core';
import { MatAutocomplete } from '@angular/material/autocomplete';
import { ControlOption, ControlOptions } from '@rxap/utilities';
import { distinctUntilChanged, Subscription, tap } from 'rxjs';
import { isUUID } from '@rxap/validator';
import { Method, MethodWithParameters } from '@rxap/pattern';
import { Mixin } from '@rxap/mixin';
import { NgControl } from '@angular/forms';
import { MatAutocomplete } from '@angular/material/autocomplete';
import { MatFormField } from '@angular/material/form-field';
import { controlValueChanges$ } from '@rxap/forms';
import { Mixin } from '@rxap/mixin';
import { OpenApiRemoteMethodParameter } from '@rxap/open-api/remote-method';
import {
  Method,
  MethodWithParameters,
} from '@rxap/pattern';
import { isDefined } from '@rxap/rxjs';
import { ExtractControlMixin } from '../mixins/extract-control.mixin';
import { ExtractFormDefinitionMixin } from '../mixins/extract-form-definition.mixin';
import {
  ControlOption,
  ControlOptions,
} from '@rxap/utilities';
import { isUUID } from '@rxap/validator';
import {
  distinctUntilChanged,
  Observable,
  Subscription,
  tap,
} from 'rxjs';
import {
  ExtractIsValueFunctionMixin,
  UseIsValueFunction,
} from '../mixins/extract-is-value-function.mixin';
import { ExtractMethodMixin } from '../mixins/extract-method.mixin';
import { UseMethodConfig } from '../mixins/extract-methods.mixin';
import { UseOptionsMethod } from '../mixins/extract-options-method.mixin';
import {
@@ -36,9 +46,10 @@ import {
  ExtractToDisplayFunctionMixin,
  UseToDisplayFunction,
} from '../mixins/extract-to-display-function.mixin';
import { OptionsFromMethodDirective, OptionsFromMethodDirectiveSettings } from './options-from-method.directive';
import { OpenApiRemoteMethodParameter } from '@rxap/open-api/remote-method';
import { controlValueChanges$ } from '@rxap/forms';
import {
  OptionsFromMethodDirective,
  OptionsFromMethodDirectiveSettings,
} from './options-from-method.directive';

export function UseAutocompleteOptionsMethod(
  method: ProviderToken<MethodWithParameters<ControlOptions, AutocompleteOptionsFromMethodDirectiveParameters>>,
@@ -157,7 +168,8 @@ export class AutocompleteOptionsFromMethodDirective<Value = any, Parameters exte

  private _subscription?: Subscription;

  public ngOnDestroy() {
  public override ngOnDestroy() {
    super.ngOnDestroy();
    this._subscription?.unsubscribe();
  }

@@ -190,7 +202,7 @@ export class AutocompleteOptionsFromMethodDirective<Value = any, Parameters exte
      // to trigger the toDisplay function in the mat-autocomplete
      distinctUntilChanged(),
      tap(async value => {
        this.setOptions(await this.loadOptions(this.parameters));
        await this.load(this.parameters);
        if (this.isValue?.(value)) {
          this.triggerAutocompleteToDisplay();
        }
@@ -198,7 +210,7 @@ export class AutocompleteOptionsFromMethodDirective<Value = any, Parameters exte
    ).subscribe();
  }

  protected override loadOptions(parameters: Parameters = {} as Parameters): Promise<ControlOptions | null> {
  protected override loadOptions(parameters: Parameters = {} as Parameters): Promise<ControlOptions | null | Observable<ControlOptions | null>> {
    if (!this.control) {
      throw new Error('The control is not yet defined');
    }
+29 −7
Original line number Diff line number Diff line
@@ -3,11 +3,11 @@ import {
  ChangeDetectorRef,
  Directive,
  inject,
  InjectFlags,
  INJECTOR,
  Injector,
  Input,
  OnChanges,
  OnDestroy,
  signal,
  SimpleChanges,
  TemplateRef,
@@ -20,12 +20,20 @@ import {
import { MatFormField } from '@angular/material/form-field';
import { RxapFormControl } from '@rxap/forms';
import { Mixin } from '@rxap/mixin';
import { Method } from '@rxap/pattern';
import {
  ControlOption,
  ControlOptions,
} from '@rxap/utilities';
import { ExtractOptionsMethodMixin } from '../mixins/extract-options-method.mixin';
import {
  isObservable,
  Observable,
  Subscription,
  tap,
} from 'rxjs';
import {
  ExtractOptionsMethodMixin,
  OptionsMethod,
} from '../mixins/extract-options-method.mixin';


export interface OptionsFromMethodTemplateContext {
@@ -48,7 +56,7 @@ export interface OptionsFromMethodDirective<Value = any, Parameters = any>
  standalone: true,
  exportAs: 'rxapOptionsFromMethod',
})
export class OptionsFromMethodDirective<Value = any, Parameters = any> implements AfterViewInit, OnChanges {
export class OptionsFromMethodDirective<Value = any, Parameters = any> implements AfterViewInit, OnChanges, OnDestroy {

  static ngTemplateContextGuard(
    dir: OptionsFromMethodDirective,
@@ -68,7 +76,7 @@ export class OptionsFromMethodDirective<Value = any, Parameters = any> implement
  public options: ControlOptions | null                                    = null;
  // eslint-disable-next-line @angular-eslint/no-input-rename
  @Input('rxapOptionsFromMethodCall')
  public method!: Method<ControlOptions, Parameters>;
  public method!: OptionsMethod<Parameters>;
  protected ngControl: NgControl | AbstractControlDirective | null         = null;
  protected matFormField: MatFormField | null                              = null;
  protected settings: OptionsFromMethodDirectiveSettings                   = {};
@@ -77,6 +85,8 @@ export class OptionsFromMethodDirective<Value = any, Parameters = any> implement
  protected readonly cdr: ChangeDetectorRef                                = inject(ChangeDetectorRef);
  private readonly template: TemplateRef<OptionsFromMethodTemplateContext> = inject(TemplateRef);

  protected _loadOptionsSubscription?: Subscription;

  public async ngAfterViewInit() {
    this.matFormField = this.injector.get(MatFormField, null);
    this.ngControl    = this.matFormField?._control.ngControl ?? this.injector.get(NgControl, null);
@@ -89,6 +99,10 @@ export class OptionsFromMethodDirective<Value = any, Parameters = any> implement
    }
  }

  ngOnDestroy() {
    this._loadOptionsSubscription?.unsubscribe();
  }

  public async ngOnChanges(changes: SimpleChanges) {
    // only try to load the options if the method is defined
    // else the initial load will be triggered in the ngAfterViewInit
@@ -102,7 +116,7 @@ export class OptionsFromMethodDirective<Value = any, Parameters = any> implement
    }
  }

  protected async loadOptions(parameters?: Parameters): Promise<ControlOptions | null> {
  protected async loadOptions(parameters?: Parameters): Promise<Observable<ControlOptions | null> | ControlOptions | null> {
    return this.method.call(parameters);
  }

@@ -139,7 +153,15 @@ export class OptionsFromMethodDirective<Value = any, Parameters = any> implement
  public async load(parameters: Parameters | undefined = this.parameters) {
    this.loading.set(true);
    try {
      this.setOptions(await this.loadOptions(parameters));
      const result = await this.loadOptions(parameters);
      if (isObservable(result)) {
        this._loadOptionsSubscription?.unsubscribe();
        this._loadOptionsSubscription = result.pipe(
          tap(options => this.setOptions(options)),
        ).subscribe();
      } else {
        this.setOptions(result);
      }
    } finally {
      this.loading.set(false);
    }
+4 −1
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ import { RxapFormControl } from '@rxap/forms';
import { Mixin } from '@rxap/mixin';
import { Method } from '@rxap/pattern';
import { ControlOptions } from '@rxap/utilities';
import { Observable } from 'rxjs';
import { ExtractControlMixin } from './extract-control.mixin';
import { ExtractFormDefinitionMixin } from './extract-form-definition.mixin';
import { ExtractMethodMixin } from './extract-method.mixin';
@@ -13,7 +14,9 @@ import {

export const OPTIONS_METHOD_NAME = 'options';

export function UseOptionsMethod(method: ProviderToken<Method<ControlOptions>>, config?: UseMethodConfig) {
export type OptionsMethod<Parameters = any> = Method<ControlOptions | Observable<ControlOptions | null> | null, Parameters>;

export function UseOptionsMethod<Parameters = any>(method: ProviderToken<OptionsMethod<Parameters>>, config?: UseMethodConfig) {
  return function (target: any, propertyKey: string) {
    UseMethod(method, OPTIONS_METHOD_NAME, config)(target, propertyKey);
  };