Navigation with a keyboard in the ember-models-table

keyboard

Some table-Components for different frameworks allow using keyboard for managing their state. ember-models-table doesn’t provide such functional, however it can be easily added.

Addon ember-keyboard is a great solution to track keyboard events. It can be installed with the following command:

1
ember i ember-keyboard

Let’s write a list with use-cases and key combinations used for them:

  • Ctrl + → – move to the next page
  • Ctrl + ← – move to the prev page
  • Ctrl + q – clear all filters (it will work only if focus isn’t in some input-field)
  • Ctrl + Shift + %N% – sort table by column #N
  • Ctrl + Shift + Num %N% – toggle column #N visibility
  • Ctrl + Shift + Num - – hide all columns
  • Ctrl + Shift + Num + – show all columns
  • Double click on row will switch it to the edit-mode
  • Esc – turn off Edit-model for all rows

We are going to extend original models-table to add the new features. First of all a few Mixins from ember-keyboard are added:

1
ember g component my-table
1
2
3
4
import {EKOnInsertMixin, EKMixin, keyDown} from 'ember-keyboard';
import ModelsTable from 'ember-models-table/components/models-table';
 
export default ModelsTable.extend(EKMixin, EKOnInsertMixin, {});

EKMixin is a Mixin that adds all needed functions to track keyboard events, doesn’t activate. Mixin EKOnInsertMixin will activate handlers for keyboard events when Component is inserted in the page (e.g. after didInsertElement). There are two another Mixins in the ember-keyboard that turn on handlers on Component init and when Component is focused. You can read more about them in the docs.

Navigation with Ctrl + arrows can be done in this way:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import {EKOnInsertMixin, EKMixin, keyDown} from 'ember-keyboard';
import ModelsTable from 'ember-models-table/components/models-table';
import {on} from '@ember/object/evented';
import {get} from '@ember/object';
 
export default ModelsTable.extend(EKMixin, EKOnInsertMixin, {
 
  leftKeyDown: on(keyDown('ctrl+ArrowLeft'), function () {
    const pageToMove = get(this, 'currentPageNumber') - 1;
    if (pageToMove >= 1) {
      this.send('gotoCustomPage', get(this, 'currentPageNumber') - 1);
    }
  }),
 
  rightKeyDown: on(keyDown('ctrl+ArrowRight'), function () {
    const pageToMove = get(this, 'currentPageNumber') + 1;
    const pagesCount = get(this, 'pagesCount');
    if (pageToMove <= pagesCount) {
      this.send('gotoCustomPage', get(this, 'currentPageNumber') + 1);
    }
  })
});

Two event handlers are declared – leftKeyDown and rightKeyDown. They are executed when user presses Ctrl + ArrowLeft and Ctrl + ArrowRight. Method keyDown takes a single parameter that is a keys combination. It returns an event-name used in the on. These names are keydown:ArrowLeft+ctrl and keydown:ArrowRight+ctrl.

Key names for Ctrl, Shift and Alt are set “as is” in the lowercase. The other names for keys can be found in the code-maps#defaults.

What happens in the handlers? Both of them are sending event called gotoCustomPage with page number required. Each one checks if the needed page number is valid as well (greater than 0 and less than maximum page number).

Cleaning up all filters is a trivial task. Its solution requires only three lines of code:

1
2
3
clearFiltersKeyDown: on(keyDown('ctrl+KeyQ'), function () {
  this.send('clearFilters');
}),

clearFiltersKeyDown is placed in the our Component.

Event handler clearFilters already exists in the models-table and it will be triggered on pressing Ctrl + q. Note that parameter for keyDown contains KeyQ and not just q. It won’t work if focus is in the input field. This restriction can be avoided by overriding inputs for all filters, but this is out scope of this small tutorial.

Calling this.send with “correct” name can also help to show and hide all columns:

1
2
3
4
5
6
7
showAllColumnsKeyDown: on(keyDown('ctrl+shift+NumpadAdd'), function () {
  this.send('showAllColumns');
}),
 
hideAllColumnsKeyDown: on(keyDown('ctrl+shift+NumpadSubtract'), function () {
  this.send('hideAllColumns');
}),

Component models-table already has needed handlers called showAllColumns and hideAllColumns. All we need is to call them while needed.

Task about toggle visibility for single column will be solved together with task about sorting by single column. Their solutions are pretty similar and based on keys combination with column number. For simplicity, we assume that the columns are less than 10. Generators for event handlers are:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function getSortEventListener(num) {
  return on(keyDown(`ctrl+shift+Digit${num}`), function () {
    if (get(this, 'processedColumns.length') <= num) {
      this.send('sort', get(this, 'processedColumns').objectAt(num - 1));
    }
  });
}
 
function getToggleVisibilityEventListener(num) {
  return on(keyDown(`ctrl+shift+Numpad${num}`), function () {
    if (get(this, 'processedColumns.length') <= num) {
    	this.send('toggleHidden', get(this, 'processedColumns').objectAt(num - 1));
    }
  });
}
const MixinWithExtraListeners = {};
 
for(let i = 1; i <= 9; i++) {
  MixinWithExtraListeners[`sortByColumn${i}`] = getSortEventListener(i);
  MixinWithExtraListeners[`toggleColumn${i}`] = getToggleVisibilityEventListener(i);
}

Functions getSortEventListener and getToggleVisibilityEventListener create event handlers for key combinations with digits (top line on your keyboard and NumPad).

Both functions can be combined into single function by adding extra parameter actionName with value equal to sort or toggleHidden.

ember-keyboard allows to handle multiple keyboard events in one function using on(keyDown()). Each keyboard handler receives a single argument – instance of KeyboardEvent. It can be used to get pressed keys combination. In this case we have a single handler that is almost ‘universal’. Which of these ways to choose is up to you.

Mixin MixinWithExtraListeners has all needed handlers and it’s added to the Component:

1
2
3
export default ModelsTable.extend(EKMixin, EKOnInsertMixin, MixinWithExtraListeners, {
  // ...
});

Only two tasks are left to do. First one is about turning on edit-mode for row on double-click on it. Second one is about turning off edit-mode for row by pressing Esc.

All stuff for editing row is placed in the row itself. That is why we are going to extend original models-table/row and add handlers there.

1
2
3
4
5
6
7
import ModelsTableRow from 'ember-models-table/components/models-table/row';
import {on} from '@ember/object/evented';
import {get} from '@ember/object';
import {EKOnFocusMixin, EKMixin, keyDown} from 'ember-keyboard';
 
export default ModelsTableRow.extend(EKMixin, EKOnFocusMixin, {
});

Handler for double click can be done without ember-keyboard:

1
2
3
4
doubleClick() {
  this.send('editRow');
  this._super(...arguments);
}

Handler editRow turns on edit-mode for row.

Turning off edit-mode is a little bit more complex and depends on data-type in the row.

1
2
3
4
cancelEditKeyDown: on(keyDown('Escape'), function() {
  this.send('cancelEditRow');
  get(this, 'record').rollbackAttributes();
})

Handler cancelEditRow turns off edit-mode. Calling get(this, 'record').rollbackAttributes() should be done only if data in the table is a list of Models from ember-data.

That’s it. All tasks are solved. Demo is available on the ember-twiddle (as usual) – Keyboard navigation demo

Features shown in the demo won’t be added to the models-table. They are very specific and not all users need them. In case while it’s needed, each user prefers his own combination. Here I showed that adding support for keyboard is not such a difficult and time-consuming task.

, ,

Add comment

Top ↑ | Main page | Back