I'm attempting to implement a small quality of life change to a text field to allow users to change the dollar amount of a text field if the cents are full.
Currently, my fields will append a decimal amount based on what has been entered when the user moves onto another field (triggering an @blur
event).
1
will become1.00
1.
will become1.00
1.5
will become1.50
The way I have it coded is to check if the field matches a regex (/((\d*\.){1}(\d{2}))/
) to see if the field is "full". If so, I want to get the user's caret/cursor/insertion point/whatever term it is to check and see if it's before or after the decimal point.
- If it's before the decimal point, i.e.
100|.00
, I want them to be able to enter more numbers. - If it's after the decimal point, i.e.
100.|00
, I don't want them to be able to enter more numbers.
This sounds like a job for refs
and selectionStart/selectionEnd
, and you'd almost be right (and you probably still are). Even after Google removed that functionality for fields with type="number"
, there was a work around using those properties. I've seen many different posts suggesting that, some dating back almost 13 years. I just can't seem to get it to work right though.
Here's what I have:
Template tag
<v-container>
<v-row v-for="(exp, index) in travelExpenseArray" :key="exp.id" style="margin-top: -20px;" >
<v-col v-show="$vuetify.breakpoint.smAndUp" cols="auto" sm="2" />
<v-col cols="auto" sm="4" >
<v-checkbox v-model="exp.checked" :label="exp.title" dense @change="checkEnabled(index, exp.checked)"/>
</v-col>
<v-col cols="auto" sm="4">
<v-card-text style="padding: 0px;">{{exp.title}} Expenses</v-card-text>
<v-text-field :ref="`vtf${index}`" outlined step=".01" v-model="exp.expense" type="number" hide-details
dense single-line :disabled="!exp.checked" hide-spin-buttons @keypress="checkValidKey($event, index)"
@click="clearCurrencyField(index)" @blur="appendZeros(index)"/>
</v-col>
<v-col v-show="$vuetify.breakpoint.smAndUp" cols="auto" sm="2" />
</v-row>
<v-row style="margin-top: -20px;">
<v-col cols="auto" sm="6" />
<v-col cols="auto" sm="4">
<v-card-text style="padding: 0px">Total Travel Expenses</v-card-text>
<v-text-field outlined v-model="totalTravelExpenses" dense disabled />
</v-col>
<v-col v-show="$vuetify.breakpoint.smAndUp" cols="auto" sm="2" />
</v-row>
</v-container>
To break it down: I have a travel expense array that I loop over and dynamically create text fields for, v-model
-ing the values from the array to the ones from the text fields. I'm fortunately able to create dynamic ref=''
attributes, which will come in handy in the script section.
Script Tag
(specifically, the checkValidKey method, where this is happening)
checkValidKey(evt, index){
let expense = this.travelExpenseArray[index].expense;
// Get the key code from the event.
const charCode = evt.which ? evt.which : evt.keyCode;
// Ensure key is a valid key ('0-9.' and numpad '0-9.').
// Prevent if there's no match.
if(!this.getValidKeys().includes(charCode)){
evt.preventDefault();
// okay so this block checks the expense against the regex
// if it matches, (tentatively) prevent the key press
// because a 'full' cost has been entered.
}else if((/((\d*\.){1}(\d{2}))/).test(expense)){
// chrome removed this functionality for `type = "number"` fields in 2014 or so
this.$refs[`vtf${index}`][0].$refs.input.type = "text";
console.log(this.$refs[`vtf${index}`][0].$refs.input.selectionStart);
// not typing all that out over and over
let selection = {
start: this.$refs[`vtf${index}`][0].$refs.input.selectionStart,
end: this.$refs[`vtf${index}`][0].$refs.input.selectionEnd
}
// check if the cursor is in a valid position
if(selection.start >= selection.end - 2){
evt.preventDefault();
}
// should probably do this to be safe; this seems like
// a really hacky solution to such a negligible problem
this.$refs[`vtf${index}`][0].$refs.input.type = "number";
return;
}
},
The comments say the same thing but first I get the value of the key pressed. If it's not in a list of valid keys, ignore the press. If it is, and the value of the field matches the regex, I get the field by it's $ref
with a little bit of black magic that I feel shouldn't work, but consider myself fortunate that it does. I convert the type to text to allow me to use selectionStart
and selectionEnd
, and I then take what are supposed to be the start and end points and store them in an object to do a little bit of shorthand. I do a comparison, and if it passes, I prevent the key from registering. Then I convert the type of field back to number and return.
Also, here's how the expenses are structured in the vuex store, in case that's useful:
travelExpenseArray: [
{id: 0, title: 'Bus', checked: false, expense: '0.00'},
{id: 1, title: 'Car', checked: false, expense: '0.00'},
{id: 2, title: 'Plane', checked: false, expense: '0.00'},
{id: 3, title: 'Train', checked: false, expense: '0.00'},
],
The issue I'm running into is that for some reason, selectionStart
and selectionEnd
are both coming up as 0
. I understand that this is likely because no text is actually selected, but there's no getCaretPosition
method or caretPosition
property. I would really love for this workaround to... work, but I also don't want to keep wasting time hoping it does when it won't.
CodePudding user response:
So what I was doing was easily accomplished (and more) by just installing a library instead of reinventing the wheel.
I just installed v-money
with npm install v-money
.
App.vue
Added the following lines:
import money from 'v-money';
Vue.use(money);
TravelExpenseForm.vue
(the component that uses this method)
Modified the v-text-field
to include the v-money="money"
directive.
Added the following object to the data option:
money: {
decimal: '.',
thousands: ',',
prefix: '$',
suffix: '',
precision: 2
}