最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to properly multicast result of HttpClient request to several components in Angular? - Stack Overflow

programmeradmin0浏览0评论

I'm trying to send the result of HttpClient post requests multiple ponents in my Angular app. I'm using a Subject and calling its next() method whenever a new post request is successfully executed. Each ponent subscribes to the service's Subject.

The faulty services is defined as

@Injectable()
export class BuildingDataService {

  public response: Subject<object> = new Subject<object>();

  constructor (private http: HttpClient) { }

  fetchBuildingData(location) {
    ...

    this.http.post(url, location, httpOptions).subscribe(resp => {
      this.response.next(resp);
    });
}

The ponents subscribe to BuildingService.response as follows

@Component({
  template: "<h1>{{buildingName}}</h1>"
  ...
})

export class SidepanelComponent implements OnInit {
  buildingName: string;

  constructor(private buildingDataService: BuildingDataService) { }

  ngOnInit() {
    this.buildingDataService.response.subscribe(resp => {
        this.buildingName = resp['buildingName'];
      });
  }

  updateBuildingInfo(location) {
    this.buildingDataService.fetchBuildingData(location);
  }
}

updateBuildingInfo is triggered by users clicking on a map.

Retrieving the data from the server and passing it to the ponents works: I can output the payloads to the console in each ponent. However, the ponents' templates fail to update.

After Googling and fiddling for most of today I found that this implementation does not trigger Angular's change detection. The fix is to either

  • wrap my call to next() in the service in NgZone.run(() => { this.response.next(resp); }
  • call ApplicationRef.tick() after this.title = resp['title'] in the ponent.

Both solutions feel like dirty hacks for such a trivial use case. There must be a better way to achieve this.

My question therefore is: what is the proper way to fetch data once and send it off to several ponents?

I'd furthermore like to understand why my implementation escapes Angular's change detection system.

EDIT it turns out I was initiating my call to HttpClient outside of Angular's zone hence it could not detect my changes, see my answer for more details.

I'm trying to send the result of HttpClient post requests multiple ponents in my Angular app. I'm using a Subject and calling its next() method whenever a new post request is successfully executed. Each ponent subscribes to the service's Subject.

The faulty services is defined as

@Injectable()
export class BuildingDataService {

  public response: Subject<object> = new Subject<object>();

  constructor (private http: HttpClient) { }

  fetchBuildingData(location) {
    ...

    this.http.post(url, location, httpOptions).subscribe(resp => {
      this.response.next(resp);
    });
}

The ponents subscribe to BuildingService.response as follows

@Component({
  template: "<h1>{{buildingName}}</h1>"
  ...
})

export class SidepanelComponent implements OnInit {
  buildingName: string;

  constructor(private buildingDataService: BuildingDataService) { }

  ngOnInit() {
    this.buildingDataService.response.subscribe(resp => {
        this.buildingName = resp['buildingName'];
      });
  }

  updateBuildingInfo(location) {
    this.buildingDataService.fetchBuildingData(location);
  }
}

updateBuildingInfo is triggered by users clicking on a map.

Retrieving the data from the server and passing it to the ponents works: I can output the payloads to the console in each ponent. However, the ponents' templates fail to update.

After Googling and fiddling for most of today I found that this implementation does not trigger Angular's change detection. The fix is to either

  • wrap my call to next() in the service in NgZone.run(() => { this.response.next(resp); }
  • call ApplicationRef.tick() after this.title = resp['title'] in the ponent.

Both solutions feel like dirty hacks for such a trivial use case. There must be a better way to achieve this.

My question therefore is: what is the proper way to fetch data once and send it off to several ponents?

I'd furthermore like to understand why my implementation escapes Angular's change detection system.

EDIT it turns out I was initiating my call to HttpClient outside of Angular's zone hence it could not detect my changes, see my answer for more details.

Share Improve this question edited Apr 30, 2018 at 7:26 Simeon Nedkov asked Apr 25, 2018 at 20:08 Simeon NedkovSimeon Nedkov 1,0948 silver badges12 bronze badges
Add a ment  | 

5 Answers 5

Reset to default 4

One way is to get an Observable of the Subject and use it in your template using async pipe:

(building | async)?.buildingName

Also, if different ponents are subscribing to the service at different times, you may have to use BehaviorSubject instead of a Subject.


@Injectable()
export class BuildingDataService {
  private responseSource = new Subject<object>();
  public response = this.responseSource.asObservable()

  constructor (private http: HttpClient) { }

  fetchBuildingData(location) {
    this.http.post(url, location, httpOptions).subscribe(resp => {
      this.responseSource.next(resp);
    });
  }
}

@Component({
  template: "<h1>{{(building | async)?.buildingName}}</h1>"
  ...
})

export class SidepanelComponent implements OnInit {
  building: Observable<any>;

  constructor(private buildingDataService: DataService) {
      this.building = this.buildingDataService.response;
  }

  ngOnInit() {
  }

  updateBuildingInfo(location) {
    this.buildingDataService.fetchBuildingData(location);
  }
}

The standard way to do this in an angular app is a getter in the service class.

get data()
{
     return data;
}

Why are you trying to plicate matters? What are the benefits, and if there are benefits they will overe the drawbacks?

I think I found the issue. I briefly mention that fetchBuildingData is triggered by users clicking on a map; that map is Leaflet as provided by the ngx-leaflet module. I bind to its click event as follows

map.on('click', (event: LeafletMouseEvent) => {
  this.buildingDataService.fetchBuildingData(event.latlng);
});

The callback is, I now realise, fired outside Angular's zone. Angular therefore fails to detect the change to this.building. The solution is to bring the callback in Angular's zone through NgZone as

map.on('click', (event: LeafletMouseEvent) => {
  ngZone.run(() => {
    this.buildingDataService.fetchBuildingData(event.latlng);
  });
});

This solves the problem and every proposed solution works like a charm.

A big thank you for the quick and useful responses. They were instrumental in helping me narrow down the problem!

To be fair: this issue is mentioned in ngx-leaflet's documentation [1]. I failed to understand the implications right away as I'm still learning Angular and there is a lot to take in.

[1] https://github./Asymmetrik/ngx-leaflet#a-note-about-change-detection

EDIT:

As your change detection seems to struggle with whatsoever detail in your object, I have another suggestion. And as I also prefer the BehaviorSubject over a sipmple Subject I adjusted even this part of code.

1st define a wrapper object. The interesting part is, that we add a second value which definitely has to be unique pared to any other wrapper of the same kind you produce.

I usually put those classes in a models.ts which I then import wherever I use this model.

export class Wrapper {
   constructor(
       public object: Object,
       public uniqueToken: number
   ){}
}

2nd in your service use this wrapper as follows.

import { Observable } from 'rxjs/Observable'; 
import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 
import { Wrapper } from './models.ts';

@Injectable()
export class BuildingDataService {
   private response: BehaviorSubject<Wrapper> = new BehaviorSubject<Wrapper>(new Wrapper(null, 0));

   constructor (private http: HttpClient) { }

   public getResponse(): Observable<Wrapper> {
        return this.response.asObservable();
   }

   fetchBuildingData(location) {
      ...
      this.http.post(url, location, httpOptions).subscribe(resp => {

         // generate and fill the wrapper
         token: number = (new Date()).getTime();
         wrapper: Wrapper = new Wrapper(resp, token);

         // provide the wrapper
         this.response.next(wrapper);
      });
   }

Import the model.ts in all subscribing classes in order to be able to handle it properly and easily. Let your Subscribers subscribe to the getResponse()-method.

This must work. Using this wrapper + unique token technique I always solved such change detection problems eventually.

The code of sabith is wrong (but the idea is the correct)

  //declare a source as Subject
  private responseSource = new Subject<any>(); //<--change object by any

  public response = this.responseSource.asObservable() //<--use"this"

  constructor (private http: HttpClient) { }

  fetchBuildingData(location) {
    this.http.post(url, location, httpOptions).subscribe(resp => {
      this.responseSource.next(resp);
    });
  }

Must be work

Another idea is simply using a getter in your ponent

//In your service you make a variable "data"
data:any
fetchBuildingData(location) {
    this.http.post(url, location, httpOptions).subscribe(resp => {
      this.data=resp
    });
  }

//in yours ponents

get data()
{
     return BuildingDataService.data
}

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论