1

I have written a table and in it all contain input fields. For this template I have developed a keyCode navigation. A navigation up, down, left and right is possible. What do I have to add to my code so that the cursor is focused directly in the when navigating?

My Code:

// Snippet from HTML
...
<tbody formArrayName="rows">
  <tr *ngFor="let rowControl of rows.controls; let rowIndex = index">
   <td [formGroupName]="rowIndex" appArrowKeyNav *ngFor="let column of displayedColumns; let columnIndex = index;">
   <div>
        <span>
               <label>
                   <input [formControl]="rowControl.get(column.attribute)">
                </label>
         </span>
     </div>
   </td>
 </tr>
</tbody>
// TS
public move(object: any) {
    const inputToArray = this.inputs.toArray();
    const cols = this.displayedColumns.length;
    let index = inputToArray.findIndex((x) => x.element === object.element);
    // console.log('Index found:', index);
    switch (object.action) {
      case 'UP':
        index -= cols;
        break;
      case 'DOWN':
        index += cols;
        break;
      case 'LEFT':
        index -= 1;
        break;
      case 'RIGHT':
        index += 1;
        break;
    }
    if (index >= 0 && index < this.inputs.length) {
      console.log('Navigating to index:', index);
      inputToArray[index].element.nativeElement.focus();
    }
  }
// Directive
  @HostListener('keydown', ['$event']) onKeyUp(event: KeyboardEvent) {
    switch (event.key) {
      case 'ArrowUp':
        this.keyboardService.sendMessage({ element: this.element, action: 'UP' });
        break;
      case 'ArrowLeft':
        this.keyboardService.sendMessage({ element: this.element, action: 'LEFT' });
        break;
      case 'ArrowDown':
        this.keyboardService.sendMessage({ element: this.element, action: 'DOWN' });
        break;
      case 'ArrowRight':
        this.keyboardService.sendMessage({ element: this.element, action: 'RIGHT' });
        break;
      case 'Enter':
        this.keyboardService.sendMessage({ element: this.element, action: 'ENTER' });
        break;
    }
}

Here is myStackblitz: https://stackblitz.com/edit/angular-wmfjhh-zfkyyx?file=app%2Ftable-basic-example.html

  • What is the problem with the code you show? Where does it fail? – Aviad P. Jul 31 '21 at 10:40
  • I have tried to implement, unfortunately does not work. I have implemented my code in Stackblitz. Unfortunately the table doesn't work on my Directive... Can you please tell me what I am doing wrong? My work: https://stackblitz.com/edit/angular-wmfjhh-zfkyyx?file=app%2Ftable-basic-example.html –  Jul 31 '21 at 12:38
  • What means `does not work` what do you expect. what does fail?`please be as clear as possible. assume we know nothing - because.. we do! – Jonathan Jul 31 '21 at 12:41
  • with does not work I mean that the directive in my table does not react when I click into the input field and then try to navigate with the arrow keys. I have implemented everything and do not know why it does not work. –  Jul 31 '21 at 12:44
  • Can you maybe take a look at my stackblitz? Maybe you can find the error. –  Jul 31 '21 at 12:45
  • Remove `arrow-div` from your `` it adds Comment type nodes to your query list which have no `focus()` function – Aviad P. Jul 31 '21 at 15:20
  • https://stackoverflow.com/questions/56562871/angular-6-html-table-create-dynamic-columns-and-rows/56664523#56664523 ? – Eliseo Aug 01 '21 at 17:02

2 Answers2

0

One possible way to have a table with cells that can be navigated with Arrow keys

use the id attribute to store row and col information using the *ngFor index

    <tr *ngFor="let rowControl of rows.controls; let i = index">

      <ng-container [formGroupName]="i">

        <td>
          <input [id]="'row-' + i + '-col-0'" formControlName="name" (focus)="onFocus($event)">
        </td>
        <td>
          <input [id]="'row-' + i + '-col-1'" formControlName="age" (focus)="onFocus($event)">
        </td>
        <td>
          <input [id]="'row-' + i + '-col-2'" formControlName="color" (focus)="onFocus($event)">
        </td>

      </ng-container>

    </tr>

in this case id for first row will be row-0-col-0, row-0-col-1 etc. 2nd row row-1-col-0

also there is a onFocus event handler which will set the current cell in focus

once keyUp is triggered

 @HostListener('keydown', ['$event'])
  onKeyUp(event: KeyboardEvent) {

    console.log(event.key);

    if (this.currentInputInFocus) {
      const id = this.currentInputInFocus.id;

      if (id && id.includes('row-')) {
        const arr = id.split('-');
        let row: number = Number(arr[1]);
        let col: number = Number(arr[3]);

        switch (event.key) {
          case 'ArrowUp':
            --row;
            break;

          case 'ArrowLeft':
            --col;
            break;

          case 'ArrowDown':
            ++row;
            break;

          case 'ArrowRight':
            ++col;
            break;

          case 'Enter':
            // do nothing
            break;
        }
        this.setFocus(row, col);
      }
    }
  }

get the currently focused element id, eg. 'row-0-col-0' and make changes to row or col values depending on a keypress then try to focus the new element

  private setFocus(row: number, col: number) {
    const newElementToFocusOn = document.getElementById(`row-${row}-col-${col}`);
    if (newElementToFocusOn) {
      this.currentInputInFocus = newElementToFocusOn;
      this.currentInputInFocus.focus();
    }
  }

in ngAfterViewInit initially focused cell can be set with:

  ngAfterViewInit() {
    const row = 1;
    const col = 0;
    this.setFocus(row, col);
  }

Working demo

UPDATE

to use your Stackblitz as context very few mods to make it work:

  • set the proper id attributes [id]="'row-' + rowIndex + '-col-' + columnIndex"
  • keep track of currently focused input: currentInputInFocus!: HTMLElement;
  • rest is the same as in original demo

Forked and updated your Stackblitz

UPDATE 2nd

The HTMLElement.focus() method sets focus on the specified element, if it can be focused.

Docs

One workaround is to add the tabindex="0" attribute according to this answer

Back to demo. As @Aviad P. mentioned the directive on ng-template is not going to work. Instead:

  <ng-template #otherColumns>
    <div tabindex="0" [id]="'row-' + rowIndex + '-col-' + columnIndex" arrow-div>
      Here is a Number
    </div>
  </ng-template>

Demo

UPDATE 3rd

to continue navigating you can try this on case 'RIGTH':

++col; // try to move to next column

if (col >= this.columns.length) { // check if moved past last column
  col = 0; // go to column at zero index  (1st column)
  ++row; // go to next row
}

Demo

robert
  • 5,742
  • 7
  • 28
  • 37
  • Very good. I still have one question... What do I have to change my code, so that I can navigate also in focused which are just no input fields? (Like for example the first column accountNumbers in my example). –  Jul 31 '21 at 18:03
  • do you have an idea what to add to the code so that you can jump to the next row with the keyCode on the right? I have realized that if you are in the first row and navigate to the last column to the right, it is not possible to jump to the next row. Do you have any idea? –  Aug 01 '21 at 08:24
0

You have a bug (well, at least one), you have attached your arrow-div to your <ng-template> which causes nodes of type Comment to be added to your inputs QueryList - these nodes have no focus() function so your code fails.

This is the problem area:

<ng-template arrow-div #otherColumns>
  Here is a Number
</ng-template>

Other than that, your index jumps in the wrong gaps when you press the keyboard arrows, but once you fix the previous bug, it does jump around without error.

Aviad P.
  • 32,036
  • 14
  • 103
  • 124