Reputation: 21047
I know I am not the first to ask about this, but I can't find an answer in the previous questions. I have this in one component
<div class="col-sm-5">
<laps
[lapsData]="rawLapsData"
[selectedTps]="selectedTps"
(lapsHandler)="lapsHandler($event)">
</laps>
</div>
<map
[lapsData]="rawLapsData"
class="col-sm-7">
</map>
In the controller rawLapsdata
gets mutated from time to time.
In laps
, the data is output as HTML
in a tabular format. This changes whenever rawLapsdata
changes.
My map
component needs to use ngOnChanges
as a trigger to redraw markers on a Google Map. The problem is that ngOnChanges
does not fire when rawLapsData
changes in the parent. What can I do?
import {Component, Input, OnInit, OnChanges, SimpleChange} from 'angular2/core';
@Component({
selector: 'map',
templateUrl: './components/edMap/edMap.html',
styleUrls: ['./components/edMap/edMap.css']
})
export class MapCmp implements OnInit, OnChanges {
@Input() lapsData: any;
map: google.maps.Map;
ngOnInit() {
...
}
ngOnChanges(changes: { [propName: string]: SimpleChange }) {
console.log('ngOnChanges = ', changes['lapsData']);
if (this.map) this.drawMarkers();
}
Update: ngOnChanges
is not working, but it looks as though lapsData
is being updated. In the ngOnInit
is an event listener for zoom changes that also calls this.drawmarkers
. When I change the zoom I do indeed see a change in markers. So the only issue is that I don't get the notification at the time the input data changes.
In the parent, I have this line. (Recall that the change is reflected in laps, but not in map
).
this.rawLapsData = deletePoints(this.rawLapsData, this.selectedTps);
And note that this.rawLapsData
is itself a pointer to the middle of a large json object
this.rawLapsData = this.main.data.TrainingCenterDatabase.Activities[0].Activity[0].Lap;
Upvotes: 189
Views: 271404
Reputation: 1
Simply send an object or an array in the event. NEVER send a string or number from parent to child. It will work. I had 2 child components and a parent. this is my Parent.ts : Send direct event object.
dataFrom1To2:any;
dataFrom2To1:any;
onBallPassEventFrom1(event:any){
this.dataFrom1To2 = event;
}
onBallPassEventFrom2(event:any){
this.dataFrom2To1 = event;
}
And parent.html is :
<div>I Am The Parent Component!</div>
<div>
<app-child1 (ballPassEventFrom1)="onBallPassEventFrom1($event)" [datafrom2]="dataFrom2To1"></app-child1>
</div>
<div>
<app-child2 [datafrom1]="dataFrom1To2" (ballPassEventFrom2)="onBallPassEventFrom2($event)"></app-child2>
</div>
Upvotes: 0
Reputation: 426
In .ts
file (Parent component) where you are updating your rawLapsData
do it like this:
rawLapsData = somevalue; // change detection will not happen
Solution:
rawLapsData = {...somevalue}; //for Object, change detection will happen
rawLapsData = [...somevalue]; //for Array, change detection will happen
and ngOnChanges
will called in child component
Upvotes: 27
Reputation: 1143
Not a clean solution, but you can fire the detection with:
rawLapsData = JSON.parse(JSON.stringify(rawLapsData))
Upvotes: 1
Reputation: 409
suppose you have a nested object, like
var obj = {"parent": {"child": {....}}}
If you passed the reference of the complete object, like
[wholeObj] = "obj"
In that case, you can't detect the changes in the child objects, so to overcome this problem you can also pass the reference of the child object through another property, like
[wholeObj] = "obj" [childObj] = "obj.parent.child"
So you can also detect the changes from the child objects too.
ngOnChanges(changes: SimpleChanges) {
if (changes.childObj) {// your logic here}
}
Upvotes: 0
Reputation: 549
Here's an example using IterableDiffer with ngDoCheck. IterableDiffer is especially useful if you need to track changes over time as it lets you do things like iterate over only added/changed/removed values etc.
A simple example not using all advantages of IterableDiffer, but it works and shows the principle:
export class FeedbackMessagesComponent implements DoCheck {
@Input()
messages: UserFeedback[] = [];
// Example UserFeedback instance { message = 'Ooops', type = Notice }
@HostBinding('style.display')
display = 'none';
private _iterableDiffer: IterableDiffer<UserFeedback>;
constructor(private _iterableDiffers: IterableDiffers) {
this._iterableDiffer = this._iterableDiffers.find([]).create(null);
}
ngDoCheck(): void {
const changes = this._iterableDiffer.diff(this.messages);
if (changes) {
// Here you can do stuff like changes.forEachRemovedItem()
// We know contents of this.messages was changed so update visibility:
this.display = this.messages.length > 0 ? 'block' : 'none';
}
}
}
This will now automatically show/hide depending on myMessagesArray count:
<app-feedback-messages
[messages]="myMessagesArray"
></app-feedback-messages>
Upvotes: 0
Reputation: 37
When you are manipulating the data like:
this.data.profiles[i].icon.url = '';
Then you should use in order to detect changes:
let array = this.data.profiles.map(x => Object.assign({}, x)); // It will detect changes
Since angular ngOnchanges not be able to detect changes in array, objects then we have to assign a new reference. Works everytime!
Upvotes: 0
Reputation: 7578
In my case it was changes in object value which the ngOnChange
was not capturing. A few object values are modified in response of api call. Reinitializing the object fixed the issue and caused the ngOnChange
to trigger in the child component.
Something like
this.pagingObj = new Paging(); //This line did the magic
this.pagingObj.pageNumber = response.PageNumber;
Upvotes: 0
Reputation: 171
I had to create a hack for it -
I created a Boolean Input variable and toggled it whenever array changed, which triggered change detection in the child component, hence achieving the purpose
Upvotes: 1
Reputation: 699
ok so my solution for this was:
this.arrayWeNeed.DoWhatWeNeedWithThisArray();
const tempArray = [...arrayWeNeed];
this.arrayWeNeed = [];
this.arrayWeNeed = tempArray;
And this trigger me ngOnChanges
Upvotes: 0
Reputation: 3278
I have 2 solutions to resolve your problem
ngDoCheck
to detect object
data changed or not object
to a new memory address by object = Object.create(object)
from parent component.Upvotes: 7
Reputation: 1941
I stumbled upon the same need. And I read a lot on this so, here is my copper on the subject.
If you want your change detection on push, then you would have it when you change a value of an object inside right ? And you also would have it if somehow, you remove objects.
As already said, use of changeDetectionStrategy.onPush
Say you have this component you made, with changeDetectionStrategy.onPush:
<component [collection]="myCollection"></component>
Then you'd push an item and trigger the change detection :
myCollection.push(anItem);
refresh();
or you'd remove an item and trigger the change detection :
myCollection.splice(0,1);
refresh();
or you'd change an attrbibute value for an item and trigger the change detection :
myCollection[5].attribute = 'new value';
refresh();
Content of refresh :
refresh() : void {
this.myCollection = this.myCollection.slice();
}
The slice method returns the exact same Array, and the [ = ] sign make a new reference to it, triggering the change detection every time you need it. Easy and readable :)
Regards,
Upvotes: 4
Reputation: 364747
rawLapsData
continues to point to the same array, even if you modify the contents of the array (e.g., add items, remove items, change an item).
During change detection, when Angular checks components' input properties for change, it uses (essentially) ===
for dirty checking. For arrays, this means the array references (only) are dirty checked. Since the rawLapsData
array reference isn't changing, ngOnChanges()
will not be called.
I can think of two possible solutions:
Implement ngDoCheck()
and perform your own change detection logic to determine if the array contents have changed. (The Lifecycle Hooks doc has an example.)
Assign a new array to rawLapsData
whenever you make any changes to the array contents. Then ngOnChanges()
will be called because the array (reference) will appear as a change.
In your answer, you came up with another solution.
Repeating some comments here on the OP:
I still don't see how
laps
can pick up on the change (surely it must be using something equivalent tongOnChanges()
itself?) whilemap
can't.
laps
component your code/template loops over each entry in the lapsData
array, and displays the contents, so there are Angular bindings on each piece of data that is displayed.===
checking), it still (by default) dirty checks all of the template bindings. When any of those change, Angular will update the DOM. That's what you are seeing.maps
component likely doesn't have any bindings in its template to its lapsData
input property, right? That would explain the difference.Note that lapsData
in both components and rawLapsData
in the parent component all point to the same/one array. So even though Angular doesn't notice any (reference) changes to the lapsData
input properties, the components "get"/see any array contents changes because they all share/reference that one array. We don't need Angular to propagate these changes, like we would with a primitive type (string, number, boolean). But with a primitive type, any change to the value would always trigger ngOnChanges()
– which is something you exploit in your answer/solution.
As you probably figured out by now object input properties have the same behavior as array input properties.
Upvotes: 221
Reputation: 166
Change detection is not triggered when you change a property of an object (including nested object). One solution would be to reassign a new object reference using 'lodash' clone() function.
import * as _ from 'lodash';
this.foo = _.clone(this.foo);
Upvotes: 7
Reputation: 1211
As an extension to Mark Rajcok's second solution
Assign a new array to rawLapsData whenever you make any changes to the array contents. Then ngOnChanges() will be called because the array (reference) will appear as a change
you can clone the contents of the array like this:
rawLapsData = rawLapsData.slice(0);
I am mentioning this because
rawLapsData = Object.assign({}, rawLapsData);
didn't work for me. I hope this helps.
Upvotes: 14
Reputation: 16067
Not the cleanest approach, but you can just clone the object each time you change the value?
rawLapsData = Object.assign({}, rawLapsData);
I think I would prefer this approach over implementing your own ngDoCheck()
but maybe someone like @GünterZöchbauer could chime in.
Upvotes: 42
Reputation: 95
Use ChangeDetectorRef.detectChanges()
to tell Angular to run a change detection when you edit a nested object (that it misses with its dirty checking).
Upvotes: 5
Reputation: 102
Here's a hack that just got me out of trouble with this one.
So a similar scenario to the OP - I've got a nested Angular component that needs data passed down to it, but the input points to an array, and as mentioned above, Angular doesn't see a change as it does not examine the contents of the array.
So to fix it I convert the array to a string for Angular to detect a change, and then in the nested component I split(',') the string back to an array and its happy days again.
Upvotes: 3
Reputation: 658205
If the data comes from an external library you might need to run the data upate statement within zone.run(...)
. Inject zone: NgZone
for this. If you can run the instantiation of the external library within zone.run()
already, then you might not need zone.run()
later.
Upvotes: 9
Reputation: 21047
My 'hack' solution is
<div class="col-sm-5">
<laps
[lapsData]="rawLapsData"
[selectedTps]="selectedTps"
(lapsHandler)="lapsHandler($event)">
</laps>
</div>
<map
[lapsData]="rawLapsData"
[selectedTps]="selectedTps" // <--------
class="col-sm-7">
</map>
selectedTps changes at the same time as rawLapsData and that gives map another chance to detect the change through a simpler object primitive type. It is NOT elegant, but it works.
Upvotes: 3