1

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.

Arpan Banerjee
  • 826
  • 12
  • 25
  • Could you please show `PhotoSevice`? You don't need an event emitter in this case. You need a `Subject`. – ruth Mar 22 '20 at 07:58
  • yes i have attached the photo service, and yes i have achieved the requirement by simply defining a variable in the photo service and setting it in photo component, then i have accessed it from my photo-detail component.It worked. Pls check my questions once, i have added some more details. Pls compare the behaviour with http calls and event emitter and explain me whats goin on. – Arpan Banerjee Mar 22 '20 at 08:58
  • I have posted an answer. In the future please try to remove non-essential parts of the code from the question. – ruth Mar 22 '20 at 12:57

2 Answers2

1

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

saivishnu tammineni
  • 1,092
  • 7
  • 14
  • i have edited my question and pasted some additional details to help u understand my question better, Here are my comments on your points--1) Yes 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, If this happens then whats happeing with the http call in albums component, pls compare those two and explain me. – Arpan Banerjee Mar 22 '20 at 08:54
  • the reason why data is available inside the view is because the data arrived by then from album service – saivishnu tammineni Mar 22 '20 at 11:14
  • okay, but why is it not available in photo-detail component view?? – Arpan Banerjee Mar 22 '20 at 11:19
  • 1
    In photo detail component the component is created after the event is emitted so it cannot get it as i specified in the actual answer. For easy understanding put a log when you emit value and a log inside the constructor of photo detail component constructor. You will see that event was emitted first and then log inside constructor is logged. – saivishnu tammineni Mar 22 '20 at 11:22
1

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.

ruth
  • 29,535
  • 4
  • 30
  • 57
  • okay i get your point, but i have seen a piece of code where event emitter was used in the service.... – Arpan Banerjee Mar 23 '20 at 04:15
  • 1
    It will work because [`EventEmitter`](https://github.com/angular/angular/blob/master/packages/core/src/event_emitter.ts) is a simple extension of `Subject`. But it goes against the basic use case of `EventEmitter` in [Angular](https://angular.io/api/core/EventEmitter). From Angular docs: Use in components with the @Output directive to emit custom events synchronously or asynchronously. Then that piece of code is using `EventEmitter` wrong. – ruth Mar 23 '20 at 06:38
  • 1
    In your case, if you continue using `EventEmitter` the photo details will never load in `photo-detail.component.ts`. Because you are emitting the url first and subscribing to it later. As as said before, `EventEmitter` uses a `Subject` internally. So when you subscribe to the observable, nothing will happen until it emits the next value. That is why the image isn't loaded. For your usecase, you need to use `BehaviorSubject`. – ruth Mar 23 '20 at 06:43
  • Okay i understand the use case of event emitter and behavioral subjects, but regarding my album component and http request, u said that it i because of isLoading property which makes sure that the list has got data and so it displays in my view, but i removed the isLoading logic and it is still in my view, so pls explain me how is it possible to access it in my view if i cannot access it anywhere in my template, pls explain how does it work behind the scenes. – Arpan Banerjee Mar 26 '20 at 07:15
  • 1
    That is because Angular knows when the value of `listAlbums` is changed. That is beyond the scope of the question to explain here. You can read up on it [here](https://blog.angular-university.io/how-does-angular-2-change-detection-really-work/). – ruth Mar 26 '20 at 08:17
  • Thanks for being patient and explaining me, I am new to angular and self learning it. I will look forward to stackoverflow and you for further queries. Thanks again, means a lot! – Arpan Banerjee Mar 26 '20 at 08:37
  • Because you are emitting the url first and subscribing to it later. As as said before, EventEmitter uses a Subject internally. So when you subscribe to the observable, nothing will happen until it emits the next value.---- yes i tested this out, when i click the button to enlarge the first time, there is no value of url in my "photo-detail.component.ts", then when i go back and click the button again i can see the value, but it never renders it in the view. – Arpan Banerjee Mar 26 '20 at 09:26
  • I tried using Subjects, it was same as event emitters, but it worked when i used Behaviour subjects!! I will have to find out the diff bw event emitter/subjects/behaviour subjects.! – Arpan Banerjee Mar 26 '20 at 09:44
  • 1
    The difference is there in point 2 of part 1 of my answer. More info here: https://stackoverflow.com/q/43348463/6513921 – ruth Mar 26 '20 at 10:30