Creating a form with validation and tooltips

Last modified by Alexander Colesnicov on 2020/01/28

This sample shows how to create a form with validation and tooltips. It demonstrates regular expression validation as well as a more complex validation using a groovy script.

Download the sample in XAR format.

How does my form look like when validation errors are shown

formValidationEdit.png

Sample Pages

The following pages are used:

  • ValidationSample.WebHome Home page where you can find this documentation
  • ValidationSample.ValidationSampleClass Class with definitions of fields, regular expressions and error message translations strings
  • ValidationSample.ValidationSampleSheet Sheet presenting the document in create, edit and view mode including validation error messages
  • ValidationSample.ValidationSampleTemplate Template of a document
  • ValidationSample.ValidationGroovy Groovy validation script for complex validations
  • ValidationSample.Translations Translations of the texts, tooltips and error messages. This shows an example of the naming conventions for tooltips and pretty names
  • ValidationSample.Val, ValidationSample.Val_0 and ValidationSample.Val_1 Sample documents

How to create validations using regular expressions

To create validation first you need to define your class and set the regular expression to validate the fields you want to validate. 

Then, to perform validation after a standard "Save" in the form, following code is needed:

<input type="hidden" name="xvalidate" value="1" />

The code above is sufficient to perform validation with regular expressions defined in the Class properties.

Pay attention to the Validation Regular Expression and Validation Message fields. The first one is a Java Regular Expression pattern and the second one is a translation string. For the sample class we created we have:

  • first_name
    /^.{2,20}$/ -> this field needs to be between 2 characters and 20 characters. If the field can have new lines, enable the dotall mode by adding an s at the end of the regex (/^.{2,20}$/s), otherwise a regex that contains a new line will not pass validation.
    val_firstname_toolong -> XWiki will lookup this translation string in the translations pages
  • last_name
    /^.{2,20}$/ -> this field needs to be between 2 and 20 characters.
  • email
    /.*@.*.com$/ -> this field must contain @ and finish with .com
  • age
     no validation set for age. This will be handled by the groovy script
  • usphone
    /^[0-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$/ -> the phone number must be made of digits separated by - in the form 000-000-0000

Other Validation Regular Expression examples:

  • do not match XWiki.XWikiGuest, but allow it to be inside the string: /^(?!XWiki.XWikiGuest$).*/
  • forbid XWiki.XWikiGuest anywhere in the string: /^(?!.*XWiki.XWikiGuest).*/

formValidationClass.png

To trigger the validation dynamically, the following Velocity code should be called:

## this will launch a validation on a document. All errors are added to the context.
$doc.validate()

How to create validations using a groovy script

To create complex validations you have to use a Groovy script. 

Invoking Groovy script

Groovy validation script can be invoked using two approaches:

  1. Using HTML code in the form, the validation on a standard "Save" of a document:
    ## Force server side validation.
    <input type="hidden" name="xvalidate" value="1" />
    ## Specify the page that holds the Groovy script that should be used for validation.
    <input type="hidden" name="xvalidation" value="ValidationSample.ValidationGroovy" />
  2. Or dynamically using Velocity code:
    ## set the page, which Groovy script will be used for validation
    $doc.setValidationScript("ValidationSample.ValidationGroovy")
    ## invoke document validation.
    $doc.validate()

After document validation all errors are added to the context.

Groovy script sample

Here is the sample groovy script:

Do not use the {{groovy}} macro when creating your script, just paste your code in the wiki editor.

import com.xpn.xwiki.validation.*;
import com.xpn.xwiki.*;
import com.xpn.xwiki.doc.*;
import com.xpn.xwiki.objects.*;

public class Val implements XWikiValidationInterface  {
   public boolean validateDocument(XWikiDocument doc, XWikiContext context) {
     // You can log in the app server output to check your code
     // System.out.println("validation is called");
     def res = true;
      def obj = doc.getObject("ValidationSample.ValidationSampleClass");
      def first_name = obj.getStringValue("first_name");
      def last_name = obj.getStringValue("last_name");
      def age = obj.getIntValue("age");
     // You can log in the app server output to check your code
     // System.out.println("Age: " + age);
     // System.out.println("First name: " + first_name);
     // System.out.println("Last name: " + last_name);
  
     if (first_name.equals(last_name)) {
       // You can log in the app server output to check your code
       // System.out.println("firstname");
       // This stores the validation error message. The translation string is "val_firstname_lastname"
       XWikiValidationStatus.addErrorToContext("ValidationSample.ValidationSampleClass", "", "", "val_firstname_lastname", context);
        res = false;
     }
     if (age<20 || age>24) {
       // You can log in the app server output to check your code
       // System.out.println("age");
       // This stores the validation error message. The translation string is "val_age_incorrect"
       XWikiValidationStatus.addErrorToContext("ValidationSample.ValidationSampleClass", "age", "Age", "val_age_incorrect", context);
        res = false;
     }
     return res;
   }
   public boolean validateObject(BaseObject object, XWikiContext context) {
     return true;
   }
}

How to display validation error messages

The sheet can access the validation error messages using:

#foreach ($error in $xcontext.validationStatus.errors)
  <p class="text-danger">$services.localization.render($error)</p>
#end
#foreach ($exception in $xcontext.validationStatus.exceptions)
  <p class="text-danger">$exception</p>
#end

Display the validation error messages next to the field

For a given field (e.g. first name) you can show the validation error message using:

#set ($fieldName = 'first_name')
#set ($xclass = $xwiki.getDocument('ValidationSample.ValidationSampleClass').xWikiClass)
#set ($fieldDefinition = $xclass.get($fieldName))
#set ($validationMessage = $fieldDefinition.getValue('validationMessage'))
#set ($hasError = $xcontext.validationStatus.errors.contains($validationMessage))
#if ($hasError)
  <p class="text-danger">$services.localization.render($validationMessage)</p>
#end

How to display the field pretty name with tooltip

We can use Bootstrap to show a tooltip after the field pretty name.

#set ($fieldName = 'first_name')
#set ($localClassReference = 'ValidationSample.ValidationSampleClass')
#set ($mandatory = true)
<dt>
  <label for="${localClassReference}_0_$fieldName">
    $doc.displayPrettyName($fieldName)##
    #if ($mandatory && $xcontext.action == 'edit')
      <sup class="text-danger">*</sup>
    #end
  </label>
  #set ($tooltipKey = "${localClassReference}_${fieldName}_tooltip")
  #if ($services.localization.get($tooltipKey) && $xcontext.action == 'edit')
    <a href="#tooltip" data-toggle="popover" data-trigger="focus"
        data-content="$escapetool.xml($services.localization.render($tooltipKey))">
      $services.icon.renderHTML('info')
    </a>
  #end
</dt>

In order to activate the tooltips you need to use some JavaScript code that you can put in a JavaScriptExtension object:

require(['jquery', 'bootstrap'], function($) {
 // Activate all popovers.
 $('[data-toggle="popover"]').popover();
});

Complete presentation sheet of the document

The content of the final presentation sheet is:

{{velocity output="false"}}
#set ($xclass = $xwiki.getDocument('ValidationSample.ValidationSampleClass').xWikiClass)
#set ($validationMessages = $collectionstool.set)

#**
 * This macros displays a field and it's tool tip.
 *#
#macro (showField $fieldName $mandatory)
  #set ($fieldDefinition = $xclass.get($fieldName))
  #set ($validationMessage = $fieldDefinition.getValue('validationMessage'))
  #set ($discard = $validationMessages.add($validationMessage))
  #set ($hasError = $xcontext.validationStatus.errors.contains($validationMessage))
  #set ($localClassReference = $services.model.serialize($xclass.reference, 'local'))
  <dt class="form-group#if ($hasError) has-error#end">
    <label class="control-label" for="${localClassReference}_0_$fieldName">
      $doc.displayPrettyName($fieldName)##
      #if ($mandatory && $xcontext.action == 'edit')
        <sup class="text-danger">*</sup>
      #end
    </label>
    #set ($tooltipKey = "${localClassReference}_${fieldName}_tooltip")
    #if ($services.localization.get($tooltipKey) && $xcontext.action == 'edit')
      <a href="#tooltip" data-toggle="popover" data-trigger="focus"
          data-content="$escapetool.xml($services.localization.render($tooltipKey))">
        $services.icon.renderHTML('info')
      </a>
    #end
    <span class="xHint">$fieldDefinition.getValue('hint')</span>
  </dt>
  <dd class="form-group#if ($hasError) has-error#end">
    #set ($output = $doc.display($fieldName))
    $stringtool.removeEnd($stringtool.removeStart($output, '{{html clean="false" wiki="false"}}'), '{{/html}}')
    #if ($hasError)
      <div class="text-danger">$escapetool.xml($services.localization.render($validationMessage))</div>
    #end
  </dd>
#end

#**
 * This macro shows all the remaining errors (that are not bound to a particular form field).
 *#
#macro (showRemainingErrors)
  #set ($remainingValidationMessages = [])
  #foreach ($error in $xcontext.validationStatus.errors)
    #if (!$validationMessages.contains($error))
      #set ($discard = $remainingValidationMessages.add($error))
    #end
  #end
  #if ($remainingValidationMessages.size() > 0 ||
      ($xcontext.validationStatus.exceptions && $xcontext.validationStatus.exceptions.size() > 0))
    <div class="box errormessage">
      Validation errors
      <ul>
        #foreach ($error in $xcontext.validationStatus.errors)
          <li>$escapetool.xml($services.localization.render($error))</li>
        #end
        #foreach ($exception in $xcontext.validationStatus.exceptions)
          <li>$escapetool.xml($services.localization.render($exception))</li>
        #end
      </ul>
    </div>
  #end
#end
{{/velocity}}

{{velocity}}
#set ($discard = $xwiki.jsx.use('ValidationSample.ValidationSampleSheet'))
{{html clean="false"}}
<div class="xform">
  ## Force server side validation.
  <input type="hidden" name="xvalidate" value="1" />
  ## Set the validation script.
  <input type="hidden" name="xvalidation" value="ValidationSample.ValidationGroovy" />
  #set ($discard = $doc.use('ValidationSample.ValidationSampleClass'))
  <dl>
    #showField('first_name', true)
    #showField('last_name', true)
    #showField('age', true)
    #showField('email', true)
    #showField('usphone', true)
    #showField('text', false)
  </dl>
  #showRemainingErrors
</div>
{{/velocity}}

And don't forget about the JavaScriptExtension object on the sheet page:

require(['jquery', 'bootstrap'], function($) {
 // Activate all popovers.
 $('[data-toggle="popover"]').popover();
 // Focus the first field with validation error.
 $('.form-group.has-error input, .form-group.has-error textarea, .form-group.has-error select').first().focus();
});

Client Side Validation

It's important to have server side validation for security (the client side validation can be easily bypassed) but client side validation as you type can improve the user's experience and save server load from forms submitted over and over again. To do validation on the client side we recommend using the standard HTML5 form validation as much as possible or a jQuery plugin such as jQuery Validation Plugin.

Tags:
   

Get Connected