commit ae7c32e4ad2bc7ffa6ce729f220cc9438e1c4e25
parent e3e104dfe68a7d79f7e4270755461505584ea960
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date: Mon, 9 Aug 2021 14:21:13 -0700
Implement DecimalInput component
Diffstat:
2 files changed, 455 insertions(+), 0 deletions(-)
diff --git a/src/components/DecimalInput.vue b/src/components/DecimalInput.vue
@@ -0,0 +1,195 @@
+<template>
+ <input
+ ref="input"
+ @blur="onblur"
+ @keydown="onkeydown"
+ @keypress="onkeypress"
+ v-model="stringValue">
+</template>
+
+<script>
+export default {
+ name: 'DecimalInput',
+
+ props: {
+ /**
+ * The input value
+ */
+ value: {
+ type: Number,
+ default: 0,
+ },
+
+ /**
+ * The minimum value
+ */
+ min: {
+ type: Number,
+ default: null,
+ },
+
+ /**
+ * The maximum value
+ */
+ max: {
+ type: Number,
+ default: null,
+ },
+
+ /**
+ * The number of digits to show after the decimal point
+ */
+ digits: {
+ type: Number,
+ default: 1,
+ validator: function(value) {
+ return value > 0;
+ },
+ },
+ },
+
+ data: function() {
+ return {
+ /**
+ * The internal value
+ */
+ internalValue: this.value.toFixed(this.digits),
+ };
+ },
+
+ computed: {
+ /**
+ * The value of the input element
+ */
+ stringValue: {
+ get: function() {
+ return this.internalValue;
+ },
+ set: function(newValue) {
+ // Parse new value
+ let parsedValue = this.parse(newValue);
+
+ // Allow input to be '' or '-' or '.'
+ if (newValue === '' || newValue === '-' || newValue === '.') {
+ this.internalValue = newValue;
+ }
+
+ // Enforce minimum
+ else if (this.min !== null && parsedValue < this.min) {
+ this.internalValue = this.min.toFixed(this.digits);
+ }
+
+ // Enforce maximum
+ else if (this.max !== null && parsedValue > this.max) {
+ this.internalValue = this.max.toFixed(this.digits);
+ }
+
+ // Allow valid numbers
+ else if (!isNaN(parsedValue)) {
+ this.internalValue = newValue;
+ }
+
+ // Make sure input element is updated
+ if (this.$refs.input.value === newValue) {
+ // Setter was called by the input element
+ if (this.internalValue !== newValue) {
+ // The value was corrected, so the input element must be updated
+ this.$refs.input.value = this.internalValue;
+ }
+ }
+ },
+ },
+
+ /**
+ * The value of the component
+ */
+ decValue: {
+ get: function() {
+ let parsedValue = parseFloat(this.stringValue);
+ return isNaN(parsedValue) ? this.defaultValue : parsedValue;
+ },
+ set: function(newValue) {
+ this.stringValue = newValue.toFixed(this.digits);
+ }
+ },
+
+ /**
+ * The default value of the component
+ */
+ defaultValue: function() {
+ if (0 < this.min || 0 > this.max) {
+ return this.min;
+ }
+ else {
+ return 0;
+ }
+ }
+ },
+
+ watch: {
+ /**
+ * Update the component value when the value prop changes
+ * @param {Number} newValue The new prop value
+ */
+ value: function(newValue) {
+ if (newValue !== this.decValue) {
+ this.decValue = newValue;
+ }
+ },
+
+ /**
+ * Emit the input event when the component value changes
+ * @param {Number} newValue The new component value
+ */
+ decValue: function(newValue) {
+ this.$emit('input', newValue);
+ },
+ },
+
+ methods: {
+ /**
+ * Restrict input to valid keys
+ * @param {Object} e The keypress event args
+ */
+ onkeypress: function(e) {
+ let valid = ['.', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
+ if (!valid.includes(e.key)) {
+ /* key was not valid */
+ e.preventDefault();
+ }
+ },
+
+ /**
+ * Process up and down arrow presses
+ * @param {Object} e The keydown event args
+ */
+ onkeydown: function(e) {
+ if (e.key === 'ArrowUp') {
+ this.decValue++;
+ e.preventDefault();
+ }
+ else if (e.key === 'ArrowDown') {
+ this.decValue--;
+ e.preventDefault();
+ }
+ },
+
+ /**
+ * Reformat display value
+ * @param {Object} e The blur event args
+ */
+ onblur: function(e) {
+ this.stringValue = this.decValue.toFixed(this.digits);
+ },
+
+ /**
+ * Parse a decimal from a string
+ * @param {String} value The string
+ * @returns {Number} The parsed decimal
+ */
+ parse: function(value) {
+ return Number(value);
+ }
+ },
+}
+</script>
diff --git a/tests/unit/DecimalInput.spec.js b/tests/unit/DecimalInput.spec.js
@@ -0,0 +1,260 @@
+import { expect } from 'chai';
+import { mount } from '@vue/test-utils';
+import DecimalInput from '@/components/DecimalInput.vue';
+
+describe('DecimalInput.vue', () => {
+ it('value should be 0.0 by default', () => {
+ // Initialize component
+ const wrapper = mount(DecimalInput);
+
+ // Assert value is 0.0
+ expect(wrapper.find('input').element.value).to.equal('0.0');
+ });
+
+ it('should read value prop', () => {
+ // Initialize component
+ const wrapper = mount(DecimalInput, {
+ propsData: { value: 1 }
+ });
+
+ // Assert value is 1.0
+ expect(wrapper.find('input').element.value).to.equal('1.0');
+ });
+
+ it('up arrow should increment value', async () => {
+ // Initialize component
+ const wrapper = mount(DecimalInput);
+
+ // Press up arrow
+ await wrapper.trigger('keydown', { key: 'ArrowUp' });
+
+ // Assert value is 1.0 and input event was emitted
+ expect(wrapper.find('input').element.value).to.equal('1.0');
+ expect(wrapper.emitted().input).to.deep.equal([[1.0]]);
+ });
+
+ it('down arrow should increment value', async () => {
+ // Initialize component
+ const wrapper = mount(DecimalInput);
+
+ // Press down arrow
+ await wrapper.trigger('keydown', { key: 'ArrowDown' });
+
+ // Assert value is -1.0 and input event was emitted
+ expect(wrapper.find('input').element.value).to.equal('-1.0');
+ expect(wrapper.emitted().input).to.deep.equal([[-1.0]]);
+ });
+
+ it('should fire input event when value changes', async () => {
+ // Initialize component
+ const wrapper = mount(DecimalInput);
+
+ // Set value to 1
+ wrapper.find('input').element.value = '1.0';
+ await wrapper.find('input').trigger('input');
+
+ // Assert input event was emitted
+ expect(wrapper.emitted().input).to.deep.equal([[1.0]]);
+ });
+
+ it('should accept numerical values', async () => {
+ // Initialize component
+ const wrapper = mount(DecimalInput);
+
+ // Try to set value to 1
+ wrapper.find('input').element.value = '1';
+ await wrapper.find('input').trigger('input');
+
+ // Assert value was accepted and input event was emitted
+ expect(wrapper.find('input').element.value).to.equal('1');
+ expect(wrapper.emitted().input).to.deep.equal([[1.0]]);
+ });
+
+ it('should accept decimal values', async () => {
+ // Initialize component
+ const wrapper = mount(DecimalInput, {
+ propsData: { value: 1 }
+ });
+
+ // Try to set value to 1.5
+ wrapper.find('input').element.value = '1.5';
+ await wrapper.find('input').trigger('input');
+
+ // Assert value was accepted and input event was emitted
+ expect(wrapper.find('input').element.value).to.equal('1.5');
+ expect(wrapper.emitted().input).to.deep.equal([[1.5]]);
+ });
+
+ it('should not accept non numerical values', async () => {
+ // Initialize component
+ const wrapper = mount(DecimalInput, {
+ propsData: { value: 1 }
+ });
+
+ // Try to set value to a
+ wrapper.find('input').element.value = 'a';
+ await wrapper.find('input').trigger('input');
+
+ // Assert value was not accepted and no events were emitted
+ expect(wrapper.find('input').element.value).to.equal('1.0');
+ expect(wrapper.emitted().input).to.be.undefined;
+ });
+
+ it('should format input value on blur', async () => {
+ // Initialize component
+ const wrapper = mount(DecimalInput, {
+ propsData: { value: 1 }
+ });
+
+ // Set value to '01'
+ wrapper.find('input').element.value = '01';
+ await wrapper.find('input').trigger('input');
+
+ // Assert value was not updated and no events were emitted
+ expect(wrapper.find('input').element.value).to.equal('01');
+ expect(wrapper.emitted().input).to.be.undefined;
+
+ // Trigger blur event
+ await wrapper.find('input').trigger('blur');
+
+ // Assert value was formatted but no events were emitted
+ expect(wrapper.find('input').element.value).to.equal('1.0');
+ expect(wrapper.emitted().input).to.be.undefined;
+ });
+
+ it('should allow input to be empty until blur', async () => {
+ // Initialize component
+ const wrapper = mount(DecimalInput, {
+ propsData: { value: 5 }
+ });
+
+ // Set value to ''
+ wrapper.find('input').element.value = '';
+ await wrapper.find('input').trigger('input');
+
+ // Assert value is '' and input event was emitted with default value
+ expect(wrapper.find('input').element.value).to.equal('');
+ expect(wrapper.emitted().input).to.deep.equal([[0.0]]);
+
+ // Trigger blur event
+ await wrapper.find('input').trigger('blur');
+
+ // Assert value is the default value but no new events were emitted
+ expect(wrapper.find('input').element.value).to.equal('0.0');
+ expect(wrapper.emitted().input).to.deep.equal([[0.0]]);
+ });
+
+ it('should allow input to be "-" until blur', async () => {
+ // Initialize component
+ const wrapper = mount(DecimalInput, {
+ propsData: { value: 5 }
+ });
+
+ // Set value to '-'
+ wrapper.find('input').element.value = '-';
+ await wrapper.find('input').trigger('input');
+
+ // Assert value is '-' and input event was emitted with default value
+ expect(wrapper.find('input').element.value).to.equal('-');
+ expect(wrapper.emitted().input).to.deep.equal([[0.0]]);
+
+ // Trigger blur event
+ await wrapper.find('input').trigger('blur');
+
+ // Assert value is the default value but no new events were emitted
+ expect(wrapper.find('input').element.value).to.equal('0.0');
+ expect(wrapper.emitted().input).to.deep.equal([[0.0]]);
+ });
+
+ it('should allow input to be "." until blur', async () => {
+ // Initialize component
+ const wrapper = mount(DecimalInput, {
+ propsData: { value: 5 }
+ });
+
+ // Set value to '.'
+ wrapper.find('input').element.value = '.';
+ await wrapper.find('input').trigger('input');
+
+ // Assert value is '.' and input event was emitted with default value
+ expect(wrapper.find('input').element.value).to.equal('.');
+ expect(wrapper.emitted().input).to.deep.equal([[0.0]]);
+
+ // Trigger blur event
+ await wrapper.find('input').trigger('blur');
+
+ // Assert value is the default value but no new events were emitted
+ expect(wrapper.find('input').element.value).to.equal('0.0');
+ expect(wrapper.emitted().input).to.deep.equal([[0.0]]);
+ });
+
+ it('default value should be the minimum if 0.0 is not valid', async () => {
+ // Initialize component
+ const wrapper = mount(DecimalInput, {
+ propsData: { value: 3, max: 4, min: 2 }
+ });
+
+ // Set value to '' and trigger blur event so value must be updated
+ wrapper.find('input').element.value = '';
+ await wrapper.find('input').trigger('input');
+ await wrapper.find('input').trigger('blur');
+
+ // Assert value is 2 and input event was emitted
+ expect(wrapper.find('input').element.value).to.equal('2.0');
+ expect(wrapper.emitted().input).to.deep.equal([[2.0]]);
+ });
+
+ it('should not allow input to be below the minimum', async () => {
+ // Initialize component
+ const wrapper = mount(DecimalInput, {
+ propsData: { min: 10, value: 20 }
+ });
+
+ // Try to set value to 9, which is below the minimum
+ wrapper.find('input').element.value = '9.0';
+ await wrapper.find('input').trigger('input');
+
+ // Assert value is 10 and input event was emitted
+ expect(wrapper.find('input').element.value).to.equal('10.0');
+ expect(wrapper.emitted().input).to.deep.equal([[10.0]]);
+
+ // Try to decrement value
+ await wrapper.trigger('keydown', { key: 'ArrowDown' });
+
+ // Assert value is still 10 and no new event were emitted
+ expect(wrapper.find('input').element.value).to.equal('10.0');
+ expect(wrapper.emitted().input).to.deep.equal([[10.0]]);
+ });
+
+ it('should not allow input to be above the maximum', async () => {
+ // Initialize component
+ const wrapper = mount(DecimalInput, {
+ propsData: { max: 10 }
+ });
+
+ // Try to set value to 11, which is above the maximum
+ wrapper.find('input').element.value = '11.0';
+ await wrapper.find('input').trigger('input');
+
+ // Assert value is 10 and input event was emitted
+ expect(wrapper.find('input').element.value).to.equal('10.0');
+ expect(wrapper.emitted().input).to.deep.equal([[10.0]]);
+
+ // Try to increment value
+ await wrapper.trigger('keydown', { key: 'ArrowUp' });
+
+ // Assert value is still 10 and no new events were emitted
+ expect(wrapper.find('input').element.value).to.equal('10.0');
+ expect(wrapper.emitted().input).to.deep.equal([[10.0]]);
+ });
+
+ it('should format value according to digits prop', async () => {
+ // Initialize component
+ const wrapper = mount(DecimalInput, {
+ propsData: { digits: 3 }
+ });
+
+ // Assert value is correctly formatted
+ expect(wrapper.find('input').element.value).to.equal('0.000');
+ });
+});