Vue + Vuex: input value not set by store if no store change - vue.js

<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>

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 + Vue-Paginate: Array will not refresh once empty

I am using vue-paginate in my app and I've noticed that once my array is empty, refreshing its value to an array with contents does not display.
<paginate
name="recipes"
:list="recipes"
:per="16"
class="p-0"
>
<transition-group name="zoom">
<div v-for="recipe in paginated('recipes')" :key="recipe.id">
<recipe class=""
:recipe="recipe"
:ref="recipe.id"
></recipe>
</div>
</transition-group>
</paginate>
This is how things get displayed, and my recipe array changes depending on a search. If I type in "b" into my search, results for banana, and bbq would show. If I typed "ba" the result for bbq is removed, and once I backspace the search to "b" it would re-appear as expected.
If I type "bx" every result is removed and when I backspace the search to "b", no results re-appear.
Any idea why this might happen?
UPDATE
When I inspect the component in chrome I see:
currentPage:-1
pageItemsCount:"-15-0 of 222"
Even though the list prop is:
list:Array[222]
Paginate needs a key in order to know when to re-render after the collection it's looking at reaches a length of zero. If you add a key to the paginate element, things should function as expected.
<paginate
name="recipes"
:list="recipes"
:per="16"
class="p-0"
:key="recipes ? recipes.length : 0" // You need some key that will update when the filtered result updates
>
See "Filtering the paginated list" is not working on vue-paginate node for a slightly more in depth answer.
I found a hacky workaround that fixed it for my app. First, I added a ref to my <paginate></paginate> component ref="paginator". Then I created a computed property:
emptyArray () {
return store.state.recipes.length == 0
}
then I created a watcher that looks for a change from length == 0 to length != 0:
watch: {
emptyArray: function(newVal, oldVal) {
if ( newVal === false && oldVal === true ) {
setTimeout(() => {
if (this.$refs.paginator) {
this.$refs.paginator.goToPage(page)
}
}, 100)
}
}
}
The timeout was necessary otherwise the component thought there was no page 1.
Using :key in the element has certain bugs. It will not work properly if you have multiple search on the table. In that case input will lose focus by typing single character. Here is the better alternative:
computed:{
searchFilter() {
if(this.search){
//Your Search condition
}
}
},
watch:{
searchFilter(newVal,oldVal){
if ( newVal.length !==0 && oldVal.length ===0 ) {
setTimeout(() => {
if (this.$refs.paginator) {
this.$refs.paginator[0].goToPage(1)
}
}, 50)
}
}
},

Form number input does not work with keys

<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.

What's the proper way to implement formatting on v-model in Vue.js 2.0

For a simple example: textbox to input currency data.
The requirement is to display user input in "$1,234,567" format and remove decimal point.
I have tried vue directive. directive's update method is not called when UI is refreshed due to other controls. so value in textbox reverts to the one without any formatting.
I also tried v-on:change event handler. But I don't know how to call a global function in event handler. It is not a good practice to create a currency convert method in every Vue object.
So what is the standard way of formatting in Vue 2.0 now?
Regards
Please check this working jsFiddle example: https://jsfiddle.net/mani04/bgzhw68m/
In this example, the formatted currency input is a component in itself, that uses v-model just like any other form element in Vue.js. You can initialize this component as follows:
<my-currency-input v-model="price"></my-currency-input>
my-currency-input is a self-contained component that formats the currency value when the input box is inactive. When user puts cursor inside, the formatting is removed so that user can modify the value comfortably.
Here is how it works:
The my-currency-input component has a computed value - displayValue, which has get and set methods defined. In the get method, if input box is not active, it returns formatted currency value.
When user types into the input box, the set method of displayValue computed property emits the value using $emit, thus notifying parent component about this change.
Reference for using v-model on custom components: https://v2.vuejs.org/v2/guide/components.html#Form-Input-Components-using-Custom-Events
Here is a working example: https://jsfiddle.net/mani04/w6oo9b6j/
It works by modifying the input string (your currency value) during the focus-out and focus-in events, as follows:
<input type="text" v-model="formattedCurrencyValue" #blur="focusOut" #focus="focusIn"/>
When you put the cursor inside the input box, it takes this.currencyValue and converts it to plain format, so that user can modify it.
After the user types the value and clicks elsewhere (focus out), this.currencyValue is recalculated after ignoring non-numeric characters, and the display text is formatted as required.
The currency formatter (reg exp) is a copy-paste from here: How can I format numbers as money in JavaScript?
If you do not want the decimal point as you mentioned in question, you can do this.currencyValue.toFixed(0) in the focusOut method.
I implemented a component. According to Mani's answer, it should use $emit.
Vue.component('currency', {
template: '<input type="text"' +
' class="form-control"' +
' :placeholder="placeholder""' +
' :title="title"' +
' v-model="formatted" />',
props: ['placeholder', 'title', 'value'],
computed: {
formatted: {
get: function () {
var value = this.value;
var formatted = currencyFilter(value, "", 0);
return formatted;
},
set: function (newValue) {
var cleanValue = newValue.replace(",", "");
var intValue = parseInt(cleanValue, 10);
this.value = 0;
this.value = intValue;
}
}
}
}
);
Using Vue custom directives + .toLocaleString() is also a very good option.
Vue.directive("currency", {
bind(el, binding, vnode) {
el.value = binding.value && Number(binding.value).toLocaleString('en-US', {style: 'currency', currency: !binding.arg ? 'USD' : binding.arg });
el.onblur = function(e) {
e.target.value = Number(e.target.value).toLocaleString('en-US', {style: 'currency', currency: !binding.arg ? 'USD' : binding.arg});
};
el.onfocus = function(e) {
e.target.value =
e.target.value && Number(e.target.value.replace(/[^\d.]/g, ""));
};
el.oninput = function(e) {
vnode.context.$data[binding.expression] = e.target.value;
};
}
});
Here is the example link: https://codepen.io/Mahmoud-Zakaria/pen/YzPvNmO