Vuetify: [Enhancement] v-text-field with decimal amount

Created on 11 Oct 2017  ยท  41Comments  ยท  Source: vuetifyjs/vuetify

Displaying currency is often in a format of 3.50 but as a number it is displayed as 3.5.

An option to choose the amount of decimals in a v-text-field that has type="number" would be nice here.

Property: decimals as number

If you choose 3, then it would always display contents of the text field as 3.500.

Perhaps it will display 3.5 while typing and 3.500 after blur. I imagine this would make the implementation alot simpler.

VTextField enhancement

Most helpful comment

If there is an interest, I'm willing to modify AutoNumeric as needed to make it work with v-text-field.
What would be the way to go about integrating it with that component?

All 41 comments

This would probably be an extension of the current masking system to allow arbitrary mask lengths,

I would like to see this implemented to support locales and different currency formats. Most (maybe all?) european countries use comma as decimal separator. A lot of european countries also use space as thousands separator, while others use dot.

It's not only about separators, it's also about grouping digits, some country use 3 digits in a group (1.234.567,00) and some use 2 (12.34.567,00)

Not sure how useful the following link is, but I post it anyways:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString

Seems to me that recent additions to javascript and new browsers aim to make the multi-locale-currency-formatting-hell a thing of the past.

@valpet Although it is supported at a certain extent, it's purpose is mainly for display. Using it in an input field is still a programming-hell thing because as-you-type editing is not internally supported. Given a myriad of international character sets, it's overwhelmingly challenging to detect what's considered separators, what's considered numerically acceptable, and how to control the cursor position when you do character inserts and deletions.

Here's an example:

This input is set to display locales='ar-EG' and options={ style: 'currency', currency: 'AED' }.
The moment I entered '1' it showed the surrounding prefix and suffix, then when I entered '4' a separator got inserted but the cursor was behind the '4'.
oct-16-2017 18-38-45

Until a feature like this is implemented by Vuetify natively, if you're using Vuetify ร  la carte, you could extend VTextField in a custom FormattedInput.js component with something like:

import {VTextField} from 'vuetify'

export default {
  name: 'formatted-input',
  extends: VTextField,
  props: {
    blurredFormat: {
      type: Function,
      default: (v) => {
        return v
      }
    }
  },
  methods: {
    showValue () {
      if (!this.isFocused) {
        this.lazyValue = this.blurredFormat(this.inputValue)
      } else {
        this.lazyValue = this.inputValue
      }
    }
  },
  watch: {
    isFocused () {
      this.showValue()
    }
  },
  mounted () {
    this.showValue()
  }
}

Then import and use your new component, add a :blurred-format="myFormatMethod" prop to it, and create a "myFormatMethod" that accepts the unformatted value in its first attribute and returns the formatted value.

There's probably a better way to do this, but it's what I'm currently doing until something better comes along.

If there is an interest, I'm willing to modify AutoNumeric as needed to make it work with v-text-field.
What would be the way to go about integrating it with that component?

@AlexandreBonneau that would be awesome as there currently isn't any working currency support for Vuetify. The v-money directive comes close, but it won't update when the model changes. There was a PR to introduce dynamic masks (and cover numeric / currency masking) but this has been closed. I think integration with AutoNumeric would be really useful

@plasmid87 well, if you only want a vue component that integrates AutoNumeric, you can directly use the official vue-autoNumeric component (and it support v-model updates as well as other external changes ;)).

However since my goal is to have an input that behaves like Vuetify's v-text-field, I've whipped up a quick and dirty hack by 'vuetifying' vue-autonumeric, by creating a ~monster~ component named v-autonumeric ๐Ÿ˜ฑ.

It works for what I need, but a real integration where the v-text-field could use an external library like AutoNumeric is really the only way to go there, because, let me repeat it again; it's pretty ugly now :)

If you want to play with it, have fun with that temporary component.
Did I say it's a hack? ๐Ÿ˜

@AlexandreBonneau hey this is a great hack, thank you for sharing it! ๐Ÿ‘๐Ÿ˜€ I agree with you that directives on v-text-field that are compatible with AutoNumeric would be ideal and solve this issue. The trouble with using another component instead is that we can't easily use things like the built-in validation hooks, styling, hints, etc. without maintaining this sibling component just for localized numerics and currency.

It might be possible to extend v-text-field, override genInput and inject vue-autonumeric... there might be a more elegant way of doing this, but I'll take a look

For an Excel like number input, I use the following pattern:

Template

<v-text-field
  v-model="amount"
  @blur="focus.amount = false"
  @focus="focus.amount = true"/>

Script

  data() {
    return {
      focus: {
        amount: false
      },
      amount_: '0'
    }
  },
  computed: {
    amount: {
      get() {
        return mask(this.amount_, 2, this.focus.amount)
      },
      set(value) {
        this.amount_ = unmask(value)
      }
    }
  }

Helper

export const isDecimal = function(value) {
  return /^-?(?:0|0\.\d*|[1-9]\d*\.?\d*)$/.test(value)
}

export const unmask = function(value, ds = ',') {
  return value.replace(ds, '.')
}

export const mask = function(value, dp = -1, editing = false, ds = ',', gs = '.') {
  if (editing || !isDecimal(value)) {
    return value.replace('.', ds)
  }

  const parts = value.split(/\./)

  let dec = parts[1] || ''

  if (dp >= 0) {
    dec = dec.length < dp ? dec.padEnd(dp, '0') : dec.substr(0, dp)
  }

  if (dec) {
    dec = ds + dec
  }

  return parts[0].replace(/(\d)(?=(?:\d{3})+$)/g, '$1' + gs) + dec
}

It is worth looking at https://github.com/text-mask/text-mask for integration with vuetify. Masking is better than what vueitfy currently has and has number formatter as well.

Apparently text-mask converted from directive to component on 2017/02/28:
https://github.com/text-mask/text-mask/pull/360
https://github.com/text-mask/text-mask/issues/511

I'm new to Vue/Vuetify: I take it this would complicate integrating text-mask in to Vuetify?

Well in the last 6 months I've come across my own issue 4 times... Too bad nothing has been done with it yet.
I get that there are a ton of possible options to add here, but I think the biggest problem for currency here is that there is no (easy) way to change the decimal and thousands separators.
For finances that is simply a must-have and complementing that with the prefix option that already exists, it is good enough in most scenarios.

Browsers already support Number.toLocaleString(), why not simply add support for that? Add a method or emit for the raw value and it's simple but effective.

Well, on my end I tried integrating AutoNumeric with Vuetify, but with the ever-changing refactoring and mainly the fact that I don't know much about the Vuetify code organization, I unfortunately did not advance much.

If a Vuetify dev is willing to team up (and do a coding session with shared screens for instance), I'm up for the task :)

Feel free to try out my implemention here:
https://github.com/paulpv/vuetify-number-field

I ran in to complications making it a standalone Node package, so for now you would just have to copy it's code directly in to the Vuetify's code's components folder.

I just wrote a simple component that does what I need. Feel free to use it.
In my case it is specifically for Dutch formatting but that can be easily changed.

Basically it displays 10000,10 and 10000,1 as 10.000,10 and reverts back to the former on focus. Should handle the comma, minus and NaN. Hopefully this helps someone :)

See gist here

Beautiful @Christilut!

Just updated it for some minor fixes. And if you notice a bug, feel free to let me know (or fix :nerd_face: )

Gist it!

@Christilut I trying to adjust this to pt-BR (BRL), but don't have success.
It's nice if anyone implements this directly to Vuetify ๐Ÿ˜„

I'll try to make it a bit more generic. But I can't promise it will work
with everything, too many possible options out there

On Thu, Jun 7, 2018, 03:26 Everton Nogueira notifications@github.com
wrote:

I trying to adjust this to pt-BR (BRL), but don't have success.
It's nice if anyone implements this directly to Vuetify ๐Ÿ˜„

โ€”
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/vuetifyjs/vuetify/issues/2138#issuecomment-395263467,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ADT4A2XdU59LAepHDcg0_V37s2hyiJB3ks5t6IFfgaJpZM4P1V1O
.

You can find the gist here

If you find any bugs, report them there and I'll try to fix them and update the gist. I've tested โ‚ฌ and $ which seem to work. If it feels robust enough I'll turn it into a package :)

@ehvetsi try the gist version. If it doesn't work, let me know what values you're using and what you expect to see and I'll check it out.

Thanks, it's working to me! ๐Ÿ‘

Any updates? I also need a currency mask, and would love to have it without workarounds.

I haven't had time to follow the latest Vuetify improvements, so I'm not sure if implementing AutoNumeric into a VTextField is easier now, but I'm still willing to integrate it with Vuetify, given a dev guidance!

I haven't had time to follow the latest Vuetify improvements, so I'm not sure if implementing AutoNumeric into a VTextField is easier now, but I'm still willing to integrate it with Vuetify, given a dev guidance!

Hi, I've created simple integration for autonumeric lib, may it helps :)

Edit Vuetify and AutoNumeric

Can't you just change the step attribute?

<v-text-field
    v-model="height"
    min="0"
    step=".1"
    type="number"
></v-text-field>

@rvboris this is great!

What is this this.$refs.field.$refs.input trickery? :)

@rvboris this is great!

What is this this.$refs.field.$refs.input trickery? :)

No tricks, just access to child ref of component

https://vuejs.org/v2/guide/components-edge-cases.html#Accessing-Child-Component-Instances-amp-Child-Elements

consider using https://sarahdayan.github.io/dinero.js in a similar way moment.js is used to format time?

@waspinator it looks like dinero.js is a library to format a given amount/currency/locale, but does not manage any live user input like a v-text-field component do (and like AutoNumeric does).
I'm not sure this would add the needed user interaction management to Vuetify.

masks have been removed in 2.0

masks have been removed in 2.0

I'm terrified with this. I had many issues doing the decimal mask, the mask that came with vuetify was just perfect for the other cases. I think this is a great lost for the project. Vuetify is awesome, I love you guys but we need to talk when the subject is removal of features, maybe deprecate and remove in a next version?

BEWARE: DOES NOT WORK

Unfortunately, below solution is not working as expected. After you reset value by clicking Reset Button, if you hover text field, you see the field is updated to old value. Sorry.

Any solution would be appreciated.


~I was searching a solution, tried a simple workaround with autonumeric by installing autonumeric and adding it directly to DOM element. I wanted to share here. It seems OK currently, but I didn't test it throughly. Please post if you disregard:~

~I added a rule, counter and model access to test quickly as below:~

$ npm install autonumeric

<template>
  <div>
    <v-text-field ref="priceField" v-model="price" label="Price" counter="10" :rules="rules"></v-text-field>
    <button @click="price = 0">Reset Button</button>
  </div>
</template>
<script>
  import AutoNumeric from "autonumeric";

  export default {
    data: function() {
      return {
        price: 0,
        rules: [v => v.length <= 7 || "Max 7 characters"];
     };
    },
    mounted: function() {
      const vuetifyField = this.$refs.priceField; // Please note `ref="priceField"` in `v-text-field`.
      const inputField = vuetifyField.$refs.input; // Raw DOM Element.
      const e = new AutoNumeric(inputField); // Add autonumeric to DOM field.
    }
  };
</script>

I just made a very hacky but working VNumericField based on AutoNumeric and VTextField. v-model is working even if set multiple times later after mounting. The v-model is the raw value of AutoNumeric (getNumber())

// VNumericField.vue extending VTextField
<script>
import { VTextField } from "vuetify/lib";
import AutoNumeric from "autonumeric";

export default {
  extends: VTextField,
  props: {
    anOptions: { // autonumeric options (see doc)
      type: Object,
      required: false,
      default() {
        return {};
      }
    }
  },
  data() {
    return {
      anElement: null, // autonumeric instance
      changedByInput: false // Flag to know if the value is changed by user input 
    };
  },
  mounted() {
    // Create the AutoNumeric instance on the VTextField input element
    this.anElement = new AutoNumeric(this.$refs.input, this.anOptions);
    // Set the AutoNumeric  default value
    this.anElement.set(this.value);
  },
  methods: {
    onInput() {
      // User has changed the input
      this.changedByInput = true; // set the flag to true
      this.updateVModel(); // emit v-model
    },
    updateVModel() { // emit raw value
      if (this.anElement !== null) {
        this.$emit("input", this.anElement.getNumber());
      }
    },
    genInput() {
      const listeners = Object.assign({}, this.listeners$);
      delete listeners["change"];
      let element = this.$createElement("input", {
        style: {},
        attrs: {
          ...this.attrs$,
          autofocus: this.autofocus,
          disabled: this.disabled,
          id: this.computedId,
          placeholder: this.placeholder,
          readonly: this.readonly,
          type: this.type
        },
        on: {
          blur: this.onBlur,
          input: this.onInput,
          focus: this.onFocus,
          keydown: this.onKeyDown,
          "autoNumeric:formatted": () => {
            this.changedByInput = false; // Remove the flag when autonumeric finish formatting
          }
        },
        ref: "input"
      });
      return element;
    }
  },
  watch: {
    value(newVal) {
      // Check if the last v-model update is fired by the input
      if (!this.changedByInput) {
        this.anElement.set(this.value); // Set the AutoNumeric raw value
      }
    }
  }
};
</script>

BEWARE: DOES NOT WORK

Unfortunately, below solution is not working as expected. After you reset value by clicking Reset Button, if you hover text field, you see the field is updated to old value. Sorry.

Any solution would be appreciated.

~I was searching a solution, tried a simple workaround with autonumeric by installing autonumeric and adding it directly to DOM element. I wanted to share here. It seems OK currently, but I didn't test it throughly. Please post if you disregard:~

~I added a rule, counter and model access to test quickly as below:~

$ npm install autonumeric

<template>
  <div>
    <v-text-field ref="priceField" v-model="price" label="Price" counter="10" :rules="rules"></v-text-field>
    <button @click="price = 0">Reset Button</button>
  </div>
</template>
<script>
  import AutoNumeric from "autonumeric";

  export default {
    data: function() {
      return {
        price: 0,
        rules: [v => v.length <= 7 || "Max 7 characters"];
     };
    },
    mounted: function() {
      const vuetifyField = this.$refs.priceField; // Please note `ref="priceField"` in `v-text-field`.
      const inputField = vuetifyField.$refs.input; // Raw DOM Element.
      const e = new AutoNumeric(inputField); // Add autonumeric to DOM field.
    }
  };
</script>

there is a problem when the object is inside a dialog or v-if="false", how do you solve that??

I just made a very hacky but working VNumericField based on AutoNumeric and VTextField. v-model is working even if set multiple times later after mounting. The v-model is the raw value of AutoNumeric (getNumber())

// VNumericField.vue extending VTextField
<script>
import { VTextField } from "vuetify/lib";
import AutoNumeric from "autonumeric";

export default {
  extends: VTextField,
  props: {
    anOptions: { // autonumeric options (see doc)
      type: Object,
      required: false,
      default() {
        return {};
      }
    }
  },
  data() {
    return {
      anElement: null, // autonumeric instance
      changedByInput: false // Flag to know if the value is changed by user input 
    };
  },
  mounted() {
    // Create the AutoNumeric instance on the VTextField input element
    this.anElement = new AutoNumeric(this.$refs.input, this.anOptions);
    // Set the AutoNumeric  default value
    this.anElement.set(this.value);
  },
  methods: {
    onInput() {
      // User has changed the input
      this.changedByInput = true; // set the flag to true
      this.updateVModel(); // emit v-model
    },
    updateVModel() { // emit raw value
      if (this.anElement !== null) {
        this.$emit("input", this.anElement.getNumber());
      }
    },
    genInput() {
      const listeners = Object.assign({}, this.listeners$);
      delete listeners["change"];
      let element = this.$createElement("input", {
        style: {},
        attrs: {
          ...this.attrs$,
          autofocus: this.autofocus,
          disabled: this.disabled,
          id: this.computedId,
          placeholder: this.placeholder,
          readonly: this.readonly,
          type: this.type
        },
        on: {
          blur: this.onBlur,
          input: this.onInput,
          focus: this.onFocus,
          keydown: this.onKeyDown,
          "autoNumeric:formatted": () => {
            this.changedByInput = false; // Remove the flag when autonumeric finish formatting
          }
        },
        ref: "input"
      });
      return element;
    }
  },
  watch: {
    value(newVal) {
      // Check if the last v-model update is fired by the input
      if (!this.changedByInput) {
        this.anElement.set(this.value); // Set the AutoNumeric raw value
      }
    }
  }
};
</script>

try to set v-model to zero, and it doesnt work

finally this solution works for me https://phiny1.github.io/v-currency-field/config.html#component-props

Modified @Webifi's code above. Working example here: https://codesandbox.io/s/vuetify-money-input-ixd66

// money-text-field.js
import { VTextField } from "vuetify/lib";

var formatter = new Intl.NumberFormat("en-IE", {
  style: "currency",
  currency: "EUR"
});

export default {
  name: "money-text-field",
  extends: VTextField,
  data: () => ({
    inputValue: null
  }),
  props: {
    blurredFormat: {
      type: Function,
      default: (v) => {
        // Format and remove the currency symbol - use prefix="โ‚ฌ" attribute for cleaner look
        if (v) return formatter.format(parseFloat(v)).slice(1);
      }
    }
  },
  methods: {
    showValue() {
      if (!this.isFocused) {
        // Store the value before change
        this.inputValue = this.lazyValue;
        this.lazyValue = this.blurredFormat(this.lazyValue);
      } else {
        // Show unformatted value on focus
        this.lazyValue = this.inputValue;
      }
    }
  },
  watch: {
    isFocused() {
      this.showValue();
    },
    value() {
        // Handle v-model updates
        if (!this.isFocused) {
            this.showValue();
        }
    }
  },
  mounted() {
    this.showValue();
  }
};
Was this page helpful?
0 / 5 - 0 ratings

Related issues

dohomi picture dohomi  ยท  3Comments

smousa picture smousa  ยท  3Comments

jofftiquez picture jofftiquez  ยท  3Comments

gluons picture gluons  ยท  3Comments

sebastianmacias picture sebastianmacias  ยท  3Comments