Advanced - Defining custom renderers for properties

November 14, 2023

Custom property rendering

We saw previously how to customize dropdown list in engines but how do you correctly display the data selected by the user. Keeping with the Country example, if you remember the code of the initializer, in the value we store only the ISO code of the selected country. But what we want to display to the end user is the country name in his language and maybe the flag also.

How the built in renderers are declared

Renderers are initialized in the same manner as the initializers, in the same Spring file applicationcontext-jcr.xml. Each renderer must extend the ChoiceListRenderer interface.

<bean id="countryInitializerRenderer" class="org.jahia.services.content.nodetypes.initializers.CountryChoiceListInitializerAndRendererImpl"/>
<bean id="countryFlagInitializer" class="org.jahia.services.content.nodetypes.initializers.CountryFlagChoiceListInitializerAndRendererImpl"/>
<bean id="choiceListRenderers" class="org.jahia.services.content.nodetypes.renderer.ChoiceListRendererService" factory-method="getInstance">
    <property name="renderers">
        <map>
            <entry key="country" value-ref="countryInitializerRenderer"/>
            <entry key="flagcountry" value-ref="countryFlagInitializer"/>
        </map>
    </property>
</bean>

Declaring custom renderers

A custom renderer has to implement the ModuleChoiceListRenderer interface. Most of the time, it is also more convenient to extend the AbstractChoiceListRenderer abstract class. And you can even use the same java class to be both the initializer and the renderer implementations.

@Component(service = {ModuleChoiceListInitializer.class, ModuleChoiceListRenderer.class})
public class MyCustomInitializer extends AbstractChoiceListRenderer implements ModuleChoiceListInitializer, ModuleChoiceListRenderer {
    public String getKey() {
        return "my-choicelist-initializer";
    }

In case you need to provide several renderers for the same initializer, the java class for the initializer would embed only the default renderer, and then you would need some additional java classes for the additional renderers.

@Component(service = {ModuleChoiceListRenderer.class})
public class MyCustomRenderer extends AbstractChoiceListRenderer implements ModuleChoiceListRenderer {
    public String getKey() {
        return "my-choicelist-renderer";
    }

To declare a custom renderer, you need to expose them as OSGi services. In the previous example, we use the DS annotation @Component to achieve that.

As a ModuleChoiceListRenderer, this renderer will be automatically detected by the system, and registered in the ChoiceListRendererService, with the key specified in class. You can then use this new renderer in your jsp scripts.

Using a renderer to display a property value in a JSP

Renderers are used through a tag in the templates, the tag can directly render the content of the property in its own manner, or can simply return a map of objects.

Example from the CountryFlagChoiceListInitializerAndRendererImpl :

public class CountryFlagChoiceListInitializerAndRendererImpl implements ChoiceListInitializer, ChoiceListRenderer {
    public Map<String, Object> getObjectRendering(RenderContext context, JCRPropertyWrapper propertyWrapper)
            throws RepositoryException {
        Map<String, Object> map = new HashMap<String, Object>();
        final String displayName = new Locale("en", propertyWrapper.getValue().getString()).getDisplayCountry(
                context.getMainResource().getLocale());
        final String enDisplayName = new Locale("en", propertyWrapper.getValue().getString()).getDisplayCountry(
                Locale.ENGLISH);
        String flagPath = "/css/images/flags/shadow/flag_" + enDisplayName.toLowerCase().replaceAll(" ", "_") + ".png";
        File f = new File(JahiaContextLoaderListener.getServletContext().getRealPath(flagPath));
        if (!f.exists()) {
            flagPath = "/css/blank.gif";
        }
        map.put("displayName", displayName);
        map.put("flag", context.getRequest().getContextPath()+flagPath);
        return map;
    }

    public String getStringRendering(RenderContext context, JCRPropertyWrapper propertyWrapper)
            throws RepositoryException {
        final String displayName = new Locale("en", propertyWrapper.getValue().getString()).getDisplayCountry(
                context.getMainResource().getLocale());
        final String enDisplayName = new Locale("en", propertyWrapper.getValue().getString()).getDisplayCountry(
                Locale.ENGLISH);
        String flagPath = "/css/images/flags/shadow/flag_" + enDisplayName.toLowerCase().replaceAll(" ", "_") + ".png";
        File f = new File(JahiaContextLoaderListener.getServletContext().getRealPath(flagPath));
        if (!f.exists()) {
            flagPath = "/css/blank.gif";
        }
        return "<img src=\""+context.getRequest().getContextPath()+flagPath+"\"><span>"+displayName+"</span>";
    }
}

Example of usage in the templates :

<span class="jobtxt">${fn:escapeXml(values.town)},&nbsp;<jcr:nodePropertyRenderer node="${currentNode}" name="country" renderer="flagcountry"/></span>

Or :

<jcr:nodePropertyRenderer node="${currentNode}" name="country" renderer="flagcountry" var="country"/>
<span class="jobtxt">${fn:escapeXml(values.town)},&nbsp;<img src="${country.flag}"/>&nbsp;${country.displayName}</span>

In order to be able to use the renderers to render single values out of the list and also to use the renderer in the backend, where the RenderContext - which is not available in backend code - is replaced by the Locale, you also need to implement methods passing a propertyValue, at minimum one like this example from the CountryChoiceListInitializerAndRendererImpl:

public String getStringRendering(Locale locale, ExtendedPropertyDefinition propDef, Object propertyValue) throws RepositoryException {
    return new Locale("en", propertyValue.toString()).getDisplayCountry(locale);
}

If a property renderer requires more attributes from the RenderContext - not just the Locale - then you have to throw an UnsupportedOperationException, like in this example from the CountryFlagChoiceListInitializerAndRendererImpl:

public Map<String, Object> getObjectRendering(Locale locale,ExtendedPropertyDefinition propDef, 
        Object propertyValue) throws RepositoryException {
    throw new UnsupportedOperationException("This renderer does not work without RenderContext");
}

public String getStringRendering(Locale locale, propDef, Object propertyValue) 
        throws RepositoryException {
    throw new UnsupportedOperationException("This renderer does not work without RenderContext");
}