Arpan Banerjee
Arpan Banerjee

Reputation: 1016

Asynchronous call in angular using event emitter and services for cross-component communication

cannot store the value received from subscribe method in a template variable.

photo-detail component

import { Component, OnInit, Input } from "@angular/core";
import { PhotoSevice } from "../photo.service";
import { Photo } from "src/app/model/photo.model";

@Component({
  selector: "app-photo-detail",
  templateUrl: "./photo-detail.component.html",
  styleUrls: ["./photo-detail.component.css"]
})
export class PhotoDetailComponent implements OnInit {

  url: string;

  constructor(private photoService: PhotoSevice) {
    this.photoService.photoSelected.subscribe(data => {
      this.url = data;
      console.log(this.url);
    });
    console.log(this.url);
  }

  ngOnInit() {

  }
}

the outside console.log gives undefined, and nothing is rendered in the view, but inside the subscibe method i can see the value.So, how can i display it in my view?

photos component

import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Params, Router } from "@angular/router";
import { FnParam } from "@angular/compiler/src/output/output_ast";
import { AlbumService } from "../service/album.service";
import { Photo } from "../model/photo.model";
import { PhotoSevice } from "./photo.service";

@Component({
  selector: "app-photos",
  templateUrl: "./photos.component.html",
  styleUrls: ["./photos.component.css"]
})
export class PhotosComponent implements OnInit {
  selectedAlbumId: string;
  photoList: Photo[] = [];
  photoSelected: Photo;
  isLoading: Boolean;
  constructor(
    private rout: ActivatedRoute,
    private albumService: AlbumService,
    private router: Router,
    private photoService: PhotoSevice
  ) { }

  ngOnInit() {
    this.isLoading = true;
    this.rout.params.subscribe((params: Params) => {
      this.selectedAlbumId = params["id"];
      this.getPhotos(this.selectedAlbumId);
    });
  }

  getPhotos(id: string) {
    this.albumService.fetchPhotos(this.selectedAlbumId).subscribe(photo => {
      this.photoList = photo;
      this.isLoading = false;
    });
  }

  displayPhoto(url: string, title: string) {

    console.log(url);
    this.photoService.photoSelected.emit(url);
    this.router.navigate(["/photo-detail"]);
  }
}

please explain me how this works and how to work around it so that i can store and display the value received from subscribing and asynchronous call in a template view.

here are the views of the two components---

photo.component.html

<div *ngIf="isLoading">
  <h3>Loading...</h3>
</div>

<div class="container" *ngIf="!isLoading">
  <div class="card-columns">
    <div *ngFor="let photo of photoList" class="card">
      <img
        class="card-img-top"
        src="{{ photo.thumbnailUrl }}"
        alt="https://source.unsplash.com/random/300x200"
      />
      <div class="card-body">
        <a
          class="btn btn-primary btn-block"
          (click)="displayPhoto(photo.url, photo.title)"
          >Enlarge Image</a
        >
      </div>
    </div>
  </div>
</div>

photo-detail.component.ts

<div class="container">
  <div class="card-columns">
    <div class="card">
      <img class="card-img-top" src="{{ url }}" />
    </div>
  </div>
</div>

photo.service.ts

import { Injectable } from "@angular/core";
import { EventEmitter } from "@angular/core";

@Injectable({ providedIn: "root" })
export class PhotoSevice {
  photoSelected = new EventEmitter();
 // urlService: string;
}

here is a link to my github repo, i have kept the code in comments and used a different approach there. If you check the albums component there also i have subscribed to http request and assigned the value in the template variable of albums component. there also the value comes as undefined oustide the subscibe method, but i am able to access it in template.

https://github.com/Arpan619Banerjee/angular-accelerate

here are the details of albums component and service pls compare this with the event emitter case and explain me whats the difference-- albums.component.ts

import { Component, OnInit } from "@angular/core";
import { AlbumService } from "../service/album.service";
import { Album } from "../model/album.model";

@Component({
  selector: "app-albums",
  templateUrl: "./albums.component.html",
  styleUrls: ["./albums.component.css"]
})
export class AlbumsComponent implements OnInit {
  constructor(private albumService: AlbumService) {}
  listAlbums: Album[] = [];
  isLoading: Boolean;

  ngOnInit() {
    this.isLoading = true;
    this.getAlbums();
  }

  getAlbums() {
    this.albumService.fetchAlbums().subscribe(data => {
      this.listAlbums = data;
      console.log("inside subscibe method-->" + this.listAlbums); // we have data here
      this.isLoading = false;
    });
    console.log("outside subscribe method----->" + this.listAlbums); //empty list==== but somehow we have the value in the view , this doesn t work
    //for my photo and photo-detail component.
  }
}

albums.component.html

<div *ngIf="isLoading">
  <h3>Loading...</h3>
</div>
<div class="container" *ngIf="!isLoading">
  <h3>Albums</h3>
  <app-album-details
    [albumDetail]="album"
    *ngFor="let album of listAlbums"
  ></app-album-details>
</div>

album.service.ts

import { Injectable } from "@angular/core";
import { HttpClient, HttpParams } from "@angular/common/http";
import { map, tap } from "rxjs/operators";
import { Album } from "../model/album.model";
import { Observable } from "rxjs";
import { UserName } from "../model/user.model";

@Injectable({ providedIn: "root" })
export class AlbumService {
  constructor(private http: HttpClient) {}
  albumUrl = "http://jsonplaceholder.typicode.com/albums";
  userUrl = "http://jsonplaceholder.typicode.com/users?id=";
  photoUrl = "http://jsonplaceholder.typicode.com/photos";

  //get the album title along with the user name
  fetchAlbums(): Observable<any> {
    return this.http.get<Album[]>(this.albumUrl).pipe(
      tap(albums => {
        albums.map((album: { userId: String; userName: String }) => {
          this.fetchUsers(album.userId).subscribe((user: any) => {
            album.userName = user[0].username;
          });
        });
        // console.log(albums);
      })
    );
  }

  //get the user name of the particular album with the help of userId property in albums
  fetchUsers(id: String): Observable<any> {
    //let userId = new HttpParams().set("userId", id);
    return this.http.get(this.userUrl + id);
  }

  //get the photos of a particular album using the albumId
  fetchPhotos(id: string): Observable<any> {
    let selectedId = new HttpParams().set("albumId", id);
    return this.http.get(this.photoUrl, {
      params: selectedId
    });
  }
}

enter image description here

I have added console logs in the even emitters as told in the comments and this is the behavior i got which is expected.

Upvotes: 1

Views: 5897

Answers (2)

Barremian
Barremian

Reputation: 31105

Question's a two-parter.

Part 1 - photos and photo-detail component

  1. EventEmitter is used to emit variables decorated with a @Output decorator from a child-component (not a service) to parent-component. It can then be bound to by the parent component in it's template. A simple and good example can be found here. Notice the (notify)="receiveNotification($event)" in app component template.

  2. For your case, using a Subject or a BehaviorSubject is a better idea. Difference between them can be found in my other answer here. Try the following code

photo.service.ts

import { Injectable } from "@angular/core";
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: "root" })
export class PhotoSevice {
  private photoSelectedSource = new BehaviorSubject<string>(undefined);

  public setPhotoSelected(url: string) {
    this.photoSelectedSource.next(url);
  }

  public getPhotoSelected() {
    return this.photoSelectedSource.asObservable();
  }
}

photos.component.ts

export class PhotosComponent implements OnInit {
  .
  .
  .
  displayPhoto(url: string, title: string) {
    this.photoService.setPhotoSelected(url);
    this.router.navigate(["/photo-detail"]);
  }
}

photo-detail.component.ts

  constructor(private photoService: PhotoSevice) {
    this.photoService.getPhotoSelected().subscribe(data => {
      this.url = data;
      console.log(this.url);
    });
    console.log(this.url);
  }

photo-detail.component.html

<ng-container *ngIf="url">
  <div class="container">
    <div class="card-columns">
      <div class="card">
        <img class="card-img-top" [src]="url"/>
      </div>
    </div>
  </div>
</ng-container>

Part 2 - albums component and service

The call this.albumService.fetchAlbums() returns a HTTP GET Response observable. You are subscribing to it and updating the member variable value and using it in the template.

From your comment on the other answer:

i understand the behaviour and why the outside console.log is underfined, its beacuse the execution context is diff for async calls and it first executes the sync code and then comes the async code

I am afraid the difference between synchronous and asynchronous call is not as simple as that. Please see here for a good explanation of difference between them.

albums.components.ts

  getAlbums() {
    this.albumService.fetchAlbums().subscribe(data => {
      this.listAlbums = data;
      console.log("inside subscibe method-->" + this.listAlbums); // we have data here
      this.isLoading = false;
    });
    console.log("outside subscribe method----->" + this.listAlbums); //empty list==== but somehow we have the value in the view , this doesn t work
    //for my photo and photo-detail component.
  }

albums.component.html

<div *ngIf="isLoading">
  <h3>Loading...</h3>
</div>
<div class="container" *ngIf="!isLoading">
  <h3>Albums</h3>
  <app-album-details
    [albumDetail]="album"
    *ngFor="let album of listAlbums"
  ></app-album-details>
</div>

The question was to explain why the template displays the albums despite console.log("outside subscribe method----->" + this.listAlbums); printing undefined. In simple words, when you do outside console log, this.listAlbums is actually undefined in that it hasn't been initialized yet. But in the template, there is a loading check *ngIf="!isLoading". And from the controller code, isLoading is only set to false when listAlbums is assigned a value. So when you set isLoading to false it is assured that listAlbums contains the data to be shown.

Upvotes: 1

saivishnu tammineni
saivishnu tammineni

Reputation: 1242

I think you are trying to display a selected image in a photo detail component which gets the photo to display from a service.

The question doesn't mention how you are creating the photo detail component.

  1. Is the component created after a user selects a photo to dislay?
  2. Is the component created even before user selects a photo to display?

I think the first is what you are trying to do. If so there are two things...

  1. When you are subscribing inside the constructor, the code inside the subscribe runs after some time when the observable emits. in the mean time the code after the subscription i.e console.log(url) (the outside one) will run and so it will be undefined.

  2. If the subscription happens after the event is emitted i.e you have emitted the event with url but by then the component didn't subscribe to the service event. so the event is lost and you don't get anything. For this you can do few things

    a. Add the photo whose details are to be shown to the url and get it in the photo details component.

    b. Convert the subject / event emitter in the service to behavioural subject. This will make sure that even if you subscribe at a later point of time you still get the event last emitted.

    c. If the photo details component is inside the template of the photo component send the url as an input param (@Input() binding).

Hope this helps

Upvotes: 1

Related Questions