0

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();
Alexander Staroselsky
  • 37,209
  • 15
  • 79
  • 91
bobthemac
  • 1,172
  • 6
  • 26
  • 59
  • I see there are three votes to close, did I not give enough information? If not what do you need? – bobthemac Sep 01 '18 at 16:18
  • In your original example, if you instead log inside the `subscribe()`, are you seeing the data you expect? `subscribe((data) => console.log(data));` – Alexander Staroselsky Sep 01 '18 at 16:43
  • Yes I see the json posted from the server it's just not saving to the other array and both are shown as type `Event[]` – bobthemac Sep 01 '18 at 16:46
  • Can you clarify the intent/need for the multiple `rxjs` operators? I'm familiar with seeing the `map` operator in code that imports and uses the `HttpModule`, but one of the advantages of the `HttpClientModule` is that it handles type-checking and serialization for you. The example [StackBlitz](https://stackblitz.com/edit/angular-mxetee) I provide in my solution below removes the `rxjs` operators and is still able to correctly handle serialization to the specified type. – ericksoen Sep 01 '18 at 22:07
  • I don't know I am only half way through the angular course and the instructor likes to show you multiple implementations then change them later showing you how to do it badly then how it should be done. – bobthemac Sep 01 '18 at 22:30

3 Answers3

4

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!

atwright147
  • 3,694
  • 4
  • 31
  • 57
Alexander Staroselsky
  • 37,209
  • 15
  • 79
  • 91
  • Sorry but same issue this.events is still empty – bobthemac Sep 01 '18 at 16:31
  • When/where exactly are you trying to use `this.events`, if you are attempting to log it prior resolving it will be undefined. – Alexander Staroselsky Sep 01 '18 at 16:32
  • it's not undefined it's empty I define it at the top shown as `[]` the get runs in a function that returns this.events – bobthemac Sep 01 '18 at 16:35
  • You please would need to show how you are using this.events and calling the get() in your component exactly. You are absolutely sure you are getting data from the server? There must be something else going on. – Alexander Staroselsky Sep 01 '18 at 16:37
  • @bobthemac I've updated the answer, you simply cannot return `this.events` from the service like that as it resolves before `subscribe()` executes. Instead return the `Observable` instead and execute the subscribe in the @Component. Thanks! – Alexander Staroselsky Sep 01 '18 at 18:41
  • Thanks for your help, I did try that but it broke some other parts looked at the code from a tutorial and just created a set function and passed `events` to that and then set `this.events` probably not the best but it works. Thanks again I will look at your code more in depth later. – bobthemac Sep 01 '18 at 18:52
1

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

Yousuf Jawwad
  • 3,037
  • 7
  • 33
  • 60
  • I am part way through an angular course, haven't got to observables yet. I haven't used async in the template, and haven't gotten to the upgrade everything to httpclient part either. Thanks for the answer though some good point in their all taken onboard. – bobthemac Sep 01 '18 at 22:00
0

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);
    }
}
ericksoen
  • 821
  • 8
  • 14