```
## Center number
```html
```
## Sizes
```html
```
## Without controls
```html
```
## Rounded
```html
```
## Not inputtable
The input is not inputtable, but still allow to change the value by controls.
```html
```
## Readonly
```html
```
## Disabled
```html
```
## Customize attributes for the input element
```html
```
## Props
| Name | Type | Default | Options | Description |
| --- | --- | --- | --- | --- |
| attrs | `Object` | - | - | Specify attributes for the built-in input element. |
| center | `boolean` | `false` | - | Indicate if the number is center or not. |
| controls | `boolean` | `false` | - | Indicate if the controls is visible or not. |
| disabled | `boolean` | `false` | - | Indicate if the component is disabled or not. |
| inline | `boolean` | `false` | - | Indicate if the input is inline or not. |
| inputtable | `boolean` | `true` | - | Indicate if the input element is inputtable or not. |
| max | `number` | `Infinity` | - | The maximum value. |
| min | `number` | `-Infinity` | - | The minimum value. |
| name | `string` | - | - | The name of the input element. |
| placeholder | `string` | - | - | The placeholder of the input element. |
| readonly | `boolean` | `false` | - | Indicate if the component is read only or not. |
| rounded | `boolean` | `false` | - | Indicate if the number is rounded or not. |
| size | `string` | - | small, large | The size of the component. |
| step | `number` | `1` | - | The increment of each step. |
| modelValue | `number` | - | - | The binding value. |
## Events
| Name | Parameters | Description |
| --- | --- | --- |
| update:model-value | `(newValue, oldValue)` | Fire when the value is changed. |
> Native events that bubble up from child elements are also available.
```html
```
================================================
FILE: src/index.ts
================================================
import VueNumberInput from './vue-number-input.vue';
export default VueNumberInput;
================================================
FILE: src/shims.d.ts
================================================
declare module '*.vue' {
const content: any;
export default content;
}
declare module '*.md' {
const content: any;
export default content;
}
================================================
FILE: src/vue-number-input.vue
================================================
================================================
FILE: stylelint.config.js
================================================
module.exports = {
extends: 'stylelint-config-recommended-vue/scss',
plugins: [
'stylelint-order',
],
rules: {
'no-descending-specificity': null,
'no-empty-source': null,
'order/properties-alphabetical-order': true,
},
};
================================================
FILE: tests/events.spec.ts
================================================
import { mount } from '@vue/test-utils';
import VueNumberInput from '../src';
describe('events', () => {
describe('custom', () => {
it('should trigger the custom `update:model-value` event', (done) => {
const wrapper = mount({
components: {
VueNumberInput,
},
methods: {
onModelValueChange(newValue: number) {
expect(newValue).toBe(1);
done();
},
},
template: '',
});
wrapper.get('.vue-number-input__input').setValue('1');
});
});
describe('native', () => {
it('should trigger the native `change` event', (done) => {
const wrapper = mount({
components: {
VueNumberInput,
},
methods: {
onChange(event: Event) {
expect(event.type).toBe('change');
expect((event.target as HTMLInputElement).value).toBe('1');
done();
},
},
template: '',
});
wrapper.get('.vue-number-input__input').setValue('1');
});
it('should trigger the native `input` event', (done) => {
const wrapper = mount({
components: {
VueNumberInput,
},
methods: {
onInput(event: Event) {
expect(event.type).toBe('input');
expect((event.target as HTMLInputElement).value).toBe('1');
done();
},
},
template: '',
});
wrapper.get('.vue-number-input__input').setValue('1');
});
});
});
================================================
FILE: tests/methods.spec.ts
================================================
import { mount } from '@vue/test-utils';
import VueNumberInput from '../src';
describe('methods', () => {
describe('increase', () => {
it('should increase the number', () => {
const wrapper = mount(VueNumberInput);
expect(wrapper.vm.value).toBeNaN();
wrapper.vm.increase();
expect(wrapper.vm.value).toBe(1);
});
it('should not increase the number when the current value is equal to the maximum value', () => {
const wrapper = mount(VueNumberInput, {
props: {
max: 0,
modelValue: 0,
},
});
expect(wrapper.vm.value).toBe(0);
wrapper.vm.increase();
expect(wrapper.vm.value).toBe(0);
});
});
describe('decrease', () => {
it('should decrease the number', () => {
const wrapper = mount(VueNumberInput);
expect(wrapper.vm.value).toBeNaN();
wrapper.vm.decrease();
expect(wrapper.vm.value).toBe(-1);
});
it('should not decrease the number when the current value is equal to the maximum value', () => {
const wrapper = mount(VueNumberInput, {
props: {
min: 0,
modelValue: 0,
},
});
expect(wrapper.vm.value).toBe(0);
wrapper.vm.decrease();
expect(wrapper.vm.value).toBe(0);
});
});
describe('setValue', () => {
it('should change the value', () => {
const wrapper = mount(VueNumberInput);
expect(wrapper.vm.value).toBeNaN();
wrapper.vm.setValue(1);
expect(wrapper.vm.value).toBe(1);
});
it('should transform the given value when it is less than the minimum value', () => {
const wrapper = mount(VueNumberInput, {
props: {
min: 0,
},
});
expect(wrapper.vm.value).toBeNaN();
wrapper.vm.setValue(-1);
expect(wrapper.vm.value).toBe(0);
});
it('should transform the given value when it is greater than the maximum value', () => {
const wrapper = mount(VueNumberInput, {
props: {
max: 0,
},
});
expect(wrapper.vm.value).toBeNaN();
wrapper.vm.setValue(1);
expect(wrapper.vm.value).toBe(0);
});
it('should not transform the given value when the maximum value is less than the minimum value', () => {
const wrapper = mount(VueNumberInput, {
props: {
max: -10,
min: 10,
},
});
expect(wrapper.vm.value).toBeNaN();
wrapper.vm.setValue(1);
expect(wrapper.vm.value).toBe(1);
});
});
});
================================================
FILE: tests/others.spec.ts
================================================
import { mount } from '@vue/test-utils';
import VueNumberInput from '../src';
describe('others', () => {
it('should fix the `0.30000000000000004` problem', (done) => {
const wrapper = mount({
components: {
VueNumberInput,
},
data() {
return {
value: 0.1,
};
},
template: '',
});
expect(wrapper.vm.value).toBe(0.1);
wrapper.get('.vue-number-input__button--plus').trigger('click').then(() => {
expect(wrapper.vm.value).toBe(0.3);
done();
});
});
it('should update the model value when value changed', (done) => {
const wrapper = mount({
components: {
VueNumberInput,
},
data() {
return {
value: 0,
};
},
template: '',
});
expect(wrapper.vm.value).toBe(0);
wrapper.get('.vue-number-input__input').setValue(1).then(() => {
expect(wrapper.vm.value).toBe(1);
done();
});
});
it('should not update the value when paste nothing', (done) => {
const wrapper = mount({
components: {
VueNumberInput,
},
data() {
return {
value: 0,
};
},
template: '',
});
expect(wrapper.vm.value).toBe(0);
wrapper.get('.vue-number-input__input').trigger('paste').then(() => {
expect(wrapper.vm.value).toBe(0);
done();
});
});
});
================================================
FILE: tests/props.spec.ts
================================================
import { mount } from '@vue/test-utils';
import VueNumberInput from '../src';
describe('props', () => {
describe('attrs', () => {
it('should be undefined by default', () => {
const wrapper = mount(VueNumberInput);
expect(wrapper.props('attrs')).toBeUndefined();
});
it('should apply the given attributes', () => {
const wrapper = mount(VueNumberInput, {
props: {
attrs: {
tabindex: 0,
},
},
});
expect(wrapper.props('attrs')).toEqual({
tabindex: 0,
});
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).tabIndex).toBe(0);
});
});
describe('center', () => {
it('should not be center by default', () => {
const wrapper = mount(VueNumberInput);
expect(wrapper.props('center')).toBe(false);
expect(wrapper.classes()).not.toContain('vue-number-input--center');
});
it('should be center', () => {
const wrapper = mount(VueNumberInput, {
props: {
center: true,
},
});
expect(wrapper.props('center')).toBe(true);
expect(wrapper.classes()).toContain('vue-number-input--center');
});
});
describe('controls', () => {
it('should not display the controls by default', () => {
const wrapper = mount(VueNumberInput);
expect(wrapper.props('controls')).toBe(false);
expect(wrapper.classes()).not.toContain('vue-number-input--controls');
expect(wrapper.find('.vue-number-input__button').exists()).toBe(false);
});
it('should display the controls', () => {
const wrapper = mount(VueNumberInput, {
props: {
controls: true,
},
});
expect(wrapper.props('controls')).toBe(true);
expect(wrapper.classes()).toContain('vue-number-input--controls');
expect(wrapper.findAll('.vue-number-input__button').length).toBe(2);
});
it('should increase the number when click the plus control', (done) => {
const wrapper = mount(VueNumberInput, {
props: {
controls: true,
},
});
expect(wrapper.vm.value).toBeNaN();
wrapper.get('.vue-number-input__button--plus').trigger('click').then(() => {
expect(wrapper.vm.value).toBe(1);
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('1');
done();
});
});
it('should decrease the number when click the minus control', (done) => {
const wrapper = mount(VueNumberInput, {
props: {
controls: true,
},
});
expect(wrapper.vm.value).toBeNaN();
wrapper.get('.vue-number-input__button--minus').trigger('click').then(() => {
expect(wrapper.vm.value).toBe(-1);
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('-1');
done();
});
});
});
describe('disabled', () => {
it('should not be disabled by default', () => {
const wrapper = mount(VueNumberInput, {
props: {
controls: true,
},
});
expect(wrapper.props('disabled')).toBe(false);
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).disabled).toBe(false);
expect((wrapper.get('.vue-number-input__button--plus').element as HTMLButtonElement).disabled).toBe(false);
expect((wrapper.get('.vue-number-input__button--minus').element as HTMLButtonElement).disabled).toBe(false);
});
it('should by disabled', () => {
const wrapper = mount(VueNumberInput, {
props: {
controls: true,
disabled: true,
},
});
expect(wrapper.props('disabled')).toBe(true);
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).disabled).toBe(true);
expect((wrapper.get('.vue-number-input__button--plus').element as HTMLButtonElement).disabled).toBe(true);
expect((wrapper.get('.vue-number-input__button--minus').element as HTMLButtonElement).disabled).toBe(true);
});
});
describe('inline', () => {
it('should not be inline by default', () => {
const wrapper = mount(VueNumberInput);
expect(wrapper.props('inline')).toBe(false);
expect(wrapper.classes()).not.toContain('vue-number-input--inline');
});
it('should be inline', () => {
const wrapper = mount(VueNumberInput, {
props: {
inline: true,
},
});
expect(wrapper.props('inline')).toBe(true);
expect(wrapper.classes()).toContain('vue-number-input--inline');
});
});
describe('inputtable', () => {
it('should be inputtable by default', () => {
const wrapper = mount(VueNumberInput, {
props: {
controls: true,
},
});
expect(wrapper.props('inputtable')).toBe(true);
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).readOnly).toBe(false);
expect((wrapper.get('.vue-number-input__button--plus').element as HTMLButtonElement).disabled).toBe(false);
expect((wrapper.get('.vue-number-input__button--minus').element as HTMLButtonElement).disabled).toBe(false);
});
it('should not be inputtable', () => {
const wrapper = mount(VueNumberInput, {
props: {
controls: true,
inputtable: false,
},
});
expect(wrapper.props('inputtable')).toBe(false);
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).readOnly).toBe(true);
expect((wrapper.get('.vue-number-input__button--plus').element as HTMLButtonElement).disabled).toBe(false);
expect((wrapper.get('.vue-number-input__button--minus').element as HTMLButtonElement).disabled).toBe(false);
});
});
describe('max', () => {
it('should be `Infinity` by default', () => {
const wrapper = mount(VueNumberInput);
expect(wrapper.props('max')).toBe(Infinity);
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).max).toBe('Infinity');
});
it('should be equal to the given value', () => {
const wrapper = mount(VueNumberInput, {
props: {
max: 10,
},
});
expect(wrapper.props('max')).toBe(10);
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).max).toBe('10');
});
it('should not be greater than the given maximum value', () => {
const wrapper = mount(VueNumberInput, {
props: {
modelValue: 11,
max: 10,
},
});
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('10');
});
it('should fix the out of range value', () => {
const wrapper = mount(VueNumberInput, {
props: {
max: 10,
},
});
wrapper.get('.vue-number-input__input').setValue('11').then(() => {
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('10');
});
});
});
describe('min', () => {
it('should be `-Infinity` by default', () => {
const wrapper = mount(VueNumberInput);
expect(wrapper.props('min')).toBe(-Infinity);
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).min).toBe('-Infinity');
});
it('should be equal to the given value', () => {
const wrapper = mount(VueNumberInput, {
props: {
min: -10,
},
});
expect(wrapper.props('min')).toBe(-10);
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).min).toBe('-10');
});
it('should not be less than the given minimum value', () => {
const wrapper = mount(VueNumberInput, {
props: {
modelValue: -11,
min: -10,
},
});
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('-10');
});
it('should fix the out of range value', () => {
const wrapper = mount(VueNumberInput, {
props: {
min: -10,
},
});
wrapper.get('.vue-number-input__input').setValue('-11').then(() => {
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('-10');
});
});
});
describe('name', () => {
it('should be undefined by default', () => {
const wrapper = mount(VueNumberInput);
expect(wrapper.props('name')).toBeUndefined();
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).name).toBe('');
});
it('should be equal to the given value', () => {
const wrapper = mount(VueNumberInput, {
props: {
name: 'digit',
},
});
expect(wrapper.props('name')).toBe('digit');
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).name).toBe('digit');
});
});
describe('placeholder', () => {
it('should be undefined by default', () => {
const wrapper = mount(VueNumberInput);
expect(wrapper.props('placeholder')).toBeUndefined();
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).placeholder).toBe('');
});
it('should be equal to the given value', () => {
const wrapper = mount(VueNumberInput, {
props: {
placeholder: 'Number input',
},
});
expect(wrapper.props('placeholder')).toBe('Number input');
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).placeholder).toBe('Number input');
});
});
describe('readonly', () => {
it('should not be read-only by default', () => {
const wrapper = mount(VueNumberInput, {
props: {
controls: true,
},
});
expect(wrapper.props('readonly')).toBe(false);
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).readOnly).toBe(false);
expect((wrapper.get('.vue-number-input__button--plus').element as HTMLButtonElement).disabled).toBe(false);
expect((wrapper.get('.vue-number-input__button--minus').element as HTMLButtonElement).disabled).toBe(false);
});
it('should be read-only', () => {
const wrapper = mount(VueNumberInput, {
props: {
controls: true,
readonly: true,
},
});
expect(wrapper.props('readonly')).toBe(true);
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).readOnly).toBe(true);
expect((wrapper.get('.vue-number-input__button--plus').element as HTMLButtonElement).disabled).toBe(true);
expect((wrapper.get('.vue-number-input__button--minus').element as HTMLButtonElement).disabled).toBe(true);
});
});
describe('rounded', () => {
it('should not round the number by default', () => {
const wrapper = mount(VueNumberInput, {
props: {
modelValue: 1.5,
},
});
expect(wrapper.props('rounded')).toBe(false);
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('1.5');
});
it('should round the number', () => {
const wrapper = mount(VueNumberInput, {
props: {
modelValue: 1.5,
rounded: true,
},
});
expect(wrapper.props('rounded')).toBe(true);
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('2');
});
});
describe('size', () => {
it('should be small size', () => {
const wrapper = mount(VueNumberInput, {
props: {
size: 'small',
},
});
expect(wrapper.props('size')).toBe('small');
expect(wrapper.classes()).toContain('vue-number-input--small');
});
it('should be large size', () => {
const wrapper = mount(VueNumberInput, {
props: {
size: 'large',
},
});
expect(wrapper.props('size')).toBe('large');
expect(wrapper.classes()).toContain('vue-number-input--large');
});
});
describe('step', () => {
it('should be `1` by default', (done) => {
const wrapper = mount(VueNumberInput, {
props: {
controls: true,
},
});
expect(wrapper.props('step')).toBe(1);
wrapper.get('.vue-number-input__button--plus').trigger('click').then(() => {
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).step).toBe('1');
done();
});
});
it('should match the given value', (done) => {
const wrapper = mount(VueNumberInput, {
props: {
controls: true,
step: 2,
},
});
expect(wrapper.props('step')).toBe(2);
wrapper.get('.vue-number-input__button--plus').trigger('click').then(() => {
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).step).toBe('2');
done();
});
});
});
describe('modelValue', () => {
it('should be `NaN` by default', () => {
const wrapper = mount(VueNumberInput);
expect(wrapper.props('modelValue')).toBeNaN();
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('');
});
it('should be equal to the given value', () => {
const wrapper = mount(VueNumberInput, {
props: {
modelValue: 10,
},
});
expect(wrapper.props('modelValue')).toBe(10);
expect((wrapper.get('.vue-number-input__input').element as HTMLInputElement).value).toBe('10');
});
});
});
================================================
FILE: tsconfig.eslint.json
================================================
{
"extends": "./tsconfig",
"include": [
"*.js",
".*.js",
"docs/**/*",
"src/**/*",
"tests/**/*"
]
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"strict": true,
"target": "esnext"
},
"include": [
"src/**/*",
"docs/**/*",
"tests/**/*"
]
}
================================================
FILE: webpack.config.js
================================================
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/dist/plugin').default;
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = (env) => ({
mode: env.production ? 'production' : 'development',
entry: './docs',
output: {
path: path.resolve(__dirname, './docs/dist'),
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
},
{
test: /\.ts$/,
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/],
},
},
{
test: /\.vue$/,
loader: 'vue-loader',
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
],
},
{
test: /\.md$/,
use: [
'vue-loader',
{
loader: 'markdown-to-vue-loader',
options: {
componentWrapper: '',
tableClass: 'table',
tableWrapper: '',
},
},
],
},
],
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: './docs/index.html',
}),
new MiniCssExtractPlugin(),
new VueLoaderPlugin(),
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
}),
],
externals: env.production ? {
vue: 'Vue',
} : {},
resolve: {
alias: {
vue$: 'vue/dist/vue.esm-bundler',
},
extensions: ['.js', '.json', '.ts', '.d.ts', '.vue'],
},
});