Асинхронный вызов в angular с использованием эмиттера событий и сервисов для межкомпонентной связи

невозможно сохранить значение, полученное методом подписки, в переменной шаблона.

компонент фото-подробности

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() {

  }
}

внешний console.log дает undefined, и в представлении ничего не отображается, но внутри метода подписки я вижу значение. Итак, как я могу отобразить его в своем представлении?

компонент фотографий

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"]);
  }
}

пожалуйста, объясните мне, как это работает и как обойти это, чтобы я мог хранить и отображать значение, полученное от подписки и асинхронного вызова, в представлении шаблона.

вот взгляды двух компонентов---

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;
}

вот ссылка на мой репозиторий github, я сохранил код в комментариях и использовал там другой подход. Если вы проверите компонент альбомов, я также подписался на http-запрос и присвоил значение в переменной шаблона компонента альбомов. там также значение приходит как неопределенное вне метода подписки, но я могу получить к нему доступ в шаблоне.

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

вот подробная информация о компоненте и службе альбомов, пожалуйста, сравните это со случаем генератора событий и объясните мне, в чем разница - 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
    });
  }
}

введите здесь описание изображения

Я добавил журналы консоли в четные эмиттеры, как сказано в комментариях, и это ожидаемое поведение, которое я получил.


person Arpan Banerjee    schedule 22.03.2020    source источник
comment
Не могли бы вы показать PhotoSevice? В этом случае вам не нужен эмиттер событий. Вам нужен Subject.   -  person Michael D    schedule 22.03.2020
comment
да, я подключил фотосервис, и да, я выполнил требование, просто определив переменную в фотосервисе и установив ее в фотокомпоненте, после чего я получил к нему доступ из моего компонента фотодеталей. Это сработало. Пожалуйста, проверьте мои вопросы один раз, я добавил некоторые подробности. Пожалуйста, сравните поведение с http-вызовами и генератором событий и объясните мне, что происходит.   -  person Arpan Banerjee    schedule 22.03.2020
comment
Я разместил ответ. В будущем постарайтесь удалить из вопроса ненужные части кода.   -  person Michael D    schedule 22.03.2020


Ответы (2)


Вопрос состоит из двух частей.

Часть 1 — фотографии и компонент фото-деталей

  1. EventEmitter используется для генерации переменных, украшенных декоратором @Output, из дочернего компонента< /em> (не служба) в родительский компонент. Затем его можно связать с родительским компонентом в его шаблоне. Простой и хороший пример можно найти здесь. Обратите внимание на (notify)="receiveNotification($event)" в шаблоне компонента приложения.

  2. В вашем случае используйте Subject или BehaviorSubject — лучшая идея. Разницу между ними можно найти в другом моем ответе здесь. Попробуйте следующий код

фото.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"]);
  }
}

фото-detail.component.ts

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

фото-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>

Часть 2 – компонент и служба альбомов

Вызов this.albumService.fetchAlbums() возвращает наблюдаемый ответ HTTP GET. Вы подписываетесь на него, обновляете значение переменной-члена и используете его в шаблоне.

Из вашего комментария к другому ответу:

я понимаю поведение и почему внешний console.log недоопределен, потому что контекст выполнения отличается для асинхронных вызовов, и сначала выполняется код синхронизации, а затем идет асинхронный код

Боюсь, разница между синхронным и асинхронным вызовом не так проста. См. здесь хорошее объяснение различий между ними.

альбомы.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.
  }

альбомы.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>

Вопрос был в том, чтобы объяснить, почему шаблон отображает альбомы несмотря на то, что console.log("outside subscribe method----->" + this.listAlbums); печатает undefined. Проще говоря, когда вы делаете внешний журнал консоли, this.listAlbums на самом деле undefined в том смысле, что он еще не был инициализирован. Но в шаблоне есть проверка загрузки *ngIf="!isLoading". И из кода контроллера isLoading устанавливается на false только тогда, когда listAlbums присваивается значение. Поэтому, когда вы устанавливаете isLoading в false, гарантируется, что listAlbums содержит данные, которые должны быть показаны.

person Michael D    schedule 22.03.2020
comment
Хорошо, я понял вашу точку зрения, но я видел фрагмент кода, в котором в службе использовался эмиттер событий.... - person Arpan Banerjee; 23.03.2020
comment
Это сработает, потому что EventEmitter — это простой расширение Subject. Но это противоречит основному варианту использования EventEmitter в Angular. Из документов Angular: используйте в компонентах директиву @Output для синхронной или асинхронной генерации пользовательских событий. Тогда этот фрагмент кода использует EventEmitter неправильно. - person Michael D; 23.03.2020
comment
В вашем случае, если вы продолжите использовать EventEmitter, сведения о фотографии никогда не будут загружены в photo-detail.component.ts. Потому что вы сначала отправляете URL-адрес, а потом подписываетесь на него. Как было сказано ранее, EventEmitter использует Subject внутри. Поэтому, когда вы подписываетесь на наблюдаемое, ничего не произойдет, пока оно не выдаст следующее значение. Вот почему изображение не загружается. Для вашего варианта использования вам нужно использовать BehaviorSubject. - person Michael D; 23.03.2020
comment
Хорошо, я понимаю вариант использования эмиттера событий и поведенческих субъектов, но что касается моего компонента альбома и http-запроса, вы сказали, что это из-за свойства isLoading, которое гарантирует, что в списке есть данные, и поэтому он отображается в моем представлении, но я удалена логика isLoading, и она все еще в моем представлении, поэтому, пожалуйста, объясните мне, как можно получить к ней доступ в моем представлении, если я не могу получить к ней доступ где-либо в моем шаблоне, пожалуйста, объясните, как это работает за кулисами. - person Arpan Banerjee; 26.03.2020
comment
Это потому, что Angular знает, когда значение listAlbums изменяется. Это выходит за рамки вопроса, чтобы объяснить здесь. Вы можете прочитать об этом здесь. - person Michael D; 26.03.2020
comment
Спасибо за терпение и объяснение, я новичок в angular и сам изучаю его. Я с нетерпением жду stackoverflow и вас для дальнейших запросов. Еще раз спасибо, это очень важно! - person Arpan Banerjee; 26.03.2020
comment
Потому что вы сначала отправляете URL-адрес, а потом подписываетесь на него. Как было сказано ранее, EventEmitter использует субъект внутри себя. Поэтому, когда вы подписываетесь на наблюдаемое, ничего не произойдет, пока оно не выдаст следующее значение. ---- да, я проверил это, когда я нажимаю кнопку, чтобы увеличить в первый раз, в моей фотографии нет значения URL .component.ts, затем, когда я возвращаюсь и снова нажимаю кнопку, я вижу значение, но оно никогда не отображает его в представлении. - person Arpan Banerjee; 26.03.2020
comment
Я пытался использовать темы, это было то же самое, что и генераторы событий, но это сработало, когда я использовал темы поведения! Мне нужно будет найти источник событий diff bw/субъекты/субъекты поведения.! - person Arpan Banerjee; 26.03.2020

Я думаю, вы пытаетесь отобразить выбранное изображение в компоненте сведений о фотографии, который получает фотографию для отображения из службы.

В вопросе не упоминается, как вы создаете компонент детализации фотографии.

  1. Создается ли компонент после того, как пользователь выбирает фотографию для отображения?
  2. Создается ли компонент еще до того, как пользователь выберет фотографию для отображения?

Я думаю, что первое - это то, что вы пытаетесь сделать. Если так, то есть две вещи...

  1. Когда вы подписываетесь внутри конструктора, код внутри подписки запускается через некоторое время, когда наблюдаемое испускает. в то же время код после подписки, т. е. console.log(url) (внешний), будет запущен, поэтому он будет неопределенным.

  2. Если подписка происходит после отправки события, то есть вы отправили событие с URL-адресом, но к тому времени компонент не подписался на событие службы. поэтому событие теряется, и вы ничего не получаете. Для этого вы можете сделать несколько вещей

    а. Добавьте фотографию, сведения о которой должны отображаться, к URL-адресу и получите ее в компоненте сведений о фотографии.

    б. Преобразование субъекта/эмиттера событий в службе в поведенческий субъект. Это гарантирует, что даже если вы подпишетесь в более поздний момент времени, вы все равно получите последнее отправленное событие.

    в. Если компонент сведений о фотографии находится внутри шаблона компонента фотографии, отправьте URL-адрес в качестве входного параметра привязки (@Input()).

Надеюсь это поможет

person saivishnu tammineni    schedule 22.03.2020
comment
я отредактировал свой вопрос и вставил некоторые дополнительные детали, чтобы помочь вам лучше понять мой вопрос. Вот мои комментарии по вашим пунктам: 1) Да, я понимаю поведение и почему внешний console.log недоопределен, потому что контекст выполнения diff для асинхронных вызовов, и он сначала выполняет код синхронизации, а затем приходит асинхронный код. Если это произойдет, то что происходит с HTTP-вызовом в компоненте альбомов, пожалуйста, сравните эти два и объясните мне. - person Arpan Banerjee; 22.03.2020
comment
причина, по которой данные доступны внутри представления, заключается в том, что к тому времени данные поступили из службы альбомов. - person saivishnu tammineni; 22.03.2020
comment
хорошо, но почему это недоступно в представлении компонента фото-детали ?? - person Arpan Banerjee; 22.03.2020
comment
В компоненте сведений о фотографии компонент создается после того, как событие было отправлено, поэтому он не может получить его, как я указал в фактическом ответе. Для простоты понимания поместите журнал, когда вы выдаете значение, и журнал внутри конструктора конструктора компонента фотодетали. Вы увидите, что событие было сгенерировано первым, а затем журнал внутри конструктора зарегистрирован. - person saivishnu tammineni; 22.03.2020