TNG / ngqp

Declaratively synchronize form controls with the URL
https://tng.github.io/ngqp
MIT License
81 stars 8 forks source link

Provide a ControlValueAccessor for mat-select from @angular/material #94

Closed Airblader closed 5 years ago

Airblader commented 5 years ago

What's your idea?

Unfortunately, mat-select does not provide a ControlValueAccessor but uses another mechanism instead (see https://github.com/angular/material2/issues/6549). Given the popularity of the library, we should consider providing our own implementation for it similar to what @MrWolfZ did here:

https://github.com/MrWolfZ/ngrx-forms/blob/dd557704f5bf99576827ad790a8d4680f40ecb0c/example-app/src/app/material/mat-select-view-adapter.ts#L17

For completeness sake it should be mentioned that this can also be achied using our ControlValueAccessorDirective (at least in my quick tests with static options it worked fine).

We should probably do this in a separate package and adapt the schematics to detect if @angular/material is installed and then also install this package.

Describe the solution you'd like

A version adapted to our needs:

import { AfterViewInit, Directive, forwardRef, OnDestroy } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatSelect } from '@angular/material';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Directive({
  selector: 'mat-select[queryParamName], mat-select[queryParam]',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => MatSelectAccessorDirective),
      multi: true,
    },
  ],
})
export class MatSelectAccessorDirective implements ControlValueAccessor, AfterViewInit, OnDestroy {

  private value: unknown;
  private readonly destroy$ = new Subject<void>();

  constructor(private matSelect: MatSelect) {
  }

  public ngAfterViewInit(): void {
    this.matSelect.options.changes.pipe(
      takeUntil(this.destroy$)
    ).subscribe(() => Promise.resolve().then(() => this.matSelect.writeValue(this.value)));
  }

  public ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  public registerOnChange(fn: any): void {
    this.matSelect.registerOnChange(value => {
      this.value = value;
      fn(value);
    });
  }

  public registerOnTouched(fn: any): void {
    this.matSelect.registerOnTouched(fn);
  }

  public setDisabledState(isDisabled: boolean): void {
    this.matSelect.setDisabledState(isDisabled);
  }

  public writeValue(value: unknown): void {
    this.value = value;

    const selectedOption = this.matSelect.selected;

    if (selectedOption) {
      if (Array.isArray(selectedOption) && Array.isArray(value)) {
        if (value.length === selectedOption.length && value.every((v, i) => v === selectedOption[ i ])) {
          return;
        }
      } else if (!Array.isArray(selectedOption)) {
        if (value === selectedOption.value) {
          return;
        }
      }
    }

    setTimeout(() => this.matSelect.writeValue(value), 0);
  }

}
Airblader commented 5 years ago

Having given it some more thought I'm not comfortable, for the moment, with maintaining such an addition. It feels like more of a hack and @angular/material should just expose the CVA, I think. If someone needs to use ngqp with mat-select they can copy the directive above into their project.

broweratcognitecdotcom commented 5 years ago

Just a question about your directive above... what is 'unknown' in the writeValue function?

public writeValue(value: unknown): void {

Also, can you show the usage of the directive?

Airblader commented 5 years ago

@broweratcognitecdotcom It's a type in TypeScript, see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#new-unknown-top-type.

Also, can you show the usage of the directive?

You don't need to explicitly use it, just add the directive to your code and some module and it will apply automatically. :-)

broweratcognitecdotcom commented 5 years ago

Ok. But I am getting: ERROR TypeError: this.getQueryParamGroup(...)._registerOnChange is not a function.

My code:

createParamGroup() { this.paramGroup = this.queryParamBuilder.group({ zone_id: this.queryParamBuilder.stringParam('zone_id', {emptyOn: this.prefs.filters.zone_id ? this.prefs.filters.zone_id : this.zones[0]}), }); }

template:

{{'@cognitec/fvv-visits/VisitsTimeMachineComponent/Zone' | translate}} {{zone}}
Airblader commented 5 years ago

How are you using paramGroup in the template?