Adobe

RTE Validation for AEM 6.2, The Right Way

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));
The Digital Essentials, Part 3
The Digital Essentials, Part 3

Developing a robust digital strategy is both a challenge and an opportunity. Part 3 of the Digital Essentials series explores five of the essential technology-driven experiences customers expect, which you may be missing or not fully utilizing.

Get the Guide

 
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!

About the Author

Ahmed is an Adobe Technical Lead and expert in the Adobe Experience Cloud.

More from this Author

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Subscribe to the Weekly Blog Digest:

Sign Up