Philip Hendry's Blog

Binding Keypresses using Mousetrap in Aurelia

Feb 7, 2016 • Aurelia, JavaScript • 3 min read

I wanted to enhance my Aurelia app with some keyboard shortcuts so immediately searched the internet for a library that would overcome browsers differences for me whilst making it easy to set up key presses and their callbacks.

I fairly quickly found Mousetrap which I instantly liked - love it's URL!

But whilst I quickly configure my keypresses I noticed my Aurelia view models started to look a little messy and had a hard-reference to Mousetrap.

I tried a couple of approaches but gravitated to having a custom element (I'll explain more on that later) and so here's keyboard-element.js:

  import {inject, customElement, bindable} from "aurelia-framework";
  import MouseTrap from "mousetrap";

  @customElement('keyboard')
  @inject(Element)
  export class KeyboardElement {
    @bindable keycombo = "";
    @bindable onkeypress = function defaultOperation(event, keypress) { };

    constructor(element) {
      this.element = element;
    }

    bind() {
      this.mousetrap = new MouseTrap(this.element);
      this.bindKeyboard();
    }

    unbind() {
      this.mousetrap.reset();
      delete this.mousetrap;
    }

    bindKeyboard() {
      this.mousetrap.reset();
      this.mousetrap.bind(
        this.keycombo,
        (event, combo) => this.onkeypress({ event: event, keypress: combo })
      );
    }
  }

This is now the only code that pulls in Mousetrap and provides two properties keycombo and onkeypress for the consumer to configure:

  <keyboard 
      keycombo.bind="keys" 
      onkeypress.call="onkeypress(event, keypress, item)">
    ...
  </keyboard>

The consuming view model supplies the keypresses it wants to receive. Given this is passed to Mousetrap the specification of keys is taken right from it's documentation :

  keys = ["tab", "shift+tab", "alt+h", "alt+del"];

Initially I had tried using a custom attribute to define the key presses but I found it difficult to define a callback within which this was appropriately set - after all, these key presses will occur in a particular HTML context from which I want to extract information.

To explain what I mean about this context look at the definition of the callback from the consuming view model:

  onkeypress(event, keypress, item) {
    switch (keypress) {
      case "tab":
        ...
        return true;
      case "shift+tab":
       ...
       return true;
      case "alt+h":
        ...
        return false;
      case "alt+del":
        ...
        return false;
      }
  }

If you compare it's signature with the default callback in the custom element you should notice that event and keypress are provided by the custom element but where does item come from! That's actually defined by the consuming view! I'll list the consuming view template again but this time give a little more context:

  <div repeat.for="item of items">
    <keyboard 
        keycombo.bind="keys" 
        onkeypress.call="onkeypress(event, keypress, item)">
      <input type="text" value.bind="item.text1">
      <input type="text" value.bind="item.text2">
    </keyboard>
  </div>

What this now shows is that I've placed the keyboard custom element inside a repeat.for. The .call binding can now define the event and keypress properties (which are mapped in the custom element) but Aurelia allows us to pass our own properties which will automatically be forwarded on! This is great since now I can pass in item which is the current item from the items collection. My onkeypress callback in the consuming view model can us information from that item parameter to make decisions on how that keypress should be actioned. For example, in my case alt+del deletes the current row in the repeat which has focus.

I like using the custom element so far because of the following advantages:

To be honest, the only thing I don't like is the switch statement in the callback used to detect which key press occurred - but for the moment I can live with that given the advantages already listed.

Post by: Philip Hendry