How to trigger change detection in Angular2? [duplicate]
Asked Answered
B

3

17

I'm creating a Facebook service that calls the Facebook javascript api and am wondering how to best implement change detection when my values are updated.

I have a UserService which has a currentUser property that is a BehaviorSubject:

currentUser: Subject<User> = new BehaviorSubject<User>(new User(null));

And when I want to update the user in response to the facebook javascript sdk telling me the user has logged in or logged out, I update that and need to call tick() on an ApplicationRef:

updateUser(user: User) {
  console.log('UserService.updateUser:', user);
  this.currentUser.next(user);
  this.appRef.tick(); // UI does not update without this
}

constructor(facebook: Facebook) {
  this.facebook.facebookEvents.filter(x => x != null 
         && x.eventName == 'auth.authResponseChange')
      .subscribe((event) => {
        this.updateUser(new User(event.data));
      }
}

In my component I store the 'currentUser' from the user service in the constructor and bind to the value property:

<h2>Logged into Facebook as {{currentUser.value.name}}</h2>
<p>Is this you? &nbsp; <img src="{{currentUser.value.profilePicUrl}}"></p>

Am I doing something wrong? Is there a better way than having to call ApplicationRef.tick() after a change triggered from an external library?

Edit

I tried using NgZone and that doesn't work, using a different event that returns posts in a feed as the service pages through them:

constructor(userService: UserService, private ref: ApplicationRef, private zone: NgZone)

...

this.postsSubject.subscribe((post) => {
  this.zone.runOutsideAngular(() => { // doesn't do anything
    this.posts.push(post);
    console.log('postsSubject POST, count is ', this.posts.length);
    ref.tick(); // required to update bindings
  });
}

The console shows the count incrementing, but the html binding {{posts.length}} is only updated if I add the ref.tick() call...

I think I saw somewhere that you can make available 'inputs' to any component from the top-level app component which might be the way to go for the logged in user, but not other calls like getting posts in a feed...

Bullroarer answered 20/2, 2016 at 6:33 Comment(2)
I dont know but if this could help you then fine, #35501390Absent
currentUser is a BehaviorSubject. You have to subscribe to it using the async pipe {{ (currentUser | async)?.value?.name}}.Verlinevermeer
C
20

Am I doing something wrong? Is there a better way than having to call ApplicationRef.tick() after a change triggered from an external library?

It depends. If you're calling the Facebook APIs outside of Angular (i.e., registering asynchronous event handlers outside Angular), then you'll need to manually run change detection as part of handling any events. You can either

  • inject NgZone and then call run() on that object, which will execute the passed (callback) function inside the Angular zone and then automatically call ApplicationRef().tick(), which will run change detection for the entire app, or
  • inject ApplicationRef and call tick() yourself, or
  • inject ChangeDetectorRef and call detectChanges() yourself -- this will only run change detection on the one component and its children

Note, if you inject NgZone, you don't want to use runOutsideAngular(). That will execute the passed (callback) function outside the Angular zone, and it will not call ApplicationRef().tick(), so change detection will not execute.

If you call the Facebook APIs inside Angular, you might be able to avoid all of the above, because then Angular (via its use of Zone.js) should monkey-patch the asynchronous event calls. Then, when your events fire, ApplicationRef.tick() will automatically be called. See https://mcmap.net/q/638596/-view-is-not-updated-on-change-in-angular2 for more discussion on this approach.

Counterscarp answered 20/2, 2016 at 18:32 Comment(2)
So maybe the best way to avoid whole-app change detection would be to use the ChangeDetectorRef for the component and subscribe to the user change event and call it from there?Bullroarer
@JasonGoemaat, if only one component's view needs to update, then that is probably the most efficient way to do it. (Note that the component and its children are changed detected with detectChanges(). I will update my answer.)Counterscarp
L
13

I'm not sure if it's better. There a couple of other ways I can think of.

Using NgZone

You can inject NgZone and execute the run method with a callback:

 NgZone.run(()=>this.currentUser.next(user));

Using setTimeout

setTimeout will trigger change detection automatically:

setTimeout(()=>this.currentUser.next(user));
Lareine answered 20/2, 2016 at 7:22 Comment(4)
The right one is ngZone, we had similar issue with GoogleMaps placing pins on the map.Sorcim
you can't use NgZone.run, one must inject its instance.Brazzaville
And Using setTimeout is not right, it only trigger the outterZone(aka Zone.current) of instance of NgZone. So it will not trigger angular's change detection.Brazzaville
@e-cloud, are you sure about that? setTimeout most definitely runs inside the Angular zone (forkInnerZoneWithAngularBehavior) and upon leaving drains the micro/macro tasks and triggers change detection.Lareine
B
5

Could you give us the way you initialize the Facebook provider and you your service in your component?

Option #1

Your behavior makes me change that the Facebook code is running outside an Angular zone, so Angular change detection doesn't run when an event fires. You

You could also run manually the change detection:

import { Component, ChangeDetectorRef } from 'angular2/core';

@Component({
  (...)
})
export class MyComponent {
  constructor(private cdr:ChangeDetectorRef) {
  }

  someMethod() {
    this.cdr.detectChanges();
  }
}

See these questions:

Option #2

Perhaps you event is fired before the component register a callback on the currentUser subject. In this case, you could trigger the event later...

Bus answered 20/2, 2016 at 8:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.