3

I want to fill up an array when my Angular app starts and use it for Material Autocomplete. I can receive a JSON from my PHP backend. In ngOnInit I can even give it to the array and log it out. However later my array remains undefined. How should I get it right so that the contents finally show up in my options list?

app.component.html:

<form class="example-form">
  <mat-form-field class="example-full-width">
    <input type="text" placeholder="Pick one" aria-label="Number" matInput [formControl]="myControl" [matAutocomplete]="auto">
    <mat-autocomplete #auto="matAutocomplete">
      <mat-option *ngFor="let option of filteredOptions | async" [value]="option">
        {{option}}
      </mat-option>
    </mat-autocomplete>
  </mat-form-field>
</form>

app.component.ts:

 import {Component, OnInit, AfterViewInit} from '@angular/core';
import {FormControl} from '@angular/forms';
import {Observable} from 'rxjs';
import {map, startWith, takeUntil, switchMap} from 'rxjs/operators';
import { ServerService } from './server.service';

/**
 * @title Filter autocomplete
 */
@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.css'],
})
export class AppComponent implements OnInit {
  myControl = new FormControl();
  megyek: Observable<string[]>;
  filteredOptions: Observable<string[]>;

  constructor(private serverService: ServerService) { }

  ngOnInit() {
    // don't manually subscribe!
    this.megyek = this.serverService.getMegyek();

    // use switchmap, if user types fast
    this.filteredOptions = this.myControl.valueChanges.pipe(
      startWith(''),
      switchMap(value => this._filter(value))
    );
  }

  private _filter(value: string): string[] {
    const filterValue = value.toLowerCase();
    return this.megyek
      .filter(option => option.toLowerCase().includes(filterValue));
  }
}

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { ServerService } from './server.service';
import { HttpModule } from '@angular/http';


import { AppComponent } from './app.component';

import {
  MatButtonModule,
  MatFormFieldModule,
  MatInputModule,
  MatRippleModule,
  MatAutocompleteModule,
} from '@angular/material';

import {BrowserAnimationsModule} from '@angular/platform-browser/animations';

@NgModule({
  exports: [
    MatButtonModule,
    MatFormFieldModule,
    MatInputModule,
    MatRippleModule,
    MatAutocompleteModule,
    ReactiveFormsModule,
    BrowserAnimationsModule,
    FormsModule,
    HttpModule
  ],
  declarations: [],
  imports: []
})
export class MaterialModule {}

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    MaterialModule,
    BrowserModule,
  ],
  providers: [ServerService],
  bootstrap: [
    AppComponent,
  ],
  schemas: [],
})
export class AppModule { }

server.service.ts

import {throwError as observableThrowError,  Observable } from 'rxjs';

import {catchError, map} from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { Headers, Http, Response } from '@angular/http';

@Injectable()
export class ServerService {
  constructor(private http: Http) {}
  storeServers(servers: any[]) {
    const headers = new Headers({'Content-Type': 'application/json'});
    // return this.http.post('https://udemy-ng-http.firebaseio.com/data.json',
    //   servers,
    //   {headers: headers});
    return this.http.put('https://udemy-ng-http.firebaseio.com/data.json',
      servers,
      {headers: headers});
  }
  getMegyek() {
    return this.http.get('http://localhost/Varosok/Controller/ControllerCity.php?content=megyek').pipe(
      map(
        (response: Response) => {
          console.log(response);
          const data = response.json();
          /*for (const megye of data) {
            megye.trim();
          }*/
          return data;
        }
      ),
      catchError(
        (error: Response) => {
          console.log(error);
          return observableThrowError('Something went wrong');
        }
      ), );
  }
  getAppName() {
    return this.http.get('https://udemy-ng-http.firebaseio.com/appName.json').pipe(
      map(
        (response: Response) => {
          return response.json();
        }
      ));
  }
}
Henrik Hollósi
  • 223
  • 1
  • 5
  • 16

2 Answers2

3

Remember that your request to populate megyek is asynchronous. So when AfterViewInit is executed, megyek has no value (yet) but is undefined, therefore it throws error. Keep megyek as Observable and don't use AfterViewInit. So try:

myControl = new FormControl();
megyek: Observable<string[]>;
filteredOptions: Observable<string[]>;

constructor(private serverService: ServerService) { }

ngOnInit() {
  // don't manually subscribe!
  this.megyek = this.serverService.getMegyek();

  // use switchmap, if user types fast
  this.filteredOptions = this.myControl.valueChanges.pipe(
    startWith(''),
    switchMap(value => this._filter(value))
  );
}

Also in your filter, you need to use map, to get to each field in your array, and also I changed it to rxjs 6 with pipe as seems you are using it.

private _filter(value: string): Observable<string[]> {
  return this.megyek.pipe(
    map(options => options.filter(option => option.toLowerCase().includes(value)))
  )
}

DEMO: StackBlitz

AT82
  • 71,416
  • 24
  • 140
  • 167
  • Copypasted the exact same code you recommended, result is 2 errors: "Type 'Observable' is not assignable to type 'Observable'. Type 'string' is not assignable to type 'string[]'." "Property 'filter' does not exist on type 'Observable'." – Henrik Hollósi Jan 21 '19 at 21:21
  • 1
    That's a type issue. Somewhere you have declared `Observable` (response from API?) That sounds weird, since you can't iterate an string. Somewhere you also have declared `string[]`. Both `filteredOptions` and `megyek` should be Observable arrays, so check how you are typing the data. – AT82 Jan 22 '19 at 06:17
  • 1
    Okay, I saw that I had one issue, I had wrong return type in filter function. So that is where the latter error was from (edited answer). Sorry about that ;) But the `Observable` issue is somewhere in your code though. – AT82 Jan 22 '19 at 06:54
  • I modified the code as recommended and got an error stating that "filter" is missing. I added import 'rxjs/add/operator/filter'; and it solved the problem. I then got another error that toLowerCase does not exist. I deleted that leaving only .filter(option => option.toLowerCase().includes(filterValue));. The Autocomplete box still wasn't populated with data but if I modified the html to *ngFor="let option of megyek" it was populated. Now I only have to make this filterable. Any ideas? Getting lost in the many observables... – Henrik Hollósi Jan 22 '19 at 12:30
  • 1
    Oooh, had to try the code and see what was wrong :D There was a mix with rxjs 5 / 6 in there, didn't notice that myself even, partly since we are dealing with both at work, so getting confused myself :D Anyway, answer updated and demo added. Please, in future you could also create a demo showcasing the issue, that way you can get help (and correct answer lol) waaaay faster, when there is something test. – AT82 Jan 22 '19 at 13:22
  • Thank you for your help, this was very useful, I learnt a lot. You are a lifesaver. :) And thanks for the advice on the demo. :) – Henrik Hollósi Jan 22 '19 at 14:41
  • 1
    You are very welcome, glad we came to a solution... finally, after me messing up mostly :D Have a nice day and happy coding! A small suggestion, I now see you are using Http, it's deprecated and have been for long, use HttpClient instead :) Useful stuff comes with it as well, for example interceptor! Check it out: https://angular.io/guide/http – AT82 Jan 22 '19 at 14:54
1
Your return value of array is any[] and you are now passing to an observable. 
Change this (filteredOptions: Observable<string[]>) to (filteredOptions:any[])
change the ngOnit() code to this
(
this.serverService.getMegyek().pipe(**inside here you can use takeUntil to unsubscribe**)
    .subscribe(
      (megyek: any[]) => {
        this.megyek = megyek;
        console.log(this.megyek);
      },
      (error) => console.log(error)
    );
)


if you html view use it like this: (
  <mat-autocomplete #auto="matAutocomplete">
      <mat-option *ngFor="let option of filteredOptions" [value]="option">
        {{option}}
      </mat-option>
    </mat-autocomplete>
*** Don't your array don't come out in a form of key:value? like opion.name and co..
if so, change this {{option}} to something like {{option.name}}
)
Monycell
  • 21
  • 2
  • Modified filteredOptions to any[] and used ".pipe(takeUntil(timer(100)))". Error: "Type 'Observable' is missing the following properties from type 'any[]': length, pop, push, concat, and 25 more." – Henrik Hollósi Jan 21 '19 at 17:34
  • You didn't use the takeUntil well. I will advice you to just remove the takeUntil and use only pipe for now. If you want to learn how to use the takeUntil I will send you a code to take a look at. I didn't remember to tell you, you need to edit that (ngAfterViewInit()) pipe goes with observable, and you are not returning observable again. let me write the code and send to you – Monycell Jan 21 '19 at 17:49
  • Show my how the console of megyek data is coming out. Let me use it and write it for you in full. check this link too: https://stackoverflow.com/questions/41161352/trigger-valuechange-with-initialized-value-angular2 – Monycell Jan 21 '19 at 17:57
  • Added full Angular source code to question. You can email me with the source code if you have a solution, see my profile. Data is coming from my PHP MySQL backend. – Henrik Hollósi Jan 21 '19 at 21:29