182

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;
luiscla27
  • 4,956
  • 37
  • 49
Simon H
  • 20,332
  • 14
  • 71
  • 128
  • Your code doesn't show how the data is updated or what type the data is. Is an new instance assigned or is just a property of the value modified? – Günter Zöchbauer Jan 14 '16 at 18:29
  • @GünterZöchbauer I added the line from the parent component – Simon H Jan 14 '16 at 18:33
  • I guess wrapping this line in `zone.run(...)` should do it then. – Günter Zöchbauer Jan 14 '16 at 18:34
  • 4
    Your array (reference) is not changing, so `ngOnChanges()` will not be called. You can use `ngDoCheck()` and implement your own logic to determine if the array contents have changed. `lapsData` is updated because it has/is a reference to the same array as `rawLapsData`. – Mark Rajcok Jan 14 '16 at 18:35
  • @MarkRajcok this feels more like my problem as the update is triggered within angular, but I am using a deeply nested object. Is there some way of using an interim variable to create a new reference, or some otherway to signal downwards to a child? – Simon H Jan 14 '16 at 18:45
  • Well, if your code is changing `rawLapsData`, one option you have is to create a new array rather than modify the array contents. Then `ngOnChanges()` will kick in. – Mark Rajcok Jan 14 '16 at 18:47
  • OK, so I have found a hack by passing another property to map that changes at the same time as `rawLapsData` and that triggers `ngOnChanges`. I know you explained it, but I still don't see how laps can pick up on the change (surely it must be using something equivalent to ngOnChanges itself?) while map can't. – Simon H Jan 14 '16 at 18:54
  • 1
    1) In the 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. 2) Even if Angular doesn't detect any changes (reference checking) to a component's input properties, it still (by default) checks all of the template bindings. That's how laps picks up on the changes. 3) The maps component likely doesn't have any bindings in its template to its lapsData input property, right? That would explain the difference. – Mark Rajcok Jan 14 '16 at 20:00
  • OK, so it was the passing of the data further down the component tree that was saving me. Thanks - that was not entirely intuitive. – Simon H Jan 14 '16 at 20:29
  • Since the problem seems to be that the reference to the object is not changed you should make sure that your update function `deletePoints` returns a new object. – TylerDurden Jan 21 '16 at 07:46
  • laps, maps... could I see your project? sounds interesting – Becario Senior Mar 05 '19 at 11:24
  • https://github.com/simonh1000/tcxeditor-ng2 – Simon H Mar 05 '19 at 11:42

19 Answers19

214

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:

  1. Implement ngDoCheck() and perform your own change detection logic to determine if the array contents have changed. (The Lifecycle Hooks doc has an example.)

  2. 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 to ngOnChanges() itself?) while map can't.

  • In the 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.
  • Even when Angular doesn't detect any changes to a component's input properties (using === 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.
  • The 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.

Community
  • 1
  • 1
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • 1
    Yes, I was mutating a deeply nested object, and I guess that made it hard for Angular to spot changes in the structure. Not my preference to mutate but this is translated XML and I can't afford to lose any of the surrounding data as I want to recreate the xml again at the end – Simon H Jan 14 '16 at 20:40
  • 10
    @SimonH, "hard for Angular to spot changes in the structure" -- Just to be clear, Angular doesn't even look inside input properties that are arrays or objects for changes. It only looks to see if the value changed -- for objects and arrays, the value is the reference. For primitive types, the value is... the value. (I'm not sure I have all the jargon straight, but you get the idea.) – Mark Rajcok Jan 14 '16 at 20:44
  • 17
    Great answer. The Angular2 team desperately needs to publish a detailed, authoritative document on change detection's internals. –  Aug 12 '16 at 03:34
  • If I do the functionality in doCheck, in my case the do check is calling so many times. Can you please tell me any other way? – Mr_Perfect Dec 21 '17 at 08:59
  • @MarkRajcok can you please help me to solve this issue https://stackoverflow.com/questions/50166996/ngonchanges-not-get-triggered-in-angular4 – Nikson May 04 '18 at 04:07
  • can someone provide a working example? is NgDoCheck on the Child or parent component?, and once I see changes, how do I send it to grandchild element? –  Dec 27 '19 at 00:53
42

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.

David
  • 15,652
  • 26
  • 115
  • 156
  • If you are not sure if a targeted browser would support , you can also stringify into a json string and parse back to json, which will also create a new object... – Guntram Aug 02 '17 at 15:02
  • 1
    @Guntram Or a polyfill? – David Jul 14 '18 at 15:59
24

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

Danish Dullu
  • 396
  • 2
  • 9
14

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.

decebal
  • 1,151
  • 3
  • 18
  • 38
9

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.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • As noted in the comments to the OP, the changes were not external but deep within a json object – Simon H Jan 14 '16 at 19:05
  • 1
    As your answer says, still we need to run something for keeping thing in sync in Angular2, like angular1 it was `$scope.$apply`? – Pankaj Parkar Jan 14 '16 at 20:12
  • 1
    If something is started from outside Angular, the API, patched by zone isn't used and Angular doesn't get notified about possible changes. Yes, `zone.run` is similar to `$scope.apply`. – Günter Zöchbauer Jan 14 '16 at 20:17
7

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);
Anton Nikprelaj
  • 166
  • 1
  • 2
6

I have 2 solutions to resolve your problem

  1. Use ngDoCheck to detect object data changed or not
  2. Assign object to a new memory address by object = Object.create(object) from parent component.
giapnh
  • 2,950
  • 24
  • 20
5

Use ChangeDetectorRef.detectChanges() to tell Angular to run a change detection when you edit a nested object (that it misses with its dirty checking).

vich
  • 11,836
  • 13
  • 49
  • 66
Tim Phillips
  • 95
  • 1
  • 3
  • But how ? I am trying to use this, I want to trigger changeDetection in a child after parent pushed a new item in a 2 way bounded [( Collection )]. – Deunz Oct 04 '18 at 12:17
  • 2
    The problem is not that change detection does not occur, but that change detection does not detect the changes ! so this does not seem to be a solution ! why did this answer got five upvotes? – Shahryar Saljoughi Feb 26 '20 at 23:26
  • @ShahryarSaljoughi the answer got upvotes because its one of the solutions. `detectChanges()` will force a re render. – user2599052 May 09 '21 at 10:37
3

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.

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
Simon H
  • 20,332
  • 14
  • 71
  • 128
  • I find it hard to track all the changes on various components in template syntax, specially on mid/large scale apps. Usually I use shared event emitter and subscription to pass the data (quick solution), or implement Redux pattern (via Rx.Subject) for this (when there's time to plan) ... – Sasxa Jan 14 '16 at 22:29
3

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.

3

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,

Deunz
  • 1,776
  • 19
  • 32
1

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
Rishabh Wadhwa
  • 171
  • 2
  • 8
  • 1
    This one is the best solution : ngDoCheck, or changeDetectionStrategy.onPush are really bad ideas,. Don't know why you need to scroll this much to actually have a descent solution. – Igor Beaufils Aug 04 '22 at 09:29
  • @IgorBeaufils actually you have to ALWAYS use changeDetectionStrategy.onPush as this is what makes your angular app more performant, but you also have to understand the consequences regarding why some particular data aren't rendering if you switch to OnPush strategy. Regarding this solution I also don't consider it to be clean because you are adding unnecessary data and complexity to the component. The accepted answer has all the explanation why Angular doesn't detect changes in an object type data and what has to be done in order to achieve the detection. – Victor Muresanu Jul 31 '23 at 08:13
  • @VictorMuresanu "actually you have to ALWAYS use changeDetectionStrategy.onPush as this is what makes your angular app more performant". You're right BUT in case you didn't do it from the start of the project it's not a good idea to do it only in one instance because of a bug. It's a totally other logic of angular programmation, you need to manage all side effects and there can be many. – Igor Beaufils Aug 01 '23 at 15:58
1

Not a clean solution, but you can fire the detection with:

rawLapsData = JSON.parse(JSON.stringify(rawLapsData))
Miguel Carvalhais Matos
  • 1,113
  • 2
  • 13
  • 21
0

ok so my solution for this was:

this.arrayWeNeed.DoWhatWeNeedWithThisArray();
const tempArray = [...arrayWeNeed];
this.arrayWeNeed = [];
this.arrayWeNeed = tempArray;

And this trigger me ngOnChanges

DanielWaw
  • 669
  • 7
  • 22
0

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;
Kailas
  • 7,350
  • 3
  • 47
  • 63
0

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!

Manjeet Yadav
  • 37
  • 1
  • 7
0

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>
ccondrup
  • 549
  • 1
  • 9
  • 21
0

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}
}
Balaj Khan
  • 2,490
  • 2
  • 17
  • 30
Raza Ellahi
  • 379
  • 2
  • 7
  • 20
0

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>
jess
  • 1
  • 1