How to defer form input binding until user clicks the submit button? - vue.js

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?

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>

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

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/vue#2.2.6/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>

Related

vue js sync modifier doesn't update the input value

I have a beginner question about sync modifier in vuejs. In my example, i want to change the value of inputs depending on focus event. The value is an Object inputsData and i'm getting it from the app. In parent i'm passing it to the child where it is rendering. I set an timer because i want to emit an server request. As you can see in the handleFocusFromChild Methode it change me the dataToBeChanged with newData (se also log after 4 seconds). As i understood from vue guid it should to update also the input value but it doens't, and i don't understand why, because dataToBeChanged have now the new values from newData. Can someone explain me why and how should i do to make it work?
Here i'm using the the Parent:
import Parent from "./parent.js";
Vue.component("app", {
components: {
Parent
},
template: `
<div>
<parent :inputsData="{
'firstElement':{'firstInputValue':'Hi there'},
'secondElement':{'secondInputValue':'Bye there'}
}"></parent>
</div>
`
});
Here is the Parent:
import Child from "./child.js";
export default {
name: "parent",
components: {
Child
},
props: {
inputsData: Object
},
template: `
<div>
<child #focusEvent="handleFocusFromChild"
:value.sync="inputsData.firstElement.firstInputValue"></child>
<child #focusEvent="handleFocusFromChild"
:value.sync="inputsData.secondElement.secondInputValue"></child>
</div>
`,
computed: {
dataToBeChanged: {
get: function() {
return this.inputsData;
},
set: function(newValue) {
this.$emit("update:inputsData", newValue);
}
}
},
methods: {
handleFocusFromChild: function() {
var newData = {
firstElement: { firstInputValue: "Hi there is changed" },
secondElement: { secondInputValue: "Bye there is changed" }
};
setTimeout(function() {
this.dataToBeChanged = newData;
}, 3000);
setTimeout(function() {
console.log(this.dataToBeChanged);
}, 4000);
}
}
};
Here is the child:
export default {
template: `
<div class="form-group">
<div class="input-group">
<input #focus="$emit('focusEvent', $event)"
v-model="value">
</div>
</div>
`,
props: {
value: String
}
};
you child component should emit "this.$emit('update:value', newValue)" as event
take a look over the docs: https://br.vuejs.org/v2/guide/components-custom-events.html
Also a way to do it is like this:
export default {
template: `
<div class="form-group">
<div class="input-group">
<input #focus="$emit('focusEvent', $event)"
v-model="valueProp">
</div>
</div>
`,
props: {
value: String
},
computed: {
valueProp:{
get(){
return this.value
},
set(val){
return this.$emit("update:value", val);
}
},
}
methods: {
handleFocus() {
this.$emit("focusEvent");
}
}
};

VueJs Watch per key of a model

I have an object (model), as an input field,for a search method, and I want that the watcher to detect changes per key, and not for all.
Right now ,if an input is changed, the watcher is called 10 times (the number of inputs that I have).
<b-form-input
v-model="search[field.column]"
type="search"
id="filterInput"
placeholder="search.."
></b-form-input>
watch:{
search: {
handler(){
// do somenthing
},
deep: true
}
}
You can watch a computed value that return the specific field that you want to watch.
Vue.config.devtools = false;
Vue.config.productionTip = false;
var app = new Vue({
el: '#app',
data: {
form: {
name : '',
lastname: ''
}
},
computed: {
formName() {
return this.form.name;
}
},
watch: {
formName() {
console.log("Name has changed")
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
Name :
<input v-model="form.name" />
Lastname :
<input v-model="form.lastname" />
</div>
You can explicitly watch the required key only.
new Vue({
el: '#app',
data: {
search: {
name: { placeholder: 'search name', value: '' },
age: { placeholder: 'search age', value: '' },
country: { placeholder: 'search country', value: '' }
}
},
watch:{
'search.name.value': { // Explicitly watch the required key.
handler(){
console.log('name changed...')
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<form>
<input
v-for="(value, key, i) in search"
:key="i"
v-model="search[key].value"
type="search"
id="filterInput"
:placeholder="value.placeholder"
/>
</form>
</div>
So, here watch's handler is called only when the name is searched & does not get called when age or country is searched.

Vue.js Component with v-model

I have been able to accomplish a single level deep of v-model two-way binding on a custom component, but need to take it one level deeper.
Current working code:
<template lang="html">
<div class="email-edit">
<input ref="email" :value="value.email" #input="updateInput()"/>
<input ref="body" :value="value.body" #input="updateInput()"/>
</div>
</template>
<script type="text/javascript">
import LineEditor from './LineEditor.vue'
export default {
components: {
LineEditor
},
computed: {
},
methods: {
updateInput: function(){
this.$emit('input',{
email: this.$refs.email.value,
body: this.$refs.body.value
})
}
},
data: function(){
return {}
},
props: {
value: {
default: {
email: "",
body: ""
},
type:Object
}
}
}
</script>
Used like this: <email-edit-input v-model="emailModel" />
However, if I add this piece, the value no longer propagates upwards:
<div class="email-edit">
<line-editor ref="email" :title="'Email'" :value="value.email" #input="updateInput()"/>
<input ref="body" :value="value.body" #input="updateInput()"/>
</div>
</template>
<script type="text/javascript">
import LineEditor from './LineEditor.vue'
export default {
components: {
LineEditor
},
computed: {
},
methods: {
updateInput: function(){
this.$emit('input',{
email: this.$refs.email.value,
body: this.$refs.body.value
})
}
},
data: function(){
return {}
},
props: {
value: {
default: {
email: "",
body: ""
},
type:Object
}
}
}
</script>
Using this second custom component:
<template lang="html">
<div class="line-edit">
<div class="line-edit__title">{{title}}</div>
<input class="line-edit__input" ref="textInput" type="text" :value="value" #input="updateInput()" />
</div>
</template>
<script type="text/javascript">
export default {
components: {
},
computed: {
},
methods: {
updateInput: function(){
this.$emit('input', this.$refs.textInput.value)
}
},
data: function(){
return {}
},
props: {
title:{
default:"",
type:String
},
value: {
default: "",
type: String
}
}
}
</script>
The first code-block works fine with just an input. However, using two custom components does not seem to bubble up through both components, only the LineEditor. How do I get these values to bubble up through all custom components, regardless of nesting?
I've updated your code a bit to handle using v-model on your components so that you can pass values down the tree and also back up the tree. I also added watchers to your components so that if you should update the email object value from outside the email editor component, the updates will be reflected in the component.
console.clear()
const LineEditor = {
template:`
<div class="line-edit">
<div class="line-edit__title">{{title}}</div>
<input class="line-edit__input" type="text" v-model="email" #input="$emit('input',email)" />
</div>
`,
watch:{
value(newValue){
this.email = newValue
}
},
data: function(){
return {
email: this.value
}
},
props: {
title:{
default:"",
type:String
},
value: {
default: "",
type: String
}
}
}
const EmailEditor = {
components: {
LineEditor
},
template:`
<div class="email-edit">
<line-editor :title="'Email'" v-model="email" #input="updateInput"/>
<input :value="value.body" v-model="body" #input="updateInput"/>
</div>
`,
watch:{
value(newValue){console.log(newValue)
this.email = newValue.email
this.body = newValue.body
}
},
methods: {
updateInput: function(value){
this.$emit('input', {
email: this.email,
body: this.body
})
},
},
data: function(){
return {
email: this.value.email,
body: this.value.body
}
},
props: {
value: {
default: {
email: "",
body: ""
},
type: Object
}
}
}
new Vue({
el:"#app",
data:{
email: {}
},
components:{
EmailEditor
}
})
<script src="https://unpkg.com/vue#2.2.6/dist/vue.js"></script>
<div id="app">
<email-editor v-model="email"></email-editor>
<div>
{{email}}
</div>
<button #click="email={email:'testing#email', body: 'testing body' }">change</button>
</div>
In the example above, entering values in the inputs updates the parent. Additionally I added a button that changes the parent's value to simulate the value changing outside the component and the changes being reflected in the components.
There is no real reason to use refs at all for this code.
In my case, having the passthrough manually done on both components did not work. However, replacing my first custom component with this did:
<line-editor ref="email" :title="'Email'" v-model="value.email"/>
<input ref="body" :value="value.body" #input="updateInput()"/>
Using only v-model in the first component and then allowing the second custom component to emit upwards did the trick.

How do you activate a class for individual elements within an array? [Vue.js]

I want to activate a class for each input individually. I have two inputs bound to the same v-model and class. I have a method that checks for something to be true, and if true enables the bound class. Currently it enables the class on all inputs. (The end goal is to search multiple inputs for an element within an array and if it matches, the class activates only for that element)
<input v-model="highlightTest" id="1" v-bind:class="{ active: active }" v-on:keyup="Highlighting"></input>
<input v-model="highlightTest" id="2" v-bind:class="{ active: active }" v-on:keyup="Highlighting"></input>
Highlighting: function() {
if (this.highlightTest != '') {
this.active = true;
}
else {
this.active = false;
}
How about this:
<template>
<input v-for="(hi,index) of highlights" v-model="highlights[]" v-bind:class="{ active: highlights[index] }" v-on:keyup="highlighting(index)"></input>
</template>
<script>
export default{
data() {
return {
highlights: []
};
},
created() {
this.$http.get('some/api').then(res => {
// map: convert 0,1 to false,true
this.highlights = res.json().map(h => h==1);
});
},
methods: {
highlighting(index) {
if (this.highlights[index]) {
// this.highlights[index] = false won't let vue detect the data change(thus no view change)
this.highlights.splice(index, 1, false);
} else {
this.highlights.splice(index, 1, true);
}
}
}
}
</script>
Here's one way to do it (sorry for the delay btw)
HTML:
<div id="app">
<p :class="{'active': activateWord(word)}" v-for="word in words">#{{ word }}</p>
<input type="text" v-model="inputText">
</div>
CSS:
.active {
color: red;
}
JS:
const app = new Vue({
el: '#app',
data: {
inputText: '',
words: [
'foo',
'bar'
]
},
methods: {
activateWord(word) {
return this.inputText === word
},
},
})
here's a fiddle

Focus input of freshly added item

So I have a list of items and list of inputs linked to each item via v-for and v-model.
I click a button and add new item to that list. I want to focus input which is linked to newly added item.
Can't figure out how to achieve this goal.
<div id="app">
<div v-for="item in sortedItems">
<input v-model="item">
</div>
<button #click="addItem">
add
</button>
</div>
new Vue({
el: '#app',
data: {
items: []
},
methods: {
addItem: function() {
this.items.push(Math.random());
}
},
computed: {
sortedItems: function() {
return this.items.sort(function(i1, i2) {
return i1 - i2;
})
}
}
})
Here's fiddle with sorted list
https://jsfiddle.net/sfL91r95/1/
Thanks
Update: inspired by pkawiak's comment, a directive-based solution. I found that calling focus in the bind section didn't work; I had to use nextTick to delay it.
Vue.directive('focus-on-create', {
// Note: using Vue 1. In Vue 2, el would be a parameter
bind: function() {
Vue.nextTick(() => {
this.el.focus();
})
}
})
new Vue({
el: '#app',
data: {
items: []
},
methods: {
addItem: function() {
this.items.push(Math.random());
}
},
computed: {
sortedItems: function() {
return this.items.sort(function(i1, i2) {
return i1 - i2;
})
}
}
})
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<div id="app">
<div v-for="item in sortedItems">
<input v-focus-on-create v-model="item">
</div>
<button #click="addItem">
add
</button>
</div>
Original answer:
Make your input a component so that you can give it a ready hook.
const autofocus = Vue.extend({
template: '<input v-model="item" />',
props: ['item'],
ready: function() {
this.$el.focus();
}
})
new Vue({
el: '#app',
data: {
items: []
},
methods: {
addItem: function() {
this.items.push(Math.random());
}
},
components: {
autofocus
},
computed: {
sortedItems: function() {
return this.items.sort(function(i1, i2) {
return i1 - i2;
})
}
}
})
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<div id="app">
<div v-for="item in sortedItems">
<autofocus :item="item"></autofocus>
</div>
<button #click="addItem">
add
</button>
</div>
I Like to extend #Roy's answer.
if you are using any UI framework then it will create DIV and within the DIV input tag will be created so this Snippet will handle that case.
Vue.directive('focus-on-create', {
bind: function(el) {
Vue.nextTick(() => {
el.querySelector('input').focus()
})
}
})