File

src/lib/directives/options-from-method.directive.ts

Extends

AfterViewInit OnChanges ExtractOptionsMethodMixin

Implements

AfterViewInit OnChanges OnDestroy

Metadata

Index

Properties
Methods
Inputs

Inputs

rxapOptionsFromMethodCall
Type : OptionsMethod<ControlOptions<Value> | Parameters>
rxapOptionsFromMethodParameters
Type : Parameters
rxapOptionsFromMethodResetOnChange
Type : Value

Methods

Public Async load
load(parameters: Parameters | undefined)
Parameters :
Name Type Optional Default value
parameters Parameters | undefined No this.parameters
Returns : any
Protected Async loadOptions
loadOptions(parameters?: Parameters)
Parameters :
Name Type Optional
parameters Parameters Yes
Returns : Promise<Observable | ControlOptions | null>
Static ngTemplateContextGuard
ngTemplateContextGuard(dir: OptionsFromMethodDirective, ctx: any)
Parameters :
Name Type Optional
dir OptionsFromMethodDirective No
ctx any No
Protected renderTemplate
renderTemplate()
Returns : void
Protected setOptions
setOptions(options: ControlOptions | null)
Parameters :
Name Type Optional
options ControlOptions | null No
Returns : void

Properties

Protected Optional _loadOptionsSubscription
Type : Subscription
Protected Readonly cdr
Type : ChangeDetectorRef
Default value : inject(ChangeDetectorRef)
Protected Readonly injector
Type : Injector
Default value : inject(INJECTOR)
Readonly loading
Default value : signal<boolean>(true)
Protected matFormField
Type : MatFormField | null
Default value : null
Protected ngControl
Type : NgControl | AbstractControlDirective | null
Default value : null
Public options
Type : ControlOptions | null
Default value : null
Protected settings
Type : OptionsFromMethodDirectiveSettings
Default value : {}
Protected Readonly viewContainerRef
Type : ViewContainerRef
Default value : inject(ViewContainerRef)
import {
  AfterViewInit,
  ChangeDetectorRef,
  Directive,
  inject,
  INJECTOR,
  Injector,
  Input,
  OnChanges,
  OnDestroy,
  signal,
  SimpleChanges,
  TemplateRef,
  ViewContainerRef,
} from '@angular/core';
import {
  AbstractControlDirective,
  NgControl,
} from '@angular/forms';
import { MatFormField } from '@angular/material/form-field';
import { RxapFormControl } from '@rxap/forms';
import { Mixin } from '@rxap/mixin';
import {
  ControlOption,
  ControlOptions,
} from '@rxap/utilities';
import {
  isObservable,
  Observable,
  Subscription,
  tap,
} from 'rxjs';
import {
  ExtractOptionsMethodMixin,
  OptionsMethod,
} from '../mixins/extract-options-method.mixin';


export interface OptionsFromMethodTemplateContext {
  $implicit: ControlOption;
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface OptionsFromMethodDirectiveSettings {
}

// eslient-disable-next-line @typescript-eslint/no-empty-interface
export interface OptionsFromMethodDirective<Value = any, Parameters = any>
  extends AfterViewInit, OnChanges, ExtractOptionsMethodMixin {
}

@Mixin(ExtractOptionsMethodMixin)
@Directive({
  // eslint-disable-next-line @angular-eslint/directive-selector
  selector: '[rxapOptionsFromMethod]',
  standalone: true,
  exportAs: 'rxapOptionsFromMethod',
})
export class OptionsFromMethodDirective<Value = any, Parameters = any> implements AfterViewInit, OnChanges, OnDestroy {

  static ngTemplateContextGuard(
    dir: OptionsFromMethodDirective,
    ctx: any,
  ): ctx is OptionsFromMethodTemplateContext {
    return true;
  }

  readonly loading = signal<boolean>(true);

  @Input('rxapOptionsFromMethodParameters')
  public parameters?: Parameters;

  @Input('rxapOptionsFromMethodResetOnChange')
  public resetOnChange?: Value;

  public options: ControlOptions | null                                    = null;
  // eslint-disable-next-line @angular-eslint/no-input-rename
  @Input('rxapOptionsFromMethodCall')
  public method!: OptionsMethod<ControlOptions<Value>, Parameters>;
  protected ngControl: NgControl | AbstractControlDirective | null         = null;
  protected matFormField: MatFormField | null                              = null;
  protected settings: OptionsFromMethodDirectiveSettings                   = {};
  protected readonly viewContainerRef: ViewContainerRef                    = inject(ViewContainerRef);
  protected readonly injector: Injector                                    = inject(INJECTOR);
  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);
    this.control      = (this.ngControl?.control as RxapFormControl) ?? undefined;
    this.method ??= this.extractOptionsMethod();
    // ensure that the options are loaded. It is possible that the ngOnChange is triggered before the ngAfterViewInit
    // then the options are not loaded.
    if (!this.options) {
      await this.load(this.parameters);
    }
  }

  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
    if (!this.method) {
      console.debug('The method is not yet defined');
      return;
    }
    const parametersChanges = changes['parameters'];
    if (parametersChanges) {
      await this.load(parametersChanges.currentValue);
    }
  }

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

  protected renderTemplate() {

    if (!this.options) {
      throw new Error('The options are not yet loaded');
    }

    this.viewContainerRef.clear();

    for (const option of this.options) {
      this.viewContainerRef.createEmbeddedView(this.template, {$implicit: option});
    }

    if (this.resetOnChange !== undefined) {
      this.control?.reset(this.resetOnChange);
    }

    this.cdr.detectChanges();

  }

  protected setOptions(options: ControlOptions | null) {
    if (!options) {
      this.options = null;
      this.viewContainerRef.clear();
    } else {
      this.options = options.slice();
      this.renderTemplate();
    }
  }

  public async load(parameters: Parameters | undefined = this.parameters) {
    this.loading.set(true);
    try {
      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);
    }
  }

}


results matching ""

    No results matching ""