View is not updated on change in Angular2
Asked Answered
C

2

19

I've started exploring Angular2 (I'm coming with Angular1 and a bit of React background) and I got stuck with a problem.

I want to bind certain keystrokes to actions in my component, so I've decided to use Angular2 lifecycle to bind/unbind actions.

However, if I do something from within a Mousetrap callback, it works, but it's not rendered and a change is not visible until a digest cycle is run.

Do I need to run something explicitly to update a view

Could somebody help me to figure out what is going on? Any help would be very appreciated.


import {Component} from 'angular2/core';
const Mousetrap = require('mousetrap');

@Component({
  template: `<div>
    Video template: Mode {{ mode }}
    <input type="number" [(ngModel)]="mode"/>
  </div>`
})
export class Video {

  public mode: number;

  constructor() {
    this.mode = 0;
  }

  ngOnInit() {

    console.log('hello Video component');
    Mousetrap.bind('d', () => console.log('this.mode=', this.mode));
    Mousetrap.bind('i', () => this.incrementMode()); // doesn't work

    this.incrementMode(); // works
    this.incrementMode(); // works
    setTimeout(() => this.incrementMode(), 4000); // works

  }

  incrementMode() {
    console.log('incMode', this.mode++);
  };

  ngOnDestroy() {
    console.log('bye bye Video component');
    Mousetrap.unbind(['d', 'i']);
  }

}
Cynde answered 4/1, 2016 at 14:1 Comment(0)
O
21

Although @Günter's answer is absolutely correct I want to propose a different solution.

The problem with Mousetrap library is that it creates its instance outside of the angular zone (see here). But to fire change detection after each async event the instance should be instantiated inside the angular zone. You have two options to achieve this:

  1. Use dependency injection:
bootstrap(App, [provide(Mousetrap, { useFactory: () => new Mousetrap() }) ]);

// ...

@Component({
  selector: 'my-app', 
  // ...
})
export class App {
  constructor(@Inject(Mousetrap) mousetrap) {
    this.mousetrap = mousetrap;
    // ...
  }
  //...
}
  1. Just create instance of Mousetrap inside of the constructor:
@Component({
  selector: 'my-app', 
  // ...
})
export class App {
  constructor() {
    this.mousetrap = new Mousetrap();
    // ...
  }
  //...
}

In both cases you will have the ability to use the mousetrap instance like this:

ngOnInit() {
  this.mousetrap.bind('i', () => this.incrementMode()); // It works now!!!
  // ...
}

Now you don't need to use ngZone.run() in every bind call. In case of dependency injection you also can use this mousetrap instance in any component/service of your application (not only in App component).

See this plunk. I use dependency injection there.

Oviparous answered 4/1, 2016 at 14:54 Comment(4)
That's a pretty clever solution. Would it work for every/most libs out there?Ferro
@EricMartinez Well, there are too many js libraries with absolutely crazy implementations. So I definitely would not guarantee that this will work for every/most libraries. But basic principle is simple: you have to make sure that "async related" functions (such as addEventListener) was called inside angular zone - that's all.Oviparous
Thanks for this. Now I finally understand Savkin's statements: "Angular 2 uses Zone.js to know when this check is required. This means that you do not need to call scope.$apply to integrate with third-party libraries." -- from The Core Concepts of Angular 2 blog post, the ZONES section. I had this on my to-be-figured-out/I-don't-understand list for quite some time. (I assume the Angular 2 equivalent of Angular 1's scope.$apply is ApplicationRef.tick(). But we don't need that if we use this technique.)Conidiophore
@MarkRajcok You're very welcome. I'd say angular2's ApplicationRef.tick() is kind of equivalent to angular1's $rootScope.$digest(), ngZone.run(cb) is kind of equivalent to $rootScope.$apply(cb) and changeDetectorRef.detectChanges() is kind of equivalent to $scope.$digest().Oviparous
C
9

If MouseTrap is something outside Angular you might need to inject NgZone and run your code like

  Mousetrap.bind('i', () => this.ngZone.run(() => this.incrementMode()));
Concise answered 4/1, 2016 at 14:5 Comment(2)
Thanks for an information about NgZone, I made it work like this Mousetrap.bind('i', () => this.ngZone.run(() => this.incrementMode())); Cynde
Thanks for the feedback :) Updated my answer.Journalistic

© 2022 - 2024 — McMap. All rights reserved.