Create Keyboard Shortcuts with RxJS

The cleanest way to create and orchestrate Keyboard Shortcuts with RxJS.

Authors
Gary Großgarten Gary Großgarten
Published at

I recently tried to add some keyboard shortcuts to my Angular app. 🤯 But don't worry, the solution is quite simple. At least Brent Rambo approves.

Brent Rambo - Shortcut master

In this Quick Tip, I'll show you what I came up with, using RxJS. This demonstration is done in an Angular Workspace scaffolded with the Angular CLI.

Implementation

The shortcut function below can be used to effortlessly create Observables for any keyboard shortcut. A keyboard shortcut is an array of keycodes (event.code), each representing a key of your keyboard. Grab the KeyCode Enum here.

See the comments in the code for explanation:

export const shortcut = (shortcut: KeyCode[]): Observable<KeyboardEvent[]> => {
  // Observables for all keydown and keyup events
  const keyDown$ = fromEvent<KeyboardEvent>(document, 'keydown');
  const keyUp$ = fromEvent<KeyboardEvent>(document, 'keyup');

  // All KeyboardEvents - emitted only when KeyboardEvent changes (key or type)
  const keyEvents$ = merge(keyDown$, keyUp$).pipe(
    distinctUntilChanged((a, b) => a.code === b.code && a.type === b.type),
    share()
  );

  // Create KeyboardEvent Observable for specified KeyCode
  const createKeyPressStream = (charCode: KeyCode) =>
    keyEvents$.pipe(filter((event) => event.code === charCode.valueOf()));

  // Create Event Stream for every KeyCode in shortcut
  const keyCodeEvents$ = shortcut.map((s) => createKeyPressStream(s));

  // Emit when specified keys are pressed (keydown).
  // Emit only when all specified keys are pressed at the same time.
  // More on combineLatest below
  return combineLatest(keyCodeEvents$).pipe(
    filter<KeyboardEvent[]>((arr) => arr.every((a) => a.type === 'keydown'))
  );
};
**[combineLatest](https://rxjs-dev.firebaseapp.com/api/index/function/combineLatest)**: Whenever any input `Observable` emits, `combineLatest` will compute and emit all the latest values of all inputs. However, `combineLatest` will wait for all input `Observables` to emit at least once, before it starts emitting results.

Usage

Usage is simple. Just call the shortcut function with your desired KeyCode combination. Then subscribe to the result and handle the keyboard shortcut. More examples can be found in the repo.

const commaDot$ = shortcut([KeyCode.Comma, KeyCode.Period]);

const ctrlO$ = merge(
  shortcut([KeyCode.ControlLeft, KeyCode.KeyO]),
  shortcut([KeyCode.ControlRight, KeyCode.KeyO])
);

commaDot$.pipe(tap(() => doSomething())).subscribe();

Bonus

The shortcut function emits whenever the specified keys are pressed simultaneously, no matter in which order they were pressed. If the sequence of the key presses is also important to you, you can use the pipeable operator below.

export function sequence() {
  return (source: Observable<KeyboardEvent[]>) => {
    return source.pipe(
      filter((arr) => {
        const sorted = [...arr]
          .sort((a, b) => (a.timeStamp < b.timeStamp ? -1 : 1))
          .map((a) => a.code)
          .join();
        const seq = arr.map((a) => a.code).join();
        return sorted === seq;
      })
    );
  };
}

Then use it like this

const abc$ = shortcut([KeyCode.KeyA, KeyCode.KeyB, KeyCode.KeyC]).pipe(
  sequence()
);

Now the abc$ Observable will only emit when the keys are pressed sequentially (a->b->c).

Limitations

Beware that keyboard shortcuts could collide with global shortcuts specified by your OS or Browser (e.g. Spotlight on Mac for Cmd+Space). Also there are cases where keyup events will be skipped when certain keys are pressed down. This is particularly the case with the cmd key (KeyCode.MetaRight and KeyCode.MetaLeft) on mac.


If you have further questions on the article or just want to say hi, feel free to slide into my twitter dms.🐦

Sponsor us

Did you find this post useful? We at notiz.dev write about our experiences developing Apps, Websites and APIs and develop Open Source tools. Your support would mean a lot to us 🙏. Receive a reward by sponsoring us on Patreon or start with a one-time donation on GitHub Sponsors.

Table of Contents

Top of Page Comments Related Articles

Related Posts

Find more posts like this one.

Authors
Gary Großgarten
September 23, 2021

Media Queries with RxJS

Media Queries | Practical examples with RxJS
RxJS Angular Read More
Authors
Marc Stammerjohann
April 26, 2020

Migrate Git Repository to Git Large File Storage (LFS)

How to migrate an existing Git Repository to use Git Large File Storage (LFS).
Quick Tip Git Read More
Authors
Marc Stammerjohann
October 23, 2020

How to manage multiple Java JDK versions on macOS X

How to manage multiple Java JDK versions on macOS X using homebrew.
Quick Tip Java Read More
Authors
Marc Stammerjohann
March 19, 2020

How to manage multiple Node.js versions on macOS X

How to manage multiple Node.js versions on macOS X using homebrew.
Quick Tip Node Read More

more coming soon

Get in Touch!

Sign up for our newsletter

Sign up for our newsletter to stay up to date. Sent every other week.

We care about the protection of your data. Read our Privacy Policy.