├── .browserslistrc ├── .eslintignore ├── .stylelintignore ├── babel.config.js ├── .husky ├── pre-commit └── commit-msg ├── .gitignore ├── src ├── index.ts ├── shims.d.ts ├── README.md └── vue-number-input.vue ├── commitlint.config.js ├── postcss.config.js ├── lint-staged.config.js ├── tsconfig.eslint.json ├── stylelint.config.js ├── tsconfig.json ├── docs ├── index.ts ├── components │ └── demo-block.vue ├── index.html └── app.vue ├── jest.config.js ├── .github └── workflows │ ├── ci.yml │ └── deploy.yml ├── .eslintrc.js ├── LICENSE ├── rollup.config.js ├── tests ├── others.spec.ts ├── events.spec.ts ├── methods.spec.ts └── props.spec.ts ├── webpack.config.js ├── README.md ├── package.json └── CHANGELOG.md /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | not ie 11 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.local* 2 | coverage 3 | dist 4 | node_modules 5 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | *.local* 2 | coverage 3 | dist 4 | node_modules 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env'], 3 | }; 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.local* 2 | *.log 3 | *.map 4 | .DS_Store 5 | coverage 6 | dist 7 | node_modules 8 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import VueNumberInput from './vue-number-input.vue'; 2 | 3 | export default VueNumberInput; 4 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | '@commitlint/config-conventional', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | stylelint: { 4 | fix: true, 5 | }, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{js,ts,vue}': 'eslint --fix', 3 | '*.{css,scss,vue}': 'stylelint --fix', 4 | }; 5 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "include": [ 4 | "*.js", 5 | ".*.js", 6 | "docs/**/*", 7 | "src/**/*", 8 | "tests/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | const content: any; 3 | 4 | export default content; 5 | } 6 | 7 | declare module '*.md' { 8 | const content: any; 9 | 10 | export default content; 11 | } 12 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'stylelint-config-recommended-vue/scss', 3 | plugins: [ 4 | 'stylelint-order', 5 | ], 6 | rules: { 7 | 'no-descending-specificity': null, 8 | 'no-empty-source': null, 9 | 'order/properties-alphabetical-order': true, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "moduleResolution": "node", 5 | "resolveJsonModule": true, 6 | "strict": true, 7 | "target": "esnext" 8 | }, 9 | "include": [ 10 | "src/**/*", 11 | "docs/**/*", 12 | "tests/**/*" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /docs/index.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './app.vue'; 3 | import DemoBlock from './components/demo-block.vue'; 4 | import VueNumberInput from '../src'; 5 | 6 | const app = createApp(App); 7 | 8 | app.component(VueNumberInput.name, VueNumberInput); 9 | app.component(DemoBlock.name, DemoBlock); 10 | app.mount('#app'); 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | collectCoverage: true, 4 | coverageDirectory: 'coverage', 5 | coverageReporters: ['html', 'lcov', 'text'], 6 | moduleFileExtensions: ['js', 'ts', 'vue'], 7 | transform: { 8 | '^.+\\.js$': 'babel-jest', 9 | '^.+\\.vue$': '@vue/vue3-jest', 10 | }, 11 | testEnvironment: 'jsdom', 12 | testMatch: ['**/tests/*.spec.ts'], 13 | }; 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: 16 18 | - run: npm install 19 | - run: npm run lint 20 | - run: npm run build 21 | - run: npm test 22 | - run: npm run test:coverage 23 | -------------------------------------------------------------------------------- /docs/components/demo-block.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 33 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | tags: 6 | - v2.* 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: 16 18 | - run: npm install 19 | - run: npm run build:docs 20 | - run: | 21 | cd docs/dist 22 | git init 23 | git config user.name "${{ github.actor }}" 24 | git config user.email "${{ github.actor }}@users.noreply.github.com" 25 | git add --all 26 | git commit --message "♥" 27 | git push --force https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git master:gh-pages 28 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'airbnb-typescript/base', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:vue/vue3-recommended', 11 | ], 12 | parser: 'vue-eslint-parser', 13 | parserOptions: { 14 | parser: '@typescript-eslint/parser', 15 | project: 'tsconfig.eslint.json', 16 | sourceType: 'module', 17 | extraFileExtensions: ['.vue'], 18 | }, 19 | plugins: [ 20 | '@typescript-eslint', 21 | 'import', 22 | 'vue', 23 | ], 24 | rules: { 25 | '@typescript-eslint/no-explicit-any': 'off', 26 | '@typescript-eslint/no-var-requires': 'off', 27 | 'no-restricted-properties': 'off', 28 | }, 29 | overrides: [ 30 | { 31 | files: ['tests/**/*.ts'], 32 | env: { 33 | jest: true, 34 | }, 35 | }, 36 | ], 37 | }; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2018-present Chen Fengyuan 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | vue-number-input 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import createBanner from 'create-banner'; 2 | import postcss from 'rollup-plugin-postcss'; 3 | import typescript from 'rollup-plugin-typescript2'; 4 | import vue from 'rollup-plugin-vue'; 5 | import { pascalCase } from 'change-case'; 6 | import { terser } from 'rollup-plugin-terser'; 7 | import pkg from './package.json'; 8 | 9 | const name = pascalCase(pkg.name.replace(/^.+\//, '')); 10 | const banner = createBanner({ 11 | data: { 12 | year: '2018-present', 13 | }, 14 | template: 'inline', 15 | }); 16 | 17 | export default ['umd', 'esm'].map((format) => ({ 18 | input: 'src/vue-number-input.vue', 19 | output: ['development', 'production'].map((mode) => { 20 | const output = { 21 | banner, 22 | format, 23 | name, 24 | file: pkg.main, 25 | globals: { 26 | vue: 'Vue', 27 | }, 28 | }; 29 | 30 | if (format === 'esm') { 31 | output.file = pkg.module; 32 | } 33 | 34 | if (mode === 'production') { 35 | output.compact = true; 36 | output.file = output.file.replace(/(\.js)$/, '.min$1'); 37 | output.plugins = [ 38 | terser(), 39 | ]; 40 | } 41 | 42 | return output; 43 | }), 44 | external: Object.keys(pkg.peerDependencies), 45 | plugins: [ 46 | typescript({ 47 | tsconfigOverride: { 48 | compilerOptions: { 49 | declaration: format === 'esm', 50 | }, 51 | exclude: [ 52 | 'src/index.ts', 53 | 'docs', 54 | 'tests', 55 | ], 56 | }, 57 | }), 58 | vue({ 59 | preprocessStyles: true, 60 | }), 61 | postcss({ 62 | extensions: ['.css', '.scss'], 63 | minimize: true, 64 | }), 65 | ], 66 | })); 67 | -------------------------------------------------------------------------------- /tests/others.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import VueNumberInput from '../src'; 3 | 4 | describe('others', () => { 5 | it('should fix the `0.30000000000000004` problem', (done) => { 6 | const wrapper = mount({ 7 | components: { 8 | VueNumberInput, 9 | }, 10 | data() { 11 | return { 12 | value: 0.1, 13 | }; 14 | }, 15 | template: '', 16 | }); 17 | 18 | expect(wrapper.vm.value).toBe(0.1); 19 | wrapper.get('.vue-number-input__button--plus').trigger('click').then(() => { 20 | expect(wrapper.vm.value).toBe(0.3); 21 | done(); 22 | }); 23 | }); 24 | 25 | it('should update the model value when value changed', (done) => { 26 | const wrapper = mount({ 27 | components: { 28 | VueNumberInput, 29 | }, 30 | data() { 31 | return { 32 | value: 0, 33 | }; 34 | }, 35 | template: '', 36 | }); 37 | 38 | expect(wrapper.vm.value).toBe(0); 39 | wrapper.get('.vue-number-input__input').setValue(1).then(() => { 40 | expect(wrapper.vm.value).toBe(1); 41 | done(); 42 | }); 43 | }); 44 | 45 | it('should not update the value when paste nothing', (done) => { 46 | const wrapper = mount({ 47 | components: { 48 | VueNumberInput, 49 | }, 50 | data() { 51 | return { 52 | value: 0, 53 | }; 54 | }, 55 | template: '', 56 | }); 57 | 58 | expect(wrapper.vm.value).toBe(0); 59 | wrapper.get('.vue-number-input__input').trigger('paste').then(() => { 60 | expect(wrapper.vm.value).toBe(0); 61 | done(); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /tests/events.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import VueNumberInput from '../src'; 3 | 4 | describe('events', () => { 5 | describe('custom', () => { 6 | it('should trigger the custom `update:model-value` event', (done) => { 7 | const wrapper = mount({ 8 | components: { 9 | VueNumberInput, 10 | }, 11 | methods: { 12 | onModelValueChange(newValue: number) { 13 | expect(newValue).toBe(1); 14 | done(); 15 | }, 16 | }, 17 | template: '', 18 | }); 19 | 20 | wrapper.get('.vue-number-input__input').setValue('1'); 21 | }); 22 | }); 23 | 24 | describe('native', () => { 25 | it('should trigger the native `change` event', (done) => { 26 | const wrapper = mount({ 27 | components: { 28 | VueNumberInput, 29 | }, 30 | methods: { 31 | onChange(event: Event) { 32 | expect(event.type).toBe('change'); 33 | expect((event.target as HTMLInputElement).value).toBe('1'); 34 | done(); 35 | }, 36 | }, 37 | template: '', 38 | }); 39 | 40 | wrapper.get('.vue-number-input__input').setValue('1'); 41 | }); 42 | 43 | it('should trigger the native `input` event', (done) => { 44 | const wrapper = mount({ 45 | components: { 46 | VueNumberInput, 47 | }, 48 | methods: { 49 | onInput(event: Event) { 50 | expect(event.type).toBe('input'); 51 | expect((event.target as HTMLInputElement).value).toBe('1'); 52 | done(); 53 | }, 54 | }, 55 | template: '', 56 | }); 57 | 58 | wrapper.get('.vue-number-input__input').setValue('1'); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const VueLoaderPlugin = require('vue-loader/dist/plugin').default; 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | 7 | module.exports = (env) => ({ 8 | mode: env.production ? 'production' : 'development', 9 | entry: './docs', 10 | output: { 11 | path: path.resolve(__dirname, './docs/dist'), 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js$/, 17 | use: 'babel-loader', 18 | }, 19 | { 20 | test: /\.ts$/, 21 | loader: 'ts-loader', 22 | options: { 23 | appendTsSuffixTo: [/\.vue$/], 24 | }, 25 | }, 26 | { 27 | test: /\.vue$/, 28 | loader: 'vue-loader', 29 | }, 30 | { 31 | test: /\.scss$/, 32 | use: [ 33 | MiniCssExtractPlugin.loader, 34 | 'css-loader', 35 | 'sass-loader', 36 | ], 37 | }, 38 | { 39 | test: /\.md$/, 40 | use: [ 41 | 'vue-loader', 42 | { 43 | loader: 'markdown-to-vue-loader', 44 | options: { 45 | componentWrapper: '', 46 | tableClass: 'table', 47 | tableWrapper: '
', 48 | }, 49 | }, 50 | ], 51 | }, 52 | ], 53 | }, 54 | plugins: [ 55 | new HtmlWebpackPlugin({ 56 | filename: 'index.html', 57 | template: './docs/index.html', 58 | }), 59 | new MiniCssExtractPlugin(), 60 | new VueLoaderPlugin(), 61 | new webpack.DefinePlugin({ 62 | __VUE_OPTIONS_API__: true, 63 | __VUE_PROD_DEVTOOLS__: false, 64 | }), 65 | ], 66 | externals: env.production ? { 67 | vue: 'Vue', 68 | } : {}, 69 | resolve: { 70 | alias: { 71 | vue$: 'vue/dist/vue.esm-bundler', 72 | }, 73 | extensions: ['.js', '.json', '.ts', '.d.ts', '.vue'], 74 | }, 75 | }); 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-number-input 2 | 3 | [![Coverage Status](https://img.shields.io/codecov/c/github/fengyuanchen/vue-number-input.svg)](https://codecov.io/gh/fengyuanchen/vue-number-input) [![Downloads](https://img.shields.io/npm/dm/@chenfengyuan/vue-number-input.svg)](https://www.npmjs.com/package/@chenfengyuan/vue-number-input) [![Version](https://img.shields.io/npm/v/@chenfengyuan/vue-number-input.svg)](https://www.npmjs.com/package/@chenfengyuan/vue-number-input) [![Gzip Size](https://img.shields.io/bundlephobia/minzip/@chenfengyuan/vue-number-input.svg)](https://unpkg.com/@chenfengyuan/vue-number-input/dist/vue-number-input.js) 4 | 5 | > Number input component for Vue 3. For Vue 2, check out the [`v1`](https://github.com/fengyuanchen/vue-number-input/tree/v1) branch. 6 | 7 | - [Docs](src/README.md) 8 | - [Demo](https://fengyuanchen.github.io/vue-number-input) 9 | 10 | ## Main npm package files 11 | 12 | ```text 13 | dist/ 14 | ├── vue-number-input.js (UMD, default) 15 | ├── vue-number-input.min.js (UMD, compressed) 16 | ├── vue-number-input.esm.js (ECMAScript Module) 17 | ├── vue-number-input.esm.min.js (ECMAScript Module, compressed) 18 | └── vue-number-input.d.ts (TypeScript Declaration File) 19 | ``` 20 | 21 | ## Getting started 22 | 23 | ### Installation 24 | 25 | Using npm: 26 | 27 | ```shell 28 | npm install vue@3 @chenfengyuan/vue-number-input@2 29 | ``` 30 | 31 | Using pnpm: 32 | 33 | ```shell 34 | pnpm add vue@3 @chenfengyuan/vue-number-input@2 35 | ``` 36 | 37 | Using Yarn: 38 | 39 | ```shell 40 | yarn add vue@3 @chenfengyuan/vue-number-input@2 41 | ``` 42 | 43 | Using CDN: 44 | 45 | ```html 46 | 47 | 48 | ``` 49 | 50 | ### Usage 51 | 52 | ```js 53 | import { createApp } from 'vue'; 54 | import VueNumberInput from '@chenfengyuan/vue-number-input'; 55 | 56 | const app = createApp({}); 57 | 58 | app.component(VueNumberInput.name, VueNumberInput); 59 | ``` 60 | 61 | ```html 62 | 63 | ``` 64 | 65 | ## Browser support 66 | 67 | Same as Vue 3. 68 | 69 | ## Versioning 70 | 71 | Maintained under the [Semantic Versioning guidelines](https://semver.org/). 72 | 73 | ## License 74 | 75 | [MIT](https://opensource.org/licenses/MIT) © [Chen Fengyuan](https://chenfengyuan.com/) 76 | -------------------------------------------------------------------------------- /tests/methods.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import VueNumberInput from '../src'; 3 | 4 | describe('methods', () => { 5 | describe('increase', () => { 6 | it('should increase the number', () => { 7 | const wrapper = mount(VueNumberInput); 8 | 9 | expect(wrapper.vm.value).toBeNaN(); 10 | wrapper.vm.increase(); 11 | expect(wrapper.vm.value).toBe(1); 12 | }); 13 | 14 | it('should not increase the number when the current value is equal to the maximum value', () => { 15 | const wrapper = mount(VueNumberInput, { 16 | props: { 17 | max: 0, 18 | modelValue: 0, 19 | }, 20 | }); 21 | 22 | expect(wrapper.vm.value).toBe(0); 23 | wrapper.vm.increase(); 24 | expect(wrapper.vm.value).toBe(0); 25 | }); 26 | }); 27 | 28 | describe('decrease', () => { 29 | it('should decrease the number', () => { 30 | const wrapper = mount(VueNumberInput); 31 | 32 | expect(wrapper.vm.value).toBeNaN(); 33 | wrapper.vm.decrease(); 34 | expect(wrapper.vm.value).toBe(-1); 35 | }); 36 | 37 | it('should not decrease the number when the current value is equal to the maximum value', () => { 38 | const wrapper = mount(VueNumberInput, { 39 | props: { 40 | min: 0, 41 | modelValue: 0, 42 | }, 43 | }); 44 | 45 | expect(wrapper.vm.value).toBe(0); 46 | wrapper.vm.decrease(); 47 | expect(wrapper.vm.value).toBe(0); 48 | }); 49 | }); 50 | 51 | describe('setValue', () => { 52 | it('should change the value', () => { 53 | const wrapper = mount(VueNumberInput); 54 | 55 | expect(wrapper.vm.value).toBeNaN(); 56 | wrapper.vm.setValue(1); 57 | expect(wrapper.vm.value).toBe(1); 58 | }); 59 | 60 | it('should transform the given value when it is less than the minimum value', () => { 61 | const wrapper = mount(VueNumberInput, { 62 | props: { 63 | min: 0, 64 | }, 65 | }); 66 | 67 | expect(wrapper.vm.value).toBeNaN(); 68 | wrapper.vm.setValue(-1); 69 | expect(wrapper.vm.value).toBe(0); 70 | }); 71 | 72 | it('should transform the given value when it is greater than the maximum value', () => { 73 | const wrapper = mount(VueNumberInput, { 74 | props: { 75 | max: 0, 76 | }, 77 | }); 78 | 79 | expect(wrapper.vm.value).toBeNaN(); 80 | wrapper.vm.setValue(1); 81 | expect(wrapper.vm.value).toBe(0); 82 | }); 83 | 84 | it('should not transform the given value when the maximum value is less than the minimum value', () => { 85 | const wrapper = mount(VueNumberInput, { 86 | props: { 87 | max: -10, 88 | min: 10, 89 | }, 90 | }); 91 | 92 | expect(wrapper.vm.value).toBeNaN(); 93 | wrapper.vm.setValue(1); 94 | expect(wrapper.vm.value).toBe(1); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chenfengyuan/vue-number-input", 3 | "version": "2.0.1", 4 | "description": "Number input component for Vue 3.", 5 | "main": "dist/vue-number-input.js", 6 | "module": "dist/vue-number-input.esm.js", 7 | "types": "dist/vue-number-input.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "build": "rollup -c --environment BUILD:production", 13 | "build:docs": "webpack --env production", 14 | "build:types": "move-file dist/vue-number-input.vue.d.ts dist/vue-number-input.d.ts", 15 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", 16 | "clean": "del-cli dist", 17 | "lint": "npm run lint:js && npm run lint:css", 18 | "lint:css": "stylelint **/*.{css,scss,vue} --fix", 19 | "lint:js": "eslint . --ext .js,.ts,.vue --fix", 20 | "prepare": "husky install", 21 | "release": "npm run clean && npm run lint && npm run build && npm run build:types && npm run build:docs && npm test && npm run changelog", 22 | "serve": "webpack serve --hot --open", 23 | "start": "npm run serve", 24 | "test": "jest", 25 | "test:coverage": "cat coverage/lcov.info | codecov" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/fengyuanchen/vue-number-input.git" 30 | }, 31 | "keywords": [ 32 | "number", 33 | "input", 34 | "vue", 35 | "vue3", 36 | "vue-component", 37 | "front-end", 38 | "web" 39 | ], 40 | "author": "Chen Fengyuan (https://chenfengyuan.com/)", 41 | "license": "MIT", 42 | "bugs": "https://github.com/fengyuanchen/vue-number-input/issues", 43 | "homepage": "https://fengyuanchen.github.io/vue-number-input", 44 | "devDependencies": { 45 | "@babel/core": "^7.17.2", 46 | "@babel/preset-env": "^7.16.11", 47 | "@commitlint/cli": "^16.2.1", 48 | "@commitlint/config-conventional": "^16.2.1", 49 | "@types/jest": "^27.4.0", 50 | "@typescript-eslint/eslint-plugin": "^5.11.0", 51 | "@typescript-eslint/parser": "^5.11.0", 52 | "@vue/test-utils": "^2.0.0-rc.18", 53 | "@vue/vue3-jest": "^27.0.0-alpha.4", 54 | "babel-jest": "^27.5.1", 55 | "babel-loader": "^8.2.3", 56 | "change-case": "^4.1.2", 57 | "codecov": "^3.8.3", 58 | "conventional-changelog-cli": "^2.2.2", 59 | "create-banner": "^2.0.0", 60 | "css-loader": "^6.6.0", 61 | "del-cli": "^4.0.1", 62 | "eslint": "^8.9.0", 63 | "eslint-config-airbnb-typescript": "^16.1.0", 64 | "eslint-plugin-import": "^2.25.4", 65 | "eslint-plugin-vue": "^8.4.1", 66 | "html-webpack-plugin": "^5.5.0", 67 | "husky": "^7.0.4", 68 | "jest": "^27.5.1", 69 | "lint-staged": "^12.3.3", 70 | "markdown-to-vue-loader": "^3.1.3", 71 | "mini-css-extract-plugin": "^2.5.3", 72 | "move-file-cli": "^3.0.0", 73 | "postcss": "^8.4.6", 74 | "rollup": "^2.67.2", 75 | "rollup-plugin-postcss": "^4.0.2", 76 | "rollup-plugin-terser": "^7.0.2", 77 | "rollup-plugin-typescript2": "^0.31.2", 78 | "rollup-plugin-vue": "^6.0.0", 79 | "sass": "^1.49.7", 80 | "sass-loader": "^12.4.0", 81 | "style-loader": "^3.3.1", 82 | "stylelint": "^14.5.0", 83 | "stylelint-config-recommended-scss": "^5.0.2", 84 | "stylelint-config-recommended-vue": "^1.3.0", 85 | "stylelint-order": "^5.0.0", 86 | "ts-jest": "^27.1.3", 87 | "ts-loader": "^9.2.6", 88 | "tslib": "^2.3.1", 89 | "typescript": "^4.5.5", 90 | "vue": "^3.2.31", 91 | "vue-loader": "^17.0.0", 92 | "webpack": "^5.68.0", 93 | "webpack-cli": "^4.9.2", 94 | "webpack-dev-server": "^4.7.4" 95 | }, 96 | "peerDependencies": { 97 | "vue": "^3.0.0" 98 | }, 99 | "publishConfig": { 100 | "access": "public" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Number Input 2 | 3 | > Number input with optional controls. 4 | 5 | ## Basic usage 6 | 7 | ```html 8 | 12 | 13 | 22 | ``` 23 | 24 | ## Step 25 | 26 | ```html 27 | 30 | ``` 31 | 32 | ## Inline 33 | 34 | ```html 35 | 41 | ``` 42 | 43 | ## Center number 44 | 45 | ```html 46 | 49 | ``` 50 | 51 | ## Sizes 52 | 53 | ```html 54 | 59 | 60 | 65 | ``` 66 | 67 | ## Without controls 68 | 69 | ```html 70 | 75 | 76 | 81 | ``` 82 | 83 | ## Rounded 84 | 85 | ```html 86 | 89 | ``` 90 | 91 | ## Not inputtable 92 | 93 | The input is not inputtable, but still allow to change the value by controls. 94 | 95 | ```html 96 | 99 | ``` 100 | 101 | ## Readonly 102 | 103 | ```html 104 | 107 | ``` 108 | 109 | ## Disabled 110 | 111 | ```html 112 | 115 | ``` 116 | 117 | ## Customize attributes for the input element 118 | 119 | ```html 120 | 123 | ``` 124 | 125 | ## Props 126 | 127 | | Name | Type | Default | Options | Description | 128 | | --- | --- | --- | --- | --- | 129 | | attrs | `Object` | - | - | Specify attributes for the built-in input element. | 130 | | center | `boolean` | `false` | - | Indicate if the number is center or not. | 131 | | controls | `boolean` | `false` | - | Indicate if the controls is visible or not. | 132 | | disabled | `boolean` | `false` | - | Indicate if the component is disabled or not. | 133 | | inline | `boolean` | `false` | - | Indicate if the input is inline or not. | 134 | | inputtable | `boolean` | `true` | - | Indicate if the input element is inputtable or not. | 135 | | max | `number` | `Infinity` | - | The maximum value. | 136 | | min | `number` | `-Infinity` | - | The minimum value. | 137 | | name | `string` | - | - | The name of the input element. | 138 | | placeholder | `string` | - | - | The placeholder of the input element. | 139 | | readonly | `boolean` | `false` | - | Indicate if the component is read only or not. | 140 | | rounded | `boolean` | `false` | - | Indicate if the number is rounded or not. | 141 | | size | `string` | - | small, large | The size of the component. | 142 | | step | `number` | `1` | - | The increment of each step. | 143 | | modelValue | `number` | - | - | The binding value. | 144 | 145 | ## Events 146 | 147 | | Name | Parameters | Description | 148 | | --- | --- | --- | 149 | | update:model-value | `(newValue, oldValue)` | Fire when the value is changed. | 150 | 151 | > Native events that bubble up from child elements are also available. 152 | 153 | ```html 154 | 157 | 158 | 173 | ``` 174 | -------------------------------------------------------------------------------- /docs/app.vue: -------------------------------------------------------------------------------- 1 | 115 | 116 | 143 | 144 | 236 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.0.1](https://github.com/fengyuanchen/vue-number-input/compare/v2.0.0...v2.0.1) (2022-02-13) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * round the normalized new value ([30aaa6a](https://github.com/fengyuanchen/vue-number-input/commit/30aaa6a4a74b99e177477fb21f8ad178d13bb8af)), closes [#32](https://github.com/fengyuanchen/vue-number-input/issues/32) 7 | 8 | 9 | 10 | # [2.0.0](https://github.com/fengyuanchen/vue-number-input/compare/v2.0.0-rc.2...v2.0.0) (2022-02-07) 11 | 12 | 13 | 14 | # [2.0.0-rc.2](https://github.com/fengyuanchen/vue-number-input/compare/v2.0.0-rc.1...v2.0.0-rc.2) (2022-01-22) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * correct the declaration file name ([7f96219](https://github.com/fengyuanchen/vue-number-input/commit/7f96219dc88afa516244db39715b087bcb04dbcf)) 20 | 21 | 22 | 23 | # [2.0.0-rc.1](https://github.com/fengyuanchen/vue-number-input/compare/v2.0.0-rc...v2.0.0-rc.1) (2022-01-08) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * add `oldValue` to `update:modelValue` event ([4fa92f0](https://github.com/fengyuanchen/vue-number-input/commit/4fa92f03047ebe5dbc45a5e531575d1df081f20c)) 29 | * avoid specifying `NaN` value to number input ([101abce](https://github.com/fengyuanchen/vue-number-input/commit/101abce5375f938fd06301877a84cb25c88df71c)), closes [#31](https://github.com/fengyuanchen/vue-number-input/issues/31) 30 | 31 | 32 | ### Features 33 | 34 | * add auto-generated declaration file ([5ecd316](https://github.com/fengyuanchen/vue-number-input/commit/5ecd316699c3e60d0f7b6d591c0d3476e3f03cd2)) 35 | 36 | 37 | 38 | # [2.0.0-rc](https://github.com/fengyuanchen/vue-number-input/compare/v2.0.0-beta.1...v2.0.0-rc) (2021-06-12) 39 | 40 | 41 | 42 | # [2.0.0-beta.1](https://github.com/fengyuanchen/vue-number-input/compare/v2.0.0-beta...v2.0.0-beta.1) (2021-04-10) 43 | 44 | 45 | ### Bug Fixes 46 | 47 | * round the new value in the last setp ([c43a69f](https://github.com/fengyuanchen/vue-number-input/commit/c43a69f5b67c5e02fbd1eccb007c947aa5482f8e)) 48 | * should emit a single value ([cb8bed9](https://github.com/fengyuanchen/vue-number-input/commit/cb8bed9b9470882588e8b281466cea088afc0009)) 49 | 50 | 51 | ### Features 52 | 53 | * support to clear the input value through the keyboard ([fd9faf2](https://github.com/fengyuanchen/vue-number-input/commit/fd9faf23737fb568426fbbe0d879c076cb6cf419)), closes [#28](https://github.com/fengyuanchen/vue-number-input/issues/28) 54 | 55 | 56 | 57 | # [2.0.0-beta](https://github.com/fengyuanchen/vue-number-input/compare/v2.0.0-alpha...v2.0.0-beta) (2021-01-16) 58 | 59 | 60 | 61 | # [2.0.0-alpha](https://github.com/fengyuanchen/vue-number-input/compare/v1.2.1...v2.0.0-alpha) (2021-01-08) 62 | 63 | 64 | * refactor!: upgrade to Vue 3 ([65be700](https://github.com/fengyuanchen/vue-number-input/commit/65be700df03c167abb347c546ba579035d59fc16)) 65 | 66 | 67 | ### BREAKING CHANGES 68 | 69 | * drop support for Vue 2. 70 | 71 | 72 | 73 | ## [1.2.1](https://github.com/fengyuanchen/vue-number-input/compare/v1.2.0...v1.2.1) (2020-01-18) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * add `tabindex` attribute to control buttons ([05869bc](https://github.com/fengyuanchen/vue-number-input/commit/05869bcb058d06608085141b0260962b6f028262)), closes [#20](https://github.com/fengyuanchen/vue-number-input/issues/20) 79 | 80 | 81 | 82 | # [1.2.0](https://github.com/fengyuanchen/vue-number-input/compare/v1.1.2...v1.2.0) (2019-10-19) 83 | 84 | 85 | ### Features 86 | 87 | * add new `attrs` prop ([e8b1498](https://github.com/fengyuanchen/vue-number-input/commit/e8b1498fa485253392afe505c854f6967d6e5990)) 88 | 89 | 90 | 91 | ## [1.1.2](https://github.com/fengyuanchen/vue-number-input/compare/v1.1.1...v1.1.2) (2019-10-02) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * hide the spin box in number input box in Firefox ([580ec5c](https://github.com/fengyuanchen/vue-number-input/commit/580ec5c960c6b9bd4984631a1932321443673ba5)), closes [#17](https://github.com/fengyuanchen/vue-number-input/issues/17) 97 | 98 | 99 | 100 | ## [1.1.1](https://github.com/fengyuanchen/vue-number-input/compare/v1.1.0...v1.1.1) (2019-03-09) 101 | 102 | 103 | ### Bug Fixes 104 | 105 | * add missing `-webkit-` prefix to `appearance` property ([f71d0ae](https://github.com/fengyuanchen/vue-number-input/commit/f71d0aeb2e6211e2b37dd66cc348fb035247c207)), closes [#12](https://github.com/fengyuanchen/vue-number-input/issues/12) 106 | * force to override the number in the input box ([7d816d4](https://github.com/fengyuanchen/vue-number-input/commit/7d816d4ec7652160aae2b330e9502ce2e256fc9a)), closes [#13](https://github.com/fengyuanchen/vue-number-input/issues/13) 107 | 108 | 109 | 110 | # [1.1.0](https://github.com/fengyuanchen/vue-number-input/compare/v1.0.0...v1.1.0) (2019-01-26) 111 | 112 | 113 | ### Features 114 | 115 | * register as a Vue plugin ([b23b09e](https://github.com/fengyuanchen/vue-number-input/commit/b23b09ef0e14601f0bb8b673bd2313b77d6c2e28)) 116 | * register the component automatically once loaded ([e86eab1](https://github.com/fengyuanchen/vue-number-input/commit/e86eab1db441100f71e667e5edb297c1030f6830)) 117 | 118 | 119 | 120 | # [1.0.0](https://github.com/fengyuanchen/vue-number-input/compare/v0.5.3...v1.0.0) (2018-12-20) 121 | 122 | 123 | ### Bug Fixes 124 | 125 | * avoid the size to less than 1px ([03d804d](https://github.com/fengyuanchen/vue-number-input/commit/03d804d418d1b21c971fe0e7df8a50cec2413bd9)), closes [#10](https://github.com/fengyuanchen/vue-number-input/issues/10) 126 | 127 | 128 | ### Performance Improvements 129 | 130 | * avoid to trigger change event when created ([b1cd06f](https://github.com/fengyuanchen/vue-number-input/commit/b1cd06f901f5c2567e94cb3353f326514ac30ed0)) 131 | 132 | 133 | 134 | ## [0.5.3](https://github.com/fengyuanchen/vue-number-input/compare/v0.5.2...v0.5.3) (2018-12-01) 135 | 136 | 137 | ### Bug Fixes 138 | 139 | * sync value only when it is different ([7100b1f](https://github.com/fengyuanchen/vue-number-input/commit/7100b1f01923a9dd4e5927e181a6f1fb15ae8711)), closes [#8](https://github.com/fengyuanchen/vue-number-input/issues/8) 140 | * update input value automatically ([b2dae7f](https://github.com/fengyuanchen/vue-number-input/commit/b2dae7f5358914d9e863b2a0604eaa2201ed7b1d)), closes [#9](https://github.com/fengyuanchen/vue-number-input/issues/9) 141 | 142 | 143 | 144 | ## [0.5.2](https://github.com/fengyuanchen/vue-number-input/compare/v0.5.1...v0.5.2) (2018-08-05) 145 | 146 | 147 | ### Bug Fixes 148 | 149 | * the 0.30000000000000004 problem ([8f5a3e0](https://github.com/fengyuanchen/vue-number-input/commit/8f5a3e0f63ed46417fa80d597aa7c246fde65ba4)), closes [#6](https://github.com/fengyuanchen/vue-number-input/issues/6) 150 | 151 | 152 | 153 | ## [0.5.1](https://github.com/fengyuanchen/vue-number-input/compare/v0.5.0...v0.5.1) (2018-06-09) 154 | 155 | 156 | ### Bug Fixes 157 | 158 | * change the input value only when it is mounted ([cada801](https://github.com/fengyuanchen/vue-number-input/commit/cada8012e9428e633188eab93b19cff6e35f8f17)), closes [#4](https://github.com/fengyuanchen/vue-number-input/issues/4) 159 | 160 | 161 | 162 | # [0.5.0](https://github.com/fengyuanchen/vue-number-input/compare/v0.4.1...v0.5.0) (2018-06-06) 163 | 164 | 165 | ### Bug Fixes 166 | 167 | * off auto complete ([d14af46](https://github.com/fengyuanchen/vue-number-input/commit/d14af4611b182615298e841db5806e00fac34615)) 168 | 169 | 170 | ### Features 171 | 172 | * add `placeholder` prop ([331caa6](https://github.com/fengyuanchen/vue-number-input/commit/331caa6a2a1828043a0df778ddd175cd63dce5c1)) 173 | * add `rounded` prop ([99dd179](https://github.com/fengyuanchen/vue-number-input/commit/99dd179d8e9b0438e867248e540d6f2a737a225d)) 174 | 175 | 176 | 177 | ## [0.4.1](https://github.com/fengyuanchen/vue-number-input/compare/v0.4.0...v0.4.1) (2018-05-27) 178 | 179 | 180 | 181 | # [0.4.0](https://github.com/fengyuanchen/vue-number-input/compare/v0.3.0...v0.4.0) (2018-05-27) 182 | 183 | 184 | ### Features 185 | 186 | * add `inputtable` prop ([98131c9](https://github.com/fengyuanchen/vue-number-input/commit/98131c9d6890b7a0a4aa4f41b13891a766054f2e)), closes [#2](https://github.com/fengyuanchen/vue-number-input/issues/2) 187 | 188 | 189 | 190 | # [0.3.0](https://github.com/fengyuanchen/vue-number-input/compare/v0.2.0...v0.3.0) (2018-03-25) 191 | 192 | 193 | 194 | # [0.2.0](https://github.com/fengyuanchen/vue-number-input/compare/v0.1.0...v0.2.0) (2018-03-21) 195 | 196 | 197 | 198 | # 0.1.0 (2018-03-13) 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /src/vue-number-input.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 244 | 245 | 419 | -------------------------------------------------------------------------------- /tests/props.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import VueNumberInput from '../src'; 3 | 4 | describe('props', () => { 5 | describe('attrs', () => { 6 | it('should be undefined by default', () => { 7 | const wrapper = mount(VueNumberInput); 8 | 9 | expect(wrapper.props('attrs')).toBeUndefined(); 10 | }); 11 | 12 | it('should apply the given attributes', () => { 13 | const wrapper = mount(VueNumberInput, { 14 | props: { 15 | attrs: { 16 | tabindex: 0, 17 | }, 18 | }, 19 | }); 20 | 21 | expect(wrapper.props('attrs')).toEqual({ 22 | tabindex: 0, 23 | }); 24 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).tabIndex).toBe(0); 25 | }); 26 | }); 27 | 28 | describe('center', () => { 29 | it('should not be center by default', () => { 30 | const wrapper = mount(VueNumberInput); 31 | 32 | expect(wrapper.props('center')).toBe(false); 33 | expect(wrapper.classes()).not.toContain('vue-number-input--center'); 34 | }); 35 | 36 | it('should be center', () => { 37 | const wrapper = mount(VueNumberInput, { 38 | props: { 39 | center: true, 40 | }, 41 | }); 42 | 43 | expect(wrapper.props('center')).toBe(true); 44 | expect(wrapper.classes()).toContain('vue-number-input--center'); 45 | }); 46 | }); 47 | 48 | describe('controls', () => { 49 | it('should not display the controls by default', () => { 50 | const wrapper = mount(VueNumberInput); 51 | 52 | expect(wrapper.props('controls')).toBe(false); 53 | expect(wrapper.classes()).not.toContain('vue-number-input--controls'); 54 | expect(wrapper.find('.vue-number-input__button').exists()).toBe(false); 55 | }); 56 | 57 | it('should display the controls', () => { 58 | const wrapper = mount(VueNumberInput, { 59 | props: { 60 | controls: true, 61 | }, 62 | }); 63 | 64 | expect(wrapper.props('controls')).toBe(true); 65 | expect(wrapper.classes()).toContain('vue-number-input--controls'); 66 | expect(wrapper.findAll('.vue-number-input__button').length).toBe(2); 67 | }); 68 | 69 | it('should increase the number when click the plus control', (done) => { 70 | const wrapper = mount(VueNumberInput, { 71 | props: { 72 | controls: true, 73 | }, 74 | }); 75 | 76 | expect(wrapper.vm.value).toBeNaN(); 77 | wrapper.get('.vue-number-input__button--plus').trigger('click').then(() => { 78 | expect(wrapper.vm.value).toBe(1); 79 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('1'); 80 | done(); 81 | }); 82 | }); 83 | 84 | it('should decrease the number when click the minus control', (done) => { 85 | const wrapper = mount(VueNumberInput, { 86 | props: { 87 | controls: true, 88 | }, 89 | }); 90 | 91 | expect(wrapper.vm.value).toBeNaN(); 92 | wrapper.get('.vue-number-input__button--minus').trigger('click').then(() => { 93 | expect(wrapper.vm.value).toBe(-1); 94 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('-1'); 95 | done(); 96 | }); 97 | }); 98 | }); 99 | 100 | describe('disabled', () => { 101 | it('should not be disabled by default', () => { 102 | const wrapper = mount(VueNumberInput, { 103 | props: { 104 | controls: true, 105 | }, 106 | }); 107 | 108 | expect(wrapper.props('disabled')).toBe(false); 109 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).disabled).toBe(false); 110 | expect((wrapper.get('.vue-number-input__button--plus').element as HTMLButtonElement).disabled).toBe(false); 111 | expect((wrapper.get('.vue-number-input__button--minus').element as HTMLButtonElement).disabled).toBe(false); 112 | }); 113 | 114 | it('should by disabled', () => { 115 | const wrapper = mount(VueNumberInput, { 116 | props: { 117 | controls: true, 118 | disabled: true, 119 | }, 120 | }); 121 | 122 | expect(wrapper.props('disabled')).toBe(true); 123 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).disabled).toBe(true); 124 | expect((wrapper.get('.vue-number-input__button--plus').element as HTMLButtonElement).disabled).toBe(true); 125 | expect((wrapper.get('.vue-number-input__button--minus').element as HTMLButtonElement).disabled).toBe(true); 126 | }); 127 | }); 128 | 129 | describe('inline', () => { 130 | it('should not be inline by default', () => { 131 | const wrapper = mount(VueNumberInput); 132 | 133 | expect(wrapper.props('inline')).toBe(false); 134 | expect(wrapper.classes()).not.toContain('vue-number-input--inline'); 135 | }); 136 | 137 | it('should be inline', () => { 138 | const wrapper = mount(VueNumberInput, { 139 | props: { 140 | inline: true, 141 | }, 142 | }); 143 | 144 | expect(wrapper.props('inline')).toBe(true); 145 | expect(wrapper.classes()).toContain('vue-number-input--inline'); 146 | }); 147 | }); 148 | 149 | describe('inputtable', () => { 150 | it('should be inputtable by default', () => { 151 | const wrapper = mount(VueNumberInput, { 152 | props: { 153 | controls: true, 154 | }, 155 | }); 156 | 157 | expect(wrapper.props('inputtable')).toBe(true); 158 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).readOnly).toBe(false); 159 | expect((wrapper.get('.vue-number-input__button--plus').element as HTMLButtonElement).disabled).toBe(false); 160 | expect((wrapper.get('.vue-number-input__button--minus').element as HTMLButtonElement).disabled).toBe(false); 161 | }); 162 | 163 | it('should not be inputtable', () => { 164 | const wrapper = mount(VueNumberInput, { 165 | props: { 166 | controls: true, 167 | inputtable: false, 168 | }, 169 | }); 170 | 171 | expect(wrapper.props('inputtable')).toBe(false); 172 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).readOnly).toBe(true); 173 | expect((wrapper.get('.vue-number-input__button--plus').element as HTMLButtonElement).disabled).toBe(false); 174 | expect((wrapper.get('.vue-number-input__button--minus').element as HTMLButtonElement).disabled).toBe(false); 175 | }); 176 | }); 177 | 178 | describe('max', () => { 179 | it('should be `Infinity` by default', () => { 180 | const wrapper = mount(VueNumberInput); 181 | 182 | expect(wrapper.props('max')).toBe(Infinity); 183 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).max).toBe('Infinity'); 184 | }); 185 | 186 | it('should be equal to the given value', () => { 187 | const wrapper = mount(VueNumberInput, { 188 | props: { 189 | max: 10, 190 | }, 191 | }); 192 | 193 | expect(wrapper.props('max')).toBe(10); 194 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).max).toBe('10'); 195 | }); 196 | 197 | it('should not be greater than the given maximum value', () => { 198 | const wrapper = mount(VueNumberInput, { 199 | props: { 200 | modelValue: 11, 201 | max: 10, 202 | }, 203 | }); 204 | 205 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('10'); 206 | }); 207 | 208 | it('should fix the out of range value', () => { 209 | const wrapper = mount(VueNumberInput, { 210 | props: { 211 | max: 10, 212 | }, 213 | }); 214 | 215 | wrapper.get('.vue-number-input__input').setValue('11').then(() => { 216 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('10'); 217 | }); 218 | }); 219 | }); 220 | 221 | describe('min', () => { 222 | it('should be `-Infinity` by default', () => { 223 | const wrapper = mount(VueNumberInput); 224 | 225 | expect(wrapper.props('min')).toBe(-Infinity); 226 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).min).toBe('-Infinity'); 227 | }); 228 | 229 | it('should be equal to the given value', () => { 230 | const wrapper = mount(VueNumberInput, { 231 | props: { 232 | min: -10, 233 | }, 234 | }); 235 | 236 | expect(wrapper.props('min')).toBe(-10); 237 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).min).toBe('-10'); 238 | }); 239 | 240 | it('should not be less than the given minimum value', () => { 241 | const wrapper = mount(VueNumberInput, { 242 | props: { 243 | modelValue: -11, 244 | min: -10, 245 | }, 246 | }); 247 | 248 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('-10'); 249 | }); 250 | 251 | it('should fix the out of range value', () => { 252 | const wrapper = mount(VueNumberInput, { 253 | props: { 254 | min: -10, 255 | }, 256 | }); 257 | 258 | wrapper.get('.vue-number-input__input').setValue('-11').then(() => { 259 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('-10'); 260 | }); 261 | }); 262 | }); 263 | 264 | describe('name', () => { 265 | it('should be undefined by default', () => { 266 | const wrapper = mount(VueNumberInput); 267 | 268 | expect(wrapper.props('name')).toBeUndefined(); 269 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).name).toBe(''); 270 | }); 271 | 272 | it('should be equal to the given value', () => { 273 | const wrapper = mount(VueNumberInput, { 274 | props: { 275 | name: 'digit', 276 | }, 277 | }); 278 | 279 | expect(wrapper.props('name')).toBe('digit'); 280 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).name).toBe('digit'); 281 | }); 282 | }); 283 | 284 | describe('placeholder', () => { 285 | it('should be undefined by default', () => { 286 | const wrapper = mount(VueNumberInput); 287 | 288 | expect(wrapper.props('placeholder')).toBeUndefined(); 289 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).placeholder).toBe(''); 290 | }); 291 | 292 | it('should be equal to the given value', () => { 293 | const wrapper = mount(VueNumberInput, { 294 | props: { 295 | placeholder: 'Number input', 296 | }, 297 | }); 298 | 299 | expect(wrapper.props('placeholder')).toBe('Number input'); 300 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).placeholder).toBe('Number input'); 301 | }); 302 | }); 303 | 304 | describe('readonly', () => { 305 | it('should not be read-only by default', () => { 306 | const wrapper = mount(VueNumberInput, { 307 | props: { 308 | controls: true, 309 | }, 310 | }); 311 | 312 | expect(wrapper.props('readonly')).toBe(false); 313 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).readOnly).toBe(false); 314 | expect((wrapper.get('.vue-number-input__button--plus').element as HTMLButtonElement).disabled).toBe(false); 315 | expect((wrapper.get('.vue-number-input__button--minus').element as HTMLButtonElement).disabled).toBe(false); 316 | }); 317 | 318 | it('should be read-only', () => { 319 | const wrapper = mount(VueNumberInput, { 320 | props: { 321 | controls: true, 322 | readonly: true, 323 | }, 324 | }); 325 | 326 | expect(wrapper.props('readonly')).toBe(true); 327 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).readOnly).toBe(true); 328 | expect((wrapper.get('.vue-number-input__button--plus').element as HTMLButtonElement).disabled).toBe(true); 329 | expect((wrapper.get('.vue-number-input__button--minus').element as HTMLButtonElement).disabled).toBe(true); 330 | }); 331 | }); 332 | 333 | describe('rounded', () => { 334 | it('should not round the number by default', () => { 335 | const wrapper = mount(VueNumberInput, { 336 | props: { 337 | modelValue: 1.5, 338 | }, 339 | }); 340 | 341 | expect(wrapper.props('rounded')).toBe(false); 342 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('1.5'); 343 | }); 344 | 345 | it('should round the number', () => { 346 | const wrapper = mount(VueNumberInput, { 347 | props: { 348 | modelValue: 1.5, 349 | rounded: true, 350 | }, 351 | }); 352 | 353 | expect(wrapper.props('rounded')).toBe(true); 354 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('2'); 355 | }); 356 | }); 357 | 358 | describe('size', () => { 359 | it('should be small size', () => { 360 | const wrapper = mount(VueNumberInput, { 361 | props: { 362 | size: 'small', 363 | }, 364 | }); 365 | 366 | expect(wrapper.props('size')).toBe('small'); 367 | expect(wrapper.classes()).toContain('vue-number-input--small'); 368 | }); 369 | 370 | it('should be large size', () => { 371 | const wrapper = mount(VueNumberInput, { 372 | props: { 373 | size: 'large', 374 | }, 375 | }); 376 | 377 | expect(wrapper.props('size')).toBe('large'); 378 | expect(wrapper.classes()).toContain('vue-number-input--large'); 379 | }); 380 | }); 381 | 382 | describe('step', () => { 383 | it('should be `1` by default', (done) => { 384 | const wrapper = mount(VueNumberInput, { 385 | props: { 386 | controls: true, 387 | }, 388 | }); 389 | 390 | expect(wrapper.props('step')).toBe(1); 391 | wrapper.get('.vue-number-input__button--plus').trigger('click').then(() => { 392 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).step).toBe('1'); 393 | done(); 394 | }); 395 | }); 396 | 397 | it('should match the given value', (done) => { 398 | const wrapper = mount(VueNumberInput, { 399 | props: { 400 | controls: true, 401 | step: 2, 402 | }, 403 | }); 404 | 405 | expect(wrapper.props('step')).toBe(2); 406 | wrapper.get('.vue-number-input__button--plus').trigger('click').then(() => { 407 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).step).toBe('2'); 408 | done(); 409 | }); 410 | }); 411 | }); 412 | 413 | describe('modelValue', () => { 414 | it('should be `NaN` by default', () => { 415 | const wrapper = mount(VueNumberInput); 416 | 417 | expect(wrapper.props('modelValue')).toBeNaN(); 418 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe(''); 419 | }); 420 | 421 | it('should be equal to the given value', () => { 422 | const wrapper = mount(VueNumberInput, { 423 | props: { 424 | modelValue: 10, 425 | }, 426 | }); 427 | 428 | expect(wrapper.props('modelValue')).toBe(10); 429 | expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('10'); 430 | }); 431 | }); 432 | }); 433 | --------------------------------------------------------------------------------