Each change on state destroys user input if one-way bound - javascript

Consider a simple clock display and an input which I bound one-way to keep control over old/new state:
<div>{{ time }}</div>
<input ref="text" type="text" :value="text" style="width:95%">
<button type="button" #click="saveOnDiff">Save</button>
createApp({
...,
methods: {
saveOnDiff() {
const current = this.$refs.text.value;
// Compare current with old text + update if it changed.
...
}
},
mounted() {
const instance = this;
setInterval(() => instance.time = new Date(), 1000);
}
}).mount('#app');
The clock is updated each second. Unfortunately, this update spoils the input. Try it here: https://jsfiddle.net/dL78tsh9
How can I reduce binding updates to the absolute necessary ones? Some extra switch on one-way bindings like :value.lazy="text" would be helpful...

Changing time on each and every second will cause the whole template to be re-run after every 1 second. Which results everything in that template getting updated.
When a user types into <input> element, You aren't storing that value anywhere. You've got a :value to poke the value in but you aren't updating it when the value changes. The result will be that when Vue re-renders everything it will jump back to its original value.
Possible solution : Kindly ensure that your data is kept in sync with what the user types in. This could be done using v-model and watcher to get new and old values and based on that you can achieve this requirmeent.
You can try something like this (This is not a perfect solution but it will give you an idea) :
const {
createApp
} = Vue
const characterWiseDiff = (left, right) => right
.split("")
.filter(function(character, index) {
return character != left.charAt(index);
})
.join("");
createApp({
data() {
return {
result: "",
text: "Try to change me here",
time: new Date(),
oldVal: null
}
},
watch: {
text(newVal, oldVal) {
this.oldVal = oldVal;
}
},
methods: {
saveOnDiff() {
if (!this.oldVal) this.oldVal = this.text
const current = this.$refs.text.value;
console.log(current, this.oldVal)
if (current === this.oldVal) {
this.result = "No changes have been made!";
} else {
this.result = `Saved! Your changes were: "${characterWiseDiff(current, this.oldVal)}"`;
}
}
},
mounted() {
const instance = this;
setInterval(() => instance.time = new Date(), 1000);
}
}).mount('#app');
<script src="https://unpkg.com/vue#3.2.41/dist/vue.global.js"></script>
<div id="app">
<div>{{ time }}</div>
<input ref="text" type="text" v-model="text" style="width:95%">
<button type="button" #click="saveOnDiff">Save</button>
{{ result }}
</div>

As far as I know, there's no way to trick VueJs to not re-render a specific field.
When the time changes, your existing virtual DOM has a value for "text" and the newly generated virtual DOM has a different value so... VueJS re renders it.
UPDATE:
Based on #Tolbxela comment, looks like you could use v-once to only render the field once, and ignore the future DOM updates.
https://vuejs.org/api/built-in-directives.html#v-once
Alternative
If you want to control old/new state, why don't you just use two-way binding and save both states?
Something like this:
const {
createApp
} = Vue
const characterWiseDiff = (left, right) => right
.split("")
.filter(function(character, index) {
return character != left.charAt(index);
})
.join("");
createApp({
data() {
return {
result: "",
text: "Try to change me here",
previousText: "Try to change me here",
time: new Date(),
}
},
methods: {
saveOnDiff() {
if (this.text === this.previousText) {
this.result = "No changes have been made!";
} else {
this.result = `Saved! Your changes were: "${characterWiseDiff(this.previousText, this.text)}"`;
this.previousText = this.text;
}
}
},
mounted() {
const instance = this;
setInterval(() => instance.time = new Date(), 1000);
}
}).mount('#app');
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div>{{ time }}</div>
<div>
<input id="mockId" ref="text" type="text" v-model="text" style="width:95%">
<button type="button" #click="saveOnDiff">Save</button>
</div>
{{ result }}
</div>
https://jsfiddle.net/dmxLuf9w/6/

The best solution is to not use the Vue reactivity for timer updates at all. See my UPDATE 2 below.
The simplest way to fix it is to replace :value with v-model
UPDATE 1:
We need an other data field to store the input value.
<input ref="text" type="text" v-model="input" style="width:95%">
Check the sample below.
But it is not a good solution for complex apps, since every second you whole app HTML is refreshed. This can cause problems with rendering and lags.
UPDATE 1:
I have missed the other logic of comparing values. Here is the well working solution
UPDATE 2:
This question helped me to understand the whole problem with template rendering in Vue.
TransitionGroup lag when using requestAnimationFrame
And here is a good article about Improve Vue Performance with v-once + v-memo
CODE:
const placeholder = "Try to change me here"
const {
createApp
} = Vue
const characterWiseDiff = (left, right) => right
.split("")
.filter(function(character, index) {
return character != left.charAt(index);
})
.join("");
createApp({
data() {
return {
result: "",
text: placeholder,
input: placeholder,
time: new Date(),
}
},
methods: {
saveOnDiff() {
const current = this.input
if (current === this.text) {
this.result = "No changes have been made!";
} else {
this.result = `Saved! Your changes were: "${characterWiseDiff(this.text, current)}"`;
this.text = current;
}
}
},
mounted() {
const instance = this;
setInterval(() => instance.time = new Date(), 1000);
}
}).mount('#app');
<div id="app">
<div>{{ time }}</div>
<input ref="text" type="text" v-model="input" style="width:95%">
<button type="button" #click="saveOnDiff">Save</button>
{{ result }}
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>

The simplest way to fix it is to use v-memo="[text]":
<input ref="text" type="text" :value="text" style="width:95%" v-memo="[text]">
The <input> will only be updated when my text property has updated and not the any other property like the time property.
Test here:
https://jsfiddle.net/dL78tsh9/47/

Related

Warning: Invalid value for prop `value` on <input> tag

I'm getting this warning and I can't find out why: "Invalid value for prop value on tag. Either remove it from the element, or pass a string or number value to keep it in the DOM."
const [inputValues, setInputValues] = useState({});
const sendPost = async () => {
if (inputValues.link.length > 0) {
setPostList([...postList, inputValues]);
setInputValues('');
} else {
console.log('Empty inputs. Try again.');
}
};
const onInputChange = (event) => {
const name = event.target.name;
const value = event.target.value;
setInputValues(values => ({...values, [name]: value}))
};
const renderConnectedContainer = () => (
<div className="connected-container">
<form
onSubmit={(event) => {
event.preventDefault();
sendPost();
}}
>
<input
type="text"
placeholder="Enter title!"
name="title" value={inputValues.title || ""}
onChange={onInputChange}/>
<input
type="text"
placeholder="Enter link!"
name="link"
value={inputValues.link || ""}
onChange={onInputChange}/>
<textarea
type="text-area"
placeholder="Enter description!"
name="description"
value={inputValues.description || ""}
onChange={onInputChange}/>
<button type="submit" className="cta-button submit-post-button">Submit</button>
</form>
</div>
);
In the sendPost handler, you are resetting the inputValues state to an empty string '' instead of back to the initial state of an empty object {}. inputValues.title is ok since it's just undefined, but inputValues.link (i.e. ''.link) is actually a deprecated function.
String.prototype.link
Deprecated: This feature is no longer recommended. Though some browsers might still support it, it may have already been removed from
the relevant web standards, may be in the process of being dropped, or
may only be kept for compatibility purposes. Avoid using it, and
update existing code if possible; see the compatibility table at the
bottom of this page to guide your decision. Be aware that this feature
may cease to work at any time.
The link() method creates a string representing the code for an
<a> HTML element to be used as a hypertext link to another URL.
Just reset the inputValues back to the initial state.
const sendPost = async () => {
if (inputValues.link.length > 0) {
setPostList([...postList, inputValues]);
setInputValues({});
} else {
console.log('Empty inputs. Try again.');
}
};

Vue.js input value not reflecting value in component data

I have the following input:
<input :value="inputAmount" #input="handleAmountInput($event)" placeholder="Enter amount..." type="text">
I don't want 2-way binding with inputAmount because I want to clean the input of non-numeric characters in the handleAmountInput() function whenever the user inputs something:
handleAmountInput(e) {
const cleanInput = e.target.value.replace(/\D/g, '');
this.inputAmount = cleanInput;
console.log(cleanInput);
},
The issue is, the input itself doesn't reflect this cleaned up string set to inputAmount. If I show inputAmount in a separate element or console.log it like in the snippet above, it shows up just fine, but binding the value to the input with :value doesn't seem to work and shows the full inputted string, including non-numeric characters. What am I doing wrong here and how do I get the input to show the cleaned up string?
I'm not yet sure why exactly your code doesn't work as I would expect it to, but the way to fix it is to use both v-model and #input handler at the same time...
const app = Vue.createApp({
data() {
return {
inputAmount: ''
}
},
methods: {
handleAmountInput(e) {
this.inputAmount = e.target.value.replace(/\D/g, '');
console.log(this.inputAmount);
},
},
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.1.5/dist/vue.global.js"></script>
<div id='app'>
<input v-model="inputAmount" #input="handleAmountInput($event)" placeholder="Enter amount..." type="text">
<pre>{{ inputAmount }}</pre>
</div>
Update
Ok, I now understand the reason why your code does not work. What happens:
Value of inputAmount is for example '123' (reflected in <input>)
User types a
Your #input handler is called. It receives the value '123a', do it's job creating cleaned value '123' and assigns it into inputAmount
From Vue POV the value of inputAmount did not changed at all so no re-render is required and <input> still shows '123a' even tho inputAmount has a value of '123'
So another way of fixing your code is just to assign some value <input> can never produce into inputAmount 1st just to trigger the update (demo below)
const app = Vue.createApp({
data() {
return {
inputAmount: ''
}
},
methods: {
handleAmountInput(e) {
this.inputAmount = null
this.inputAmount = e.target.value.replace(/\D/g, '');
console.log(this.inputAmount);
},
},
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.1.5/dist/vue.global.js"></script>
<div id='app'>
<input :value="inputAmount" #input="handleAmountInput($event)" placeholder="Enter amount..." type="text">
<pre>{{ inputAmount }}</pre>
</div>
Have you tried using #change event
<input :value="message" #change="getInput($event)" placeholder="edit me" />
Use computed getter setter instead, Link :
example :
computed: {
inputAmount: {
get(){
//perform your logic
return 'value'
},
set(newValue){
this.value= newValue;
}
}
}
use v-model="inputAmount"? please see: https://cn.vuejs.org/v2/guide/forms.html
then you can just edit like this.inputAmount= this.inputAmount.replace(/\D/g, '');

React not triggering re-rendering of form inputs after state is updated via onChange

I am trying to build out a verification code page.
If I create an individual state for each input box, and then use the code below, it works appropriately.
<input type="number" value={inputOne} className={styles.codeInput} onChange={e => setInputOne(e.target.value}/>
However, I was trying to consolidate the state for all four of the input boxes, into one state object.
Now, when I type in a number, it moves on to the next input, but it never renders the value. In dev tools, I see the value flash like it updates, but it still stays as "value" and not "value="1"" for example.
However, if I do anything else to my code, like for example, change a p tag's text, then suddenly it updates and the inputs show the correct value.
I'm just trying to figure out what I'm doing wrong here.
Here's my current code.
import { useState } from 'react'
import styles from '../../styles/SASS/login.module.scss'
export default function Verify(params) {
const [verifCode, setVerifCode] = useState(['','','','']);
const inputHandler = (e, index) => {
// get event target value
let value = e.target.value;
// update state
let newState = verifCode;
newState[index] = value;
setVerifCode(newState);
// move focus to next input
if (e.target.nextSibling) {
e.target.nextSibling.focus()
} else {
// if at the last input, remove focus
e.target.blur();
}
}
return (
<div className={styles.verify}>
<p className={styles.title}>Verification</p>
<p className={styles.message}>Please enter the verification code we sent to your email / mobile phone.</p>
<div className={styles.form}>
<input type="number" value={verifCode[0]} className={styles.codeInput} onChange={e => inputHandler(e, 0)}/>
<input type="number" value={verifCode[1]} className={styles.codeInput} onChange={e => inputHandler(e, 1)}/>
<input type="number" value={verifCode[2]} className={styles.codeInput} onChange={e => inputHandler(e, 2)}/>
<input type="number" value={verifCode[3]} className={styles.codeInput} onChange={e => inputHandler(e, 3)}/>
</div>
<div className={styles.footer}>
<button>Verify Code</button>
</div>
</div>
)
};
I believe the problem lies in the following code
// update state
let newState = verifCode;
newState[index] = value;
setVerifCode(newState);
First line of the code just adds a pointer to the value verifCode.
You modify an element in that array, but newState is still the same variable verifCode. Even though the array elements have changed essentially it is still same variable (same reference).
Try something like:
// update state
const newState = [...verifCode]; // create a copy of the old verifCode, creating new array
newState[index] = value; // modify element
setVerifCode(newState); // set the new array to the state

Prepopulated form input element doesn't accept first input when submit btn is disabled

I have a form and a input element that gets a default value via :value="something". I want the submit button for this form to be disabled if
The input's value is falsey
Or, the input's value is the same as the default value.
My problem:
When I first click on the input (and the submit btn is disabled) and type a single thing, the input element doesn't change visually but I can see in the console that its value changed and the submit button no longer becomes disabled.
It seems as if I can't change the value of the input unless this submit button isn't disabled. However, if I get rid of my #input, then I'm able to type into this input fine. So, my guess is that it's something wrong with my event handler.
I've been unable to find a post with my problem, it was also hard to think of what exactly to google since this problem is so strange to me.
When the input matches the old value, it correctly disables the button. When I backspace when the input is 'a' it produces 'apple'. Not sure what's happening there.
Minimal working example of my problem:
<template>
<div>
<form>
<input :default-value="something" :value="something" #input="onInput"/>
<button type="submit" :disabled="isDisabled">submit</button>
</form>
</div>
</template>
<script>
export default {
name: "test",
data () {
return {
something: "apple",
isDisabled: true
}
},
methods: {
onInput (event) {
console.log("Current value: " + event.target.value);
console.log("Default value: " + event.target.getAttribute("default-value"));
console.log("isDisabled: " + (event.target.value === event.target.getAttribute("default-value")));
this.isDisabled = !event.target.value || (event.target.value === event.target.getAttribute("default-value"));
}
}
}
</script>
Vue SFC REPL: https://vuep.run/2192b4fb
Help very much appreciated!
Use vue computed property and of course, use v-model rather :value
const app = new Vue({
el: '#app',
name: "test",
data() {
return {
defaultVal: "apple",
something: "apple",
};
},
computed: {
isDisabled() {
return this.defaultVal === this.something;
},
},
methods: {
onInput (event) {
console.log("Current value: " + event.target.value);
console.log("Default value: " + event.target.getAttribute("default-value"));
console.log("isDisabled: " + (event.target.value === event.target.getAttribute("old-name")));
// don't do this now
// this.isDisabled = !event.target.value || (event.target.value === event.target.getAttribute("old-name"));
}
},
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<form>
<input :default-value="defaultVal" v-model="something" #keyup="onInput"/>
<button type="submit" :disabled="isDisabled">submit</button>
</form>
</div>

Vue JS focus next input on enter

I have 2 inputs and want switch focus from first to second when user press Enter.
I tried mix jQuery with Vue becouse I can't find any function to focus on something in Vue documentation:
<input v-on:keyup.enter="$(':focus').next('input').focus()"
...>
<input ...>
But on enter I see error in console:
build.js:11079 [Vue warn]: Property or method "$" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option. (found in anonymous component - use the "name" option for better debugging messages.)warn # build.js:11079has # build.js:9011keyup # build.js:15333(anonymous function) # build.js:10111
build.js:15333 Uncaught TypeError: $ is not a function
You can try something like this:
<input v-on:keyup.enter="$event.target.nextElementSibling.focus()" type="text">
JSfiddle Example
Update
In case if the target element is inside form element and next form element is not a real sibling then you can do the following:
html
<form id="app">
<p>{{ message }}</p>
<input v-on:keyup.enter="goNext($event.target)" type="text">
<div>
<input type="text">
</div>
</form>
js
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!',
focusNext(elem) {
const currentIndex = Array.from(elem.form.elements).indexOf(elem);
elem.form.elements.item(
currentIndex < elem.form.elements.length - 1 ?
currentIndex + 1 :
0
).focus();
}
}
})
JSFiddle Example
Following up from #zurzui here is in my opinion a cleaner alternative using the $refs API (https://v2.vuejs.org/v2/api/#vm-refs).
Using the $refs API, can allow you to target element in a simpler fashion without traversing the DOM.
Example: https://jsfiddle.net/xjdujke7/1/
After some tests, it's working
new Vue({
el:'#app',
methods: {
nextPlease: function (event) {
document.getElementById('input2').focus();
}
}
});
<script src="https://vuejs.org/js/vue.js"></script>
<div id='app'>
<input type="text" v-on:keyup.enter="nextPlease">
<input id="input2" type="text">
</div>
directives: {
focusNextOnEnter: {
inserted: function (el,binding,vnode) {
let length = vnode.elm.length;
vnode.elm[0].focus();
let index = 0;
el.addEventListener("keyup",(ev) => {
if (ev.keyCode === 13 && index<length-1) {
index++;
vnode.elm[index].focus();
}
});
for (let i = 0;i<length-1;i++) {
vnode.elm[i].onfocus = (function(i) {
return function () {
index = i;
}
})(i);
}
}
}
}
// use it
<el-form v-focusNextOnEnter>
...
</el-form>
Try this:
<input ref="email" />
this.$refs.email.focus()
Whilst I liked the directives answer due to it working with inputs inside other elements (style wrappers and so on), I found it was a little inflexible for elements that come and go, especially if they come and go according to other fields. It also did something more.
Instead, I've put together the following two different directives. Use them in your HTML as per:
<form v-forcusNextOnEnter v-focusFirstOnLoad>
...
</form>
Define them on your Vue object (or in a mixin) with:
directives: {
focusFirstOnLoad: {
inserted(el, binding, vnode) {
vnode.elm[0].focus();
},
},
focusNextOnEnter: {
inserted(el, binding, vnode) {
el.addEventListener('keyup', (ev) => {
let index = [...vnode.elm.elements].indexOf(ev.target);
if (ev.keyCode === 13 && index < vnode.elm.length - 1) {
vnode.elm[index + 1].focus();
}
});
},
},
},
On an enter key pressed, it looks for the index of the current input in the list of inputs, verifies it can be upped, and focuses the next element.
Key differences: length and index are calculated at the time of the click, making it more suitable for field addition/removal; no extra events are needed to change a cached variable.
Downside, this will be a little slower/more intensive to run, but given it's based off UI interaction...
Vue.js's directive is good practice for this requirement.
Define a global directive:
Vue.directive('focusNextOnEnter', {
inserted: function (el, binding, vnode) {
el.addEventListener('keyup', (ev) => {
if (ev.keyCode === 13) {
if (binding.value) {
vnode.context.$refs[binding.value].focus()
return
}
if (!el.form) {
return
}
const inputElements = Array.from(el.form.querySelectorAll('input:not([disabled]):not([readonly])'))
const currentIndex = inputElements.indexOf(el)
inputElements[currentIndex < inputElements.length - 1 ? currentIndex + 1 : 0].focus()
}
})
}
})
Note: We should exclude the disabled and readonly inputs.
Usage:
<form>
<input type="text" v-focus-next-on-enter></input>
<!-- readonly or disabled inputs would be skipped -->
<input type="text" v-focus-next-on-enter readonly></input>
<input type="text" v-focus-next-on-enter disabled></input>
<!-- skip the next and focus on the specified input -->
<input type="text" v-focus-next-on-enter='`theLastInput`'></input>
<input type="text" v-focus-next-on-enter></input>
<input type="text" v-focus-next-on-enter ref="theLastInput"></input>
</form>
<input type="text" #keyup.enter="$event.target.nextElementSibling.focus() />

Categories