23

I try to use <mat-autocomplete> from Angular Material (not AngularJS) without using a reactive form. But all their examples use reactive forms...

What I try to do:
1. when something is typed in the mat-input, make an Ajax call to retrieve a list of users
2. Display the list of users in the autocomplete (display the user's name), but store the user as the model
3. When the model changes, call a function of my choice

For now I do those crazy things (I say crazy because it seems to much).

<mat-form-field fxLayout>
  <input type="text"
             matInput fxFlex="100"
             [(ngModel)]="listFilterValue"
             (keyup)="memberInputChanged(input.value)"
             (change)="memberChanged()"
             *ngIf="isAutocompleteNeeded()"
             #input
             [matAutocomplete]="auto">
</mat-form-field>

<mat-autocomplete #auto="matAutocomplete" [displayWith]="getMemberAsStr">
       <mat-option *ngFor="let member of members | async" [value]="member">
          {{ getMemberAsStr(member) }}
       </mat-option>
</mat-autocomplete>

For now, there are only console.log in my JS to see what's called, with what value so I don't share it here. Am I using the right attributes, the right logic ?

(the members property in my component is a Rxjs BehaviourSubject)

What I do for now does not work because listFilterValue is never set to anything.

A-Sharabiani
  • 17,750
  • 17
  • 113
  • 128
Valentin Coudert
  • 1,759
  • 3
  • 19
  • 44

1 Answers1

38

You should avoid calling methods in the template, this could potentially crash your browser as they are called on each change detection, see: *ngFor running an infinite loop in angular2 Technically it's not an infinite loop, but you get the point :)

As for not having form control with the autocomplete is not much different, you just swap the form control to a variable instead, you could use a template driven form if you want, or not a form at all. Here's with template driven form though:

The demo JSON used in this looks like:

"value": [
  {
    "id": 409,
    "joke": "some joke here",
    "categories": []
  }
]
<form #f="ngForm">
  <mat-form-field>
    <input matInput [matAutocomplete]="auto" 
           name="joke" #jokeField="ngModel" 
           [(ngModel)]="currentJoke" (ngModelChange)="doFilter()">
  </mat-form-field>

  <mat-autocomplete #auto="matAutocomplete">
    <mat-option *ngFor="let joke of jokes | async" [value]="joke.joke">
        {{joke.joke}}
    </mat-option>
  </mat-autocomplete>
</form>

And your TS could look like:

doFilter() {
  this.jokes = this.service.getData()
    .pipe(
      map(jokes => this.filter(jokes.value)),
  )
}

filter(values) {
  return values.filter(joke => 
    // used 'includes' here for demo, you'd want to probably use 'indexOf'
    joke.joke.toLowerCase().includes(this.currentJoke))
}

The Service would then have a variable, where we store the api data after first fetch, so that we won't call the api on each key stroke. When data is returned, we check if the variable has been populated, if so, we return an observable of that:

jokes = [];

getData() {
  return this.jokes.length ? of(this.jokes)
    : this.httpClient.get<any>('https://api.icndb.com/jokes/random/5').pipe(
      map((data) => {
        this.jokes = data.value;
        return this.jokes;
      })
    )
}

Remember to clear the jokes array if navigating away.

StackBlitz Demo

AT82
  • 71,416
  • 24
  • 140
  • 167
  • This makes sense but how do I know which joke has been selected from the autocomplete list (how do I retrieve the joke object in my component.ts) ? – Valentin Coudert Jan 30 '18 at 09:02
  • ok, seems to be the `optionSelected` event fired by `mat-autocomplete` (cf https://material.angular.io/components/autocomplete/api) – Valentin Coudert Jan 30 '18 at 09:03
  • If you notice one thing here that the auto-complete pop-up doesn't open when you click on it for the first time. You need to enter some text into the input element and after that, It'll start functioning properly. Does anyone have the workaround of solution for that issue? – Rajendrasinh Parmar Sep 22 '18 at 05:54
  • 1
    downvoted because the sample html contains two variable declarations. '#joke' and 'let joke'. So it is not clear, what [value]="joke.joke" does actually contain. and joke.title would be better to avoid confusion. – Benjamin Schäublin Nov 10 '18 at 12:36
  • 1
    @maerlin, example is based on demo json from an open api, so therefore the data is what it is. I also in beginning show how the JSON looks like, so there shouldn't be any confusion on that part. Since this is a template driven form, we need a template reference (`#joke`). `let joke` is just the name I've given in iteration. Those are not related at all, and I don't find it confusing. Sure, I can change the tempate reference name to avoid any possible confusion. But very much thank you for sharing why you downvoted, that rarely happens, so a big thank you for that! :) – AT82 Nov 10 '18 at 17:02
  • 1
    This is an amazing solution. Everything I have seen online works with reactive forms. – Matt Apr 04 '19 at 07:51
  • @Mahdi, glad to hear that it was useful for you, makes me happy! :) – AT82 Apr 04 '19 at 08:49
  • 1
    @RajendrasinhParmar this fixes that: https://stackblitz.com/edit/angular-ntkfdb-irwedj – Balazs Varhegyi Oct 29 '19 at 08:51
  • Looks like you are missing the form input in (ngModelChange)="doFilter()", you could pass it as doFilter($event), and then use that value to select jokes – Tadele Ayelegn Jul 14 '21 at 01:52
  • @RajendrasinhParmar little late but you'd need to give `this.jokes` an initial value. Basically you're showing the list with no values yet set. – toxaq May 31 '22 at 00:53
  • Why the demo is different from the answer? – Tadele Ayelegn Nov 13 '22 at 04:51