Philx94
Philx94

Reputation: 1285

How to manually focus input

I want to replicate a common item list renaming feature, where you have a list of layers and if you double click a layer, it changes the layer item to an input and that input is automatically focused with its text selected as well.

In my example, I am not able to focus() the DOM element by its ref because it says it is not defined. It only works if I click a second time on the element once its changed to an input. How do I set this autofocus?

  <div v-for="(item, i) in items">
      <div @click="changeToInput(i)" v-if="!item.input">{{item.name}}</div>
      <input ref="input" v-model="item.name" onfocus="select()" v-else>
    </div>
 changeToInput(i) {
      this.items[i].input = true;
      //this.$refs.input.focus()
 }

Here is the complete example : https://codesandbox.io/s/reverent-khayyam-2x8mp?file=/src/App.vue:481-573

Upvotes: 1

Views: 889

Answers (1)

Sphinx
Sphinx

Reputation: 10729

Two solutions:

First one: uses v-if + this.$nextTick:

v-if will insert/destroy the component when the binding expression is true/false, so in current cycle, input hasn't been in Dom tree. You have to use nextTick to wait for next cycle to get the Dom element of Input. And this.$refs.input will be one array based on how many v-if=true, so you have to filter out the this.items to find out correct index (that is why I used one combination of Array.slice and Array.filter).

Updated: The order of the elements of this.$refs.input1 is the order VNode is created. For example: clicks input2 -> input3 -> input1, the order of this.$refs.input1 is [2, 3, 1], not [1, 2, 3].

Second one: uses v-show + this.$nextTick:

It will make things easier, because v-show only update the css styles for Dom elements, it will not add/remove component instance (Vnode) from VNode tree. So the this.$refs.input will always equal this.items.length.

new Vue ({
  el:'#app',
  data() {
    return {
      items1: [
        { name: "Joe", input: false },
        { name: "Sarah", input: false },
        { name: "Jeff", input: false }
      ],
      items2: [
        { name: "Joe", input: false },
        { name: "Sarah", input: false },
        { name: "Jeff", input: false }
      ],
      refSort: {}
    };
  },
  methods: {
    changeToInput1(i) {
      this.items1[i].input = true;
      let refCount = (this.$refs.input1 && this.$refs.input1.length) || 0
      refCount < this.items1.length && (this.refSort[i] = refCount)
      this.$nextTick(() => {
        // the purpose uses this.refSort is record the order of this.$refs.input (Its order is same as the creating order of Ref), you can uncomment below line to see the root cause
        //console.log(this.$refs.input1[0] && this.$refs.input1[0].value, this.$refs.input1[1] && this.$refs.input1[1].value, this.$refs.input1[2] && this.$refs.input1[2].value)
        this.$refs.input1[this.refSort[i]].focus()
      })
    },
    changeToInput2(i) {
      this.items2[i].input = true;
      this.$nextTick(() => this.$refs.input2[i].focus())
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
  <div id="app">
    <h3>Uses v-if: <p>{{items1}}</p></h3>
    <div v-for="(item, i) in items1">
      <div @click="changeToInput1(i)" v-if="!item.input">{{item.name}}</div>
      <input ref="input1" v-model="item.name" onfocus="select()" v-else>
    </div>
    <h3>Uses v-show: <p>{{items2}}</p></h3>
    <div v-for="(item, i) in items2">
      <div @click="changeToInput2(i)" v-show="!item.input">{{item.name}}</div>
      <input ref="input2" v-model="item.name" onfocus="select()" v-show="item.input">
    </div>
  </div>

Upvotes: 2

Related Questions