Defining choicelist initializers

November 14, 2023

ChoiceList initializers allow you to extend the way a dropdown list (or combobox) is populated for end users when they edit or create content. In your definitions.cnd file, you specify that a property must use a choicelist (rendered as a dropdown list) to specify its value.

Basic usage

This simple example uses choicelist for the selector and lists the allowed values in the constraint of the definition.

[jnt:job] > jnt:content, mix:title, jmix:editorialContent 
- contract (string, choicelist) < contract1, contract2, contract3, contract4

This will render a dropdown list like this.

simple-content-list-initializer.png

This is nice, but maybe your end users want more help with choosing values. This example shows you how to use a resource bundle to modify the rendering of your dropdown list:

- contract (string, choicelist[resourceBundle]) < contract1, contract2, contract3, contract4

You will then need to provide the resource bundle keys in the module's resource bundle properties  src/main/resources/resources/[moduleName].properties file with the following content.

jnt_job.contract.contract1 = Fixed-term contract
jnt_job.contract.contract2 = Indefinite duration
jnt_job.contract.contract3 = Short Mission / Temp work

This will render a dropdown list like this.

choicelist-advanced-initializer.png

How choicelist initializers work

Choicelists can be added to a definition using a syntax such as this:

- propertyName(type, choicelist[initializer1,initializer2='paramForInitializer2',initializer3='paramForInitializer3'] < defaultValue1, defaultValue2, defaultValue3

Initializers are chained, meaning that the output of the first initializer will be passed to the second one as input, so that they may modify them, for example by using the resourceBundle initializer to load resource keys for the current display locale. Initializers may also have parameters, that will influence their behavior, and that are passed using an equal (=) sign as a separator. Here is an example using a node initializer with a parameter:

- j:theme (weakreference,choicelist[nodes='$currentSite/files/themes;jnt:folder'])

As you can see, parameters can be both useful and quite complex, making it possible to re-use initializers between property definitions while customizing them at the same time.

ChoiceList Initializers are implementations of the ChoiceListInitializer interface. All implementations are managed by the ChoiceListInitializerService. Each ChoiceListInitializer implementation is therefore associated with a keyword as in the following example:

"resourceBundle" -> ResourceBundleChoiceListInitializerImpl

When initializing the ChoiceListInitializerService, Jahia passes a map of initializers to use in the system. Each initializer is associated with a keyword. Here the ResourceBundleChoiceListInitializerImpl is attached to the resourceBundle  keyword. When writing your definition, you can chain your initializers to build more complex combinations. Each initializer will receive the list of values from its predecessors. This way, you can have one initializer that fills the values and others in the pipe that change some of those values or add properties to them. Here an example for the Templates choicelist in the layout panel.

[jmix:renderable] > jmix:layout, jmix:contentMixin mixin
- j:view (string, choicelist[templates,resourceBundle,image])

This means that to display this dropdown list, Jahia first calls the templates initializer responsible for filling the available values. Then, this list of values is passed to the resourceBundle initializer which will try to replace the labels displayed in the dropdown list with the ones found in the resource bundles, if available. Then, the updated values go to the image initializer that adds an image to be associated with each value.

Developing your own choicelist initializers

You can develop your own initializers. Custom initializers must implement the ModuleChoiceListInitializer interface.

@Component(service = {ModuleChoiceListInitializer.class})
public class CustomChoiceListInitializer implements ModuleChoiceListInitializer {

Then, implement the String getKey() method to define the keyword that it will respond to :

    @Override
    public String getKey() {
        return "customChoiceList";
    }

    @Override
    public void setKey(String key) { 
      // not needed in this example, but we must implement it 
    }

As a ModuleChoiceListInitializer, this initializer is automatically detected by the system, and registered in the ChoiceListInitializerService, with the key specified in the getKey method . You can then use this new initializer in your definitions.cnd file.

Rendering choicelists in Content Editor

When a user creates or updates a content using the Jahia UI, they will see a popup with data to enter for each field of the current definition. If a field is associated with a choicelist selector, Jahia will go through the pipe of initializers, if defined. For the UI rendering Jahia uses the displayName property of each  ChoiceListValue object returned by the getChoiceListValues method that is received from the pipe, while the value parameter is used to store in the JCR or to pass on to the next choicelist initializer in the pipe.  Here are the details of the ChoiceListInitializerInterface:

public interface ChoiceListInitializer {

    /**
     * Build a list of ChoiceListValue objects that contain the name, the displayName and the properties for each
     * entry in a choice list
     * @param epd the related property definition
     * @param param the parameter string passed to the choicelist initializer (in the definition)
     * @param values the values generated by the choicelist initializer that was called just before this one in the
     *               chain
     * @param locale the locale for which to generate the choicelist values
     * @param context This map is designed to be used freely
     *                by implementations to pass context information between initializers as it is always the same object
     *                passed to all initializers in a chain. Here are a few entries that are already used by the system:
     *                can contain: dependentProperties: a list of properties that this property depends on, to be able
     *               to modify the choicelist values based on changes in other properties (useful for example for
     *                locations going from coarse to more precise), contextNode: the node we are editing, or any other
     *                information that may be useful to specific implementations.
     * @return a list of ChoiceListValue object that contain the name, displayName and custom properties to render the 
     * choicelist values in a user interface
     */
    public List<ChoiceListValue> getChoiceListValues(ExtendedPropertyDefinition epd, String param, List<ChoiceListValue> values, Locale locale,
                                                     Map<String, Object> context);
}

Simple initializer example

This example shows how to create a simple initializer to allow users to choose a value from a dropdown list.

The code of the initializer :


    private Map<String,String> values = new LinkedHashMap<>();

    @Activate
    public void activate(Map<String, ?> props) {
        values.put("test1", "Test 1");
        values.put("test2", "Test 2");
    }

    @Override
    public List<ChoiceListValue> getChoiceListValues(ExtendedPropertyDefinition epd, 
                                                     String param, 
                                                     List<ChoiceListValue> values, 
                                                     Locale locale,
                                                     Map<String, Object> context) {
        Set<Map.Entry<String,String>> set = values.entrySet();
        return set.stream().map(entry -> new ChoiceListValue(entry.getValue(), entry.getKey())).collect(Collectors.toList());
    }

As we can see in this example, the getChoiceListValues method is building a list of ChoiceListValue objects from the values hardcoded map. 

You can find a more complex and complex example project of a custom initializer here.

Other Initializers

You can define as many initializers as you want. By default, Jahia provides you with very flexible initializers:

  • Node initializer
  • Script initializer

You can also find the list of all the built-in initializers here

Node initializer

The node initializer allows you to bind a dropdown list to the content of a node in the JCR, by defining the root path of this dropdown list and the type of child nodes you want to list.

- firstLevelCategory (weakreference,choicelist[nodes='/sites/systemsite/categories;jnt:category'])

This will create a dropdown listing all the jnt:category elements from the /categories folder.

- j:theme (weakreference,choicelist[nodes='$currentSite/files/themes;jnt:folder'])

This will create a dropdown listing all the subfolders elements of the folder themes in the current site.

Script initializer

This is the most versatile initializer provided by Jahia as it allows you to use any JSR-223 compatible script language to write your initializer. Declaring a scripted initializer is as simple as this.

- type (string,choicelist[script=type.groovy])

The initializer will look for the script in the module containing the definition in the scripts folder.

.
|-jnt_lastNews
|---html
|-jnt_news
|---html
|-resources
|-scripts
|-WEB-INF

The script extension defined the script type. For example, if your script has the name type.groovy, the script engine manager tries to find a declared script engine for the Groovy extension .

What the things must your script absolutely do? The script must return a List of ChoiceListValue.

A potential usage is to define script that interacts with external system to fill the values, like XML files, databases, and REST service. This example shows Groovy script parsing of an XML file to fill the values:

class RecordsHandler extends DefaultHandler {
    def values

    RecordsHandler(List values) {
        this.values = values
    }

    void startElement(String ns, String localName, String qName, Attributes atts) {
        switch (qName) {
            case 'books':
                String labelName = atts.getValue("title")
                values.add(new ChoiceListValue(labelName,null,new ValueImpl(labelName,PropertyType.STRING,false))); break

        }
    }
}

def newValues = new ArrayList<ChoiceListValue>();
def handler = new RecordsHandler(values)
def reader = SAXParserFactory.newInstance().newSAXParser().getXMLReader()
reader.setContentHandler(handler)
reader.parse(new InputSource(new FileInputStream(file)))
return newValues

Each script can also access to the list of values from its predecessors as it is bound to the values variable.