/* * Mixin to handle inputs that update the database. * Won't bash the field's value while it's focused, even if the database trims * or otherwise sanitizes the data captured. * * Emits a change event that requires acknowledgement with an optional error * message if something went wrong */ import { debounce } from 'lodash'; export default { inheritAttrs: false, data(){ return { error: false, ackErrors: null, focused: false, loading: false, dirty: false, safeValue: this.value, inputValue: this.value, };}, props: { value: [String, Number], debounceTime: { type: Number, default: 750, }, errorMessages: [String, Array], }, watch: { focused(newFocus){ // If the value updated while we were focused, show it now on defocus // but not if we are waiting for our own writes to get persisted // and not if there is an error in our input if (!newFocus && !this.dirty && !this.error){ this.forceSafeValueUpdate(); } // Start the loading bar on defocus if the input is dirty // It might be a lie, we aren't doing the work yet, but it feels laggy // to defocus an element and then it starts working after a delay if (!newFocus && this.dirty){ if (this.hasChangeListener) this.loading = true; } }, dirty(newDirty){ // Our changes were acknowledged, weren't in error, and we aren't focused, // make sure the internal value matches the database value if (!newDirty && !this.focused && !this.error){ this.forceSafeValueUpdate(); } }, value(newValue){ if (!this.focused){ this.safeValue = newValue; } }, safeValue(newSafeValue){ // The safe value only gets updated from the parent, so it must be valid this.error = false; this.ackErrors = null; }, }, methods: { input(val){ this.$emit('input', val); this.inputValue = val; this.dirty = true; this.debouncedChange(val); }, acknowledgeChange(error){ this.loading = false; this.dirty = false; this.error = !!error; this.ackErrors = error || null; }, change(val){ this.dirty = true; if (this.hasChangeListener) this.loading = true; this.$emit('change', val, this.acknowledgeChange); }, hasChangeListener(){ return this.$listeners && this.$listeners.change }, forceSafeValueUpdate(){ // hack to force the value to update on the child component this.safeValue = null this.$nextTick(() => this.safeValue = this.value); }, }, computed: { errors(){ let errors = this.ackErrors ? [this.ackErrors] : []; if (Array.isArray(this.errorMessages)){ errors.push(...this.errorMessages); } else if (typeof this.errorMessages === 'string' && this.errorMessages){ errors.push(this.errorMessages); } return errors; }, }, created(){ this.debouncedChange = debounce(this.change, this.debounceTime); }, beforeDestroy(){ this.debouncedChange.flush(); }, };