ng-bootstrap: Regression: ngModel doesn't work with search function bind(this) ([ngbTypeahead]="search.bind(this)")

Bug description:

In case if I use search.bind(this) ngModel isn’t working, data isn’t set to model

html:

<input
 id="typeahead-basic"
  type="text"
   [ngbTypeahead]="search.bind(this)"
   class="form-control"
   [(ngModel)]="model"/>

ts:

search(text$: Observable<string>) {
    return text$.pipe(
      debounceTime(200),
      distinctUntilChanged(),
      map(term => term.length < 2 ? []
        : this.states.filter(v => v.toLowerCase().indexOf(term.toLowerCase()) > -1).slice(0, 10))
    )
  }

This was working on v/7.0.0

Link to minimally-working StackBlitz that reproduces the issue:

https://angular-dd3z93.stackblitz.io

Versions of Angular, ng-bootstrap and Bootstrap:

Angular: 11.2.7

ng-bootstrap: 9.1.0

Bootstrap:

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 5
  • Comments: 28

Most upvoted comments

@KyleThenTR To keep things short, my scenario consisted on a table with rows rendered via a ngFor. Each row had a typeahead. I passed a parameter to each typeahead to allow them to know which row they corresponded to. To avoid passing a parameter, I simply encapsulated each row into its own Component. That way, the parameter became one of the Input properties of this new Component, so there was no need to use a parameter in the typeahead function; simple grab that information from the Component itself.

The proposed workaround is to use a pure pipe. It’s really suuuuper easy:

  1. create the pure pipe
import { Pipe, PipeTransform } from '@angular/core';


@Pipe({
  name: 'applyPure',
  pure: true, // thats important
})
export class ApplyPurePipe implements PipeTransform {
  public transform(templateValue: any, fnReference: Function, ...fnArguments: any[]): any {
    // join the inputs from both sides of the function ref
    fnArguments.unshift(templateValue);

    return fnReference.apply(null, fnArguments);
  }
}
  1. use it where you need it like this:
[ngbTypeahead]="'PARAM' | applyPure: typeaheadFunction"

with your typeaheadFunction being something along the lines of


  typeaheadFunction: (param: string) => OperatorFunction<string, readonly Result[]> = (
    param: string,
  ) => (text$: Observable<string>) => {
    return text$.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap((textTyped) =>
        typedText.length < 2 ? [] : this.searchService.search(param, textTyped),
      ),
    );
  };

really not sure if I’m a fan 🤔

I was able to work around the issue and keep the debounceTime operator by turning the search function into an arrow function and removing the .bind(this) from the template.

Perhaps related, I was using a search function with an additional parameter which is no longer working since v9.1.0, commit 81f2c59.

search = (templateParam: number) => (text$: Observable<string>) => {
    return text$.pipe(
      debounce(200),
      distinctUntilChanged(),
      map((text) => {
        // Get results using text and templateParam
      })
    );
  };

HTML:

<input
  type="text"
  class="form-control"
  [(ngModel)]="model"
  [ngbTypeahead]="search(templateParam)"
/>

Updating the function typings hasn’t worked either, the search function is triggered but the text$ observable is never updated.

search: (templateParam: number) => OperatorFunction<string, readonly string[]> = (templateParam: number) => (text$: Observable<string>) => {
    return text$.pipe(
      debounce(200),
      distinctUntilChanged(),
      map((text) => {
        // Get results using text and templateParam
      })
    );
  };

It would get the initial parameterized function, which is enough.

Use a pipe then.

Hmmn to supply arrow functions to directives? Or parameterized arrow functions i guess?

Neither. Functions that are returning different REFERENCE each time they get called is an anty pattern.

The root problem is angular detecting changes when there are no changes of course, yep.

There are changes. Angular calls your function to check if there are changes, as you return a new function reference every time it detects changes and calls the ngOnChanges of the typeahead.

Angular components’ datepicker has similar issues. They could solve it by comparing dates, but these are functions.

@KyleThenTR I ended up using other component variables to find what would have been the template parameter. I think my use case was lucky, since the parameter I need is tracked in a component variable that is indexed by the selected typeahead.

search: OperatorFunction<string, readonly string[]> = (text$: Observable<string>) => {
    return text$.pipe(
      debounce(200),
      distinctUntilChanged(),
      map((text) => {
        const templateParam = this.componentArray[this.selectedTypeaheadIndex].templateParam;
        // Get results using text and templateParam
      })
    );
  };

I have the same scenario as @athisun in my application, after updating from v7 to v10 it no longer works. I have other typeaheads with no parameters, those work correctly.

I can get away with this by refactoring my Component, but this is clearly an undocumented breaking change. Any updates?

I have the same problem. It is working on 9.0.2