├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── node.js.yml ├── .gitignore ├── LICENSE ├── README.md ├── dist └── vue-numeric.min.js ├── docs └── index.html ├── package-lock.json ├── package.json ├── src ├── index.js └── vue-numeric.vue ├── test ├── karma.config.js └── specs │ └── vue-numeric.spec.js └── webpack.config.js /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 3 | 4 | ## Proposing a change 5 | If you intend to change the public API, or make any non-trivial changes to the implementation, we recommend filing an issue. This lets us reach an agreement on your proposal before you put significant effort into it. 6 | 7 | ## Javascript style guides 8 | - Javascript must adhere to [Vue.js official recommended style](https://github.com/vuejs/eslint-plugin-vue). 9 | - Lint command: `npm run lint`. 10 | 11 | ## DocBlock 12 | Every function, methods, computed properties, props should have a docblock to explain what it does 13 | 14 | example: 15 | ```js 16 | /** 17 | * Check provided value againts maximum allowed. 18 | * @param {Number} value 19 | * @return {Boolean} 20 | */ 21 | checkMaxValue (value) { 22 | if (this.max) { 23 | if (value <= this.maxValue) return false 24 | return true 25 | } 26 | return false 27 | }, 28 | ``` 29 | 30 | ## Test 31 | - Make sure current tests is all green :white_check_mark: 32 | - If you've added code that should be tested, add tests! 33 | - Any new feature or changes should include tests for it. 34 | - Make sure code coverage % does not decrease. 35 | 36 | ## Readme 37 | Update the [README.md](https://github.com/kevinongko/vue-numeric/blob/master/README.md) with details of changes. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | > If you are reporting bugs please fill the form below otherwise feel free to delete the form. 2 | 3 | ## Expected Behavior 4 | 5 | 6 | ## Actual Behavior 7 | 8 | 9 | ## Steps to Reproduce the Problem 10 | 11 | 1. 12 | 1. 13 | 1. 14 | 15 | ## Specifications 16 | 17 | - Plugin Version: 18 | - Vue.js Version: 19 | - Browser: 20 | - OS: 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | > Make sure to read the [contribution guidelines](https://github.com/kevinongko/vue-numeric/blob/master/.github/CONTRIBUTING.md) before submitting PR. -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm install 30 | - run: npm run lint 31 | - run: npm run test 32 | - run: npm run report-coverage 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | yarn-error.log 3 | node_modules 4 | .DS_Store 5 | /test/coverage/ 6 | /.vscode 7 | /.idea 8 | *.swp 9 | *.swo 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Kevin Ongko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-numeric 2 | 3 | [![npm](https://img.shields.io/npm/v/vue-numeric.svg?style=flat-square)](https://www.npmjs.com/package/vue-numeric) 4 | [![npm](https://img.shields.io/npm/dt/vue-numeric.svg?style=flat-square)](https://www.npmjs.com/package/vue-numeric) 5 | [![npm](https://img.shields.io/npm/dm/vue-numeric.svg?style=flat-square)](https://www.npmjs.com/package/vue-numeric) 6 | [![Node.js CI](https://github.com/kevinongko/vue-numeric/actions/workflows/node.js.yml/badge.svg)](https://github.com/kevinongko/vue-numeric/actions/workflows/node.js.yml) 7 | [![Codecov](https://img.shields.io/codecov/c/github/kevinongko/vue-numeric.svg?style=flat-square)](https://codecov.io/gh/kevinongko/vue-numeric) 8 | [![npm](https://img.shields.io/npm/l/vue-numeric.svg?style=flat-square)](http://opensource.org/licenses/MIT) 9 | 10 | Input field component to display a formatted currency value based on [Vue](https://vuejs.org/). 11 | 12 | [Live Demo](https://kevinongko.github.io/vue-numeric/) 13 | 14 | **Works with Vue 2.*** 15 | 16 | ## Installation 17 | 18 | ### Install via CDN 19 | ```html 20 | 21 | 22 | 23 | 24 | 27 | ``` 28 | ### Install via NPM 29 | ```sh 30 | $ npm install vue-numeric --save 31 | ``` 32 | 33 | #### Register as Component 34 | ```js 35 | import Vue from 'vue' 36 | import VueNumeric from 'vue-numeric' 37 | 38 | export default { 39 | name: 'App', 40 | 41 | components: { 42 | VueNumeric 43 | } 44 | } 45 | ``` 46 | 47 | #### Register as Plugin 48 | ```js 49 | import Vue from 'vue' 50 | import VueNumeric from 'vue-numeric' 51 | 52 | Vue.use(VueNumeric) 53 | ``` 54 | 55 | ## Usage 56 | 57 | ![screen shot 2016-12-08 at 2 19 31 pm](https://cloud.githubusercontent.com/assets/15880638/21001265/f2322438-bd51-11e6-8985-f31a45702484.png) 58 | 59 | ### Quick example 60 | 61 | ```vue 62 | 65 | 66 | 81 | ``` 82 | 83 | ### Currency symbol 84 | 85 | Set the `currency` prop to add a currency symbol within the input. 86 | 87 | ```vue 88 | 89 | ``` 90 | 91 | ### Minimum & maximum constraint 92 | 93 | Limit the minimum and maximum value by using `min` and `max` props. 94 | 95 | - `min` defaults to `0`. 96 | - `min` and `max` accept `String` or `Number` values. 97 | 98 | ```vue 99 | 100 | ``` 101 | 102 | ### Disable/enable negative values 103 | 104 | `minus` defaults to `false` (no negative numbers). 105 | 106 | ```vue 107 | 108 | ``` 109 | 110 | ### Enable decimal precision 111 | 112 | By default the decimal value is disabled. To use decimals in the value, add the `precision` prop. 113 | - `precision` accept a `String` or `Number` numeric value. 114 | 115 | ```vue 116 | 117 | ``` 118 | 119 | ### Thousands separator 120 | - Default thousand separator's symbol is `,`. 121 | - Use the `separator` prop to change the thousands separator. 122 | - `separator` only accepts `space`, `,` or `.`. 123 | - When using the `.` or `space` as thousand separator, the decimal separator will be `,`. 124 | 125 | ```vue 126 | 127 | ``` 128 | 129 | ### Input placeholder when empty 130 | ```vue 131 | 132 | ``` 133 | 134 | ### Value when empty 135 | By default, when you clean the input the value is set to `0`. You can change this value to fit your needs. 136 | ```vue 137 | 138 | ``` 139 | 140 | ### Output Type 141 | By default the value emitted for the input event is of type `Number`. However you may choose to get a `String` instead 142 | by setting the property `output-type` to `String`. 143 | ```vue 144 | 145 | ``` 146 | 147 | ## Props 148 | |Props|Description|Required|Type|Default| 149 | |-----|-----------|--------|----|-------| 150 | |currency|Currency prefix|false|String|-| 151 | |currency-symbol-position|Position of the symbol (accepted values: `prefix` or `suffix`)|false|String|`prefix`| 152 | |max|Maximum value allowed|false|Number|9007199254740991| 153 | |min|Minimum value allowed|false|Number|-9007199254740991| 154 | |minus|Enable/disable negative values|false|Boolean|`false`| 155 | |placeholder|Input placeholder|false|String|-| 156 | |empty-value|Value when input is empty|false|Number|0| 157 | |output-type|Output Type for input event|false|String|`Number`| 158 | |precision|Number of decimals|false|Number|-| 159 | |separator|Thousand separator symbol (accepts `space`, `.` or `,`)|false|String|`,`| 160 | |decimal-separator|Custom decimal separator|false|String|-| 161 | |thousand-separator|Custom thousand separator|false|String|-| 162 | |read-only|Hide input field and show the value as text|false|Boolean|false| 163 | |read-only-class|Class for read-only element|false|String|''| 164 | |allow-clear|Use input type search|false|Boolean|false| 165 | 166 | ## License 167 | 168 | Vue-Numeric is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT) 169 | 170 | ## Support 171 | Hello, I'm Kevin the maintainer of this project in my free time (which is getting lessen these days), if this project does help you in any way please consider to support me. Thanks :smiley: 172 | - [One-time donation via Paypal](https://www.paypal.me/kevinongko) 173 | 174 | -------------------------------------------------------------------------------- /dist/vue-numeric.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("accounting-js")):"function"==typeof define&&define.amd?define("VueNumeric",["accounting-js"],t):"object"==typeof exports?exports.VueNumeric=t(require("accounting-js")):e.VueNumeric=t(e.accounting)}("undefined"!=typeof self?self:this,function(e){return function(e){function t(n){if(r[n])return r[n].exports;var a=r[n]={i:n,l:!1,exports:{}};return e[n].call(a.exports,a,a.exports,t),a.l=!0,a.exports}var r={};return t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:n})},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=1)}([function(e,t,r){"use strict";var n=r(4);r.n(n);t.a={name:"VueNumeric",props:{allowClear:{type:Boolean,default:!1,required:!1},currency:{type:String,default:"",required:!1},max:{type:Number,default:Number.MAX_SAFE_INTEGER||9007199254740991,required:!1},min:{type:Number,default:Number.MIN_SAFE_INTEGER||-9007199254740991,required:!1},minus:{type:Boolean,default:!1,required:!1},placeholder:{type:String,default:"",required:!1},emptyValue:{type:[Number,String],default:"",required:!1},precision:{type:Number,default:0,required:!1},separator:{type:String,default:",",required:!1},thousandSeparator:{default:void 0,required:!1,type:String},decimalSeparator:{default:void 0,required:!1,type:String},outputType:{required:!1,type:String,default:"Number"},value:{type:[Number,String],default:0,required:!0},readOnly:{type:Boolean,default:!1,required:!1},readOnlyClass:{type:String,default:"",required:!1},disabled:{type:Boolean,default:!1,required:!1},currencySymbolPosition:{type:String,default:"prefix",required:!1}},data:function(){return{amount:""}},computed:{amountNumber:function(){return this.unformat(this.amount)},valueNumber:function(){return this.unformat(this.value)},decimalSeparatorSymbol:function(){return void 0!==this.decimalSeparator?this.decimalSeparator:","===this.separator?".":","},thousandSeparatorSymbol:function(){return void 0!==this.thousandSeparator?this.thousandSeparator:"."===this.separator?".":"space"===this.separator?" ":","},symbolPosition:function(){return this.currency?"suffix"===this.currencySymbolPosition?"%v %s":"%s %v":"%v"}},watch:{valueNumber:function(e){this.$refs.numeric!==document.activeElement&&(this.amount=this.format(e))},readOnly:function(e,t){var r=this;!1===t&&!0===e&&this.$nextTick(function(){r.$refs.readOnly.className=r.readOnlyClass})},separator:function(){this.process(this.valueNumber),this.amount=this.format(this.valueNumber)},currency:function(){this.process(this.valueNumber),this.amount=this.format(this.valueNumber)},precision:function(){this.process(this.valueNumber),this.amount=this.format(this.valueNumber)}},mounted:function(){var e=this;(this.valueNumber||this.isDeliberatelyZero())&&(this.process(this.valueNumber),this.amount=this.format(this.valueNumber),setTimeout(function(){e.process(e.valueNumber),e.amount=e.format(e.valueNumber)},500)),this.readOnly&&(this.$refs.readOnly.className=this.readOnlyClass)},methods:{onChangeHandler:function(e){this.$emit("change",e)},onBlurHandler:function(e){this.$emit("blur",e),this.amount=this.format(this.valueNumber)},onFocusHandler:function(e){if(this.$emit("focus",e),"string"==typeof this.valueNumber&&""===this.valueNumber)return"";this.amount=Object(n.formatMoney)(this.valueNumber,{symbol:"",format:"%v",thousand:"",decimal:this.decimalSeparatorSymbol,precision:Number(this.precision)})},onInputHandler:function(){this.process(this.amountNumber)},process:function(e){"string"==typeof e&&""===e?this.$emit("input",e):(e>=this.max&&this.update(this.max),e<=this.min&&this.update(this.min),e>this.min&&e=0?this.update(this.min):this.update(0)))},update:function(e){var t=Object(n.toFixed)(e,this.precision),r="string"===this.outputType.toLowerCase()?t:Number(t);this.$emit("input",r)},format:function(e){return"string"==typeof e&&""===e?"":Object(n.formatMoney)(e,{symbol:this.currency,format:this.symbolPosition,precision:Number(this.precision),decimal:this.decimalSeparatorSymbol,thousand:this.thousandSeparatorSymbol})},unformat:function(e){var t="string"==typeof e&&""===e?this.emptyValue:e;return"string"==typeof t&&""===t?"":Object(n.unformat)(t,this.decimalSeparatorSymbol)},isDeliberatelyZero:function(){return 0===this.valueNumber&&""!==this.value}}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=r(2),a={install:function(e){e.component(n.a.name,n.a)}};n.a.install=a.install,t.default=n.a},function(e,t,r){"use strict";var n=r(0),a=r(5),i=r(3),o=i(n.a,a.a,!1,null,null,null);t.a=o.exports},function(e,t){e.exports=function(e,t,r,n,a,i){var o,u=e=e||{},s=typeof e.default;"object"!==s&&"function"!==s||(o=e,u=e.default);var l="function"==typeof u?u.options:u;t&&(l.render=t.render,l.staticRenderFns=t.staticRenderFns,l._compiled=!0),r&&(l.functional=!0),a&&(l._scopeId=a);var c;if(i?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),n&&n.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(i)},l._ssrRegister=c):n&&(c=n),c){var d=l.functional,m=d?l.render:l.beforeCreate;d?(l._injectStyles=c,l.render=function(e,t){return c.call(t),m(e,t)}):l.beforeCreate=m?[].concat(m,c):[c]}return{esModule:o,exports:u,options:l}}},function(t,r){t.exports=e},function(e,t,r){"use strict";var n=function(){var e=this,t=e.$createElement,r=e._self._c||t;return"checkbox"!=(e.allowClear?"search":"tel")||e.readOnly?"radio"!=(e.allowClear?"search":"tel")||e.readOnly?e.readOnly?r("span",{ref:"readOnly"},[e._v(e._s(e.amount))]):r("input",{directives:[{name:"model",rawName:"v-model",value:e.amount,expression:"amount"}],ref:"numeric",attrs:{placeholder:e.placeholder,disabled:e.disabled,type:e.allowClear?"search":"tel"},domProps:{value:e.amount},on:{blur:e.onBlurHandler,input:[function(t){t.target.composing||(e.amount=t.target.value)},e.onInputHandler],focus:e.onFocusHandler,change:e.onChangeHandler}}):r("input",{directives:[{name:"model",rawName:"v-model",value:e.amount,expression:"amount"}],ref:"numeric",attrs:{placeholder:e.placeholder,disabled:e.disabled,type:"radio"},domProps:{checked:e._q(e.amount,null)},on:{blur:e.onBlurHandler,input:e.onInputHandler,focus:e.onFocusHandler,change:[function(t){e.amount=null},e.onChangeHandler]}}):r("input",{directives:[{name:"model",rawName:"v-model",value:e.amount,expression:"amount"}],ref:"numeric",attrs:{placeholder:e.placeholder,disabled:e.disabled,type:"checkbox"},domProps:{checked:Array.isArray(e.amount)?e._i(e.amount,null)>-1:e.amount},on:{blur:e.onBlurHandler,input:e.onInputHandler,focus:e.onFocusHandler,change:[function(t){var r=e.amount,n=t.target,a=!!n.checked;if(Array.isArray(r)){var i=e._i(r,null);n.checked?i<0&&(e.amount=r.concat([null])):i>-1&&(e.amount=r.slice(0,i).concat(r.slice(i+1)))}else e.amount=a},e.onChangeHandler]}})},a=[],i={render:n,staticRenderFns:a};t.a=i}])}); -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | vue-numeric demo 7 | 8 | 13 | 14 | 15 |
16 |
17 |
18 |
19 |

20 | Vue Numeric 21 |

22 |

23 | Input field component to display currency value based on Vue.js 24 |

25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 |

33 | 34 | 47 | 48 |

49 |
50 |
51 | 52 |
53 |

54 | Config: 55 |

56 | 57 |
58 |
59 |

60 | 61 | 62 |

63 |
64 |
65 |

66 | 67 | 68 |

69 |
70 |
71 |

72 | 73 | 74 |

75 |
76 |
77 |

78 | 79 | 80 |

81 |
82 |
83 |

84 | 85 | 86 |

87 |
88 |
89 | 90 |

91 | 95 | 99 | 103 |

104 |
105 |
106 |

107 | 108 | Read Only 109 |

110 |
111 |
112 |

113 | 114 | Allow clear 115 |

116 |
117 |
118 |
119 | 120 |
121 | 122 | 146 |
147 | 148 | 149 | 150 | 151 | 152 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-numeric", 3 | "version": "2.5.1", 4 | "description": "Input field component to display currency value based on Vue.", 5 | "author": "Kevin Ongko", 6 | "main": "dist/vue-numeric.min.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/kevinongko/vue-numeric.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/kevinongko/vue-numeric/issues" 13 | }, 14 | "keywords": [ 15 | "component", 16 | "currency", 17 | "input", 18 | "text", 19 | "number", 20 | "numeric", 21 | "separator", 22 | "vue", 23 | "vue.js" 24 | ], 25 | "homepage": "https://github.com/kevinongko/vue-numeric#readme", 26 | "license": "MIT", 27 | "dependencies": { 28 | "accounting-js": "^1.1.1" 29 | }, 30 | "scripts": { 31 | "test": "cross-env BABEL_ENV=test ./node_modules/.bin/karma start test/karma.config.js", 32 | "test:watch": "cross-env BABEL_ENV=test ./node_modules/.bin/karma start test/karma.config.js --single-run=false", 33 | "lint": "./node_modules/.bin/eslint --ext .js,.vue src spec", 34 | "build": "./node_modules/.bin/webpack --hide-modules -p --progress", 35 | "report-coverage": "codecov" 36 | }, 37 | "devDependencies": { 38 | "avoriaz": "^4.0.0", 39 | "babel-core": "^6.26.0", 40 | "babel-loader": "^7.1.2", 41 | "babel-plugin-istanbul": "^4.1.4", 42 | "babel-preset-env": "^1.6.0", 43 | "chai": "^4.1.2", 44 | "clean-webpack-plugin": "^0.1.16", 45 | "codecov": "^3.8.1", 46 | "cross-env": "^5.0.5", 47 | "css-loader": "^0.28.7", 48 | "eslint": "^4.6.0", 49 | "eslint-plugin-vue": "^4.2.0", 50 | "karma": "^6.3.16", 51 | "karma-coverage": "^1.1.1", 52 | "karma-mocha": "^1.3.0", 53 | "karma-phantomjs-launcher": "^1.0.4", 54 | "karma-sinon-chai": "^1.3.2", 55 | "karma-sourcemap-loader": "^0.3.7", 56 | "karma-spec-reporter": "^0.0.31", 57 | "karma-webpack": "^2.0.4", 58 | "mocha": "^3.5.0", 59 | "phantomjs-prebuilt": "^2.1.15", 60 | "sinon": "^3.2.1", 61 | "sinon-chai": "^2.13.0", 62 | "vue": "^2.4.2", 63 | "vue-loader": "^13.0.4", 64 | "vue-template-compiler": "^2.4.2", 65 | "webpack": "^3.5.5" 66 | }, 67 | "babel": { 68 | "presets": [ 69 | [ 70 | "env", 71 | { 72 | "uglify": true, 73 | "modules": false, 74 | "targets": { 75 | "browsers": [ 76 | "> 1%", 77 | "last 2 versions", 78 | "not ie <= 8" 79 | ] 80 | } 81 | } 82 | ] 83 | ], 84 | "env": { 85 | "test": { 86 | "plugins": [ 87 | "istanbul" 88 | ] 89 | } 90 | } 91 | }, 92 | "eslintConfig": { 93 | "extends": [ 94 | "eslint:recommended", 95 | "plugin:vue/recommended" 96 | ] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import component from './vue-numeric.vue' 2 | 3 | const plugin = { 4 | install: Vue => { 5 | Vue.component(component.name, component) 6 | } 7 | } 8 | 9 | component.install = plugin.install 10 | 11 | export default component 12 | -------------------------------------------------------------------------------- /src/vue-numeric.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 406 | -------------------------------------------------------------------------------- /test/karma.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | 3 | var path = require('path') 4 | 5 | module.exports = config => { 6 | config.set({ 7 | browsers: ['PhantomJS'], 8 | frameworks: ['mocha', 'sinon-chai'], 9 | reporters: ['spec', 'coverage'], 10 | files: ['specs/*.spec.js'], 11 | preprocessors: { 12 | './specs/*.spec.js': ['webpack', 'sourcemap'] 13 | }, 14 | webpack: { 15 | devtool: '#inline-source-map', 16 | resolve: { 17 | extensions: ['.js', '.vue'], 18 | alias: { 19 | 'vue$': 'vue/dist/vue.esm.js', 20 | '@': path.resolve(__dirname, '../src') 21 | } 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.vue$/, 27 | loader: 'vue-loader', 28 | options: { 29 | esModule: false 30 | } 31 | }, 32 | { 33 | test: /\.js$/, 34 | loader: 'babel-loader', 35 | exclude: /node_modules/ 36 | } 37 | ] 38 | } 39 | }, 40 | webpackMiddleware: { 41 | noInfo: true 42 | }, 43 | coverageReporter: { 44 | dir: './coverage', 45 | reporters: [ 46 | { type: 'lcov', subdir: '.' }, 47 | { type: 'text-summary' } 48 | ] 49 | }, 50 | singleRun: true 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /test/specs/vue-numeric.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import Vue from 'vue' 3 | import { expect } from 'chai' 4 | import { mount } from 'avoriaz' 5 | import VueNumeric from '@/vue-numeric' 6 | import sinon from 'sinon' 7 | 8 | describe('vue-numeric.vue', () => { 9 | it('Use default decimal separator', () => { 10 | const wrapper = mount(VueNumeric, { propsData: { value: 2000 }}) 11 | expect(wrapper.data().amount).to.equal('2,000') 12 | }) 13 | 14 | it('format values with currency prefix', () => { 15 | const wrapper = mount(VueNumeric, { propsData: { value: 2000, currency: 'USD' }}) 16 | expect(wrapper.data().amount).to.equal('USD 2,000') 17 | }) 18 | 19 | it('format values with currency suffix', () => { 20 | const wrapper = mount(VueNumeric, { propsData: { value: 2000, currency: 'CZK', currencySymbolPosition: 'suffix' }}) 21 | expect(wrapper.data().amount).to.equal('2,000 CZK') 22 | }) 23 | 24 | it('format values with decimals', () => { 25 | const wrapper = mount(VueNumeric, { propsData: { value: 2000.34, precision: 2, currency: '$' }}) 26 | expect(wrapper.data().amount).to.equal('$ 2,000.34') 27 | }) 28 | 29 | it('format values with decimals even when no decimal specified', () => { 30 | const wrapper = mount(VueNumeric, { propsData: { value: 2000, precision: 2, currency: '$' }}) 31 | expect(wrapper.data().amount).to.equal('$ 2,000.00') 32 | }) 33 | 34 | it('format values with decimals rounded', () => { 35 | const wrapper = mount(VueNumeric, { propsData: { value: 2000.36, precision: 1, currency: '$' }}) 36 | expect(wrapper.data().amount).to.equal('$ 2,000.4') 37 | }) 38 | 39 | it('format values with . separator', () => { 40 | const wrapper = mount(VueNumeric, { propsData: { value: 2000000, currency: '$', separator: '.', precision: 2 }}) 41 | expect(wrapper.data().amount).to.equal('$ 2.000.000,00') 42 | }) 43 | 44 | it('format values with , separator', () => { 45 | const wrapper = mount(VueNumeric, { propsData: { value: 2000000, currency: '$', separator: ',', precision: 2 }}) 46 | expect(wrapper.data().amount).to.equal('$ 2,000,000.00') 47 | }) 48 | 49 | it('format values with space separator', () => { 50 | const wrapper = mount(VueNumeric, { propsData: { value: 2000000, currency: '$', separator: 'space', precision: 2 }}) 51 | expect(wrapper.data().amount).to.equal('$ 2 000 000,00') 52 | }) 53 | 54 | it('format values with correct decimals symbol when using different thousand separator', () => { 55 | const wrapper = mount(VueNumeric, { propsData: { value: 20000.36, precision: 1, currency: '$', separator: '.' }}) 56 | expect(wrapper.data().amount).to.equal('$ 20.000,4') 57 | }) 58 | 59 | it('outputs Number type by default', () => { 60 | const component = Vue.extend({ 61 | components: { VueNumeric }, 62 | data: () => ({ total: 100 }), 63 | template: '
', 64 | }) 65 | 66 | const wrapper = mount(component) 67 | expect(typeof wrapper.data().total).to.equal('number') 68 | }) 69 | 70 | it('outputs String if specified', () => { 71 | const component = Vue.extend({ 72 | components: { VueNumeric }, 73 | data: () => ({ total: 100 }), 74 | template: '
', 75 | }) 76 | 77 | const wrapper = mount(component) 78 | expect(typeof wrapper.data().total).to.equal('string') 79 | }) 80 | 81 | it('is tag in read-only mode', () => { 82 | const wrapper = mount(VueNumeric, { propsData: { value: 2000, currency: '$', readOnly: true, readOnlyClass: 'test-class' }}) 83 | wrapper.update() 84 | expect(wrapper.is('span')).to.equal(true) 85 | expect(wrapper.hasClass('test-class')).to.equal(true) 86 | expect(wrapper.text()).is.equal('$ 2,000') 87 | }) 88 | 89 | it('apply class when read-only mode enabled', done => { 90 | const propsData = { value: 3000, readOnly: false, readOnlyClass: 'testclass' } 91 | const wrapper = mount(VueNumeric, { propsData }) 92 | 93 | wrapper.setProps({ readOnly: true }) 94 | 95 | wrapper.instance().$nextTick(() => { 96 | expect(wrapper.instance().$el.className).to.equal('testclass') 97 | done() 98 | }) 99 | }) 100 | 101 | it('does not apply class when read-only mode disabled', done => { 102 | const propsData = { value: 3000, readOnly: true, readOnlyClass: 'testclass' } 103 | const wrapper = mount(VueNumeric, { propsData }) 104 | 105 | wrapper.setProps({ readOnly: false }) 106 | wrapper.instance().$nextTick(() => { 107 | expect(wrapper.instance().$el.className).to.equal('') 108 | done() 109 | }) 110 | }) 111 | 112 | it('is is not disabled when disabled mode is disabled', done => { 113 | const propsData = { value: 3000, disabled: true } 114 | const wrapper = mount(VueNumeric, { propsData }) 115 | 116 | wrapper.setProps({ disabled: false }) 117 | wrapper.instance().$nextTick(() => { 118 | expect(wrapper.instance().$el.hasAttribute('disabled')).to.equal(false) 119 | done() 120 | }) 121 | }) 122 | 123 | it('is disabled in disabled mode', done => { 124 | const propsData = { value: 3000, disabled: false } 125 | const wrapper = mount(VueNumeric, { propsData }) 126 | 127 | wrapper.setProps({ disabled: true }) 128 | wrapper.instance().$nextTick(() => { 129 | expect(wrapper.instance().$el.hasAttribute('disabled')).to.equal(true) 130 | done() 131 | }) 132 | }); 133 | 134 | it('cannot exceed max props', () => { 135 | const component = Vue.extend({ 136 | components: { VueNumeric }, 137 | data: () => ({ total: 150 }), 138 | template: '
', 139 | }) 140 | 141 | const wrapper = mount(component) 142 | expect(wrapper.data().total).to.equal(100) 143 | }) 144 | 145 | it('cannot below min props', () => { 146 | const component = Vue.extend({ 147 | components: { VueNumeric }, 148 | data: () => ({ total: 150 }), 149 | template: '
', 150 | }) 151 | 152 | const wrapper = mount(component) 153 | expect(wrapper.data().total).to.equal(200) 154 | }) 155 | 156 | it('process valid value ', () => { 157 | const component = Vue.extend({ 158 | components: { VueNumeric }, 159 | data: () => ({ total: 100 }), 160 | template: '
', 161 | }) 162 | 163 | const wrapper = mount(component) 164 | expect(wrapper.data().total).to.equal(100) 165 | }) 166 | 167 | it('allow minus value when minus props is true', () => { 168 | const component = Vue.extend({ 169 | components: { VueNumeric }, 170 | data: () => ({ total: -150 }), 171 | template: '
', 172 | }) 173 | 174 | const wrapper = mount(component) 175 | expect(wrapper.data().total).to.equal(-150) 176 | }) 177 | 178 | it('disallow minus value when minus props is false', () => { 179 | const component = Vue.extend({ 180 | components: { VueNumeric }, 181 | data: () => ({ total: -150 }), 182 | template: '
', 183 | }) 184 | 185 | const wrapper = mount(component) 186 | expect(wrapper.data().total).to.equal(0) 187 | }) 188 | 189 | it('updates delayed value with format if without focus', done => { 190 | const el = document.createElement('div') 191 | const vm = new Vue({ 192 | el, 193 | components: { VueNumeric }, 194 | data: () => ({ total: 0 }), 195 | template: '
', 196 | }).$mount() 197 | 198 | setTimeout(() => { 199 | vm.total = 3000 200 | }, 100); 201 | 202 | setTimeout(() => { 203 | expect(vm.$el.firstChild.value.trim()).to.equal('3,000') 204 | done() 205 | }, 500); 206 | }) 207 | 208 | it('remove space if currency props undefined', () => { 209 | const wrapper = mount(VueNumeric, {propsData: { value: 2000 }}) 210 | expect(wrapper.data().amount).to.equal('2,000') 211 | }) 212 | 213 | it('format value on blur', () => { 214 | const wrapper = mount(VueNumeric, {propsData: { value: 2000 }}) 215 | wrapper.trigger('blur') 216 | expect(wrapper.data().amount).to.equal('2,000') 217 | }) 218 | 219 | it('on focus empty string return empty string', () => { 220 | const wrapper = mount(VueNumeric, {propsData: { value: '', separator: '.', precision: 2 }}) 221 | wrapper.trigger('focus') 222 | expect(wrapper.data().amount).to.equal('') 223 | }) 224 | 225 | it('remove thousand separator and symbol on focus with , decimal', () => { 226 | const wrapper = mount(VueNumeric, {propsData: { value: 2000.21, separator: '.', precision: 2 }}) 227 | wrapper.trigger('focus') 228 | expect(wrapper.data().amount).to.equal('2000,21') 229 | }) 230 | 231 | it('remove thousand separator and symbol on focus with . decimal', () => { 232 | const wrapper = mount(VueNumeric, {propsData: { value: 2000.21, separator: ',', precision: 2 }}) 233 | wrapper.trigger('focus') 234 | expect(wrapper.data().amount).to.equal('2000.21') 235 | }) 236 | 237 | it('process value on input', () => { 238 | const process = sinon.stub() 239 | const wrapper = mount(VueNumeric, { propsData: { value: 2000 }, methods: { process }}) 240 | wrapper.trigger('input') 241 | expect(process.called).to.equal(true) 242 | }) 243 | 244 | it('does not show placeholder when value defined', () => { 245 | const wrapper = mount(VueNumeric, { propsData: { value: 2000, placeholder: 'number here' }}) 246 | expect(wrapper.data().amount).to.equal('2,000') 247 | }) 248 | 249 | it('does not sets the value to 0 when no empty value is provided and input is empty', () => { 250 | const wrapper = mount(VueNumeric, { propsData: { value: '' }}) 251 | expect(wrapper.data().amount).to.equal('') 252 | }) 253 | 254 | it('uses the provided empty value when input is empty', () => { 255 | const wrapper = mount(VueNumeric, { propsData: { value: '', emptyValue: 1 }}) 256 | expect(wrapper.data().amount).to.equal('1') 257 | }) 258 | 259 | it('apply min props value if user input negative value when minus props disabled', () => { 260 | const component = Vue.extend({ 261 | components: { VueNumeric }, 262 | data: () => ({ total: -200 }), 263 | template: '
', 264 | }) 265 | 266 | const wrapper = mount(component) 267 | expect(wrapper.data().total).to.equal(150) 268 | }) 269 | 270 | it('apply 0 value if user input negative value when minus props disabled and min props is negative too', () => { 271 | const component = Vue.extend({ 272 | components: { VueNumeric }, 273 | data: () => ({ total: -200 }), 274 | template: '
', 275 | }) 276 | 277 | const wrapper = mount(component) 278 | expect(wrapper.data().total).to.equal(0) 279 | }) 280 | 281 | it('apply new separator immediately if it is changed', () => { 282 | const wrapper = mount(VueNumeric, { propsData: { value: 2000, separator: ',' }}) 283 | wrapper.setProps({ separator: '.' }) 284 | expect(wrapper.data().amount).to.equal('2.000') 285 | }) 286 | 287 | it('apply new currency prop immediately if it is changed', () => { 288 | const wrapper = mount(VueNumeric, { propsData: { value: 0, currency: '$' }}) 289 | wrapper.setProps({ currency: 'USD' }) 290 | expect(wrapper.data().amount).to.equal('USD 0') 291 | }) 292 | 293 | it('apply new precision immediately if it is changed', () => { 294 | const wrapper = mount(VueNumeric, { propsData: { value: 2000.17, precision: 2 }}) 295 | wrapper.setProps({ precision: 1 }) 296 | expect(wrapper.data().amount).to.equal('2,000.2') 297 | }) 298 | 299 | it('allow to use arbitrary separators', () => { 300 | const wrapper = mount(VueNumeric, { 301 | propsData: { 302 | value: 1000.94 , 303 | precision: 2, 304 | thousandSeparator: ' ', 305 | decimalSeparator: ',' 306 | } 307 | }) 308 | expect(wrapper.data().amount).to.equal('1 000,94') 309 | }) 310 | 311 | it('emit change event', () => { 312 | const process = sinon.stub() 313 | const wrapper = mount(VueNumeric, { propsData: { value: 2000 }, methods: { process }}) 314 | wrapper.trigger('change') 315 | expect(process.called).to.equal(true) 316 | }) 317 | 318 | it('initial value is 0 if zero is passed', () => { 319 | const wrapper = mount(VueNumeric, { propsData: { value: 0}}) 320 | expect(wrapper.data().amount).to.equal('0') 321 | }) 322 | 323 | it('when format empty string should return empty string', () => { 324 | const wrapper = mount(VueNumeric, {propsData: { value: '', separator: '.', precision: 2 }}) 325 | expect(wrapper.vm.format('')).to.equal('') 326 | }) 327 | 328 | it('emit input event when process empty string', () => { 329 | const update = sinon.stub() 330 | const wrapper = mount(VueNumeric, {propsData: { value: '', separator: '.', precision: 2 }, methods: { update }}) 331 | const spy = sinon.spy(wrapper.vm, '$emit') 332 | wrapper.trigger('input') 333 | expect(update.called).to.equal(false) 334 | expect(spy.args[0][0]).to.equal('input') 335 | }) 336 | }) 337 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const path = require('path') 4 | const CleanWebpackPlugin = require('clean-webpack-plugin') 5 | 6 | module.exports = { 7 | context: __dirname, 8 | resolve: { 9 | modules: [ 10 | path.resolve(__dirname, 'src'), 11 | 'node_modules' 12 | ], 13 | alias: { 14 | 'vue$': 'vue/dist/vue.esm.js' 15 | }, 16 | extensions: ['.js', '.json', '.vue'] 17 | }, 18 | entry: './src/index.js', 19 | externals: { 20 | 'accounting-js': { 21 | commonjs: 'accounting-js', 22 | commonjs2: 'accounting-js', 23 | amd: 'accounting-js', 24 | root: 'accounting' 25 | } 26 | }, 27 | output: { 28 | path: path.resolve(__dirname, 'dist'), 29 | filename: "vue-numeric.min.js", 30 | library: 'VueNumeric', 31 | libraryTarget: 'umd', 32 | umdNamedDefine: true 33 | }, 34 | module: { 35 | rules: [ 36 | { 37 | test: /\.vue$/, 38 | loader: 'vue-loader' 39 | }, 40 | { 41 | test: /\.js$/, 42 | loader: 'babel-loader', 43 | exclude: path.resolve(__dirname, 'node_modules') 44 | } 45 | ] 46 | }, 47 | plugins: [ 48 | new CleanWebpackPlugin(['./dist']) 49 | ], 50 | devtool: false, 51 | performance: { 52 | hints: false 53 | } 54 | } --------------------------------------------------------------------------------