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']);
  }

}
Evan Plaice
  • 13,944
  • 6
  • 76
  • 94
Jan Vorcak
  • 19,261
  • 14
  • 54
  • 90

2 Answers2

20

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.

alexpods
  • 47,475
  • 10
  • 100
  • 94
  • That's a pretty clever solution. Would it work for every/most libs out there? – Eric Martinez Jan 04 '16 at 15:02
  • 4
    @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. – alexpods Jan 04 '16 at 15:16
  • 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](http://victorsavkin.com/post/118372404541/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.) – Mark Rajcok Jan 04 '16 at 22:44
  • 2
    @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()`. – alexpods Jan 04 '16 at 23:07
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()));
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567