mirror of
https://github.com/wintercms/winter.git
synced 2024-06-28 05:33:29 +02:00
Add AJAX form validation to Snowboard (#777)
Refs:
- https://wintercms.com/docs/snowboard/extras#ajax-validation
- Workshop Theme examples: 431e761c05
This commit is contained in:
parent
9b2282b280
commit
1ee713afae
@ -30,8 +30,6 @@ body>div.flash-message.error{background:#bb380c}
|
||||
body>div.flash-message.warning{background:#b87410}
|
||||
body>div.flash-message.info{background:#3f91cc}
|
||||
@media (max-width:768px){body>div.flash-message{left:10px;right:10px;top:10px;margin-left:0;width:auto}}
|
||||
[data-request][data-request-validate] [data-validate-for]:not(.visible),
|
||||
[data-request][data-request-validate] [data-validate-error]:not(.visible){display:none}
|
||||
a.wn-loading:after,
|
||||
button.wn-loading:after,
|
||||
span.wn-loading:after,
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -503,12 +503,14 @@ class Request extends Snowboard.PluginBase {
|
||||
if (error instanceof Error) {
|
||||
this.processErrorMessage(error.message);
|
||||
} else {
|
||||
let skipError = false;
|
||||
|
||||
// Process validation errors
|
||||
if (error.X_WINTER_ERROR_FIELDS) {
|
||||
this.processValidationErrors(error.X_WINTER_ERROR_FIELDS);
|
||||
skipError = this.processValidationErrors(error.X_WINTER_ERROR_FIELDS);
|
||||
}
|
||||
|
||||
if (error.X_WINTER_ERROR_MESSAGE) {
|
||||
if (error.X_WINTER_ERROR_MESSAGE && !skipError) {
|
||||
this.processErrorMessage(error.X_WINTER_ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
@ -626,12 +628,16 @@ class Request extends Snowboard.PluginBase {
|
||||
processValidationErrors(fields) {
|
||||
if (typeof this.options.handleValidationErrors === 'function') {
|
||||
if (this.options.handleValidationErrors.apply(this, [this.form, fields]) === false) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow plugins to cancel the validation errors being handled
|
||||
this.snowboard.globalEvent('ajaxValidationErrors', this.form, fields, this);
|
||||
if (this.snowboard.globalEvent('ajaxValidationErrors', this.form, fields, this) === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
213
modules/system/assets/js/snowboard/extras/FormValidation.js
Normal file
213
modules/system/assets/js/snowboard/extras/FormValidation.js
Normal file
@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Adds AJAX-driven form validation to Snowboard requests.
|
||||
*
|
||||
* Documentation for this feature can be found here:
|
||||
* https://wintercms.com/docs/snowboard/extras#ajax-validation
|
||||
*
|
||||
* @copyright 2022 Winter.
|
||||
* @author Ben Thomson <git@alfreido.com>
|
||||
*/
|
||||
export default class FormValidation extends Snowboard.Singleton {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
construct() {
|
||||
this.errorBags = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines listeners.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
listens() {
|
||||
return {
|
||||
ready: 'ready',
|
||||
ajaxStart: 'clearValidation',
|
||||
ajaxValidationErrors: 'doValidation',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ready event handler.
|
||||
*/
|
||||
ready() {
|
||||
this.collectErrorBags(document);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves validation errors from an AJAX response and passes them through to the error bags.
|
||||
*
|
||||
* This handler returns false to cancel any further validation handling, and prevents the flash
|
||||
* message that is displayed by default for field errors in AJAX requests from showing.
|
||||
*
|
||||
* @param {HTMLFormElement} form
|
||||
* @param {Object} invalidFields
|
||||
* @param {Request} request
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
doValidation(form, invalidFields, request) {
|
||||
if (request.element.dataset.requestValidate === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (!form) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const errorBags = this.errorBags.filter((errorBag) => errorBag.form === form);
|
||||
errorBags.forEach((errorBag) => {
|
||||
this.showErrorBag(errorBag, invalidFields);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any validation errors in the given form.
|
||||
*
|
||||
* @param {Promise} promise
|
||||
* @param {Request} request
|
||||
* @returns {void}
|
||||
*/
|
||||
clearValidation(promise, request) {
|
||||
if (request.element.dataset.requestValidate === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!request.form) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errorBags = this.errorBags.filter((errorBag) => errorBag.form === request.form);
|
||||
errorBags.forEach((errorBag) => {
|
||||
this.hideErrorBag(errorBag);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects error bags (elements with "data-validate-error" attribute) and links them to a
|
||||
* placeholder and form.
|
||||
*
|
||||
* The error bags will be initially hidden, and will only show when validation errors occur.
|
||||
*
|
||||
* @param {HTMLElement} rootNode
|
||||
*/
|
||||
collectErrorBags(rootNode) {
|
||||
rootNode.querySelectorAll('[data-validate-error], [data-validate-for]').forEach((errorBag) => {
|
||||
const form = errorBag.closest('form[data-request-validate]');
|
||||
|
||||
// If this error bag does not reside within a validating form, remove it
|
||||
if (!form) {
|
||||
errorBag.parentNode.removeChild(errorBag);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find message list node, if available
|
||||
let messageListElement = null;
|
||||
if (errorBag.matches('[data-validate-error]')) {
|
||||
messageListElement = errorBag.querySelector('[data-message]');
|
||||
}
|
||||
|
||||
// Create a placeholder node
|
||||
const placeholder = document.createComment('');
|
||||
|
||||
// Register error bag and replace with placeholder
|
||||
const errorBagData = {
|
||||
element: errorBag,
|
||||
form,
|
||||
validateFor: (errorBag.dataset.validateFor)
|
||||
? errorBag.dataset.validateFor.split(/\s*,\s*/)
|
||||
: '*',
|
||||
placeholder,
|
||||
messageListElement: (messageListElement)
|
||||
? messageListElement.cloneNode(true)
|
||||
: null,
|
||||
messageListAnchor: null,
|
||||
customMessage: (errorBag.dataset.validateFor)
|
||||
? (errorBag.textContent !== '' || errorBag.childNodes.length > 0)
|
||||
: false,
|
||||
};
|
||||
|
||||
// If an message list element exists, create another placeholder to act as an anchor point
|
||||
if (messageListElement) {
|
||||
const messageListAnchor = document.createComment('');
|
||||
messageListElement.parentNode.replaceChild(messageListAnchor, messageListElement);
|
||||
errorBagData.messageListAnchor = messageListAnchor;
|
||||
}
|
||||
|
||||
errorBag.parentNode.replaceChild(placeholder, errorBag);
|
||||
|
||||
this.errorBags.push(errorBagData);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides an error bag, replacing the error messages with a placeholder node.
|
||||
*
|
||||
* @param {Object} errorBag
|
||||
*/
|
||||
hideErrorBag(errorBag) {
|
||||
if (errorBag.element.isConnected) {
|
||||
errorBag.element.parentNode.replaceChild(errorBag.placeholder, errorBag.element);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an error bag with the given invalid fields.
|
||||
*
|
||||
* @param {Object} errorBag
|
||||
* @param {Object} invalidFields
|
||||
*/
|
||||
showErrorBag(errorBag, invalidFields) {
|
||||
if (!this.errorBagValidatesField(errorBag, invalidFields)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!errorBag.element.isConnected) {
|
||||
errorBag.placeholder.parentNode.replaceChild(errorBag.element, errorBag.placeholder);
|
||||
}
|
||||
|
||||
if (errorBag.validateFor !== '*') {
|
||||
if (!errorBag.customMessage) {
|
||||
const firstField = Object.keys(invalidFields)
|
||||
.filter((field) => errorBag.validateFor.includes(field))
|
||||
.shift();
|
||||
[errorBag.element.innerHTML] = invalidFields[firstField];
|
||||
}
|
||||
} else if (errorBag.messageListElement) {
|
||||
// Remove previous error messages
|
||||
errorBag.element.querySelectorAll('[data-validation-message]').forEach((message) => {
|
||||
message.parentNode.removeChild(message);
|
||||
});
|
||||
|
||||
Object.entries(invalidFields).forEach((entry) => {
|
||||
const [, errors] = entry;
|
||||
|
||||
errors.forEach((error) => {
|
||||
const messageElement = errorBag.messageListElement.cloneNode(true);
|
||||
messageElement.dataset.validationMessage = '';
|
||||
messageElement.innerHTML = error;
|
||||
errorBag.messageListAnchor.after(messageElement);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
[errorBag.element.innerHTML] = invalidFields[Object.keys(invalidFields).shift()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given error bag applies for the given invalid fields.
|
||||
*
|
||||
* @param {Object} errorBag
|
||||
* @param {Object} invalidFields
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
errorBagValidatesField(errorBag, invalidFields) {
|
||||
if (errorBag.validateFor === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Object.keys(invalidFields)
|
||||
.filter((field) => errorBag.validateFor.includes(field))
|
||||
.length > 0;
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import Flash from './extras/Flash';
|
||||
import FlashListener from './extras/FlashListener';
|
||||
import FormValidation from './extras/FormValidation';
|
||||
import Transition from './extras/Transition';
|
||||
import AttachLoading from './extras/AttachLoading';
|
||||
import StripeLoader from './extras/StripeLoader';
|
||||
@ -18,6 +19,7 @@ if (window.Snowboard === undefined) {
|
||||
Snowboard.addPlugin('transition', Transition);
|
||||
Snowboard.addPlugin('flash', Flash);
|
||||
Snowboard.addPlugin('flashListener', FlashListener);
|
||||
Snowboard.addPlugin('formValidation', FormValidation);
|
||||
Snowboard.addPlugin('attachLoading', AttachLoading);
|
||||
Snowboard.addPlugin('stripeLoader', StripeLoader);
|
||||
})(window.Snowboard);
|
||||
|
@ -153,17 +153,6 @@ body > div.flash-message {
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Form Validation
|
||||
// --------------------------------------------------
|
||||
|
||||
[data-request][data-request-validate] [data-validate-for],
|
||||
[data-request][data-request-validate] [data-validate-error] {
|
||||
&:not(.visible) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Element Loader
|
||||
// --------------------------------------------------
|
||||
|
Loading…
x
Reference in New Issue
Block a user