Search code examples
javascriptangularasynchronouscomponentsangular-event-emitter

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.


Solution

  • 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.