So firstly a little background. I am developing an application that is using agm maps to display in realtime the location of multiple users. I have recently been working on improving performance as there can be thousands of users each representing a map marker or dom element. I have changed the parent and child component to both use the onpush strategy, the child component renders the map markers using an ngFor loop, i have also added trackBy to this loop. Location updates are received via signalr, the object is updated correctly but even after change detection has ran the markers position is not being updated correctly on the map.
The two components below are cut down versions to help identify the problem.
Firstly the Parent Map component
#Component({
selector: 'app-map',
template: '<agm-map>
<app-map-markers #mapMarkers [users]="users"></app-map-markers>
</agm-map>',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MapComponent implements OnInit, OnDestroy {
users: MapUser[];
private handleLocationUpdate(updatedUser: any) {
const user = this.users.find(this.findIndexToUpdate, updatedUser.Id);
if (user !== null && user !== undefined) {
user.lat = updatedUser.Lat;
user.lng = updatedUser.Lng;
user.lastSeen = updatedUser.LastSeen;
const userIndex = this.handledUsers.indexOf(user);
this.handledUsers[userIndex] = user;
}
}
findIndexToUpdate(newItem) {
return newItem.id === this;
}
}
The users array is populated via subscibing to a http service but i left that out as it works fine. When a location update is received it is handled by the parent, ive left the function to show this, but again the function is called by subscribing to a signalr service.
Now the map markers component.
#Component({
selector: 'app-map-markers',
template: '<agm-marker *ngFor="let user of users; let i = index; trackBy: trackByUser" [latitude]="user.lat" [longitude]="user.lng" [title]="user.firstName">
</agm-marker>',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MapMarkersComponent {
#Input() users: MapUser[];
constructor() {}
public trackByUser(indx: number, value: MapUser) {
let identifier: string = indx.toString();
if ('lat' in value && 'lng' in value) {
identifier = identifier.concat(value.lastSeen.toString());
}
return identifier;
}
}
I suspect the issue is that due to onpush its not detecting the changes, however for performance reasons onpush seems to be the best strategy in this case. I only need to update on element rather than redrawing the whole list of users. But maybe im getting my wires crossed as im still fairly new to angular. Thank you for any help or suggestion.
Try calling the Change Detection 'markForCheck()' which will make angular to run change detection in next cycle.
#Component({
selector: 'app-map-markers',
template: '<agm-marker *ngFor="let user of users; let i = index; trackBy: trackByUser" [latitude]="user.lat" [longitude]="user.lng" [title]="user.firstName">
</agm-marker>',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MapMarkersComponent {
public users: MapUser[]
#Input()
set users(values: any[]){
this.users = values;
this.cdr.markForCheck();
}
constructor(private cdr: changeDetectorRef) {}
public trackByUser(indx: number, value: MapUser) {
let identifier: string = indx.toString();
if ('lat' in value && 'lng' in value) {
identifier = identifier.concat(value.lastSeen.toString());
}
return identifier;
}
}
Related
I have a service which has following method:
room-service.ts
#Injectable()
export class RoomService {
private readonly _equipment: BehaviorSubject<EquipmentDto[]> = new BehaviorSubject([]);
public equipment$ = this._equipment.asObservable();
getEquipmentForRoom(roomId: number) {
this.restService.getEquipmentForRoom(roomId).subscribe(res => {
this._equipment.next(res);
});
}
room-component.ts:
#Component()
export class RoomsComponent implements OnInit {
#Input() room: RoomEntity;
equipment$: Observable < EquipmentDto[] > ;
equipmentList: Array < EquipmentDto > ;
constructor(private equipmentService: EquipmentService) {}
ngOnInit() {
this.equipment$ = this.equipmentService.equipment$;
this.equipmentService.getEquipmentForRoom(this.room.id);
this.equipment$.subscribe(items => {
this.equipmentList = items;
});
room-component.html
<div *ngFor="let eq of room.equipmentList">
<!-- list my equipment here -->
</div>
Now I have a parent component which contains multiple Room Components (those are added programmatically based on the amount of rooms). Anyway, list of equipment is the same for each of the rooms. It looks like once subscribed, the data in first components is overwritten by the component created as the last one.
My question is, how can I get a proper data for each of the rooms using the observable from my service?
You can use this approach with single BehaviorSubject only when your data is the only source of this data.
Instead, you can change your getEquipmentForRoom(roomId: number) like this:
getEquipmentForRoom(roomId: number) {
return this.restService.getEquipmentForRoom(roomId);
}
And then subscribe to it in the compoment:
this.equipmentService.getEquipmentForRoom(this.room.id).subscribe(items => {
this.equipmentList = items;
});
And I agree with Alexander, this component should be dumb as possible.
try to build pipes instead of subscriptions
#Injectable()
export class RoomService {
// return an observable.
getEquipmentForRoom$(roomId: number) {
return this.restService.getEquipmentForRoom(roomId);
}
#Component()
export class RoomsComponent implements OnInit {
#Input() room: RoomEntity;
equipment$: Observable<EquipmentDto[]>;
constructor(private equipmentService: EquipmentService){}
ngOnInit() {
// simply share observable.
this.equipment$ = this.equipmentService.getEquipmentForRoom$(this.room.id);
});
<div *ngFor="let eq of equipment$ | async"> <!-- add async here -->
<!-- list my equipment here -->
</div>
You can fetch the data once in your parent and pass the data to your child components.
That child component (RoomsComponent) should be dumb and not doing requests
Adding onto Alexander’s answer.
The problem is that you are subscribing to the same BehaviourSubject for all the components and the components are taking the latest roomData that is emitted by the BehaviourSubject, which would be the last RoomComponent.
It would work if the BehaviuorSubject holds all the rooms which are fetched by the parent component, and passing each room data to each RoomComponent using #Input.
I have an Ionic application where I have created a component to show some data of an object. My problem is that when I update the data in the parent that hosts the component the data within the component does not update:
my-card.component.ts
#Component({
selector: 'my-card',
templateUrl: './my-card.html'
})
export class MyCard {
#Input('item') public item: any;
#Output() itemChange = new EventEmitter();
constructor() {
}
ngOnInit() {
// I do an ajax call here and populate more fields in the item.
this.getMoreData().subscribe(data => {
if (data.item){
this.item = data.item;
}
this.itemChange.emit(this.item);
});
}
}
my-card.html
<div class="comment-wrapper" *ngFor="let subitem of item.subitems">
{{subitem.title}}
</div>
And in the parent I use the component like this:
<my-card [(item)]="item"></my-card>
And the ts file for the parent:
#IonicPage()
#Component({
selector: 'page-one',
templateUrl: 'one.html',
})
export class OnePage {
public item = null;
constructor(public navCtrl: NavController, public navParams: NavParams) {
this.item = {id:1, subitems:[]};
}
addSubItem():void{
// AJAX call to save the new item to DB and return the new subitem.
this.addNewSubItem().subscribe(data => {
let newSubItem = data.item;
this.item.subitems.push(newSubItem);
}
}
}
So when I call the addSubItem() function it doesnt update the component and the ngFor loop still doesnt display anything.
You are breaking the object reference when you are making the api request. You are assigning new value, that is overwriting the input value you get from the parent, and the objects are no longer pointing to the same object, but item in your child is a completely different object. As you want two-way-binding, we can make use of Output:
Child:
import { EventEmitter, Output } from '#angular/core';
// ..
#Input() item: any;
#Output() itemChange = new EventEmitter();
ngOnInit() {
// I do an ajax call here and populate more fields in the item.
this.getMoreData(item.id).subscribe(data => {
this.item = data;
// 'recreate' the object reference
this.itemChange.emit(this.item)
});
}
Now we have the same object reference again and whatever you do in parent, will reflect in child.
If the getMoreData method returns an observable, this code needs to look as follows:
ngOnInit() {
// I do an ajax call here and populate more fields in the item.
this.getMoreData().subscribe(
updatedItem => this.item = updatedItem
);
}
The subscribe causes the async operation to execute and returns an observable. When the data comes back from the async operation, it executes the provided callback function and assigns the item to the returned item.
You declared item with #Input() decorator as:
#Input('item') public item: any;
But you use two-way binding on it:
<my-card [(item)]="item"></my-card>
If it is input only, it should be
<my-card [item]="item"></my-card>
Now if you invoke addSubItem() it should display the new added item.
this.item = this.getMoreData();
The getMoreData() doesn't make sense if you put it in your card component as you want to use the item passed via #Input()
Your component interactions are a little off. Check out the guide on the Angular docs (https://angular.io/guide/component-interaction). Specifically, using ngOnChanges (https://angular.io/guide/component-interaction#intercept-input-property-changes-with-ngonchanges) or use a service to subscribe and monitor changes between the parent and the child (https://angular.io/guide/component-interaction#parent-and-children-communicate-via-a-service).
I, am using data service to share the data between the component. However, this seems not working for me.
Got the reference from here
Angular to update UI from the child component reflect the value to the parent component
https://angularfirebase.com/lessons/sharing-data-between-angular-components-four-methods/
I tried the same logic as above but seems to not work for me.
Here is the html binding for the angular material
<mat-progress-bar mode="indeterminate" *ngIf="commonViewModel.showProgressBar()"></mat-progress-bar>
Parent component
export class AppComponent {
constructor(public commonViewModel: CommonViewModel) { }
ngOnInit() {
this.isLoding();
}
isLoding() {
console.log("app=" + this.commonViewModel.showProgressBar());
return this.commonViewModel.showProgressBar();
}
}
Child Component
export class HomeComponent {
private GetHomeItemUrl: string = "Home/GetHomeItem";
private _homeItemService: GenericHttpClientService;
constructor(public commonViewModel: CommonViewModel) {
this.getHomeItemHttpCall();
}
private getHomeItemHttpCall(): void {
this.commonViewModel.setProgressBarShow = true;
this._homeItemService.GenericHttpGet<GenericResponseObject<HomeViewModel>>(this.GetHomeItemUrl).subscribe(data => {
if (data.isSuccess) {
this.commonViewModel.setProgressBarShow = false;
console.log("home=" +this.commonViewModel.showProgressBar());
}
}, error => {
console.log(error);
});
}
}
This is my service class which hold the value as true and false
#Injectable()
export class CommonViewModel {
progressBarShow: boolean = true;
public showProgressBar(): boolean {
return this.getProgressBarShow;
}
set setProgressBarShow(flag: boolean) {
this.progressBarShow = flag;
}
get getProgressBarShow(): boolean {
return this.progressBarShow;
}
}
The console output
In the console I,can see the output as True and False. But the app never hides as I can see the app component value is always true
Where I, am doing mistake. Can please someone let me know. I, dont want to use Input and Output to share the data.
Please let me know how can I resolve this issue.
it's possible that your parent component and your child component are being injected with two different instances of the service, depending on where you "provide" it. Try providing it from your app module.
Also, if the child is a direct child of the parent, you don't need the service, you can have an EventEmitter (an #Output) in child, and communicate through that.
See the documentation at https://angular.io/api/core/EventEmitter
I think, that GSSWain's answer must be work. If not, try use a getter
<mat-progress-bar *ngIf="isLoading"></mat-progress-bar>
get isLoading(){
return this.commonViewModel.showProgressBar();
}
I have a component which receives an array of image objects as Input data.
export class ImageGalleryComponent {
#Input() images: Image[];
selectedImage: Image;
}
I would like when the component loads the selectedImage value be set to the first object of the images array. I have tried to do this in the OnInit lifecycle hook like this:
export class ImageGalleryComponent implements OnInit {
#Input() images: Image[];
selectedImage: Image;
ngOnInit() {
this.selectedImage = this.images[0];
}
}
this gives me an error Cannot read property '0' of undefined which means the images value isn't set on this stage. I have also tried the OnChanges hook but I'm stuck because i can't get information on how to observe changes of an array. How can I achieve the expected result?
The parent component looks like this:
#Component({
selector: 'profile-detail',
templateUrl: '...',
styleUrls: [...],
directives: [ImageGalleryComponent]
})
export class ProfileDetailComponent implements OnInit {
profile: Profile;
errorMessage: string;
images: Image[];
constructor(private profileService: ProfileService, private routeParams: RouteParams){}
ngOnInit() {
this.getProfile();
}
getProfile() {
let profileId = this.routeParams.get('id');
this.profileService.getProfile(profileId).subscribe(
profile => {
this.profile = profile;
this.images = profile.images;
for (var album of profile.albums) {
this.images = this.images.concat(album.images);
}
}, error => this.errorMessage = <any>error
);
}
}
The parent component's template has this
...
<image-gallery [images]="images"></image-gallery>
...
Input properties are populated before ngOnInit() is called. However, this assumes the parent property that feeds the input property is already populated when the child component is created.
In your scenario, this is not the case – the images data is being populated asynchronously from a service (hence an http request). Therefore, the input property will not be populated when ngOnInit() is called.
To solve your problem, when the data is returned from the server, assign a new array to the parent property. Implement ngOnChanges() in the child. ngOnChanges() will be called when Angular change detection propagates the new array value down to the child.
You can also add a setter for your images which will be called whenever the value changes and you can set your default selected image in the setter itself:
export class ImageGalleryComponent {
private _images: Image[];
#Input()
set images(value: Image[]) {
if (value) { //null check
this._images = value;
this.selectedImage = value[0]; //setting default selected image
}
}
get images(): Image[] {
return this._images;
}
selectedImage: Image;
}
You can resolve it by simply changing few things.
export class ImageGalleryComponent implements OnInit {
#Input() images: Image[];
selectedImage: Image;
ngOnChanges() {
if(this.images) {
this.selectedImage = this.images[0];
}
}
}
And as another one solution, you can simply *ngIf all template content until you get what you need from network:
...
<image-gallery *ngIf="imagesLoaded" [images]="images"></image-gallery>
...
And switch flag value in your fetching method:
getProfile() {
let profileId = this.routeParams.get('id');
this.profileService.getProfile(profileId).subscribe(
profile => {
this.profile = profile;
this.images = profile.images;
for (var album of profile.albums) {
this.images = this.images.concat(album.images);
}
this.imagesLoaded = true; /* <--- HERE*/
}, error => this.errorMessage = <any>error
);
}
In this way you will renderout child component only when parent will have all what child needs in static content. It's even more useful when you have some loaders/spinners that represent data fetching state:
...
<image-gallery *ngIf="imagesLoaded" [images]="images"></image-gallery>
<loader-spinner-whatever *ngIf="!imagesLoaded" [images]="images"></loader-spinner-whatever>
...
But short answer to your questions:
When inputs are available?
In OnInit hook
Why are not available to your child component?
They are, but at this particular point in time they were not loaded
What can I do with this?
Patiently wait to render child component utul you get data in asynchronous manner OR learn child component to deal with undefined input state
I'm using angular 2. I have a component with an input.
I want to be able to write some code when the input value changes.
The binding is working, and if the data is changed (from outside the component) I can see that there is change in the dom.
#Component({
selector: 'test'
})
#View({
template: `
<div>data.somevalue={{data.somevalue}}</div>`
})
export class MyComponent {
_data: Data;
#Input()
set data(value: Data) {
this.data = value;
}
get data() {
return this._data;
}
constructor() {
}
dataChagedListener(param) {
// listen to changes of _data object and do something...
}
}
You could use the lifecycle hook ngOnChanges:
export class MyComponent {
_data: Data;
#Input()
set data(value: Data) {
this.data = value;
}
get data() {
return this._data;
}
constructor() {
}
ngOnChanges([propName: string]: SimpleChange) {
// listen to changes of _data object and do something...
}
}
This hook is triggered when:
if any bindings have changed
See these links for more details:
https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html
https://angular.io/docs/ts/latest/api/core/OnChanges-interface.html
As mentioned in the comments of Thierry Templier's answer, ngOnChanges lifecycle hook can only detect changes to primitives. I found that by using ngDoCheck instead, you are able to check the state of the object manually to determine if the object's members have changed:
A full Plunker can be found here. But here's the important part:
import { Component, Input } from '#angular/core';
#Component({
selector: 'listener',
template: `
<div style="background-color:#f2f2f2">
<h3>Listener</h3>
<p>{{primitive}}</p>
<p>{{objectOne.foo}}</p>
<p>{{objectTwo.foo.bar}}</p>
<ul>
<li *ngFor="let item of log">{{item}}</li>
</ul>
</div>
`
})
export class ListenerComponent {
#Input() protected primitive;
#Input() protected objectOne;
#Input() protected objectTwo;
protected currentPrimitive;
protected currentObjectOne;
protected currentObjectTwo;
protected log = ['Started'];
ngOnInit() {
this.getCurrentObjectState();
}
getCurrentObjectState() {
this.currentPrimitive = this.primitive;
this.currentObjectOne = _.clone(this.objectOne);
this.currentObjectTwoJSON = JSON.stringify(this.objectTwo);
}
ngOnChanges() {
this.log.push('OnChages Fired.')
}
ngDoCheck() {
this.log.push('DoCheck Fired.');
if (!_.isEqual(this.currentPrimitive, this.primitive)){
this.log.push('A change in Primitive\'s state has occurred:');
this.log.push('Primitive\'s new value:' + this.primitive);
}
if(!_.isEqual(this.currentObjectOne, this.objectOne)){
this.log.push('A change in objectOne\'s state has occurred:');
this.log.push('objectOne.foo\'s new value:' + this.objectOne.foo);
}
if(this.currentObjectTwoJSON != JSON.stringify(this.objectTwo)){
this.log.push('A change in objectTwo\'s state has occurred:');
this.log.push('objectTwo.foo.bar\'s new value:' + this.objectTwo.foo.bar);
}
if(!_.isEqual(this.currentPrimitive, this.primitive) || !_.isEqual(this.currentObjectOne, this.objectOne) || this.currentObjectTwoJSON != JSON.stringify(this.objectTwo)) {
this.getCurrentObjectState();
}
}
It should be noted that the Angular documentation provides this caution about using ngDoCheck:
While the ngDoCheck hook can detect when the hero's name has changed,
it has a frightful cost. This hook is called with enormous frequency —
after every change detection cycle no matter where the change
occurred. It's called over twenty times in this example before the
user can do anything.
Most of these initial checks are triggered by Angular's first
rendering of unrelated data elsewhere on the page. Mere mousing into
another input box triggers a call. Relatively few calls reveal actual
changes to pertinent data. Clearly our implementation must be very
lightweight or the user experience will suffer.