Overview

First of all, let’s see what I mean by “Dynamic Shortcuts”. Imagine a project that has tons of users and each of them want a shortcut or hotkey to open an info modal. Certainly it’s not so appropriate to force all users to use just a specific shortcut for this action. Even some users may have a global shortcut exactly like your’s that has a completely different functionality on a different software.

And this is where “Dynamic Shortcuts” shows up! Using dynamic shortcuts lets you have different hotkeys for any action depending on what users are comfortable with. So we are required to get all the user’s custom configuration in sign-in operation.

To achieve dynamic shortcuts in angular, we need to implement two main parts:

  • Data Structure
  • Key Listener

Service And Data Structure

Start from creating a service to hold all the logic and handlers using angular cli:

Assuming you are following the angular style guide for your project structure.

ng generate service core/services/shortcut

Then, To simulate user’s shortcut configurations we create an interface to represent the shortcut data structure. Our interface can be something like this:

export interface Shortcut {
    /**
     * Our shortcut is defined here as a string 
     */
    key: string;
    /**
     * An identifier about what this shortcut is going to do
     */
    action: string;
}

And we put this interface in a file named shortcut.service.d.ts next to our shortcut service. Now we can have our example shortcuts in a ShortcutService class field:

configuration: Shortcut[] = [
    {
      key: 'Ctrl+B',
      action: 'GoToHome'
    },
    {
      key: 'Ctrl+M',
      action: 'Logout',
    }
];

So as you see, here I want to navigate to home page when I press Ctrl+B. And I want to sign out using Ctrl+M.

Implement Key Listener

Now that we have our shortcuts, we should be able to listen for keyboard keydown event on window target. To achieve this, the best way is to create an observable from event using rxjs and store it in a field in our service:

According to naming convention for observables we will name it keydown$.

keydown$ = fromEvent(window, 'keydown');

We also need a field to store the subscription so we can unsubscribe it and avoid memory leak:

keydownSubscription: Subscription;

Then we add a method to subscribe on this custom observable, so we can call it from other components to start listening:

start(): void {
    this.keydownSubscription = this.keydown$.subscribe((event: KeyboardEvent) => {
        for (const shortcut of this.configuration) {
            let shouldBeCtrl = false;
            let shouldBeAlt = false;
            let targetKey = '';

            // Parse shortcut text to know what should be keys
            const keys = shortcut.key.toLocaleLowerCase().split('+');
            for (const key of keys) {
                switch (key) {
                case 'ctrl': shouldBeCtrl = true; break;
                case 'alt': shouldBeAlt = true; break;
                default: targetKey = key;
                }
            }

            if (
                event.ctrlKey === shouldBeCtrl
                && event.altKey === shouldBeAlt
                && targetKey === event.key
            ) {
                this.actionHandler(shortcut.action);
                break;
            }
        }
    });
}

As you see in this method we are parsing every shortcut key to see what should be the event to match our shortcut. Then if every thing is what it “shouldBe”, We can call an action handler and break the loop. An action handler can be like this one:

actionHandler(action: string): void {
    switch (action) {
        case 'GoToHome': /* Ex: Navigate to home */ break;
        case 'Logout': /* Ex: Clear the local storage */ break;
        default: /* Warning about invalid action */
    }
}

And finally the last method is nothing but the stop mehtod to unsubscribe that custom observable and cancel listening for keyboard events:

stop(): void {
    if (this.keydownSubscription) {
        this.keydownSubscription.unsubscribe();
    }
}

Now Guess What?

You may have different shortcuts for different pages in your application and that is totally fine! Just add this method:

changeShortcuts(shortcuts: Shortcut[]) {
    this.configuration = shortcuts;
}

And now you can change the collection of shortcuts and action whenever you want :)

Click here to see the full service file.

import { fromEvent, Subscription } from 'rxjs';
import { Injectable } from '@angular/core';

import { Shortcut } from './shortcut.service.d';

@Injectable({
  providedIn: 'root'
})
export class ShortcutService {
  /**
   * Sample data for dynamic user configuration for shortcuts
   */
  private configuration: Shortcut[] = [
    {
      key: 'Ctrl+B',
      action: 'GoToHome'
    },
    {
      key: 'Ctrl+M',
      action: 'Logout',
    }
  ];
  /**
   * An observable from window keydown event
   */
  private keydown$ = fromEvent(window, 'keydown');
  /**
   * Store the subscription so we can unsubscribe it and avoid memory leak
   */
  private keydownSubscription: Subscription;

  /**
   * Used to start listening for keyboard events
   */
  start(): void {
    this.keydownSubscription = this.keydown$.subscribe((event: KeyboardEvent) => {

      for (const shortcut of this.configuration) {
        let shouldBeCtrl = false;
        let shouldBeAlt = false;
        let targetKey = '';

        // Parse shortcut text to know what should be keys
        const keys = shortcut.key.toLocaleLowerCase().split('+');
        for (const key of keys) {
          switch (key) {
            case 'ctrl': shouldBeCtrl = true; break;
            case 'alt': shouldBeAlt = true; break;
            default: targetKey = key;
          }
        }


        if (
          event.ctrlKey === shouldBeCtrl
          && event.altKey === shouldBeAlt
          && targetKey === event.key
        ) {
          this.actionHandler(shortcut.action);
          break;
        }
      }
    });
  }

  /**
   * Used to do some operations based on action string
   * @param action Action string
   */
  actionHandler(action: string): void {
    console.log('action: ', action);
    switch (action) {
      case 'GoToHome': /* Ex: Navigate to home */ break;
      case 'Logout': /* Ex: Clear the local storage */ break;
      default: /* Warning about invalid action */
    }
  }

  /**
   * Used to unsubscribe and cancel listening for keyboard event
   */
  stop(): void {
    if (this.keydownSubscription) {
      this.keydownSubscription.unsubscribe();
    }
  }

  /**
   * Set new shortcuts for different feature areas
   */
  changeShortcuts(shortcuts: Shortcut[]) {
    this.configuration = shortcuts;
  }
}

Click here to see the types file.

export interface Shortcut {
    /**
     * Our shortcut is defined here as a string 
     */
    key: string;
    /**
     * An identifier about what this shortcut is going to do
     */
    action: string;
}