Key up down in Angular 2 \ 4 table

Deepak picture Deepak · Sep 15, 2017 · Viewed 7.9k times · Source

How can I write a functionality for key up and key down, across the table cells in Angular? (just like how we would move the keys using up and down arrow keys across the cells in an excel sheet).

This is the code I have: http://plnkr.co/edit/vk937A1VWNPIFQYkoPsM?p=preview

<table>
      <thead>
            <tr>
                 <th class="under-below" *ngFor="let col of columns">{{col.display}}</th>
            </tr>
        </thead>

        <tbody>
            <tr *ngFor="let row of rows">
                <td *ngFor="let col of columns">

                <div *ngIf="!col.editable">{{colLabelFunction(row, col)}}</div>
                <input type="text" value="{{colLabelFunction(row, col)}}" *ngIf="col.editable">

                </td>

            </tr>


        </tbody>
      </table>

I want to move up and down on the 'Column 2' text inputs ( cells ), using keyup and keydown keys.

Answer

bryan60 picture bryan60 · Sep 15, 2017

I just noticed that I went a little further than you wanted and added right/left functionality. You can just cut those out if you'd like.

To do this you need to accomplish a few things:

  1. bind to the key events and run a function on them
  2. track your indexes and pass them into the functions
  3. get references to the elements and call the focus method on them
  4. put it all together

Item 1: binding to key events and running function

Angular provides easy use event binding like this:

<input #inputs type="text" value="{{colLabelFunction(row, col)}}" [class.hidden]="!col.editable" 
      (keyup.arrowdown)="shiftFocusDown(rowIdx, colIdx)"  
      (keyup.arrowup)="shiftFocusUp(rowIdx, colIdx)"
      (keyup.arrowright)="shiftFocusRight(rowIdx, colIdx)"
      (keyup.arrowleft)="shiftFocusLeft(rowIdx, colIdx)">

The keyup event is general and then the arrowdown etc is a shorthand provided by angular. and then you just set the function you want to trigger with any parameters.

Item 2 track your index and pass to function

you can see above wehre im calling a function on these events and passing rowIdx and colIdx, to get these variables, you declare them in ngFor like:

<tr *ngFor="let row of rows; let rowIdx = index">
     <td *ngFor="let col of columns; let colIdx = index">

index is a special variable provided in ngFor that gives the current items index. They also expose a few others like first and last that you can declare and use.

Item 3: get the element references

To do this, we had to do a couple things, one you may have noticed above where instead of using ngIf on the inputs, I set class.disabled, and then I added CSS to hide elements with class hidden in your component declaration like:

 styles: [`.hidden { display: none; }`]

I did this so that the element references never change, the reason why will be apparent later, another thing I did was add #inputs to each input item, this is a template reference I can access in component code with:

@ViewChildren('inputs') inputs;

View children can take a few different kind of arguments, in this case its taking a string and it will search the template for any template references that match the string and return them in a querylist.

Item 4: putting it together

I now have appropriate functions that execute on the desired key events, with the needed parameters, and a flat list of all my input element references.

Now I just need to write the code to make this work:

focusInput(rowIdx: number, colIdx: number) {
  console.log(rowIdx, colIdx);
  // convert ViewChildren querylist to an array to access by index
  let inputEls = this.inputs.toArray();
  // get the flat index from row/cols
  let flatIdx = (rowIdx * this.columns.length) + colIdx;
  // get that reference from the input array and use the native element focus() method
  inputEls[flatIdx].nativeElement.focus();
}

shiftFocusDown(rowIdx:number, colIdx:number) {
  console.log("DOWN", colIdx, rowIdx)
  // add 1 but don't go beyond my row range
  rowIdx = Math.min(rowIdx + 1, this.rows.length - 1);
  this.focusInput(rowIdx, colIdx);
}

shiftFocusUp(rowIdx:number, colIdx:number) {
  console.log("UP", colIdx, rowIdx);
  // up 1, but not less than 0
  rowIdx = Math.max(0, rowIdx - 1);
  this.focusInput(rowIdx, colIdx);
}

shiftFocusLeft(rowIdx:number, colIdx:number) {
  console.log("LEFT", rowIdx, colIdx);
  // left one column, and correct for non editable columns to left
  colIdx = colIdx - 1 - this.columns.slice(0, colIdx).reverse().findIndex(c => c.editable);
  // don't need edge check bc findIndex returns -1 if none found or no items, so that corrects us back to start col automatically
  this.focusInput(rowIdx, colIdx);
}

shiftFocusRight(rowIdx:number, colIdx:number) {
  console.log("RIGHT", rowIdx, colIdx);
  // right one column, and correct for non editable columns to right
  colIdx = colIdx + 1 + this.columns.slice(colIdx + 1).findIndex(c => c.editable);
  // don't need edge check bc findIndex returns -1 if none found or out of range, so that corrects us back to start col automatically
  this.focusInput(rowIdx, colIdx);
}

The code here is mostly just logic to find the next appropriate col/row index and then transforming that x,y into a flat index so I can find the correct reference in my viewchildren list and call focus() on it. The reason I needed all the element references to always be there is because otherwise the correct flat index would be harder to figure out from the x,y. if you REALLY need them to not be there though, then you could modify this logic to make it work. not impossible at all. You also could modify it to allow "wrapping" around rows pretty easily.

Finished plunkr: http://plnkr.co/edit/m4BRi5rh5gYuvlKfwhqk?p=preview