Form number input does not work with keys - vue.js

<template v-for="(paint, index) in paints">
<input type="number" v-bind:min="1" v-model.number="paint.qty">
</template>
-
var paintListApp = new Vue({
delimiters: ['${', '}'],
el: '#paintListApp',
data: {
paints: paints
},
methods: {
addToSet: function(sku, name, image) {
// method triggered when item is clicked - sends data to event bus
this.$eventHub.$emit('addToSelectedPaints', sku, name, image)
}
}
});
var paintWidget = new Vue({
el: '#paintWidget',
delimiters: ['${', '}'],
data: {
paints: []
},
created() {
// data picked up - processed by 'addToSelectedPaints'
this.$eventHub.$on('addToSelectedPaints', this.addToSelectedPaints);
},
methods: {
addToSelectedPaints: function (sku, name, image) {
var skuIndex = _.findIndex(this.paints, function (o) { return o.sku === sku; });
if (skuIndex !== -1) {
this.paints[skuIndex].qty = this.paints[skuIndex].qty + 1;
} else {
this.paints.push({
sku: sku,
name: name,
image: image,
qty: 1
});
}
}
}
});
Trying to get min values to work on number inputs. The min is respected by the browser number plus / minus controls - however, when using the keyboard, the min attribute appears to be ignored. I've tried all sorts of things from adding a method triggered by keyup etc and testing the value, through to watchers.
Keyup gets messy as when deleting, it automatically added a 1... making it difficult to type numbers above 19... (eg, you backspace to enter 2, but - it inserts a 1).
I just need to get native browser input min attribute working with keyboard input.
** Edit **
<input type="number" v-model="paint.qty" #change="paint.qty = paint.qty < 1 ? 1 : paint.qty">
Sort of solves the issue, albeit at the expense of the min attribute. Hooking into the #change event. If input is less than 1, switch it for 1. It also doesn't update until the input has lost focus - not locking the ui up. So not exactly the way I wanted it to work - but the result is the same.
** edit **
I've adapted Richard Matsens answer (the accepted one) to use an input and timeout... this behaves a bit more like the Chrome and Firefox native implementation.
<input type="number" min="1" v-model.number="paint.qty" #input="handleUpdate($event, index)">
and in the handleUpdate method:
...handleUpdate(event, index) {
var updater;
clearTimeout(updater);
this.currentIndex = index;
var paints = this.paints;
var max = this.max;
updater = setTimeout(function() {
if(event.target.value < event.target.min) {
paints[index].qty = parseInt(event.target.min);
}
if(event.target.value > max){
console.log(max);
paints[index].qty = parseInt(max);
}
}, 1000);
}...
clearing the timeout to prevent the updater bit firing too many times -
bouncing / mashing etc...

From this Validate input type number with range min/max
Most browsers “ignore” (it’s their default behavior) min and max, so that the user can freely edit the input field and type a number that’s not in the range 1-5.
From this How to detect changes in nested data, can use an #input on the control and a method() to handle the check.
Works for min="0", but say min="1" may be problematic if the user wants to type in "11".
Changed to blur event to handle above caveat.
methods: {
handleUpdate(event, index) {
if(event.target.value < event.target.min) {
this.paints[index].qty = event.target.min;
}
}
},
also add #blur() to the input
<div >
<input v-for="(paint, index) in paints"
#blur="handleUpdate($event, index)"
type="number" min="2" v-model.number="paint.qty">
</div>
For completeness, you may also want to add a validation message so that the user knows why the input value is being changed.

Related

vuejs currency input mask keyboard

I am using the following code for price entry with vue js, but it does not give the output I want.
and I want to cancel from keyboard keys (POINT). only the comma will work. By the way, I'm new among you, I would be happy if you can help me accordingly.
Sample:
0,00
1.000,00
Code
Vue.component('my-currency-input', {
template: `
<div>
<input type="text" v-model="formattedCurrencyValue" #blur="focusOut"/>
</div>`,
data: function() {
return {
currencyValue: 0,
formattedCurrencyValue: "0.00"
}
},
methods: {
focusOut: function() {
// Recalculate the currencyValue after ignoring "$" and "," in user input
this.currencyValue = parseFloat(this.formattedCurrencyValue.replace(/[^\d\.]/g, ""))
// Ensure that it is not NaN. If so, initialize it to zero.
// This happens if user provides a blank input or non-numeric input like "abc"
if (isNaN(this.currencyValue)) {
this.currencyValue = 0
}
// Format display value based on calculated currencyValue
this.formattedCurrencyValue = this.currencyValue.toFixed(2).replace(/(\d)(?=(\d{3})+(?:\.\d+)?$)/g, "$1,")
},
}
});
new Vue({
el: '#app'
});
You can cancel the key with the keydown event. Check the can and the prevent the event from happening. I created an example here: https://jsfiddle.net/4ejrfhyp/7/.

How can I implement v-model.number on my own in VueJS?

I have a text field component for numeric inputs. Basically I'm just wrapping v-text-field but in preparation for implementing it myself. It looks like this.
<template>
<v-text-field v-model.number = "content" />
</template>
<script>
export default {
name: 'NumericTextField',
props: [ 'value' ],
computed: {
content: {
get () { return this.value },
set (v) { this.$emit('input', f) },
},
}
}
</script>
This has generated user feedback that it's annoying when the text field has the string "10.2" in it and then backspace over the '2', then decimal place is automatically delete. I would like to change this behavior so that "10." remains in the text field. I'd also like to understand this from first principles since I'm relatively new to Vue.
So I tried this as a first past, and it's the most instructive of the things I've tried.
<template>
<v-text-field v-model="content" />
</template>
<script>
export default {
name: 'NumericTextField',
props: [ 'value' ],
computed: {
content: {
get () { return this.value },
set (v) {
console.log(v)
try {
const f = parseFloat(v)
console.log(f)
this.$emit('input', f)
} catch (err) {
console.log(err)
}
},
},
}
}
</script>
I read that v-model.number is based on parseFloat so I figured something like this must be happening. So it does fix the issue where the decimal place is automatically deleted. But... it doesn't even auto delete extra letters. So if I were to type "10.2A" the 'A' remains even though I see a console log with "10.2" printed out. Furthermore, there's an even worse misfeature. When I move to the start of the string and change it to "B10.2" it's immediately replaced with "NaN".
So I'd love to know a bunch of things. Why is the body of the text body immediately reactive when I change to a NaN but not immediately reactive when I type "10.2A"? Relatedly, how did I inadvertently get rid of the auto delete decimal place? I haven't even gotten to that part yet. So I'm misunderstanding data flow in Vue.
Lastly, how can I most simply provide a text box that's going to evaluate to a number for putting into my data model but not have the annoying auto delete of decimal places? The existing functionality doesn't auto delete trailing letters so I'm guessing the auto delete of decimal places was a deliberate feature that my users don't like.
I'm not 100% sure of any of this, but consider how v-model works on components. It basically is doing this:
<v-text-field
v-bind:value="content"
v-on:input="content = $event.target.value"
/>
And consider how the .number modifier works. It runs the input through parseFloat, but if parseFloat doesn't work, it leaves it as is.
So with that understanding, I would expect the following:
When you type in "10.2" and then hit backspace, "10." would be emitted via the input event, parseFloat("10.") would transform it to 10, v-on:input="content = $event.target.value" would assign it to content, and v-bind:value="content" would cause the input to display "10". So then, this is the expected behavior.
When you type in "10.2" and then hit "A", "10.2A" would be emitted via the input event, parseFloat("10.2A") would transform it to 10.2, v-on:input="content = $event.target.value" would assign it to content, and v-bind:value="content" would cause the input to display "10.2". It looks like it's failing at that very last step of causing the input to display "10.2", because the state of content is correctly being set to 10.2. If you use <input type="text" v-model.number="content" /> instead of <v-text-field v-model.number="content" />, once you blur, the text field successfully gets updated to "10.2". So it seems that the reason why <v-text-field> doesn't is due to how Vuetify is handling the v-bind:value="content" part.
When you type in "10.2" and then enter "B", in the beginning, "B10.2" would be emitted via the input event, parseFloat("B10.2") would return NaN, and thus the .number modifier would leave it as is, v-on:input="content = $event.target.value" would assign "B10.2" to content, and v-bind:value="content" would cause the input to display "B10.2". I agree that it doesn't seem right for parseFloat("10.2A") to return 10.2 but parseFloat("B10.2") to return "B10.2".
Lastly, how can I most simply provide a text box that's going to evaluate to a number for putting into my data model but not have the annoying auto delete of decimal places?
Given that the default behavior is weird, I think you're going to have to write your own custom logic for transforming the user's input. Eg. so that "10.2A" and "B10.2" both get transformed to 10.2 (or are left as is), and so that decimals are handled like you want. Something like this (CodePen):
<template>
<div id="app">
<input
v-bind:value="content"
v-on:input="handleInputEvent($event)"
/>
<p>{{ content }}</p>
</div>
</template>
<script>
export default {
data() {
return {
content: 0,
};
},
methods: {
handleInputEvent(e) {
this.content = this.transform(e.target.value);
setTimeout(() => this.$forceUpdate(), 500);
},
transform(val) {
val = this.trimLeadingChars(val);
val = this.trimTrailingChars(val);
// continue your custom logic here
return val;
},
trimLeadingChars(val) {
if (!val) {
return "";
}
for (let i = 0; i < val.length; i++) {
if (!isNaN(val[i])) {
return val.slice(i);
}
}
return val;
},
trimTrailingChars(val) {
if (!val) {
return "";
}
for (let i = val.length - 1; i >= 0; i--) {
if (!isNaN(Number(val[i]))) {
return val.slice(0,i+1);
}
}
return val;
},
},
};
</script>
The $forceUpdate seems to be necessary if you want the input field to actually change. However, it only seems to work on <input>, not <v-text-field>. Which is consistent with what we saw in the second bullet point. You can customize your <input> to make it appear and behave like <v-text-field> though.
I put it inside of a setTimeout so the user sees "I tried to type this but it got deleted" rather than "I'm typing characters but they're not appearing" because the former does a better job of indicating "What you tried to type is invalid".
Alternatively, you may want to do the transform on the blur event rather than as they type.

Vue + Vuex: input value not set by store if no store change

<template>
<input
#input="formatValue"
type="text"
:value="formattedValue"
/>
</template>
<script type="text/javascript">
import {formatPhoneNumber} from '~/utils/string';
export default {
computed: {
formattedValue: function(){
return formatPhoneNumber(this.value)
},
},
methods: {
formatValue(e) {
this.$emit('input', formatPhoneNumber(e.target.value))
}
},
props: ['value']
}
</script>
As long as the formatPhoneNumber(value) produces a different value, every thing works fine, but once the max length is reached (Since formatPhoneNumber('xx xx xx xx xx whatever') == 'xx xx xx xx xx'), the emitted value is the same as the current store one.
It is totally fine, except that as a consequence, state is not mutated and component is not re-rendered, hence formattedValue() is not called.
So I end up with xx xx xx xx xx in the store, but the input displays xx xx xx xx xx whatever as local input value varies from the store one.
How can I avoid this unexpected behavior? Moving formatPhoneNumber() to the store would not solve my issue since it would still prevent mutation, and only using formatPhoneNumber() in formattedValue() would make me end up with an un-formatted value in the store which is not what I want either.
How come Vue's input with dynamic value set still manages a local state?
To achieve what you want (I think), you could change your formatValue method to
formatValue(e) {
this.$emit('input', e.target.value = formatPhoneNumber(e.target.value));
}
So that it sets the input to the formatted phone number value. One way or another you're going to be overriding what the input produces so you might as well do it on the input event.
I would use a v-model instead of a v-value since that would give me full control over what I want to display in the input field.
In this way, you can format the input value, and then set it back in the model. It would look something like this:
<template>
<input #input="formatValue" type="text" v-model="inputModel">
</template>
<script type="text/javascript">
export default {
data() {
return {
inputModel: this.value
};
},
methods: {
formatValue() {
this.inputModel = formatPhoneNumber(this.inputModel);
this.$emit("input", this.inputModel);
}
},
props: ["value"]
};
</script>
Here's a working example I created to test this.
I think the easiest approach is a simple one-line modification to the parent's #input event, that clears the prop value before it updates it.
You still only need to emit the one value, but before working with the emitted value, clear the prop.
I've provided a snippet below (but note the additional differences in the snippet):
Instead of specifying the input field value, I opted to use v-model to bind it to a computed property that has a get and set method. This allowed me to use different logic when accessing vs modifying the data (quite handy in many situations).
By separating this logic, I was able to move the functionality from inside the input event to the set method, and eliminate the input event entirely.
new Vue({
el: "#app",
// props: ['valueProp'],
data: {
valueProp: "" //simulate prop data
},
computed: {
// --Value input element is binded to--
inputValue:{
get(){ //when getting the value, return the prop
return this.valueProp;
},
set(val){ //when the value is set, emit value
this.formatValue(val);
}
}
},
methods: {
// --Emit the value to the parent--
formatValue(val) {
this.parentFunction(this.formatPhoneNumber(val)); //simulate emitting the value
// this.$emit('input', formatPhoneNumber(val));
},
// --Simulate parent receiving emit event--
parentFunction(emittedValue){
console.log("emitted:" + emittedValue);
this.valueProp = null; //first clear it (updates the input field)
this.valueProp = emittedValue; //then assign it the emitted value
},
// --Simulate your format method--
// THIS LOGIC CAN BE IGNORED. It is just a quick implementation of a naive formatter.
// The "important" thing is it limits the length, to demonstrate exceeding the limit doesn't get reflected in the input field
formatPhoneNumber(val){
var phoneSpaces = [2,4,6,8]; //specify space formatting (space locations)
var maxLength = 10; //specify the max length
val = val.replace(/ /g,''); //remove existing formatting
if(val.length > maxLength) //limits the length to the max length
val = val.substring(0, maxLength);
// for the number of desired spaces, check each space location (working backwards) ... if value is longer than space location and space location is not a space ... add a space at the location.
for(var i = phoneSpaces.length-1; i >= 0; i--){
if(val.length > phoneSpaces[i] && val[phoneSpaces[i]] != " "){
val = val.substring(0, phoneSpaces[i]) + " " + val.substring(phoneSpaces[i], val.length);
}
}
return val
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input type="text" v-model="inputValue"/>
<label style="float: right;">
Prop Value: <span>{{valueProp}}</span>
</label>
<br>
<label >format (xx xx xx xx xx)</label>
</div>

Vue.js how to sort filtered results by date

We are launching an events application where users can search upcoming music festivals. We also want the filtered search results to display in chronological order by "startDate".
Our event cards:
<v-card flat class="pa-2 mb-3 eventcard" v-for="event in filteredEvents" :key="event.id"></v-card>
Then our computed property:
filteredEvents: function(){
return this.$store.getters.loadedEvents.filter((event) => {
return event.title.toLowerCase().match(this.search.toLowerCase())
})
}
And finally, an example of an event start date:
startDate:"2018-04-13"
Per the docs, I've tried adding "_.orderBy(this.event, 'startDate')" but I just get the error this.search.toLowerCase(...).orderBy is not a function
Any ideas? Thanks!
use .sort
export default {
filteredEvents: function() {
return this.$store.getters.loadedEvents
.filter(event => {
return event.title.toLowerCase().match(this.search.toLowerCase());
})
.sort(function(a, b) {
// Turn your strings into dates, and then subtract them
// to get a value that is either negative, positive, or zero.
return new Date(b.startDate) - new Date(a.startDate);
});
}
};
method from https://stackoverflow.com/a/10124053/5599288

Vue.JS value tied on input having the focus

Is there a way to change a value in the model when an input gets/loses focus?
The use case here is a search input that shows results as you type, these should only show when the focus is on the search box.
Here's what I have so far:
<input type="search" v-model="query">
<div class="results-as-you-type" v-if="magic_flag"> ... </div>
And then,
new Vue({
el: '#search_wrapper',
data: {
query: '',
magic_flag: false
}
});
The idea here is that magic_flag should turn to true when the search box has focus. I could do this manually (using jQuery, for example), but I want a pure Vue.JS solution.
Apparently, this is as simple as doing a bit of code on event handlers.
<input
type="search"
v-model="query"
#focus="magic_flag = true"
#blur="magic_flag = false"
/>
<div class="results-as-you-type" v-if="magic_flag"> ... </div>
Another way to handle something like this in a more complex scenario might be to allow the form to track which field is currently active, and then use a watcher.
I will show a quick sample:
<input
v-model="user.foo"
type="text"
name="foo"
#focus="currentlyActiveField = 'foo'"
>
<input
ref="bar"
v-model="user.bar"
type="text"
name="bar"
#focus="currentlyActiveField = 'bar'"
>
...
data() {
return {
currentlyActiveField: '',
user: {
foo: '',
bar: '',
},
};
},
watch: {
user: {
deep: true,
handler(user) {
if ((this.currentlyActiveField === 'foo') && (user.foo.length === 4)) {
// the field is focused and some condition is met
this.$refs.bar.focus();
}
},
},
},
In my sample here, if the currently-active field is foo and the value is 4 characters long, then the next field bar will automatically be focused. This type of logic is useful when dealing with forms that have things like credit card number, credit card expiry, and credit card security code inputs. The UX can be improved in this way.
I hope this could stimulate your creativity. Watchers are handy because they allow you to listen for changes to your data model and act according to your custom needs at the time the watcher is triggered.
In my example, you can see that each input is named, and the component knows which input is currently focused because it is tracking the currentlyActiveField.
The watcher I have shown is a bit more complex in that it is a "deep" watcher, which means it is capable of watching Objects and Arrays. Without deep: true, the watcher would only be triggered if user was reassigned, but we don't want that. We are watching the keys foo and bar on user.
Behind the scenes, deep: true is adding observers to all keys on this.user. Without deep enabled, Vue reasonably does not incur the cost of maintaining every key reactively.
A simple watcher would be like this:
watch: {
user() {
console.log('this.user changed');
},
},
Note: If you discover that where I have handler(user) {, you could have handler(oldValue, newValue) { but you notice that both show the same value, it's because both are a reference to the same user object. Read more here: https://github.com/vuejs/vue/issues/2164
Edit: to avoid deep watching, it's been a while, but I think you can actually watch a key like this:
watch: {
'user.foo'() {
console.log('user foo changed');
},
},
But if that doesn't work, you can also definitely make a computed prop and then watch that:
computed: {
userFoo() {
return this.user.foo;
},
},
watch: {
userFoo() {
console.log('user foo changed');
},
},
I added those extra two examples so we could quickly note that deep watching will consume more resources because it triggers more often. I personally avoid deep watching in favour of more precise watching, whenever reasonable.
However, in this example with the user object, if all keys correspond to inputs, then it is reasonable to deep watch. That is to say it might be.
You can use a flat by determinate a special CSS class, for example this a simple snippet:
var vm = new Vue({
el: '#app',
data: {
content: 'click to change content',
flat_input_active: false
},
methods: {
onFocus: function(event) {
event.target.select();
this.flat_input_active = true;
},
onBlur: function(event) {
this.flat_input_active = false;
}
},
computed: {
clazz: function() {
var clzz = 'control-form';
if (this.flat_input_active == false) {
clzz += ' only-text';
}
return clzz;
}
}
});
#app {
background: #EEE;
}
input.only-text { /* special css class */
border: none;
background: none;
}
<!-- libraries -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<!-- html template -->
<div id='app'>
<h1>
<input v-model='content' :class='clazz'
#focus="onFocus($event)"
#blur="onBlur"/>
</h1>
<div>
Good luck
You might also want to activate the search when the user mouses over the input - #mouseover=...
Another approach to this kind of functionality is that the filter input is always active, even when the mouse is in the result list. Typing any letters modifies the filter input without changing focus. Many implementations actually show the filter input box only after a letter or number is typed.
Look into #event.capture.