commit e3e104dfe68a7d79f7e4270755461505584ea960
parent bfa15cc01632840922978d1d3f79cfcee78417ef
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date: Mon, 9 Aug 2021 11:58:17 -0700
Refactor IntInput component
Diffstat:
2 files changed, 251 insertions(+), 62 deletions(-)
diff --git a/src/components/IntInput.vue b/src/components/IntInput.vue
@@ -1,10 +1,16 @@
<template>
- <input @keydown="keydown" @keypress="keypress" v-model="stringValue">
+ <input
+ ref="input"
+ @blur="onblur"
+ @keydown="onkeydown"
+ @keypress="onkeypress"
+ v-model="stringValue">
</template>
<script>
export default {
name: 'IntInput',
+
props: {
/**
* The input value
@@ -19,10 +25,7 @@ export default {
*/
min: {
type: Number,
- default: 0,
- validator: function(value) {
- return value >= 0;
- }
+ default: null,
},
/**
@@ -31,9 +34,6 @@ export default {
max: {
type: Number,
default: null,
- validator: function(value) {
- return value >= 0;
- }
},
},
@@ -42,88 +42,146 @@ export default {
/**
* The internal value
*/
- intValue: this.value,
-
- /**
- * The value of the input
- */
- stringValue: this.value.toString(),
+ internalValue: this.value.toString(),
};
},
- watch: {
+ computed: {
/**
- * Update the internal value from the value prop
+ * The value of the input element
*/
- value: function(newValue) {
- this.stringValue = newValue;
+ stringValue: {
+ get: function() {
+ return this.internalValue;
+ },
+ set: function(newValue) {
+ // Parse new value
+ let parsedValue = this.parse(newValue);
+
+ // Allow input to be '' or '-'
+ if (newValue === '' || newValue === '-') {
+ this.internalValue = newValue;
+ }
+
+ // Enforce minimum
+ else if (this.min !== null && parsedValue < this.min) {
+ this.internalValue = this.min.toString();
+ }
+
+ // Enforce maximum
+ else if (this.max !== null && parsedValue > this.max) {
+ this.internalValue = this.max.toString();
+ }
+
+ // 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;
+ }
+ }
+ },
},
/**
- * Trigger the input event
+ * The value of the component
*/
- intValue: function(newValue) {
- this.$emit('input', newValue);
+ intValue: {
+ get: function() {
+ let parsedValue = parseInt(this.stringValue);
+ return isNaN(parsedValue) ? this.defaultValue : parsedValue;
+ },
+ set: function(newValue) {
+ this.stringValue = newValue.toString();
+ }
},
/**
- * Validate the new value
+ * The default value of the component
*/
- stringValue: function(newValue, oldValue) {
- // Parse new value
- let parsedValue = parseInt(newValue);
-
- // Make sure value is a number
- if (isNaN(parsedValue)) {
- if (newValue === '') {
- parsedValue = this.min;
- }
- else {
- parsedValue = this.intValue;
- }
+ defaultValue: function() {
+ if (0 < this.min || 0 > this.max) {
+ return this.min;
}
-
- // Enforce minimum and maximum
- else if (this.min !== null && parsedValue < this.min) {
- parsedValue = this.min;
- }
- else if (this.max !== null && parsedValue > this.max) {
- parsedValue = this.max;
+ else {
+ return 0;
}
+ }
+ },
- // Update and format string value
- if (newValue !== parsedValue.toString()) {
- this.stringValue = parsedValue.toString();
- }
+ watch: {
+ /**
+ * Update the component value when the value prop changes
+ * @param {Number} newValue The new prop value
+ */
+ value: function(newValue) {
+ this.intValue = newValue;
+ },
- // Update intValue
- this.intValue = parsedValue;
+ /**
+ * Emit the input event when the component value changes
+ * @param {Number} newValue The new component value
+ */
+ intValue: function(newValue) {
+ this.$emit('input', newValue);
},
},
methods: {
/**
* Restrict input to numbers
+ * @param {Object} e The keypress event args
*/
- keypress: function(e) {
- if (!['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].includes(e.key)) {
- /* key press was not a number */
+ onkeypress: function(e) {
+ let validKeys = ['-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
+ if (!validKeys.includes(e.key)) {
+ /* key was not a number */
e.preventDefault();
}
},
/**
* Process up and down arrow presses
+ * @param {Object} e The keydown event args
*/
- keydown: function(e) {
+ onkeydown: function(e) {
if (e.key === 'ArrowUp') {
- this.stringValue = (parseInt(this.stringValue) + 1).toString();
+ this.intValue++;
e.preventDefault();
}
else if (e.key === 'ArrowDown') {
- this.stringValue = (parseInt(this.stringValue) - 1).toString();
+ this.intValue--;
e.preventDefault();
}
+ },
+
+ /**
+ * Reformat display value
+ * @param {Object} e The blur event args
+ */
+ onblur: function(e) {
+ this.stringValue = this.intValue.toString();
+ },
+
+ /**
+ * Parse an integer from a string
+ * @param {String} value The string
+ * @returns {Number} The parsed integer
+ */
+ parse: function(value) {
+ if (value.includes('.')) {
+ // value cannot be parsed as an integer
+ return NaN;
+ }
+ else {
+ return Number(value);
+ }
}
},
}
diff --git a/tests/unit/IntInput.spec.js b/tests/unit/IntInput.spec.js
@@ -4,93 +4,224 @@ import IntInput from '@/components/IntInput.vue';
describe('IntInput.vue', () => {
it('value should be 0 by default', () => {
+ // Initialize component
const wrapper = mount(IntInput);
+
+ // Assert value is 0
expect(wrapper.find('input').element.value).to.equal('0');
});
it('should read value prop', () => {
+ // Initialize component
const wrapper = mount(IntInput, {
propsData: { value: 1 }
});
+
+ // Assert value is 1
expect(wrapper.find('input').element.value).to.equal('1');
});
it('up arrow should increment value', async () => {
+ // Initialize component
const wrapper = mount(IntInput);
+
+ // Press up arrow
await wrapper.trigger('keydown', { key: 'ArrowUp' });
+
+ // Assert value is 1 and input event was emitted
expect(wrapper.find('input').element.value).to.equal('1');
expect(wrapper.emitted().input).to.deep.equal([[1]]);
});
it('down arrow should increment value', async () => {
- const wrapper = mount(IntInput, {
- propsData: { value: 2 }
- });
+ // Initialize component
+ const wrapper = mount(IntInput);
+
+ // Press down arrow
await wrapper.trigger('keydown', { key: 'ArrowDown' });
- expect(wrapper.find('input').element.value).to.equal('1');
- expect(wrapper.emitted().input).to.deep.equal([[1]]);
+
+ // Assert value is -1 and input event was emitted
+ expect(wrapper.find('input').element.value).to.equal('-1');
+ expect(wrapper.emitted().input).to.deep.equal([[-1]]);
});
it('should fire input event when value changes', async () => {
+ // Initialize component
const wrapper = mount(IntInput);
- await wrapper.trigger('keydown', { key: 'ArrowUp' });
+
+ // Set value to 1
+ wrapper.find('input').element.value = '1';
+ await wrapper.find('input').trigger('input');
+
+ // Assert input event was emitted
expect(wrapper.emitted().input).to.deep.equal([[1]]);
});
it('should accept numerical values', async () => {
+ // Initialize component
const wrapper = mount(IntInput);
+
+ // 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]]);
});
+ it('should not accept decimal values', async () => {
+ // Initialize component
+ const wrapper = mount(IntInput, {
+ 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 not accepted and no events were emitted
+ expect(wrapper.find('input').element.value).to.equal('1');
+ expect(wrapper.emitted().input).to.be.undefined;
+ });
+
it('should not accept non numerical values', async () => {
+ // Initialize component
const wrapper = mount(IntInput, {
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');
expect(wrapper.emitted().input).to.be.undefined;
});
- it('should remove leading zeros', async () => {
+ it('should format input value on blur', async () => {
+ // Initialize component
const wrapper = mount(IntInput, {
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');
expect(wrapper.emitted().input).to.be.undefined;
});
- it('should set empty input to minimum', async () => {
+ it('should allow input to be empty until blur', async () => {
+ // Initialize component
+ const wrapper = mount(IntInput, {
+ 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]]);
+
+ // 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');
+ expect(wrapper.emitted().input).to.deep.equal([[0]]);
+ });
+
+ it('should allow input to be "-" until blur', async () => {
+ // Initialize component
+ const wrapper = mount(IntInput, {
+ 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]]);
+
+ // 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');
+ expect(wrapper.emitted().input).to.deep.equal([[0]]);
+ });
+
+ it('default value should be the minimum if 0 is not valid', async () => {
+ // Initialize component
const wrapper = mount(IntInput, {
- propsData: { value: 5, min: 2 }
+ 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');
expect(wrapper.emitted().input).to.deep.equal([[2]]);
});
it('should not allow input to be below the minimum', async () => {
+ // Initialize component
const wrapper = mount(IntInput, {
propsData: { min: 10, value: 20 }
});
+
+ // Try to set value to 9, which is below the minimum
wrapper.find('input').element.value = '9';
await wrapper.find('input').trigger('input');
+
+ // Assert value is 10 and input event was emitted
+ expect(wrapper.find('input').element.value).to.equal('10');
+ expect(wrapper.emitted().input).to.deep.equal([[10]]);
+
+ // 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');
expect(wrapper.emitted().input).to.deep.equal([[10]]);
});
it('should not allow input to be above the maximum', async () => {
+ // Initialize component
const wrapper = mount(IntInput, {
propsData: { max: 10 }
});
+
+ // Try to set value to 11, which is above the maximum
wrapper.find('input').element.value = '11';
await wrapper.find('input').trigger('input');
+
+ // Assert value is 10 and input event was emitted
+ expect(wrapper.find('input').element.value).to.equal('10');
+ expect(wrapper.emitted().input).to.deep.equal([[10]]);
+
+ // 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');
expect(wrapper.emitted().input).to.deep.equal([[10]]);
});