vue-dotnet-validator
Version:
A vuejs validator for .NET forms.
272 lines (244 loc) • 8.43 kB
JavaScript
/* global process */
import * as defaultValidators from'./validators';
import util from './util';
export default (extraValidators = {}) => {
const validators = {
...defaultValidators,
...extraValidators
}
const validationStyles = {
afterBlur: 'after-blur', // default
afterChange: 'after-change',
afterSubmit: 'after-submit'
}
const validClass = 'field-validation-valid';
let validatorGroup = null;
return {
name: 'vue-dotnet-validator',
props: {
// Value is the value that will be validated
value: {
default: ''
},
// This parameter can be used to provide additional complex validation from your app
extraErrorMessage: {
default: ''
},
validationStyle: {
default: validationStyles.afterBlur
},
prioritizeExtraErrorMessage: {
default: false,
type: Boolean
}
},
data() {
return {
validators: [],
blurred: false,
hasValidationError: false,
// This variable is used to store the current value if not in two-way mode.
localInputValue: this.value,
isTwoWayBind: false,
hasChanged: false,
hasForced: false,
hasBlurred: false,
name: '',
field: null
};
},
updated() {
this.initialize();
},
mounted() {
validatorGroup = util.findValidatorGroup(this);
validatorGroup.addValidator(this);
this.initialize();
},
destroyed() {
this.$nextTick(() => {
validatorGroup.removeValidator(this);
});
},
methods: {
initialize() {
// already initialized
if (this.field) {
return;
}
this.field = this.resolveField(this);
if(!this.field) {
if (process.env.NODE_ENV !== 'production') {
console.warn('Field is missing. This could be an error or it will resolve if the input is mounted async.', this.$el);
}
return;
}
this.name = this.field.name;
// We need to know if 2-way binding is being used so we know where to store the adjusted value.
// This check is a little bit dirty, but the only thing that works.
// Since vue handles 2-way binding through the 'input' event, we can check if there is something listening to it.
this.isTwoWayBind = this.$options._parentListeners && !!this.$options._parentListeners.input;
this.findValidators();
this.addAriaDescribedBy();
if(this.$refs.message.innerText) {
// When we already have innerText, it means the server has output a validation error.
// We need to replace that validation message as soon as the user changes the value of the input
this.hasValidationError = true;
this.blurred = true;
} else {
this.$refs.message.classList.add(validClass);
}
if(!this.isCheckbox && !this.isRadio) {
this.field.addEventListener('blur', this.blurField);
}
this.field.addEventListener('change', this.changeField);
this.field.addEventListener('input', this.changeField);
},
resolveField(component) {
if(!component) {
return null;
}
if(component.$refs.field) {
return component.$refs.field;
}
if(component.$children.length > 0) {
return component.$children.map(child => this.resolveField(child)).filter(result => !!result)[0];
}
return null;
},
blurField(event) {
if(event && event.target.value !== '') {
this.val = event.target.value;
}
this.blurred = true;
this.hasBlurred = true;
this.$emit('blur-field', this);
this.showValidationMessage(false);
},
changeField(event) {
if(event) {
if(this.isCheckbox || this.isRadio) {
this.blurred = true; // We are not using blur-event on checkbox, so lets force blurred here.
this.val = this.isCheckbox
? event.target.checked
: event.target.checked ? event.target.value : '';
} else {
this.val = event.target.value;
}
}
this.hasChanged = true;
this.$emit('change-field', this);
this.showValidationMessage(false);
},
// Initializes custom validators by looking at the attributes in the DOM.
findValidators() {
const dataAttributes = this.field.dataset;
const validatorKeys = Object.keys(validators);
validatorKeys.forEach(validatorKey => {
const sanitzedKey = validatorKey.charAt(0).toUpperCase() + validatorKey.slice(1).toLowerCase();
const validationMessage = dataAttributes['val' + sanitzedKey];
if(!validationMessage) {
// Validator should not be activated
return;
}
this.validators.push(new validators[validatorKey](validationMessage, dataAttributes, validatorGroup));
});
},
showValidationMessage(forced = false) {
if (!forced && !this.shouldValidate) {
return;
}
if (!this.$refs.message) {
return;
}
this.$refs.message.innerHTML = this.validationMessage;
if(this.validationMessage) {
this.hasValidationError = true;
return this.$refs.message.classList.remove(validClass);
}
this.hasValidationError = false;
return this.$refs.message.classList.add(validClass);
},
addAriaDescribedBy() {
// Make kind of sure that the id does not exist yet.
// No need to force this kind of stuff, in almost any case this will be enough.
const id = `vue-validator-${parseInt(Math.random()*100)}-${this._uid}`;
this.$refs.message.id = id;
this.field.setAttribute('aria-describedby', id);
this.$refs.message.setAttribute('role', 'alert');
}
},
computed: {
shouldValidate() {
if (!this.field && this._isMounted) {
console.warn('Tring to validate without a field');
}
if (this.validationStyle === validationStyles.afterBlur && !this.hasBlurred) {
return false;
}
if (this.validationStyle === validationStyles.afterSubmit && !this.hasForced) {
return false;
}
if (this.validationStyle === validationStyles.afterChange && !this.hasChanged) {
return false;
}
return true;
},
isValid() {
if (!this.field) {
return false;
}
return this.validators.filter(validator => {
return validator.isValid(this.val);
}).length === this.validators.length && !this.extraErrorMessage && !this.hasValidationError;
},
// Returns the error-message
validationMessage() {
if (!this.field) {
return 'The field was not ready yet, please try again.';
}
let message = '';
this.validators.forEach(validator => {
const valid = validator.isValid(this.val);
if(!valid && !message) {
message = validator.getMessage();
}
});
return this.prioritizeExtraErrorMessage ? this.extraErrorMessage || message : message || this.extraErrorMessage;
},
// This is the internally used value
val: {
get() {
if(this.isTwoWayBind) {
return this.value;
}
return this.localInputValue;
},
set(value) {
this.hasChanged = true;
if(this.isTwoWayBind) {
// Two-way binding requires to emit an event in vue 2.x
return this.$emit('input', value);
}
return this.localInputValue = value;
}
},
isCheckbox() {
return this.field && this.field.type == 'checkbox';
},
isRadio() {
return this.field && this.field.type == 'radio';
}
},
watch: {
isValid() {
if(this.field && this.shouldValidate) {
this.field.setAttribute('aria-invalid', !this.isValid);
}
},
validationMessage() {
this.showValidationMessage();
}
}
}
};