Creating dynamic forms in Content Editor

October 8, 2024

Dynamic field sets

What is a dynamic field set

A dynamic field set adds fields to a form from a selected value in a choice list. Technically, a choicelist value defines a mixin whose properties will be added to the form. The fields in a dynamic field set display under the choicelist that has the values, allowing you to hide or display them. Dynamic field sets are compatible with Addmixin/templateMixin from the previous version of Jahia.

Creating a dynamic field set

Definition of a dynamic field Set

Dynamics field sets are mixins that inherit jmix:dynamicFieldset and extend the node type that uses it. This allows Content Editor to load the field set without displaying its fields. To create a dynamic field set, you first add the definition of the node which will have a dynamic field set in its creation/edition form. The definition of this dynamic field set is in the definition.cnd file of your module. The fields of a jmix:dynamicFieldset mixin won’t display in a form in Content Editor until you add them dynamically by selecting a value from a choicelist.

Activating a dynamic field set

To display fields in a dynamic field set, a choicelist value must define an addMixin property. This property specifies which mixin to add to the form when the value is selected. To do that, you can either create a choice list initializer or add a JSON definition.

Examples

The following example defines a complete location that is represented either by geocodes (jmix:geocodes) or an address (jmix:address). These mixins extend qant:location but will not display as they inherit the jmix:dynamicFieldset type. They can only be added dynamically by selecting a value in the locationType choice list.

<jnt = 'http://www.jahia.org/jahia/nt/1.0'>
<jmix = 'http://www.jahia.org/jahia/mix/1.0'>
<qant = 'http://www.jahia.org/qa/nt/1.0'>
<qamix = 'http://www.jahia.org/qa/mix/1.0'>

[qamix:completeLocation] mixin
- locationType (string, choicelist)

[qant:location] > jnt:content, qamix:completeLocation, mix:title

[jmix:geocodes] > jmix:dynamicFieldset mixin
extends = qant:location
- latitude (string)
- longitude (string)

[jmix:address] > jmix:dynamicFieldset mixin
extends = qant:location
- continent (string, choicelist[resourceBundle]) = 'europe' < 'africa', 'asia', 'america', 'europe'
- country (string, choicelist[resourceBundle])
- state (string)
- zipCode (string)
- city (string)
- street (string)

In this example, the values of the locationType choicelist are defined by the following JSON file:

{
  "name":"qamix:completeLocation",
  "fields":[
     {
        "name":"locationType",
        "valueConstraints":[
           {
              "value":{
                 "type":"String",
                 "value":"latLong"
              },
              "displayValue":"Latitude longitude",
              "propertyList":[
                 {
                    "name":"addMixin",
                    "value":"jmix:geocodes"
                 }
              ]
           },
           {
              "value":{
                 "type":"String",
                 "value":"address"
              },
              "displayValue":"Address",
              "propertyList":[
                 {
                    "name":"addMixin",
                    "value":"jmix:address"
                 }
              ]
           }
        ]
     }
  ]
}

The locationType field is overridden by this definition. The list contains two values: Latitude longitude and Address. When selecting Latitude longitude, the jmix:geocodes mixin is added to the form and the latitude and longitude fields display, as defined in the definition.cnd. When a user selects an address, the continent, country, state, zipCode, city, and street fields display.

Dynamic values

What are dynamic values?

You may already know that choicelist values can be backed by a ChoiceList initializer Java class. For more information, see Defining choicelist initializers. Content Editor allows you to create dependents choicelists. This means that a choicelist’s values depend on another choicelist property. Available values in dependents choicelists are recalculated every time the dependent choicelist value change.

Example of a country choicelist with regional values

This example shows how to create a choicelist that shows available regions depending on the country that a user selects. There are two choicelists, one for selecting countries and one for selecting regions. When a user selects:

  • France only French regions are available in the regions field set
  • England only English regions are available in the regions field set

It will look like this:

When France country is selected, only French regions are available.

image1.png

When England is selected, the Region field is reset because Ile-de-france is not an English region.

image3.png

Now English regions are available.

image4.png

Creating dynamic values

definitions (.cnd)

First, you must declare that a choicelist depends on another one at the definitions.cnd level:

image5.png

As you can see, the regionDep property uses the dependentProperties option, indicating that it depends on the countryDep property. You can also see that the regionDep is using a custom choicelist initializer: regionChoiceListInitializer.

Java Choicelist Initializer

The rest of the magic is done in the choicelist initializer itself. If you don’t know what a choicelist initializer is, see Defining choicelist initializers. When your Java Choicelist Initializer extends AbstractChoiceListInitializer like this:

image6.png

You can override this function:

image7.png

This function returns the available values for the regionDep property. One parameter of this function is important here, which is the context. The main goal here is to get the value of the countryDep property to return the appropriate set of regions. There is an important rule to follow to do so:

  • First check if the context contains the countryDep property. If the context contains the countryDep property, we use it. Do like this to do the check and get the value:
    image8.png

     

  • If countryDep is not in the context, check the node property from JCR. If the context does not contain the countryDep property, it means that the countryDep has not been changed by the user. Instead the user is opening an already existing node for editing.
    In that case, we read the countryDep property directly from the JCR on the node itself:
    image9.png
  • As you can see, the context also contains the contextNode, which is the node open to be edited. Also see that we check for multiple values, this is because dynamic values also work for multivalued choicelist. This is the rule to follow to get the countryDep value:
  • First check if countryDep is in the context
  • If not: read the node property
  • If the node or the property doesn’t exist: then return nothing Once you have the countryDep value you can check it’s value and return the appropriate set of regions depending on the countryDep value.

onChange event

Content Editor knows when a value of a field is updated and can react to this updated value. Using the component registry, you can register your own onChange callbacks and they will be called every time a value is updated for one or more specific selector types.

Using the onChange event

onChange can be really useful for creating dynamic form structure changes. For example, you can:

  • Display a new field when a value changes
  • Set a field readOnly depending on the value of another field
  • Hide a section, a field set, or field in case a value changes
  • Set a value in another field automatically, when a field is updated in the onChange callback, you have access to a lot of form information and variables. Some of these can be updated so it will directly modify the structure of the form. This way you could easily modify a section, a field set or even existing fields.

Registering custom onChange callbacks

You need to use the component registry to register a new onChange callback.

image10.png

As simple as that, you have to add the onChange with selectorType.onChange. Provide a name. In the example it’s addMixin. And two parameters are required:

  • targets
    Array of the selector types you want to listen, Since Jahia 8.0.2.0 you can use the wildcard * to listen to all selectors.
  • onChange
    The onChange callback that is called every time a field of the specified selector type(s) is updated. Available variables in the onChange callback:
  • previousValue
    The previous value
  • currentValue
    The value after
  • field
    The field object (contain a lot of technical data used to display the field)
  • context
    Contains the overall technical variables used to display the form. This object can also be used to modify the form
  • getSections()
    Allows you to always retrieve the updated sections in case a previous onChange modified them.
  • setSections()
    Allows you to override current sections in case you want to modify the form structure
  • formik
    Allows you to interact with the form values and the form state. Also contains other contextual data, that can be useful.
  • selectorType
    The current selectory type of the updated field
  • helper (not in the example)
    A set of utility functions that help to manipulate the form (see below).

onChange example

We will use the location node type defined here for our example. With the definition of the mixin jmix:address, we can fill an address with a continent, a country, a state, a zipCode, a city and a street.

[jmix:address] > jmix:dynamicFieldset mixin
 extends = qant:location
 - continent (string, choicelist[resourceBundle]) = 'europe' < 'africa', 'asia', 'america', 'europe'
 - country (string, choicelist[countryChoiceListInitializer, dependentProperties='continent'])
 - state (string)
 - zipCode (string)
 - city (string)
 - street (string)

But the state field is only useful when we are filling an address in the United States of America. In the following example, we will display the state field only when the value of the country field is equal to usa.

Country values

The country choice list is defined by a choice list initializer. The values are depending on the values selected in the continent field. When selecting America, the country field will contain usa, canada, and mexico. The state field should display when selecting usa and be hidden when selecting another value.

image11.png

onChange function

To display the state field when selecting usa, an onChange has been created. You can find the full implementation of this function on Github.

window.jahia.uiExtender.registry.add('selectorType.onChange', 'addCustomField',
    {
        targets: ['Choicelist'],
        onChange: (previousValue, currentValue, currentField, context, selectorType) => {
            // if we are adding a node of the type location
            if (context.nodeTypeName === 'location') {
                let updatedSections = context.getSections();
                let locationFields = getLocationFields(context.getSections());
                //store the state field in the window object to add it again when necessary
                window.stateField = window.stateField || locationFields.find(field => field.name === 'state');
                // Do nothing if location type is not an address
                if (currentField.name === 'locationType' && currentValue !== 'address') {
                    return;
                // Check if the state field should be displayed
                } else if (stateShouldBeDisplayed(currentField, currentValue, context.formik)) {
                   // Add the state field to the field set of qamix:completeLocation
                    updatedSections = context.getSections();
                    updatedSections
                        .find(section => section.name === 'content')
                        .fieldSets
                        .find(fieldSet => fieldSet.name === 'qamix:completeLocation')
                        .fields =
                        locationFields.reduce((fields, field) => {
                            if (field.name === 'country' && !locationFields.find(field => field.name === 'state')) {
                                return [...fields, field, window.stateField];
                            }
                            return [...fields, field];
                        }, []);
                } else {
                    // Keep all fields except the state field in qamix:completeLocation 
                    updatedSections = context.getSections();
                    updatedSections
                        .find(section => section.name === 'content')
                        .fieldSets
                        .find(fieldSet => fieldSet.name === 'qamix:completeLocation')
                        .fields =
                        locationFields.filter(field => field.name !== 'state')
                    // Empty the value for state in formik
                    context.formik.setFieldValue('state', null, true);
                }
                // Set the sections to refresh the form
                context.setSections(updatedSections);
            }
        }
    });

When accessing the sections it’s important to call the context.getSections() function to always work with updated sections. The stateShouldBeDisplayed function and the getLocationFields are available on Github.

Reminder on the component registry

All extensions are added into the registry by calling the registry.add function. The registry object can be found by importing the registry from @jahia/ui-extender. The add function takes at least 3 parameters: the type of extension to register, its unique id, and an object describing the extension. The component registry is a JavaScript object shared by each module deployed on a Jahia instance. You can add a function to the registry and a module A and use it in a module B. More documentation is available here. You can find an example of the adding of an action to the registry here. The implementation of the registry can be found on Github.

Utility functions to manipulate the forms

Content Editor has some utility functions to help to manipulate the fields in a form. These functions allow you to interact with the fields and field sets of a form. You can find these functions at here: https://github.com/Jahia/content-editor/blob/master/src/javascript/ContentEditor.helper.jsx If necessary, you can copy and modify these functions in another project. These utility functions are provided as the 6th parameter of the onChange function: onChange: (previousValue, currentValue, field, editorContext, selectorType, helper)

moveMixinToTargetFieldset

This function moves all fields in a mixin to a target field set. Definition:

moveMixinToTargetFieldset = (mixin, targetFieldset, sections, updatedField, formik)

Parameters

  • mixin
    The mixin containing the fields to move
  • targetFieldset
    A field set of the form where to move the fields
  • sections
    The list of sections containing the field sets and fields
  • updatedField
    Optional parameter, if present the field will be moved after the specified field
  • formik
    Allows interaction with the properties of formik

moveMixinToInitialFieldset

This function is similar to the moveMixinToTargetFieldset function but simply moves the fields defined by a mixin to their original location. Definition:

moveMixinToInitialFieldset = (mixin, sections, formik)

Parameters:

  • mixin
    The original mixin where to move the fields
  • sections
    List of sections containing the field set and fields
  • formik
    Allows interaction with the properties of formik

addFieldsToFieldset

This function allows adding fields to a field set. Definition:

addFieldsToFieldset = (fieldsToAdd, fieldset, afterField)

Parameters:

  • fieldsToAdd
    The fields to add in the a fieldset
  • fieldset
    The target field set. A field set can be found in the sections of the form.
  • afterField
    If this field is defined, fields will be moved after the specified field

moveFieldsToAnotherFieldset

This function moves all fields of a mixin to a target field set, but unlike the moveMixinToTargetFieldset, the values of formik are not modified in this function.

Definition:

moveFieldsToAnotherFieldset = (originFieldsetName, targetFieldsetName, sections, field)

Parameters:

  • originFieldsetName
    The name of the fieldset containing the fields to move
  • targetFieldsetName
    The name of the targeted fieldset
  • sections
    The list of sections containing the fieldsets and fields
  • field
    Optional parameter, if present the field will be moved after the specified field