Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions modules/system/assets/css/snowboard.extras.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion modules/system/assets/js/build/system.debug.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion modules/system/assets/js/build/system.js

Large diffs are not rendered by default.

14 changes: 10 additions & 4 deletions modules/system/assets/js/snowboard/ajax/Request.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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;
}

/**
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

213 changes: 213 additions & 0 deletions modules/system/assets/js/snowboard/extras/FormValidation.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 2 additions & 0 deletions modules/system/assets/js/snowboard/snowboard.extras.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
11 changes: 0 additions & 11 deletions modules/system/assets/less/snowboard.extras.less
Original file line number Diff line number Diff line change
Expand Up @@ -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
// --------------------------------------------------
Expand Down