File

src/lib/directives/input-select-options.directive.ts

Extends

OnDestroy AfterViewInit ExtractOptionsDataSourceMixin

Implements

OnDestroy AfterViewInit

Metadata

Index

Properties
Methods
Inputs

Constructor

constructor(template: TemplateRef<InputSelectOptionsTemplateContext>, dataSourceLoader: DataSourceLoader, viewContainerRef: ViewContainerRef, injector: Injector, cdr: ChangeDetectorRef, ngControl: NgControl | null, matFormField: MatFormField | null)
Parameters :
Name Type Optional
template TemplateRef<InputSelectOptionsTemplateContext> No
dataSourceLoader DataSourceLoader No
viewContainerRef ViewContainerRef No
injector Injector No
cdr ChangeDetectorRef No
ngControl NgControl | null No
matFormField MatFormField | null No

Inputs

rxapInputSelectOptionsMatAutocomplete
Type : MatAutocomplete
rxapInputSelectOptionsMetadata
Type : BaseDataSourceMetadata
rxapInputSelectOptionsViewer
Type : BaseDataSourceViewer
Default value : { id: '[rxapInputSelectOptions]' }

Methods

Protected renderTemplate
renderTemplate(options: ControlOptions | Record | null)
Parameters :
Name Type Optional Default value
options ControlOptions | Record<string | any> | null No this.options
Returns : void
Public toDisplay
toDisplay(value: any)
Parameters :
Name Type Optional
value any No
Returns : string

Properties

Protected control
Type : RxapFormControl
Protected dataSource
Type : BaseDataSource<ControlOptions | Record>
Protected options
Type : ControlOptions | Record<string | any> | null
Default value : null
Protected Optional settings
Type : InputSelectOptionsSettings<any>
Protected Readonly subscription
Default value : new Subscription()
import {
  AfterViewInit,
  ChangeDetectorRef,
  Directive,
  Inject,
  Injector,
  INJECTOR,
  Input,
  isDevMode,
  OnDestroy,
  Optional,
  TemplateRef,
  ViewContainerRef,
} from '@angular/core';
import { NgControl } from '@angular/forms';
import {
  BaseDataSource,
  BaseDataSourceMetadata,
  BaseDataSourceViewer,
  DataSourceLoader,
} from '@rxap/data-source';
import { RxapFormControl } from '@rxap/forms';
import {
  ControlOption,
  ControlOptions,
  equals,
  Required,
} from '@rxap/utilities';
import { Mixin } from '@rxap/mixin';
import {
  EMPTY,
  of,
  Subscription,
} from 'rxjs';
import {
  distinctUntilChanged,
  map,
  switchMap,
  tap,
  throttleTime,
} from 'rxjs/operators';
import {
  MAT_FORM_FIELD,
  MatFormField,
} from '@angular/material/form-field';
import { MatAutocomplete } from '@angular/material/autocomplete';
import {
  ExtractOptionsDataSourceMixin,
  UseOptionsDataSourceSettings,
} from '../mixins/extract-options-data-source.mixin';

export interface InputSelectOptionsTemplateContext {
  $implicit: ControlOption;
}

export interface InputSelectOptionsSettings<Source> extends UseOptionsDataSourceSettings<Source> {
  /**
   * true - the selected value is not send to the data source with the viewChange event.
   * Instead an empty object {} is send to trigger a data source refresh.
   *
   * The OpenApiDataSource expects a Paramters Object in the viewChange event. This
   * can couse issue if the select value object as a property that is also a parameter for
   * the open-api operation. For instaed the select object has the property company and
   * the open-api operation has a query parameter company. Then the BuildOptions method
   * in the OpenApiDataSource would create a HttpParams object with the param company and the
   * value from the select object property company. This will result in unpredictable behaviors.
   */
  ignoreSelectedValue?: boolean;

  /**
   * true - the options list is filtered by the current control value if the
   * control value is of type string. Then the display value of each option is
   * compared to the control value. If the display value of an option includes
   * the control value, then the option is used. This comprehensions it not type
   * sensitive
   */
  filteredOptions?: boolean;

  /**
   * true - the options list is only loaded once and not refreshed on each value change
   */
  loadOnce?: boolean;
}

export interface InputSelectOptionsDirective extends OnDestroy, AfterViewInit,
                                                     ExtractOptionsDataSourceMixin {
}

@Mixin(ExtractOptionsDataSourceMixin)
@Directive({
  selector: '[rxapInputSelectOptions]',
  standalone: true,
})
export class InputSelectOptionsDirective implements OnDestroy, AfterViewInit {

  @Input('rxapInputSelectOptionsViewer')
  public viewer: BaseDataSourceViewer = { id: '[rxapInputSelectOptions]' };

  @Input('rxapInputSelectOptionsMetadata')
  public metadata?: BaseDataSourceMetadata;

  @Input('rxapInputSelectOptionsMatAutocomplete')
  public matAutocomplete?: MatAutocomplete;

  protected readonly subscription = new Subscription();

  protected control!: RxapFormControl;

  protected dataSource!: BaseDataSource<ControlOptions | Record<string, any>>;

  protected options: ControlOptions | Record<string, any> | null = null;

  protected settings?: InputSelectOptionsSettings<any>;

  /**
   * This flag is used to prevent the setValue is called for each refresh
   * of the options list. This is needed because the setValue method will
   * trigger a new refresh of the options list. This results in an endless
   * call stack. This flag is set to true if the setValue method is called
   * once.
   */
  private isAutocompleteToDisplayTriggered = false;

  constructor(
    @Inject(TemplateRef)
    protected readonly template: TemplateRef<InputSelectOptionsTemplateContext>,
    @Inject(DataSourceLoader)
    protected readonly dataSourceLoader: DataSourceLoader,
    @Inject(ViewContainerRef)
    protected readonly viewContainerRef: ViewContainerRef,
    @Inject(INJECTOR)
    protected readonly injector: Injector,
    @Inject(ChangeDetectorRef)
    protected readonly cdr: ChangeDetectorRef,
    // The ngControl could be unknown on construction
    // bc it is available after the content is init
    @Optional()
    @Inject(NgControl)
    protected ngControl: NgControl | null = null,
    @Optional()
    @Inject(MAT_FORM_FIELD)
    protected matFormField: MatFormField | null = null,
  ) {
  }

  public ngAfterViewInit() {
    this.extractOptionsDatasource();
    this.viewer = {
      id: '[rxapInputSelectOptions]' + this.control.controlId,
      viewChange: this.settings?.loadOnce ? EMPTY : this.control.value$.pipe(
        throttleTime(1000),
        distinctUntilChanged((a, b) => equals(a, b)),
        map(value => {
          if (this.settings?.ignoreSelectedValue) {
            return {};
          }
          return value;
        }),
      ),
      control: this.control,
    };
    if (this.matAutocomplete) {
      this.matAutocomplete.displayWith = this.toDisplay.bind(this);
      this.settings ??= {};
      this.settings.filteredOptions ??= true;
    }
    this.loadOptions();
  }

  public toDisplay(value: any): string {
    if (!value) {
      return '';
    }
    return this.options?.find((option: any) => option.value === value)?.display ??
      (isDevMode() ? 'to display error' : '');
  }

  public ngOnDestroy() {
    this.subscription?.unsubscribe();
  }

  private loadOptions(dataSource: BaseDataSource<ControlOptions | Record<string, any>> = this.dataSource) {
    this.subscription.add(dataSource.connect(this.viewer).pipe(
      switchMap(options => {
        if (this.settings?.filteredOptions) {
          return this.control.value$.pipe(
            map(controlValue => {
              if (typeof controlValue === 'string' && controlValue) {
                controlValue = controlValue.toLowerCase();
                if (Array.isArray(options)) {
                  return options.filter(option => option.display.toLowerCase().includes(controlValue));
                } else {
                  return Object.entries(options)
                               .filter(([ value, display ]) => typeof display ===
                                 'string' &&
                                 display.toLowerCase().includes(controlValue))
                               .map(([ value, display ]) => ({
                                 value,
                                 display,
                               }));
                }
              }
              return options;
            }),
          );
        }
        return of(options);
      }),
      tap(options => this.renderTemplate(this.options = options)),
      tap(() => {
        if (this.matAutocomplete && !this.isAutocompleteToDisplayTriggered) {
          this.isAutocompleteToDisplayTriggered = true;
          // trigger a change detection after the options are rendered
          // this is needed to trigger the mat-autocomplete options to display function
          this.ngControl?.control?.setValue(this.ngControl?.control?.value);
        }
      }),
    ).subscribe());
  }

  protected renderTemplate(options: ControlOptions | Record<string, any> | null = this.options) {
    this.viewContainerRef.clear();

    if (options) {
      if (!Array.isArray(options)) {

        for (const [ value, display ] of Object.entries(options)) {
          this.viewContainerRef.createEmbeddedView(
            this.template,
            {
              $implicit: {
                value,
                display,
              },
            },
          );
        }

      } else {

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

      }
    } else {
      if (isDevMode()) {
        console.warn(`The options for the control ${ this.control.fullControlPath } is empty/null/undefined!`);
      }
    }

    this.cdr.detectChanges();
  }

}


results matching ""

    No results matching ""