115 lines
3.2 KiB
JavaScript
115 lines
3.2 KiB
JavaScript
/*
|
|
* 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, Date, Array],
|
|
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;
|
|
if (!error){
|
|
this.ackErrors = null;
|
|
} else if (typeof error === 'string'){
|
|
this.ackErrors = error;
|
|
} else {
|
|
this.ackErrors = 'Something went wrong'
|
|
console.error(error);
|
|
}
|
|
},
|
|
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();
|
|
},
|
|
};
|