I’ve looked far and wide for an RTE validation solution that is straightforward and easy to share between teams. The examples I found online seemed like patches or half-solutions that would break in the next AEM version, or when used in complex dialogs. So I set out to write one myself following my newly acquired Javascript skills. And here’s the result: An RTE validation registry that makes it simple for teams to register RTE validations just like they would a text field validation following the Granite foundation validator for AEM 6.2.
The code in this post is relevant to and tested on AEM 6.2.
Download the clientlib code as a zip package from here: rte-validator
Here is the code:
I won’t get into explaining the code as it is fully documented (jsDoc) and should be easy to follow by any Javascript developer.
The most important thing to keep in mind is that this code uses foundation validator. Please take the time to read that documentaton.
/* jshint undef: true, unused: true, esversion:5, node: true */ /* globals Coral, window, $ */ /**@author Ahmed Musallam */ (function ($window) { var foundationRegistry = 'foundation-registry'; var foundationValidator = 'foundation.validation.validator'; var foundationSelector = 'foundation.validation.selector'; var validationTooltipDataAttr = 'rte-validation.error.tooltip'; var validationErrorDataAttr = 'rte-validation.error'; // coral ui util class to hide elements https://docs.adobe.com/docs/en/aem/6-2/develop/ref/coral-ui/styles/#screen-readerOnly var screenReaderClass = 'u-coral-screenReaderOnly'; /** * a helper that set's the field to coral ui invalid * for AEM 6.2, this sets the is-invalid attribute * @param {Element | jQuery} el the element, can be an Element or jQuery * @param {Boolean} invalid true, set to invalid. false, set to valid */ function _setInvalid(el, invalid){ if(!el) return; var $el = el.length ? el : $(el); // find non hidden form fields $el = $el.find('.coral-Form-field:not(:hidden)'); var fieldAPI = $el.adaptTo('foundation-field'); // set the field to invalid if (fieldAPI && fieldAPI.setInvalid) fieldAPI.setInvalid(invalid); // if we cant, show warning else console.warn('cannot use foundation field api for this element', $el); } /** * a helper to show/hide the field info icon and tooltip * @param {Boolean} show true to show the fieldinfo, false to hide */ function _toggleFieldInfo(field, show){ if(show) field.nextAll('.coral-Form-fieldinfo').removeClass(screenReaderClass); else field.nextAll('.coral-Form-fieldinfo').addClass(screenReaderClass); } /** * adds/removes the error ui * mostly the same as: * http://localhost:4502/libs/granite/ui/components/coral/foundation/clientlibs/foundation/js/coral/validations.js * @param {Element} element the element on which the validation is hapening * @param {String} message the error message to show * @return void */ function _showRteError(element, message){ var el = $(element); var field = el.closest('.richtext-container'); // set the field to invalid (adds the red border) _setInvalid(field, true); // hide the fieldinfo element _toggleFieldInfo(field, false); var tooltip; var error = field.data(validationErrorDataAttr); // if we already set the error in the data attribue, retrieve and show if (error) { tooltip = $(error).data(validationTooltipDataAttr); tooltip.content.innerHTML = message; if (!error.parentNode) { field.after(error, tooltip); } } // if this is the first time we validate, create and add errors else { // create error icon error = new Coral.Icon(); error.icon = 'alert'; error.classList.add('coral-Form-fielderror'); // create tooltip tooltip = new Coral.Tooltip(); tooltip.variant = 'error'; tooltip.placement = field.closest('form').hasClass('coral-Form--vertical') ? 'left' : 'bottom'; tooltip.target = error; tooltip.content.innerHTML = message; // set the error and tooltip as data attributes for later use $(error).data(validationTooltipDataAttr, tooltip); field.data(validationErrorDataAttr, error); // add error and tooltip to the ui field.after(error, tooltip); } } /** * Clears error ui (for when the field is valid) * @param {*} element the element on which the validation is hapening * @return void */ function _clearRteError(element){ var el = $(element); var field = el.closest('.richtext-container'); // set the field to valid (removes the red border) _setInvalid(field, false); var error = field.data(validationErrorDataAttr); if (error) { var tooltip = $(error).data(validationTooltipDataAttr); // hide and remove both tooltip and error icon tooltip.hide(); tooltip.remove(); error.remove(); } // show the fieldinfo element _toggleFieldInfo(field, false); } /** * A helper method to register a selector * Add the data attribue selector to foundation submittable * This just means that elements with the data attribute now can be validated * This is needed because in the case of RTE, the field is hidden and hidden * fields are excluded from validation by default. * @param selector the selector to register */ function _registerSelector(selector){ $window.adaptTo(foundationRegistry).register(foundationSelector, { submittable: selector, candidate: selector, exclusion: '' }); } /** * Check if variable is a function * credit: https://stackoverflow.com/questions/5999998/how-can-i-check-if-a-javascript-variable-is-function-type * @param {*} functionToCheck */ function _isFunction(functionToCheck) { var getType = {}; return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; } /** Clear Fnction @name Clear @function @param {Element} element the element on which validation is hapening; @param {FoundationValidationValidatorContext} ctx */ /** Show Function @name Show @function @param {Element} element the element on which validation is hapening; @param {string} message the message returned from validate @param {FoundationValidationValidatorContext} ctx */ /** * RteValidator type def * @typedef {Object} RteValidator * @property {string|Functon} selector: Only the element satisfying the selector will be validated using this validator. * @property {Function} validate: The actual validation function. It must return a string of error message if the element fails. * @property {Clear} beforeClear: optional hook function to be executed before the clear function * @property {Clear} afterClear: optional hook function to be executed after the clear function * @property {Show} beforeShow: optional hook function to be executed before the clear function * @property {Show} afterShow: optional hook function to be executed after the clear function */ /** * a function to register an RTE validator * @param {*} attribute the attrubute to use for validation * @param {RteValidator} validator the validator object documented here: https://docs.adobe.com/docs/en/aem/6-2/develop/ref/granite-ui/api/jcr_root/libs/granite/ui/components/coral/foundation/clientlibs/foundation/js/validation/index.html#validator */ function registerRteValidator(validator){ if(!validator){ console.error("cannot register an empty validator"); } // first register the selector so we can use it for validation. _registerSelector(validator.selector); var rteValidator = { selector: validator.selector, validate: validator.validate, show: function(element, message, ctx){ if(_isFunction(validator.beforeShow)) validator.beforeShow(element, message, ctx); _showRteError(element, message, ctx); if(_isFunction(validator.afterShow)) validator.afterShow(element, message, ctx); }, clear: function(element, ctx){ if(_isFunction(validator.beforeClear)) validator.beforeClear(element, ctx); _clearRteError(element, ctx); if(_isFunction(validator.afterClear)) validator.afterClear(element, ctx); } }; /** * register the validator */ $window.adaptTo(foundationRegistry).register(foundationValidator, rteValidator); } // expose the window.customValidator = window.customValidator || {}; window.customValidator = window.customValidator || {}; window.customValidator.registerRteValidator = registerRteValidator; })($(window));
Now, to the fun part, let’s say you want to add a validation that makes RTE field required
(function () { // register an RTE validator to make RTE required window.customValidator.registerRteValidator({ selector: '[data-rte-required]', validate: function (element) { // if there is a value, return if ($(element).val()) return; // no value, return error message else return 'This field is required.'; } }); })();
All code above needs to be in a clientlib with the following categories:
categories=”[granite.ui.foundation,cq.authoring.dialog]”
Since the selector we chose is data-rte-required
we need to somehow add that attribute on the RTE in our dialog. Luckily, RTE granite UI widget adds the data attribute for any unknown attribute added to the field. So all we have to do is add rte-required
to the RTE field as follows:
and voila! You can create all sorts of complex validations like that without having to worry about showing or hiding error UI and messages.
When trying to submit an empty field, you’ll get the following dialog validation:
Additionally, I have added optional function hooks that can be provided and executed before and after the show/clear methods of the granite validator:
beforeClear: optional hook function to be executed before the clear function
afterClear: optional hook function to be executed after the clear function
beforeShow: optional hook function to be executed before the show function
afterShow: optional hook function to be executed after the show function
Let’s look at an example of how to use those hooks. The following code snippet is the same required validation above but with added hook methods:
(function () { window.customValidator.registerRteValidator({ selector: '[data-rte-required]', validate: function (element) { var rteValue = $(element).val(); if(rteValue.indexOf('simple') < 0){ return "the word 'simple' must be in the field" } }, beforeShow: function(element, message){ console.log("this message is printed BEFORE showing error UI")}, afterShow: function(element, message){ console.log("this message is printed AFTER showing error UI")}, beforeClear: function(element){ console.log("this message is printed BEFORE hiding error UI")}, afterClear: function(element){ console.log("this message is printed AFTER hiding error UI")} }); })();
Cheers!