Make two inputs update each other in two-way binding - vue.js

I am trying to have two inputs update each other
Scenarios:
When I type in Base-10 number in field-1, field-2 must give Base-2 equivalent number
When I type in Base-2 number in field-2, field-1 must give Base-10 equivalent number
this is just an hypothetical scenario. I will need to deal with problems like this. I have provided an even more rudimentary example in my fiddle.
This should happen in this way:
When I make changes in Input-field-1, Input-field-2 value must change according to appropriate calculation.
When I make changes in Input-field-2, Input-field-1 value must change according to appropriate calculation.
Problem:
Right now, when I change Input-field-1, value of Input-field-2 changes which produces a warning as the change in Input-field-2 is trying to change value in Input-field-1... and I guess the process continues indefinitely.
This problem works without much issues in the fiddle because Vue.js has an awesome compiler. But I'd really like to know a better way to solve this. I tried isolating the variables from the function as much as possible.
My code-snippet:
HTML:
<div id="app">
<div>
I have <input v-model="perc" style="width:3em; text-align:right; background-color:#ffffe0;" v-on:change="amtCal">% of ${{total}}<strong> which is $</strong> <input v-model="amt" style="width:3em; text-align:right; background-color:#ffffe0;" v-on:change="percCal">
</div>
</div>
JS:
new Vue({
el: '#app',
data: {
total:'200',
perc:'10',
amt:''
},
computed:{
amtCal:function(){
this.amt=this.perc/100*this.total
return 0;
},
percCal:function(){
this.perc=this.amt/this.total*100;
return 0;
}
}
})
new Vue({
el: '#app',
data: {
total: '200',
perc: '10',
amt: ''
},
computed: {
amtCal: function() {
this.amt = this.perc / 100 * this.total
return 0;
},
percCal: function() {
this.perc = this.amt / this.total * 100;
return 0;
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div>
I have <input v-model="perc" style="width:3em; text-align:right; background-color:#ffffe0;" v-on:change="amtCal">% of ${{total}}<strong> which is $</strong> <input v-model="amt" style="width:3em; text-align:right; background-color:#ffffe0;" v-on:change="percCal">
</div>
</div>
What I realize:
I've used the v-on:change property to execute a function when the value changes.
To keep the input from overwhelming the function executions, I used v-model.lazy in some other tests.
However, no matter what I do, it seems this problem will not subside as something fundamental is wrong with my approach.
There must be a better way.

I think I'd use a computed property with get and set.
https://v2.vuejs.org/v2/guide/computed.html#Computed-Setter
I've glossed over validation, etc. below but it shows the basic principle of using one data value as the definitive source of truth while the other base uses a computed property.
new Vue({
el: '#app',
data () {
return {
num10: '6'
}
},
computed: {
num2: {
get () {
return Number(this.num10).toString(2)
},
set (num) {
this.num10 = parseInt(num, 2).toString()
}
}
}
})
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<div id="app">
<div>
<label>Base 10 <input v-model="num10"></label>
</div>
<div>
<label>Base 2 <input v-model="num2"></label>
</div>
</div>

Related

Vue JS - Which is better to trigger input validation, change/key up event on an input or using watchers on it`s property

I was wondering which is the best practise to trigger input validation in Vue JS, using events such as onchange, keyup or having watchers on each property? Let`s assume that you only have around 20 inputs to deal with.
As you said you might have n number of inputs in your form on which you want to add validation. I think watcher is a good feature to use as it will update every time any changes happen in the form data.
In your data object, You can create one formData object which will contain all the input field properties and then you can put a watcher on formData.
Live Demo :
var App = new Vue({
el: '#app',
data() {
return {
formData: {
name: 'Alpha',
age: 30
}
}
},
watch: {
'formData': {
deep: true,
handler(val) {
console.log(val)
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<form>
<label for="name">Name
<input id="name" type="text" v-model="formData.name">
</label>Age
<label for="age">
<input id="age" type="number" v-model="formData.age">
</label>
</form>
</div>

Hide one item from input list that is dynamically generated unless other input from list has value in vue / vuetify

So the process is like this. I have a form that is created from api. I want to show all the inputs except for one. That one will only be shown if the user adds value to another specific input from the form.
Something like this is the idea.
<Form>
<div-for="formItem in state.formItemData">
<template v-if="formItem.one !== ''">
<form-input
v-model="invoiceForm.two"
:key="formItem.id"
ref="two"
></form-input>
</template>
</div>
</Form>
const invoiceForm = computed({
get: () => state.forms.formData,
set: (value) => {
state.forms.formData= value
}
})
The concept in your problem is very simple, you just must make use of a computed property that evaluates if there are values in input 1 and it will rerender the component showing input2.
new Vue({
el: '#app',
data: {
input1: '',
input2: ''
},
methods: {
verification() {
console.log(this.input1);
}
},
computed: {
notEmpty() {
return this.input1 !== '' && this.input1.length > 3;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input #change="verification" type="text" v-model="input1"> <input v-if="notEmpty" type="text" v-model="input2">
</div>
You should use a v-show if you're eventually going to show/hide it when rendering, especially if it's going to be multiple times. The Vue documentation says that the v-if will not be rendered when the initial condition is false.
Source: https://v3.vuejs.org/guide/conditional.html#v-if-vs-v-show

vue.js v-model on input can not differentiate between empty input and invalid input

I am trying to validate some input data (e.g. number) on an input-tag connected to some data via v-model.
The problem is, if I have invalid data (e.g. "1e"), the data will be "". Same goes obviously for empty input.
How can I differentiates empty input or invalid input?
var app = new Vue({
el: "#app",
data: {
budget: "",
},
methods: {
updateBudget() {
// do some input validation here. E.g.
if (this.budget === "") {
console.log("This is triggered on both:");
console.log("(1) empty input -> budget = ''");
console.log("(2) invalid input, e.g. -> budget = '1e'");
console.log("Problem: I can't split this in the above cases!");
};
},
},
});
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.12/dist/vue.js"></script>
<div id="app">
<input v-model="budget" type="number" placeholder="Budget" min="0" step="0.01" #input="updateBudget" />
</div>
I'd appreciate a hint. Thanks
1e is not a number (also +, -). The output is empty as it's not a valid number, try to insert 1e0 or -1, you'll be able to catch the value. There are some related issues:
It is exactly how the spec says.
How to prevent extra characters
To catch the exact number without worry about 'e' use the internal Vue implementation. It takes care of such situations. (v-model.number)
<input
v-model.number="budget"
type="number"
placeholder="Budget"
min="0"
step="0.01"
#input="updateBudget"
/>
By the way, if you want to check 'e' explicitly, I think, it's better to use 'type=text'.
I did a custom solution based on some regex. Something like:
<template>
<div id="app">
<input v-model="budget" type="text" #input="validateAndUpdateInput" />
</div>
</template>
<script>
[...]
methods: {
validateAndUpdateInput: function() {
if (!/^-?([0]?|[1-9]{1}\d{0,15})([.]{1}\d{0,2})?$/.test(this.budget)) {
this.budget = this.previousInput;
//alert("not valid");
} else {
this.previousInput = this.budget;
this.$emit("update-input", this.budget);
}
},
[...]
</script>

Vuejs - Object notation for refs

I want to group my refs in to an object and access them by object notation. With that in mind, I now have this component:
new Vue({
el: '#app',
template: `
<div>
<input ref="input.name" type="text"/>
<input ref="input.email" type="email"/>
<input ref="input.password" type="password"/>
</div>
`,
created(){
console.log(this.$refs["input.name"]); // This works fine
console.log(this.$refs.input.name); // throws "TypeError: Cannot read property 'name' of undefined"
}
})
But it only shows this:
{
input.name: input,
input.email: input,
input.password: input,
}
What I want is something like this:
{
input: {
name: input,
email: input,
password: input,
},
}
Check fiddle here
Update:
I'm well aware of v-model but that's not what I need, I'll be doing something else with DOM.
new Vue({
el: '#app',
data() {
return {
inputs: {}
}
},
template: `
<div>
<input ref="input.name" type="text" value="123"/>
<input ref="input.email" type="email"/>
<input ref="input.password" type="password"/>
<button type="button" #click="clickMe">Click ME</button>
</div>
`,
mounted() {
this.setRefs();
console.log("this.inputs", this.inputs);
},
methods: {
setRefs() {
const INPUTS = {};
for (key in this.$refs) {
if (key.indexOf(".")) {
const KEYS_LIST = key.split(".");
INPUTS[KEYS_LIST[0]] = INPUTS[KEYS_LIST[0]] || {};
INPUTS[KEYS_LIST[0]][KEYS_LIST[1]] = this.$refs[key];
}
}
this.inputs = INPUTS;
},
clickMe() {
console.log("this.inputs.input.name.value", this.inputs.input.name.value);
},
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
This cannot work: refs only support strings as keys (see the docs), so there is not way of using objects here.
Also, a string with "dot notation" does not magically transform into an object with a nested property -- it's just a string. You could work around this by manually parsing the ref strings into an object structure, as has been demonstrated by #Ilia Brykin's answer. But I honestly think that you are misunderstanding the purpose of the whole ref system if you really want to do that.
P.S.: If you drop the "dort notation" part, you could use keys like input_name and access them via this.$refs.input_name.
EDIT
Another idea: you can get all input refs by their common prefix.
Object.keys(this.$refs).filter(ref => ref.startsWith('input.')
This gives you an array of ref strings of input elements.

Call only one method from methods

On this example, I change the "name" via the method:
new Vue({
el: '#exercise',
data: {
name: 'Petr',
},
methods: {
random: function() {
return Math.random();
},
changeName: function(event) {
this.name = event.target.value;
}
}
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="exercise">
<p>VueJS is pretty cool - {{ name }}</p>
<p>{{ random() }}</p>
<div>
<input v-on:input="changeName" type="text">
</div>
</div>
But each time I call the method changeName the other method (random) also is called.
Why?
From Computed Properties - Computed Caching vs Methods:
In comparison, a method invocation will always run the function whenever a re-render happens.
And when does a re-render happen? When data changes.
And in your example data (i.e. name) changes whenever you type into the <input> (because it calls changeName).
Check the lifecycle diagram - more specifically, the "Mounted" red ball:
Check the demo below, so you see those lifecycle events are happening (and thus the re-rendering between them):
new Vue({
el: '#exercise',
data: {
name: 'Petr',
},
beforeUpdate() {
console.log('beforeUpdate executed');
},
updated() {
console.log('updated executed');
},
methods: {
random: function() {
return Math.random();
},
changeName: function(event) {
this.name = event.target.value;
}
}
})
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<div id="exercise">
<p>VueJS is pretty cool - {{ name }}</p>
<p>{{ random() }}</p>
<div>
<input v-on:input="changeName" type="text">
</div>
</div>
#acdcjunior explained it very well about this issue:
In comparison, a method invocation will always run the function whenever a re-render happens.
But what if you really want to bind the random method when you really want? Here's the solution:
Modified HTML
<div id="exercise">
<p>VueJS is pretty cool - {{ name }}</p>
<p>{{ randomNumber }}</p>
<div>
<input v-on:input="changeName" type="text">
<input type="button" #click="random" value="Generate Random Number">
</div>
</div>
In the preceding example, we are using data randomNumber. I have added a button that holds the random method to generate a random number when we click on it.
// modified data option:
data: {
name: 'Petr',
randomNumber: '' /* initialize randomNumber */
},
// modified random method
methods: {
random: function() {
/* Now, we return data randomNumber */
return this.randomNumber = Math.random();
},
changeName: function(event) {
this.name = event.target.value;
}
},
created() {
// we need to show random number when instance is created
this.random();
}
What the heck is happening here? The method random should be invoked and the random number should also be generated, right?
No. The method random isn't invoked because we've not used this method anywhere ie. not bound random() in our template binding.
So, what it means is that, the method is only invoked (after re-render as being said) if there is the method hooked somewhere inside our template.