7
votes

I am trying to use the ngx-mat-select-search component to put a mat-select style dropdown menu with a search bar in my application. https://www.npmjs.com/package/ngx-mat-select-search

I have the dropdown working fine, but I am trying to turn it into a custom directive that I can then call and reuse on multiple pages through out the app.

So far I have this: site-dropdown-component.ts

import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {FormControl} from '@angular/forms';
import {ReplaySubject, Subject} from 'rxjs';
import {MatSelect} from '@angular/material';
import {take, takeUntil} from 'rxjs/operators';

@Component({
  selector: 'app-site-dropdown',
  template: `
    <mat-form-field class="w-100">
      <mat-select [formControl]="siteCtrl" placeholder="Site" #singleSelect>
        <mat-option>
          <ngx-mat-select-search [formControl]="siteFilterCtrl" [placeholderLabel]="'Search Sites...'"></ngx-mat-select-search>
        </mat-option>
        <mat-option *ngFor="let site of filteredSites | async" [value]="site">{{site.name}}</mat-option>
      </mat-select>
    </mat-form-field>
  `
})
export class SiteDropdownComponent implements OnInit, OnDestroy, AfterViewInit {
  /** list of sites */
  protected sites: Site[] = SITES;

  /** control for the selected site */
  public siteCtrl: FormControl = new FormControl();

  /** control for the MatSelect filter keyword */
  public siteFilterCtrl: FormControl = new FormControl();

  /** list of sites filtered by search keyword */
  public filteredSites: ReplaySubject<Site[]> = new ReplaySubject<Site[]>(1);

  @ViewChild('singleSelect') singleSelect: MatSelect;

  /** Subject that emits when the component has been destroyed. */
  protected onDestroy = new Subject<void>();
  constructor() { }

  ngOnInit(): void {
    // set initial selection
    this.siteCtrl.setValue(this.sites);
    // load the initial site list
    this.filteredSites.next(this.sites.slice());
    // listen for search field value changes
    this.siteFilterCtrl.valueChanges
      .pipe(takeUntil(this.onDestroy))
      .subscribe(() => {
        this.filterSites();
      });
  }

  ngAfterViewInit(): void {
    this.setInitialValue();
  }

  ngOnDestroy(): void {
    this.onDestroy.next();
    this.onDestroy.complete();
  }

  /**
   * Sets the initial value after the filteredBanks are loaded initially
   */
  protected setInitialValue() {
    this.filteredSites
      .pipe(take(1), takeUntil(this.onDestroy))
      .subscribe(() => {
        // setting the compareWith property to a comparison function
        // triggers initializing the selection according to the initial value of
        // the form control (i.e. _initializeSelection())
        // this needs to be done after the filteredBanks are loaded initially
        // and after the mat-option elements are available
        this.singleSelect.compareWith = (a: Site, b: Site) => a && b && a.id === b.id;
      });
  }

  protected filterSites() {
    if (!this.sites) {
      return;
    }
    // get the search keyword
    let search = this.siteFilterCtrl.value;
    if (!search) {
      this.filteredSites.next(this.sites.slice());
      return;
    } else {
      search = search.toLowerCase();
    }
    // filter the sites
    this.filteredSites.next(
      this.sites.filter(site => site.name.toLowerCase().indexOf(search) > -1)
    );
  }
}


export interface Site {
  id: string;
  name: string;
}

export const SITES: Site[] = [
  {id: 'site1', name: 'Site 1'},
  {id: 'site2', name: 'Site 2'},
  {id: 'site3', name: 'Site 3'},
];

For the component im trying to use it in, i have:

<app-site-dropdown formControlName="site"></app-site-dropdown>

And inside the component class I have a form:

this.mySearchForm = this.formBuilder.group( {
  site: []
}); 

I can see and interact with the dropdown just fine, but when i submit my form, I cannot get the value of the selected option. It just always returns null when i try mySearchForm.controls['site'].value

What am I missing to be able to inject my custom dropdown component, and retrieve its value upon form submission?

UPDATE:

I was able to make it work by doing the following:

Inside site-dropdown.component.ts, I changed

protected siteCtrl: FormControl;

to

@Input() siteCtrl: FormControl;

And inside my html using the custom dropdown, i added:

<app-site-dropdown [siteCtrl]="myForm.get('site')"></app-site-dropdown>

This allowed me to save the selected value into my form on submission.

1
This was just was I was looking to for, so thanks! I implemented the server-side search, so I took out the setInitialValue() call after init and just passed in the initial value so I could have it display, but would then search after you clicked on it, but it's working great.tclark333

1 Answers

2
votes

you can get the value of the selected option by having your SiteDropdownComponent implement the ControlValueAccessor interface as follows, resulting in your SiteDropdownComponent behaving as a form control and allowing to access the value with e.g. <app-site-dropdown formControlName="site"></app-site-dropdown>:

...
import { forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-site-dropdown',
  template: ...
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => SiteDropdownComponent),
      multi: true
    }
  ],
})
export class SiteDropdownComponent implements OnInit, OnDestroy, AfterViewInit, ControlValueAccessor {
  ...

  onChange: Function = (_: any) => {};
  onTouched: Function = (_: any) => {};

  constructor() { }

  ngOnInit() {
    ...
    // call this.onChange to notify the parent component that the value has changed
    this.siteCtrl.valueChanges
      .pipe(takeUntil(this.onDestroy))
      .subscribe(value => this.onChange(value))
  }

  writeValue(value: string) {
    // set the value of siteCtrl when the value is set from outside the component
    this.siteCtrl.setValue(value);
  }

  registerOnChange(fn: Function) {
    this.onChange = fn;
  }

  registerOnTouched(fn: Function) {
    this.onTouched = fn;
  }

}

See e.g. https://github.com/bithost-gmbh/ngx-mat-select-search/blob/d7ea78d511bbec45143c58c855f013a44d0d5055/src/app/mat-select-search/mat-select-search.component.ts#L134