How to make focus() work before the DOM element shown

Viewed 755

UPDATE: it turns out there is no way to directly apply focus to textbox before DOM rendering finished


I wonder why the focus() function does not work on hidden element.

For example( I am using Vue.js ):

var vm = new Vue({
  el: "#app",
  data:{
    showtext: false
  },
  methods: {
    showTxt(ev){
      this.showtext = true
      var vm = this;
      // if I uncomment setTimeout, then the textbox can set focus
      //setTimeout(function(){
        vm.$refs.textbox.focus()
      //}, 0)
    }
  }
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
    <button @click="showTxt">
    Show Textbox and SetFocus on it
    </button>
    <div v-show="showtext">
        <input ref="textbox" type="text" />
    </div>
</div>

What I am trying to do is to click that button and show textbox and put focus in textbox, but currently, the textbox can not get focus if I directly call .focus(). It only works when I wrap a setTimeout around it(which I guess run on the next event loop). I wonder if there is any way to make the focus working without setTimeout?

Thanks

3 Answers

Preferable to use vm.$nextTick or Vue.nextTick (We don't need to care about nextTick even actually uses setTimeout, Vue will guarantee nextTick will do its job, even in future nextTick may use other approaches implement same goal).

As Vue API: nextTick says,

Defer the callback to be executed after the next DOM update cycle. Use it immediately after you’ve changed some data to wait for the DOM update.

Also you can check Vue Guide: Async Update Queue

For your case, when click the button to show the input, it will execute this.showtext=true, then execute element.focus. But actually the Dom element is still invisible (VNode is changed, but Vue hasn't re-render&patch).

So you have to use vm.$nextTick or Vue.nextTick to execute .focus after Vue re-render that input out.

Check the demo below:

Vue.config.productionTip = false
var vm = new Vue({
  el: "#app",
  data:{
    showtext: false
  },
  methods: {
    showTxt(ev){
      this.showtext = true
      var vm = this;
      console.log('Current:', this.$el.innerHTML)
      this.$nextTick(()=>{
        vm.$refs.textbox.focus()
        console.log('Nexttick:', this.$el.innerHTML)
      })
    }
  }
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
    <button @click="showTxt">
    Show Textbox and SetFocus on it
    </button>
    <div v-show="showtext">
        <input ref="textbox" type="text" />
    </div>
</div>

Why setTimeout works:

because setTimeout(,0) adds one task to the task-queue. But it will be executed at next event loop, but render will be exeucted after execute micro-tasks in current event loop.

Check HTML SPEC: event loop processing model (please look into Step 7), after current task (including this.showtext to true, data reactivity triggers re-render) already executed (it will be removed from task queue), the system will render first before pop one task (probably is setTimeout(,0) if setTimeout(,0) task is the oldest task) from the queue.

But Promise is micro-task, it will not work because it will be executed before render (please look into above event loop processing model: step 6).

Vue.config.productionTip = false
var vm = new Vue({
  el: "#app",
  data:{
    showtext: false
  },
  methods: {
    showTxt(ev){
      this.showtext = true
      var vm = this;
      new Promise((resolve, reject)=>{
        vm.$refs.textbox.focus()
        resolve()
      }).then(()=>{
        vm.$refs.textbox.focus()
      })
    }
  }
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
    <button @click="showTxt">
    Show Textbox and SetFocus on it
    </button>
    <div v-show="showtext">
        <input ref="textbox" type="text" />
    </div>
</div>

Or you can look into this Youtube Video, it will decribe better than mine.

You could get rid of setTimeout only if you use other means of setting focus, for example autofocus attribute. Without it you will have to separate setting showtext flag and call for focus into separate event "ticks".

var vm = new Vue({
  el: "#app",
  data:{
    showtext: false
  },
  methods: {
    showTxt(ev){
      this.showtext = true
      this.$refs.textbox.focus()
    }
  }
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
    <button @click="showTxt">
    Show Textbox and SetFocus on it
    </button>
    <div v-show="showtext">
        <input ref="textbox" autofocus type="text" />
    </div>
</div>

Hidden elements cannot get focus by the nature of focus entity concept. Only visible and enabled ones. In any UI system, HTML/CSS DOM included.

Related