Angular 6 HttpClient assign resulting payload to array

6.5k Views Asked by At

I am attempting to do a get call in angular the call itself works as I can put a log in the subscribe and see the returned data the issue I am having is I can't seem to assign the data to my existing array (Currently Empty) and as the code below is of type Event[]. I have tried using a map on the data array which is also of type Event[] but no luck and the same with push although I believe this is because you can't push an array. I am sure there is something simple I am missing or can't find.

Here is the call I am making and bellow that the Event model.

this.httpClient.get<Event[]>('http://127.0.0.1:5555/events-get').subscribe((data) => this.events = data); 


export class Event {
    constructor(public name: String, public date: Date, public time: Date) {}
}

I am new to angular so I could be doing it all wrong any help is much appreciated.

EDIT

I have dome some more research but still no joy maybe this is something to do with having it in subscribe. I tried some of the array clone solutions from here

EDIT 2

Looking further I see that the contents of subscribe are a function is there something I am missing with scope does my this.events need passing in some way, it's set at the class level.

EDIT 3

import { Event } from '../shared/event.model';
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Subject } from 'rxjs';
import { map } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http'

@Injectable()
export class AdminService {
    eventsChanged = new Subject<Event[]>();

    private events: Event[] = [];

    constructor(private http: Http, private httpClient: HttpClient) {}

    getEvents() {
        this.httpClient.get<Event[]>('http://127.0.0.1:5555/events-get')
        .pipe(
            map(
                (data: Event[]) => data.map(event => {
                // may need to coerce string to Date types
                    return new Event(event.name, event.date, event.time)
                })
            )
        )
        .subscribe((events: Event[]) => this.events = events);

        console.log(this.events);
        return this.events;
}

I am then using this call in my component this hasn't changed from when it worked using a local array of Event[].

 this.events = this.adminService.getEvents();
3

There are 3 best solutions below

6
On BEST ANSWER

The base issue is you are attempting to return the Event[] data from your AdminService.getEvents() method prior to httpClient.get<Event[]>() resolving/emitting and subscribe() executing/assigning, that is why it is always returning an empty array. This is simply the asynchronous nature of HttpClient and RxJS.

@Injectable()
export class AdminService {
// ...

getEvents() {
    // this happens after console.log() and return this.events
    .subscribe((events: Event[]) => this.events = events);

    // this executes before get()/subscribe() resolves, so empty [] is returned
    console.log(this.events);
    return this.events;
}

Instead return the get<Event[]>().pipe() instead for the @Component to call and utilize:

import { Event } from '../shared/event.model';
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Subject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http'

@Injectable()
export class AdminService {
  eventsChanged = new Subject<Event[]>();

  constructor(private http: Http, private httpClient: HttpClient) {}

  getEvents(): Observable<Event[]> {
    return this.httpClient.get<Event[]>('http://127.0.0.1:5555/events-get')
      .pipe(
        map(
          (data: Event[]) => data.map(event => {
            // may need to coerce string to Date types
            return new Event(event.name, event.date, event.time)
           })
        )
      );
  }

Component:

@Component({ /* ... */ })
export class EventsComponent implements OnInit {
  events$: Observable<Event[]>;

  constructor(private adminService: AdminService) {}

  ngOnInit() {
    this.events$ = this.adminService.getEvents();
  }
}

Template with async pipe:

<ul>
  <li *ngFor="let event of events$ | async">{{event.name}}</li>
</ul>

Or:

@Component({})
export class EventsComponent implements OnInit {
  events: Event[] = [];

  constructor(private adminService: AdminService) {}

  ngOnInit() {
    this.adminService.getEvents()
      .subscribe(events => this.events = events);
  }
}

Template:

<ul>
  <li *ngFor="let event of events">{{event.name}}</li>
</ul>

On a side note, HttpClient with a type will not automatically create instances of Event class from a typed get(), it is objects with a set type. You could use the RxJS map operator in combination with Array.prototype.map to create an instance of class Event for each Event typed object return from get<Event[]>. Also be careful with naming it Event as it could conflict with an existing symbol Event.

Hopefully that helps!

0
On

From reviewing your third edit, I think the map/pipe issues are a red-herring and the core issue is the implementation of an asynchronous http call inside of a public method signature that behaves synchronously, i.e., getEvents().

For example, I would expect this code snippet to behave similarly, in that the method is able to immediately return this.events, whose value is still an empty array, and then proceed with executing the specified behavior inside of the asynchronous setTimeout

private events: Event[] = [];
private event1: Event = new Event("event1", "date", "time");
private event2: Event = new Event("event2", "date", "time");
public getEvents(): Event[] {
   setTimeout(() => {
     this.events = [..., event1, event2];
   }, 5000);

   return this.events;
}

For your code example, are you able to get the desired results with a code implementation similar to this. Complete functional implementation available on StackBlitz:

export class AdminService {
...
   public getEvents(): Observable<Event[]> {
   // Returns an observable
   return this.httpClient.get<Event[]>(url);
  }
}

export class EventComponent {

constructor(private adminService: AdminService) {}
    public events: Event[] = [];

    ngOnInit() {
        // Subscribe to the observable and when the asynchronous method
        // completes, assign the results to the component property.
        this.adminService.getEvents().subscribe(res => this.events = res);
    }
}
1
On

There are multiple problems with your code.

As per your code, you are unable to understand the RxJS Observable. You call subscribe, when you are finally ready to listen. So your getEvents() method should not subscribe, rather return the observable. i.e.

getEvents(): Observable<Event[]> {
  return this.httpClient
    .get<Event[]>("http://127.0.0.1:5555/events-get")
    .pipe(
      map(data =>
        data.map(event => new Event(event.name, event.date, event.time))
      )
    );
}

Since you have used

<ul>
  <li *ngFor="let event of events$ | async">{{event.name}}</li>
</ul>

The async pipe does the subscribe for you in the html. Just expose the events$ like you have already done in the ngOnInit().

I wouldn't define my interface as Event because Event is already part of RxJS.

Also, since you are calling your console.log outside the subscription, it will always be null, unless you add a tap from RxJS like this.

import { Event } from "../shared/event.model";
import { Injectable } from "@angular/core";
import { Http, Response } from "@angular/http";
import { Subject } from "rxjs";
import { map, tap } from "rxjs/operators";
import { HttpClient } from "@angular/common/http";

@Injectable()
export class AdminService {
  constructor(private http: Http, private httpClient: HttpClient) {}

  getEvents(): Observable<Event[]> {
    return this.httpClient
      .get<Event[]>("http://127.0.0.1:5555/events-get")
      .pipe(
        map(data =>
          data.map(event => new Event(event.name, event.date, event.time))
        ),
        tap(console.log)
      );
  }
}

Also, why are you calling both Http and HttpClient. Either use the client or http.

Happy Coding