niutech
niutech

Reputation: 29922

How to defer form input binding until user clicks the submit button?

I wanted to make a two-way data binding on my form input in Vue.js 2.3. However, I cannot use the v-model directive, because I want the data to be updated only on clicking the submit button. Meanwhile, the input value may be updated from another Vue method, so it should be bound to the data property text. I made up something like this jsFiddle:

<div id="demo">
  <input :value="text" ref="input">
  <button @click="update">OK</button>
  <p id="result">{{text}}</p>
</div>
new Vue({
  el: '#demo',
  data: function() {
    return {
      text: ''
    };
  },
  methods: {
    update: function () {
        this.text = this.$refs.input.value;
    }
  }
});

It works, but it does not scale well when there are more inputs. Is there a simpler way to accomplish this, without using $refs?

Upvotes: 5

Views: 3505

Answers (3)

Bert
Bert

Reputation: 82449

With a slightly different approach than the other answers I think you can achieve something that is easily scalable.

This is a first pass, but using components, you could build your own input elements that submitted precisely when you wanted. Here is an example of an input element that works like a regular input element when it is outside of a t-form component, but only updates v-model on submit when inside a t-form.

Vue.component("t-input", {
  props:["value"],
  template:`
    <input type="text" v-model="internalValue" @input="onInput">
  `,
  data(){
    return {
      internalValue: this.value,
      wrapped: false
    }
  },
  watch:{
    value(newVal){
      this.internalValue = newVal
    }
  },
  methods:{
    update(){
      this.$emit('input', this.internalValue)
    },
    onInput(){
      if (!this.wrapped)
        this.$emit('input', this.internalValue)
    }
  },
  mounted(){
    if(this.$parent.isTriggeredForm){
      this.$parent.register(this)
      this.wrapped = true      
    }
  }
})

Here is an example of t-form.

Vue.component("t-form",{
  template:`
    <form @submit.prevent="submit">
      <slot></slot>
    </form>
  `,
  data(){
    return {
      isTriggeredForm: true,
      inputs:[]
    }
  },
  methods:{
    submit(){
      for(let input of this.inputs)
        input.update()
    },
    register(input){
      this.inputs.push(input)
    }
  }
})

Having those in place, your job becomes very simple.

<t-form>
  <t-input v-model="text"></t-input><br>
  <t-input v-model="text2"></t-input><br>
  <t-input v-model="text3"></t-input><br>
  <t-input v-model="text4"></t-input><br>
  <button>Submit</button>
</t-form>

This template will only update the bound expressions when the button is clicked. You can have as many t-inputs as you want.

Here is a working example. I included t-input elements both inside and outside the form so you can see that inside the form, the model is only updated on submit, and outside the form the elements work like a typical input.

console.clear()
//
Vue.component("t-input", {
  props: ["value"],
  template: `
    <input type="text" v-model="internalValue" @input="onInput">
  `,
  data() {
    return {
      internalValue: this.value,
      wrapped: false
    }
  },
  watch: {
    value(newVal) {
      this.internalValue = newVal
    }
  },
  methods: {
    update() {
      this.$emit('input', this.internalValue)
    },
    onInput() {
      if (!this.wrapped)
        this.$emit('input', this.internalValue)
    }
  },
  mounted() {
    if (this.$parent.isTriggeredForm) {
      this.$parent.register(this)
      this.wrapped = true
    }
  }
})

Vue.component("t-form", {
  template: `
    <form @submit.prevent="submit">
      <slot></slot>
    </form>
  `,
  data() {
    return {
      isTriggeredForm: true,
      inputs: []
    }
  },
  methods: {
    submit() {
      for (let input of this.inputs)
        input.update()
    },
    register(input) {
      this.inputs.push(input)
    }
  }
})


new Vue({
  el: "#app",
  data: {
    text: "bob",
    text2: "mary",
    text3: "jane",
    text4: "billy"
  },
})
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
<div id="app">
  <t-form>
    <t-input v-model="text"></t-input><br>
    <t-input v-model="text2"></t-input><br>
    <t-input v-model="text3"></t-input><br>
    <t-input v-model="text4"></t-input><br>
    <button>Submit</button>
  </t-form>
  Non-wrapped:
  <t-input v-model="text"></t-input>
  <h4>Data</h4>
  {{$data}}
  <h4>Update Data</h4>
  <button type="button" @click="text='jerome'">Change Text</button>
</div>

Upvotes: 1

thanksd
thanksd

Reputation: 55644

You can use an object and bind its properties to the inputs. Then, in your update method, you can copy the properties over to another object for display purposes. Then, you can set a deep watcher to update the values for the inputs whenever that object changes. You'll need to use this.$set when copying the properties so that the change will register with Vue.

new Vue({
  el: '#demo',
  data: function() {
    return {
      inputVals: {
        text: '',
        number: 0
      },
      displayVals: {}
    };
  },
  methods: {
    update() {
      this.copyObject(this.displayVals, this.inputVals);
    },
    copyObject(toSet, toGet) {
      Object.keys(toGet).forEach((key) => {
        this.$set(toSet, key, toGet[key]);
      });
    }
  },
  watch: {
    displayVals: {
      deep: true,
      handler() {
        this.copyObject(this.inputVals, this.displayVals);
      }
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="demo">
  <input v-model="inputVals.text">
  <input v-model="inputVals.number">
  <button @click="update">OK</button>
  <input v-for="val, key in displayVals" v-model="displayVals[key]">
</div>

If you're using ES2015, you can copy objects directly, so this isn't as verbose:

new Vue({
  el: '#demo',
  data() {
    return {
      inputVals: { text: '', number: 0 },
      displayVals: {}
    };
  },
  methods: {
    update() {
      this.displayVals = {...this.inputVals};
    },
  },
  watch: {
    displayVals: {
      deep: true,
      handler() {
        this.inputVals = {...this.displayVals};
      }
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="demo">
  <input v-model="inputVals.text">
  <input v-model="inputVals.number">
  <button @click="update">OK</button>
  <input v-for="val, key in displayVals" v-model="displayVals[key]">
</div>

Upvotes: 3

Decade Moon
Decade Moon

Reputation: 34286

You can use two separate data properties, one for the <input>'s value, the other for the committed value after the OK button is clicked.

<div id="demo">
  <input v-model="editText">
  <button @click="update">OK</button>
  <p id="result">{{text}}</p>
</div>
new Vue({
  el: '#demo',
  data: function() {
    return {
      editText: '',
      text: ''
    };
  },
  methods: {
    update: function () {
      this.text = this.editText;
    }
  }
});

Updated fiddle

Upvotes: 1

Related Questions