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 inNgZone.run(() => { this.response.next(resp); }
- call
ApplicationRef.tick()
afterthis.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 inNgZone.run(() => { this.response.next(resp); }
- call
ApplicationRef.tick()
afterthis.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.
5 Answers
Reset to default 4One 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
}