diff options
Diffstat (limited to 'app/assets/javascripts/gl_field_errors.js.es6')
-rw-r--r-- | app/assets/javascripts/gl_field_errors.js.es6 | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6 new file mode 100644 index 00000000000..8657e7b4abf --- /dev/null +++ b/app/assets/javascripts/gl_field_errors.js.es6 @@ -0,0 +1,167 @@ +((global) => { + /* + * This class overrides the browser's validation error bubbles, displaying custom + * error messages for invalid fields instead. To begin validating any form, add the + * class `show-gl-field-errors` to the form element, and ensure error messages are + * declared in each inputs' title attribute. + * + * Example: + * + * <form class='show-gl-field-errors'> + * <input type='text' name='username' title='Username is required.'/> + *</form> + * + * */ + + const errorMessageClass = 'gl-field-error'; + const inputErrorClass = 'gl-field-error-outline'; + + class GlFieldError { + constructor({ input, formErrors }) { + this.inputElement = $(input); + this.inputDomElement = this.inputElement.get(0); + this.form = formErrors; + this.errorMessage = this.inputElement.attr('title') || 'This field is required.'; + this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${ this.errorMessage }</p>`); + + this.state = { + valid: false, + empty: true + }; + + this.initFieldValidation(); + } + + initFieldValidation() { + // hidden when injected into DOM + this.inputElement.after(this.fieldErrorElement); + this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this)); + this.scopedSiblings = this.safelySelectSiblings(); + } + + safelySelectSiblings() { + // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled with input validity + const ignoreSelector = '.validation-ignore'; + const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreSelector})`); + const parentContainer = this.inputElement.parent('.form-group'); + + // Only select siblings when they're scoped within a form-group with one input + const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1; + + return safelyScoped ? unignoredSiblings : this.fieldErrorElement; + } + + renderValidity() { + this.renderClear(); + + if (this.state.valid) { + return this.renderValid(); + } + + if (this.state.empty) { + return this.renderEmpty(); + } + + if (!this.state.valid) { + return this.renderInvalid(); + } + + } + + handleInvalidSubmit(event) { + event.preventDefault(); + const currentValue = this.accessCurrentValue(); + this.state.valid = false; + this.state.empty = currentValue === ''; + + this.renderValidity(); + this.form.focusOnFirstInvalid.apply(this.form); + // For UX, wait til after first invalid submission to check each keyup + this.inputElement.off('keyup.field_validator') + .on('keyup.field_validator', this.updateValidity.bind(this)); + + } + + /* Get or set current input value */ + accessCurrentValue(newVal) { + return newVal ? this.inputElement.val(newVal) : this.inputElement.val(); + } + + getInputValidity() { + return this.inputDomElement.validity.valid; + } + + updateValidity() { + const inputVal = this.accessCurrentValue(); + this.state.empty = !inputVal.length; + this.state.valid = this.getInputValidity(); + this.renderValidity(); + } + + renderValid() { + return this.renderClear(); + } + + renderEmpty() { + return this.renderInvalid(); + } + + renderInvalid() { + this.inputElement.addClass(inputErrorClass); + this.scopedSiblings.hide(); + return this.fieldErrorElement.show(); + } + + renderClear() { + const inputVal = this.accessCurrentValue(); + if (!inputVal.split(' ').length) { + const trimmedInput = inputVal.trim(); + this.accessCurrentValue(trimmedInput); + } + this.inputElement.removeClass(inputErrorClass); + this.scopedSiblings.hide(); + this.fieldErrorElement.hide(); + } + } + + const customValidationFlag = 'no-gl-field-errors'; + + class GlFieldErrors { + constructor(form) { + this.form = $(form); + this.state = { + inputs: [], + valid: false + }; + this.initValidators(); + } + + initValidators () { + // select all non-hidden inputs in form + this.state.inputs = this.form.find(':input:not([type=hidden])').toArray() + .filter((input) => !input.classList.contains(customValidationFlag)) + .map((input) => new GlFieldError({ input, formErrors: this })); + + this.form.on('submit', this.catchInvalidFormSubmit); + } + + /* Neccessary to prevent intercept and override invalid form submit + * because Safari & iOS quietly allow form submission when form is invalid + * and prevents disabling of invalid submit button by application.js */ + + catchInvalidFormSubmit (event) { + if (!event.currentTarget.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + } + + focusOnFirstInvalid () { + const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0]; + firstInvalid.inputElement.focus(); + } + } + + global.GlFieldErrors = GlFieldErrors; + +})(window.gl || (window.gl = {})); |