1

I have a parent page that passes user data to a child component like so:

<ng-container *ngIf="leaderboard">
    <app-leaderboard-preview [user]="user" (click)="goToLeaderboard()"></app-leaderboard-preview>
</ng-container>

The parent page ngOnInit() subscribes to an observable that returns a user object and sets the user variable (i removed other irrelevant queries from the combineLatest):

combineLatest([this.userQuery$]).subscribe((results) => {
  Promise.all([this.parseUser(results[4])])
})

It is my understanding that ngOnChanges() will not trigger unless a new object is created, so I assign the new user object as a new object to pass to the app-leaderboard-preview component using Object.assign()

  parseUser(user) {
    return new Promise((resolve) => {
      if(user) {
        this.user = Object.assign({}, user);
        resolve(user);
      } else {
        resolve(user);
      }
    })
  }

This loads the component, just fine, but the ranking of the user can change, so when a user swipes down to refresh the page, the value should be updated, but the component does not update. I use the following code (almost carbon copy as the above) to refresh the page (without a hard reload).

  doRefresh(event) {
    if (this.user) {
      //user
      this.userQuery$ = this.db.query$(`user/find?id=${this.user.id}`);
      combineLatest([this.userQuery$]).subscribe((results) => {
        Promise.all([this.parseUser(results[4])])
      })
  }

Which then runs the parseUser method to update the user object that the app-leaderboard-preview uses.

So this should trigger the ngOnChanges because I am passing a "new" object to the component. What am I missing?

Jordan Lewallen
  • 1,681
  • 19
  • 54

2 Answers2

2

Edit

After some back and forth Jordan figured out he had an async pipe assigning to the user variable which was overriding his changes to this.user.

*ngIf = user$ | async as user

Original Answer

Object.assign({}, object) should work. Tested on Angular 7.

this.user = Object.assign({}, user);

.. or if you are using lodash you can do.

this.user = _.clone(user);
this.user = _.cloneDeep(user);

https://lodash.com/docs/4.17.11#clone

https://lodash.com/docs/4.17.11#cloneDeep

Stackblitz Demo: https://stackblitz.com/edit/ngonchangeswithobject

Ryan E.
  • 977
  • 8
  • 16
  • thanks for helping me debug this. Something is definitely off for me, because I just created an entirely separate method for parsing the user on swipe down to refresh called `parseUser1(). I set user equal to null in this method, I've tried both getters and setters and ngOnChanges() and nothing is caught by the component after init. If you're fresh out of ideas, no worries, I'll keep testing new things – Jordan Lewallen Apr 04 '19 at 18:31
  • Sorry, I forgot to mention. This method fires off the ngOnChanges(). – Ryan E. Apr 04 '19 at 18:33
  • Hmm.. what version of angular are you using? – Ryan E. Apr 04 '19 at 18:44
  • waaaaait a minute, so to simplify the question I removed the other `@Input()` that is required for my component. Do I need to "update" both to force the component to update? – Jordan Lewallen Apr 04 '19 at 18:44
  • You shouldn't need to update both.. here is a stackblitz with what I have working. https://stackblitz.com/edit/ngonchangeswithobject – Ryan E. Apr 04 '19 at 18:48
  • haha yea ok so just out of curiosity I tried using your method for the other `@Input()` that is on the component and it triggers the `ngOnChanges()` properly but my `user` object update doesn't trigger it even though I can see the new object being properly passed before assigning it to `this.user`. SO WEIRD! – Jordan Lewallen Apr 04 '19 at 18:51
  • I'm kind of confused at what is broken at this point? So ngOnChanges() works now? but your updated user object isn't being passed down to the child component? – Ryan E. Apr 04 '19 at 18:58
  • 1
    I have two inputs on my component, user and rank. I tried using Object.create() on the rank input and it triggered ngonchanges properly. The question I posted here deals with the user input, and I tried using Object.create(user) to trigger ngonchanges of the user input, but it doesn’t trigger for some reason even though I’m passing a new object to the input. Does that make more sense? Sorry for the confusion :/ – Jordan Lewallen Apr 04 '19 at 19:01
  • Hmm so the problem is half fixed. Are you sure your syntax is correct for the promise.all()? Based on this https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all I would expect something like Promise.All().then(results => { results.forEach(result => this.parseUser(result))} – Ryan E. Apr 04 '19 at 19:07
  • I updated my stackblitz - Object.assign() actually does work I just didn't write it correctly. Object.create doesn't work, I mean it does in that it creates a new object but it doesn't copy the property values over. So not what you want. I also wrapped the call in promise and it still works. So i'm not sure if you have a scoping problem or what. From what I can see it doesn't seem like that would be the case. If for whatever reason you are outside of Angular's change detection you could try manually firing that off. Otherwise I'm out of ideas without getting my hands on the code myself – Ryan E. Apr 04 '19 at 19:29
  • 1
    Seriously thanks so much for looking in to this. I ended up figuring it out and it was really dumb. I have a `ng-container` with an async `*ngIf = user$ | async as user` that the component is inside. This async `user` variable assignment in the html container was taking precedence over the `user` I was assigning in my parent component. I'm very sorry for taking up your time but because of your help I was able to find the problem. – Jordan Lewallen Apr 04 '19 at 20:11
  • @JordanLewallen No problem! glad you got it! Sometimes it just takes another set of eyes. I updated my answer with the solution, would be dope if you could accept it as the answer :). – Ryan E. Apr 04 '19 at 21:44
0

I've ran into this same issue before and curious if anyone knows how to resolve it.

The way I got around it was using get() set() methods on my input variable like this. This isn't a great solution, especially if you have more than one input variable.

  private _user: any;
  @Input()
  set user(value: any) {
    this._user = value;
    // run ngOnChanges stuff here
    this.ngOnChanges();
  }
  get user(): any {
    return this._user;
  }
Ryan E.
  • 977
  • 8
  • 16
  • I was sort of looking in to the getter/setter alternative but thought it was only possible for individual props, will give this a shot. I think there's some parentheses typos in the user `getter` in your current code, just a heads up! – Jordan Lewallen Apr 04 '19 at 17:38
  • @JordanLewallen it’s annoying their OnChanges documentation doesn’t give an example with an object and only gives one for a primitive. It’s possible this problem was fixed in later versions but my Angular 6.0.1 project still doesn’t work last I checked. – Ryan E. Apr 04 '19 at 17:45
  • so I tried to "clear" out the `user` variable just before assigning it in my `parseUser()` method by doing: `this.user = null` just above `this.user = user`, but even that doesn't seem to do the trick when using get/set. – Jordan Lewallen Apr 04 '19 at 18:21
  • I went back and looked at my code again where I was doing this and it seems I solved this in a different way than I thought. Since I had a service orchestrating changes I added an observable I could subscribe to which fires anytime I had changes. Sorry for wasting your time with this. Hopefully my other answer will work for you. Cheers. – Ryan E. Apr 04 '19 at 18:26