Restoring focus to a repeated dynamic element

I have a set of repeated input elements. The array it is bound to can change size with the user adding or deleting rows. When the user clicks on a link or button I want to restore focus to the last focused input element.

I’ve got this working partially with a focus event handler that saves the active element off:

this.lastFocused = document.activeElement as HTMLElement;

However, if I delete the row that had focus I’d like to set focus to the row before or after the one that previously had focus. I tried adding an array that holds a reference to each repeated input element, myElements . Then my focus handler can track the index of the row which was last focused. And if that row is gone I can adjust the index to give focus to.

HTML

<div repeat.for="f of model.content">
    <input id="input-${$index}" focus.trigger="setActiveField(f, $index)" ref="myElements[$index]"/>
</div>

TypeScript

setActiveField(item: any, index: number) {
  this.lastItem = item;
  this.lastFocusedIndex = index;
}

restoreFocus() {
  this.myElements[this.lastFocusedIndex].focus()
}

However, as I add and delete rows, the array of references myElements gets out of sync with the input fields. Some rows of myElements are null, others are out of order. The indices in myElements don’t match the index of the repeated input.

Is there a better way of restoring focus to my last input? Can my reference array stay in sync with my list of inputs?

I ended up solving this the plain JS way, just looking up elements with getElementById("input-" + index) and setting focus that way. I’d still like to know if I’m doing something wrong with ref as I found the behavior very confusing.

Wouldnt it be easier to store the element.previousSibling before removing, do the removal and than, in the next macrotask, apply focus to sibling?

Thanks @zewa666. The remove code only removes rows from model.content and depends on Aurelia to do the rest. So I don’t already have the element and would need to look it up either way.

I also opted to move focus to the following row if there is one. If there isn’t move focus to the previous row with the code below. Though I could accomplish that with your solution by checking if there is a nextSibling as well.

if (index >= this.model.content.length) {
  index = this.model.content.length - 1;
}
if (index < 0) {
  return;
}

element = document.getElementById("input-" + index);

Well in order to get the currently active element just use your document.activeElement. also next/prev sibling should return null if non-existent so the if becomes easier

1 Like

The $index of repeat.for is useless for dynamic elements.
You can make custom element to keep ref for HTMLElement like below.

HTML

<div repeat.for="f of model.content">
    <custom-element item.bind="f"  parent.bind="$parent"/>
</div>

in custom-element.html

<template>
    <input focus.trigger="setActiveField()" ref="element"/>
</template>

TypeScript

setActiveField(customElement: customElement) {
  this.lastItem = customElement.item;
  this.lastFocusedCustomElement = customElement;
}

restoreFocus() {
  this.lastFocusedCustomElement?.element.focus();
}

in custom-element.ts

setActiveField() {
  this.parent.setActiveField(this)
}

How about this js lib: keyboard-navigator - npm
the above lib stores xpath of current active element and tries to refocus any element matching same xpath on DOM updates.