Handling keyboard shortcuts in Angular with Redux ( NgRx )
Implementing shortcuts in Angular application built with Redux should be fairly easy.
Prerequisite for this approach is to have UI state stored in Redux. This means that we keep track of any changes in UI such as modal opening or closing in Redux state.
In the next example we will create shortcut which triggers navigation change and modal opening.
Let’s create an example app using angular-cli and ngrx schematics.
After creating a new app with
ng new ng-shortcuts-with-redux && cd ng-shortcuts-with-redux
we will install ngrx and necessary dependencies
npm install @ngrx/{store,effects,store-devtools} --savenpm install @ngrx/schematics --save-dev
with next command we will tell angular cli to use collection from ngrx/schematics as default one
ng set defaults.schematics.collection=@ngrx/schematics
Let’s create our root store within store folder with:
ng generate store State --root --statePath store --module app.module.ts
After creating store, let’s create reducer, action and effect file for Layout handling. We keep all layout state in Layout reducer.
ng generate reducer Layout --flat false --reducers store/index.tsng generate action Layout --flat falseng generate effect Layout --flat false --root true -m app.module.ts
Although ngrx/schematics helps us with file generation we will still need to manually edit files to have it work.
Our src/app/layout/layout.reducer.ts will look like this
import * as layoutActions from './layout.actions';
export interface State {
composingMessage: boolean;
loadingMessages: boolean;
}
export const initialState: State = {
composingMessage: false,
loadingMessages: false,
};
export function reducer(state = initialState, action: layoutActions.LayoutActions): State {
switch (action.type) {
case layoutActions.LayoutActionTypes.LoadingMessages:
return Object.assign({}, state, {
loadingMessages: action.payload,
});
case layoutActions.LayoutActionTypes.ComposingMessage:
return Object.assign({}, state, {
composingMessage: action.payload,
});
default:
return state;
}
}
Where composingMessage and loadingMessages will determine UI state. Note that we changed type of action in reducer parameters to LayoutActions.
our src/app/layout/layout.actions.ts file will look like this:
import { Action } from '@ngrx/store';
export enum LayoutActionTypes {
ComposingMessage = '[Layout] Compose message',
LoadingMessages = '[Layout] Load messages',
ShortcutKeyDown = '[Layout] on shortcut key down',
}
export class LayoutComposeMessage implements Action {
readonly type = LayoutActionTypes.ComposingMessage;
constructor(public payload: boolean) {}
}
export class LayoutLoadMessages implements Action {
readonly type = LayoutActionTypes.LoadingMessages;
constructor(public payload: boolean) {}
}
export class LayoutShortcutKeyDown implements Action {
readonly type = LayoutActionTypes.ShortcutKeyDown;
constructor(public payload: number) {}
}
export type LayoutActions = LayoutComposeMessage | LayoutLoadMessages | LayoutShortcutKeyDown;
And our Layout effect stored in src/app/layout/layout.effects.ts is:
import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/empty';
import 'rxjs/add/operator/debounceTime';
import { LayoutActionTypes, LayoutComposeMessage, LayoutShortcutKeyDown } from './layout.actions';
@Injectable()
export class LayoutEffects {
@Effect()
onKeyDownPress$ = this.actions$
.ofType(LayoutActionTypes.ShortcutKeyDown)
.debounceTime(300)
.switchMap((data: LayoutShortcutKeyDown) => {
switch (data.payload) {
// C keyboard button
case 67:
return Observable.of(new LayoutComposeMessage(true));
// ESC keyboard button
case 27:
return Observable.of(new LayoutComposeMessage(false));
default:
return Observable.empty();
}
});
constructor(private actions$: Actions) {}
}
Notice debounceTime put in, as we want to protect ourselves if user presses the same keyboard button multiple times. Upon clicking on character C we dispatch action for setting up compose message variable to true.
Once we have our store in place, we can create dumb component for message composition with:
ng generate component ComposeMessage -cd OnPush -m app.module.ts
We will only specify only one Input in ComposeMessageComponent
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
@Component({
selector: 'app-compose-message',
templateUrl: './compose-message.component.html',
styleUrls: ['./compose-message.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ComposeMessageComponent implements OnInit {
@Input() composeMessage: boolean;
constructor() { }
ngOnInit() {
}
}
HTML and CSS of this component can be found on Github link at the bottom of the tutorial.
Last piece of the puzzle is to add listener on app.component.ts for keyboard event and to pass value of composeMessage to ComposeMessageComponent.
Content of app.component.ts this:
import {Component, HostListener, OnInit} from '@angular/core';
import { Store } from '@ngrx/store';
import { State } from './store';
import { LayoutActionTypes } from './layout/layout.actions';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/pluck';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
composeMessage$: Observable<boolean>;
constructor(private store: Store<State>) {}
ngOnInit() {
this.composeMessage$ = this.store.select('layout').pluck('composingMessage');
} @HostListener('window:keydown', ['$event'])
onKeyDown(event) {
this.store.dispatch({
type: LayoutActionTypes.ShortcutKeyDown,
payload: event.keyCode,
});
}
}
Where we listen to every keyboard event that happens in application, and we pass value of keyCode to Action ShortcutKeyDown which is further processed in LayoutEffects class.
Output of this simple application should look like
Code on Github: https://github.com/vladotesanovic/ng-shortcuts-with-redux