diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared/directives/validation.js')
-rw-r--r-- | app/assets/javascripts/vue_shared/directives/validation.js | 132 |
1 files changed, 132 insertions, 0 deletions
diff --git a/app/assets/javascripts/vue_shared/directives/validation.js b/app/assets/javascripts/vue_shared/directives/validation.js new file mode 100644 index 00000000000..09bec78edcc --- /dev/null +++ b/app/assets/javascripts/vue_shared/directives/validation.js @@ -0,0 +1,132 @@ +import { merge } from 'lodash'; +import { s__ } from '~/locale'; + +/** + * Validation messages will take priority based on the property order. + * + * For example: + * { valueMissing: {...}, urlTypeMismatch: {...} } + * + * `valueMissing` will be displayed the user has entered a value + * after that, if the input is not a valid URL then `urlTypeMismatch` will show + */ +const defaultFeedbackMap = { + valueMissing: { + isInvalid: el => el.validity?.valueMissing, + message: s__('Please fill out this field.'), + }, + urlTypeMismatch: { + isInvalid: el => el.type === 'url' && el.validity?.typeMismatch, + message: s__('Please enter a valid URL format, ex: http://www.example.com/home'), + }, +}; + +const getFeedbackForElement = (feedbackMap, el) => + Object.values(feedbackMap).find(f => f.isInvalid(el))?.message || el.validationMessage; + +const focusFirstInvalidInput = e => { + const { target: formEl } = e; + const invalidInput = formEl.querySelector('input:invalid'); + + if (invalidInput) { + invalidInput.focus(); + } +}; + +const isEveryFieldValid = form => Object.values(form.fields).every(({ state }) => state === true); + +const createValidator = (context, feedbackMap) => ({ el, reportInvalidInput = false }) => { + const { form } = context; + const { name } = el; + + if (!name) { + if (process.env.NODE_ENV === 'development') { + // eslint-disable-next-line no-console + console.warn( + '[gitlab] the validation directive requires the given input to have "name" attribute', + ); + } + return; + } + + const formField = form.fields[name]; + const isValid = el.checkValidity(); + + // This makes sure we always report valid fields - this can be useful for cases where the consuming + // component's logic depends on certain fields being in a valid state. + // Invalid input, on the other hand, should only be reported once we want to display feedback to the user. + // (eg.: After a field has been touched and moved away from, a submit-button has been clicked, ...) + formField.state = reportInvalidInput ? isValid : isValid || null; + formField.feedback = reportInvalidInput ? getFeedbackForElement(feedbackMap, el) : ''; + + form.state = isEveryFieldValid(form); +}; + +/** + * Takes an object that allows to add or change custom feedback messages. + * + * The passed in object will be merged with the built-in feedback + * so it is possible to override a built-in message. + * + * @example + * validate({ + * tooLong: { + * check: el => el.validity.tooLong === true, + * message: 'Your custom feedback' + * } + * }) + * + * @example + * validate({ + * valueMissing: { + * message: 'Your custom feedback' + * } + * }) + * + * @param {Object<string, { message: string, isValid: ?function}>} customFeedbackMap + * @returns {{ inserted: function, update: function }} validateDirective + */ +export default function(customFeedbackMap = {}) { + const feedbackMap = merge(defaultFeedbackMap, customFeedbackMap); + const elDataMap = new WeakMap(); + + return { + inserted(el, binding, { context }) { + const { arg: showGlobalValidation } = binding; + const { form: formEl } = el; + + const validate = createValidator(context, feedbackMap); + const elData = { validate, isTouched: false, isBlurred: false }; + + elDataMap.set(el, elData); + + el.addEventListener('input', function markAsTouched() { + elData.isTouched = true; + // once the element has been marked as touched we can stop listening on the 'input' event + el.removeEventListener('input', markAsTouched); + }); + + el.addEventListener('blur', function markAsBlurred({ target }) { + if (elData.isTouched) { + elData.isBlurred = true; + validate({ el: target, reportInvalidInput: true }); + // this event handler can be removed, since the live-feedback in `update` takes over + el.removeEventListener('blur', markAsBlurred); + } + }); + + if (formEl) { + formEl.addEventListener('submit', focusFirstInvalidInput); + } + + validate({ el, reportInvalidInput: showGlobalValidation }); + }, + update(el, binding) { + const { arg: showGlobalValidation } = binding; + const { validate, isTouched, isBlurred } = elDataMap.get(el); + const showValidationFeedback = showGlobalValidation || (isTouched && isBlurred); + + validate({ el, reportInvalidInput: showValidationFeedback }); + }, + }; +} |