33

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

flyfrog
  • 752
  • 1
  • 6
  • 7

4 Answers4

49

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

tony19
  • 125,647
  • 18
  • 229
  • 307
Mani
  • 23,635
  • 6
  • 67
  • 54
  • Much better! The whole idea of masking really bothers me but this works well. – Bill Criswell Dec 13 '16 at 12:44
  • 1
    @BillCriswell Thanks! Even I liked this better than my other example, as it is a fully self-contained component, and the parent template is clean. – Mani Dec 13 '16 at 12:49
  • 4
    I loved your approach so I expanded on it a bit: https://jsfiddle.net/crswll/xxuda425/5/ – Bill Criswell Dec 13 '16 at 13:30
  • 2
    Thanks @BillCriswell, your example looks very clean! I didn't know about toLocaleString, which handles the cases where dot is used for digit separator and comma for decimal. It is much better than using a copy-pasted regular expression, which I am yet to decode how it works. Also, specifying mask as a separate input is brilliant! This will allow other types of input formatting, like date formatter, non-dollar currency type, etc. Thanks for spending time on this, I will use your jsFiddle to build my general purpose reusable input component :) – Mani Dec 13 '16 at 15:37
  • Thanks. I did the same vue component implementation last night, and posted. but the post is disappeared. – flyfrog Dec 13 '16 at 22:16
  • 1
    This is excellent, thanks, I've adopted it for my souped-up input component. I had to add a slightly messy kludge to get it to work as I wanted for currency fields, namely to truncate or round the actual value to 2 decimal places (rather than just display it thus). I added a @change handler which calls toFixed(2) on the value. I'm wondering whether this could be handled more neatly in the unmask method, though? I couldn't find a way, I have to say. – John Moore Dec 16 '16 at 12:00
  • One issue with doing `value.toFixed(2)` is that it becomes a string. You will have to remember to convert it back to a number afterwards, otherwise you will end up with results like `"2" + "2" = "22"`. I don't have any clean solution to control the precision, I can see that it will get messy. But it should be fine as long as the mess stays within the component, which can be taken as a black-box and reused everywhere. – Mani Dec 16 '16 at 13:00
  • 1
    Good point. I'm now using parseFloat after toFixed, which resolves the issue. – John Moore Dec 16 '16 at 13:26
  • Why not use international format https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat? – ericgu Feb 08 '18 at 15:35
  • Nice usefull and small. Thanks! – Orden Nov 19 '19 at 17:25
  • Good but will need to think of a way to add type="number" in... as this has to be removed for the currency to be displayed inside input – ejntaylor Apr 02 '20 at 11:20
  • @Mani Thanks so much for your solution! I am not good with regex and I need this to work for a number in cents and then emitted in cents. Meaning I could type $7.99 and it would emit 799. Can anyone help me with a solution for that? – Linx Jun 26 '21 at 00:02
8

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"/>
  1. 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.

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

Community
  • 1
  • 1
Mani
  • 23,635
  • 6
  • 67
  • 54
  • This is the only sane, not annoying approach I've seen to input masks. – Bill Criswell Dec 13 '16 at 04:16
  • Thanks, I want to have a solution that is closer to Vue architecture. I have tried to use computed property (ugly solution, I have to add a computed property for each currency input). Now when user types in "1111" (string), model value is 1111 (number). Then user enters ".22" after "1111", setter removes decimal point. But input textbox is 1111.22 since model value is still 1111 and getter won't be invoked. – flyfrog Dec 13 '16 at 05:56
  • I have added another answer to this question. As it involved a complete rewrite of the jsFiddle example, I decided to add a separate answer instead of editing this one. Please see if it meets your requirements. I use a computed property just like you specified. The decimals are handled internally, while the display shows only whatever precision you specify in `toFixed()` while formatting currency. – Mani Dec 13 '16 at 12:29
1

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

);

flyfrog
  • 752
  • 1
  • 6
  • 7
1

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