Creating dynamic forms in Content Editor
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.
When England is selected, the Region field is reset because Ile-de-france is not an English region.
Now English regions are available.
Creating dynamic values
definitions (.cnd)
First, you must declare that a choicelist depends on another one at the definitions.cnd level:
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:
You can override this function:
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:
- If countryDep is not in the context, check the node property from JCR. If the
context
does not contain thecountryDep
property, it means that thecountryDep
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:
- As you can see, the
context
also contains thecontextNode
, which is the node open to be edited. Also see that we check formultiple
values, this is because dynamic values also work for multivalued choicelist. This is the rule to follow to get thecountryDep
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.
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.
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 movetargetFieldset
A field set of the form where to move the fieldssections
The list of sections containing the field sets and fieldsupdatedField
Optional parameter, if present the field will be moved after the specified fieldformik
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 fieldssections
List of sections containing the field set and fieldsformik
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 fieldsetfieldset
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 movetargetFieldsetName
The name of the targeted fieldsetsections
The list of sections containing the fieldsets and fieldsfield
Optional parameter, if present the field will be moved after the specified field