Search code examples
vue.jsv-for

How to create unique ref for nested v-for?


I've got a nest array of objects, like this:

    taskList: [{
      taskName: "Number One"
    },{
      taskName: "Number Two",
      children: [{
        taskName: "Child One"
      },{
        taskName: "Child due"
      }]
    },{
      taskName: "Number Three"
      }]}

I loop through this list with a nested v-for and create an input for every element like this:

  <div v-for="(task, index) in taskList">
      <input :ref="'inputField' + index" type="text" v-model="task.taskName" @keydown.up="arrowPress(index, -1)">
      <div v-for="(childOne, childOneIndex) in task.children">
            <input ref="inputField" type="text" v-model="task.taskName" @keydown.up="arrowPress(childOneIndex, -1)">

I've setup an event that allows me to move focus up/down through these inputs with arrow-keys. The method for it looks like this:

      arrowPress(index, value) {
      this.$nextTick(() => this.$refs['inputField'+ (value + index)][0].focus());
      }

This works well for the parent.

But I want to be able to move between the children as well. I.e. with focus in "Number 3" when I press up I want to go to "Child Two" and then to "Child One" and then to "Number Two", etc.

I can see some solutions to this but haven't figured out how to get any of them to work:

  1. Change :ref="'inputField' + index" to ref="inputField". But how do I then know what inputField is calling to change it +-1? E.g. how to I go from inputField2 to inputField1 in my method?
  2. Add a general counter that does ++ whenever an input is added, and use :ref="'inputField' + counter". However whenever I try to do that by adding {{counter++}} after the v-for div I get an infite loop.
  3. Any other ideas?

Thanks in advance!


Solution

  • One way would be to make your references regular and sortable, then find your current position in the sorted list of refs to figure out which is the correct previous ref.

    For example, make a method to compute the ref from indexes, so you don't need to repeat the code. In this case, I made the ref be 'inputFieldXXXXYYYY' where XXXX is the parent index and YYYY is the child index:

        ref(parent, child) {
            let me = 'inputField' + parent.toString().padStart(4, '0');
            if (child !== undefined) me += child.toString().padStart(4, '0');
            return me;
        },
    

    Then in your template use ref(index) for the parent, and ref(index, childIndex) for the children:

    <div id="main">
        <div v-for="(task, index) in taskList">
            <input :ref="ref(index)" type="text" v-model="task.taskName" @keydown.up="arrowPress(index)">
            <div v-for="(child, childIndex) in task.children">
                <input :ref="ref(index, childIndex)" type="text" v-model="task.taskName" @keydown.up="arrowPress(index, childIndex)">
            </div>
        </div>
    </div>
    

    Then in your arrowPress function, the children are found in the sorted list in the correct place, and you can decrement the index to find the previous reference:

        arrowPress(parent, child) {
            let refs = Object.keys(this.$refs).filter(key => key.indexOf('inputField') === 0).sort();
            let i = refs.indexOf(this.ref(parent, child));
            i = Math.max(i - 1, 0);
            let prevRef = refs[i];
            this.$nextTick(() => this.$refs[prevRef][0].focus());
        }
    

    This works for up-arrow presses. You can see how it would extend to down-arrows.

    Working fiddle: https://jsfiddle.net/jmbldwn/a1sjk75r/24/

    Per comments, here's a potentially simpler way that involves flattening the structure, then using a single v-for in the template:

    https://jsfiddle.net/jmbldwn/u0anzm35/7/