Dynamic Dropdown component

In this post, I’m going to show you how to create a dropdown component where its options can be configured in runtime.

The idea is to have the possibility to configure the dropdown component with Options Provider – a piece of a code responsible for loading appropriate options.

The final result will look like this one:

There could be many implementations of Options Provider, each of them provides its own set of options. On the screenshot above, there are two such providers shown – Product Categories and Company Departments.

Dropdown Options Provider

Option Providers can be modeled via the following interface:


import com.day.cq.i18n.I18n;

/**
* Generic interface for all classes which provide options for a dropdown field.
*
*/
public interface DropdownOptionsProvider {

/**
* A list of options for a dropdown with the localized names, represented as {@link Option} objects.
*
* @return the list of options for a dropdown
*/
List<Option> getOptions(I18n i18n);

/**
* A name of a provider which is displayed in a configuration dialog for a dropdown component.
*
* @return the name of a dropdown options provider
*/
String getName();

/**
* Unique ID of a provider. This can be for example, a fully qualified class name of an implementation class.
*
* @return Unique ID of a provider
*/
String getID();

}

The names of Providers (eg. “Product Categories” or “Company Departments” from the screenshot above) are returned by implementations of getName() method. The purpose of getID() method will be clarified in the subsequent sections. Method getOptions() has I18n object as its argument, allowing the localization of dropdown options. Have a look at this post in order to find out how this object is created.

Option class is a simple immutable JavaBean:


public final class Option {
private final String name;
private final String value;

public Option(String name, String value) {
this.name = name;
this.value = value;
}

// getters ...
}

Dropdown Options Provider implementations

The implementations of Option Provider interface are OSGi services which encapsulate the logic for getting the options for a dropdown. These options can be determined by reading a content structure of your AEM application, loaded from some external webservice, etc. Because it’s something which is application dependent, I’ll just give you the service class definition:

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Service;

/**
* Provides a list of product categories as the options for a dropdown component. An option name is a product category name
* and option id is a product category ID (application dependent).
*/
@Component(
label = "YourApp - Product Category Dropdown Options Provider",
description = "Provides all available product categories as the options for a dynamic dropdown",
immediate = true
)
@Service(DropdownOptionsProvider.class)
public class ProductCategoryDropdownOptionsProvider implements DropdownOptionsProvider {
private static final String PROVIDER_NAME = "Product Categories";

@Override
public List<Option> getOptions(I18n i18n) {
// app specific logic for fetching product categories
}

@Override
public String getName() {
return PROVIDER_NAME;
}

@Override
public String getID() {
return this.getClass().getName();
}

}

Leveraging OSGi dynamism

We will leverage OSGi dynamism (Whiteboard pattern) in order to populate Options Provider dropdown in the runtime, shown on the screenshot above.
The idea is to create Options Provider Registry, which will manage (register/deregister) all instances of DropdownOptionsProvider implementations. Whenever a bundle containing Dropdown Options Provider implementations is deployed, they will be registered in the registry, and vice versa.


import java.util.Map;

/**
* Common interface for all registry implementations, which hold references to all available {@link DropdownOptionsProvider} instances.
*
*/
public interface DropdownOptionsProviderRegistry {

Map<String, DropdownOptionsProvider> getProviders();

DropdownOptionsProvider getProvider(String providerId);
}

// registry implementation

import org.apache.felix.scr.annotations.*;

import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component(
label = "YourApp - Dropdown Options Provider Registry",
description = "Manages providers for dynamic dropdown options",
immediate = true
)
@Service(DropdownOptionsProviderRegistry.class)
public class DropdownOptionsProviderRegistryImpl implements DropdownOptionsProviderRegistry {

@Reference(cardinality = ReferenceCardinality.MANDATORY_MULTIPLE, bind = "bind", unbind = "unbind",
referenceInterface = DropdownOptionsProvider.class, policy = ReferencePolicy.DYNAMIC)
private Map<String, DropdownOptionsProvider> providers = new ConcurrentHashMap<>();

protected void bind(DropdownOptionsProvider provider) {
providers.put(provider.getID(), provider);
}

protected void unbind(DropdownOptionsProvider provider) {
if(provider == null) {
return;
}

providers.remove(provider.getID());
}

public Map<String, DropdownOptionsProvider> getProviders() {
return Collections.unmodifiableMap(providers);
}

public DropdownOptionsProvider getProvider(String providerId) {
return providers.get(providerId);
}
}

All providers are kept in a hash map so the best candidate for provider ID is its fully qualified class name.

The final piece of the puzzle: a servlet

Options Provider dropdown from the screenshot above is Touch UI widget granite/ui/components/foundation/form/select, where’s possible to define a servlet as a data source for its options.


<!-- A snippet from Dynamic Dropdown component dialog. It defines Options Provider field -->
<optionsProvider
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/select"
fieldLabel="Options Provider"
fieldDescription="Select the provider for dropdown options"
name="./optionsProvider">
<datasource
jcr:primaryType="nt:unstructured"
sling:resourceType="yourapp/dropdownoptions/datasource"/>
</optionsProvider>

The servlet implementation looks like this:

import org.apache.sling.api.servlets.HttpConstants;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import com.adobe.granite.ui.components.ds.DataSource;
import com.adobe.granite.ui.components.ds.SimpleDataSource;
import org.apache.commons.collections.iterators.TransformIterator;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.apache.commons.collections.map.HashedMap;
import com.adobe.granite.ui.components.ds.ValueMapResource;
import org.apache.sling.api.resource.ResourceMetadata;

@SlingServlet(resourceTypes = "yourapp/dropdownoptions/datasource", methods = HttpConstants.METHOD_GET)
public class DynamicDropdownOptionsDatasource extends SlingAllMethodsServlet {

@Reference
private DropdownOptionsProviderRegistry registry;

@Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) {
ResourceResolver resourceResolver = request.getResourceResolver();
Set<Map.Entry<String, DropdownOptionsProvider>> providers = registry.getProviders().entrySet();
DataSource ds = new SimpleDataSource(new TransformIterator(providers.iterator(), o -> {
Map.Entry<String, DropdownOptionsProvider> item = (Map.Entry<String, DropdownOptionsProvider>) o;
ValueMap vm = new ValueMapDecorator(new HashedMap());
vm.put("value", item.getKey());
vm.put("text", item.getValue().getName());
return new ValueMapResource(resourceResolver, new ResourceMetadata(), JcrConstants.NT_UNSTRUCTURED, vm);
}));

request.setAttribute(DataSource.class.getName(), ds);
}
}

The important thing to mention here is that ‘optionsProvider’ JCR property, which corresponds to Options Provider dropdown field (from the component dialog) will keep provider ID (which is fully qualified class name of a provider implementation) once the changes made in the component dialog are saved.

Putting it all together – the Dynamic Dropdown component

Now we have the support for dynamic dropdown implemented. The question is: how the options are rendered? I’ll give you key code snippets in order to understand the mechanism, leaving to you the component implementation as the exercise.

The code snippet, which defines Options Provider field in the component dialog: given above.

HTL snippet of the component script:

<sly data-sly-use.model=“yournamespace.DynamicDropdown">
<!-- ... -->
<select name="${model.name}" data-sly-list.optionItem="${model.options}">
<option value="${optionItem.value}">${optionItem.name}</option>
</select>
</sly

Sling Model snippet:

import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.collections.CollectionUtils;
import com.day.cq.i18n.I18n;

@Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class DynamicDropdown {
@ValueMapValue
private String name;

@ValueMapValue
private String optionsProvider;

@Inject
private DropdownOptionsProviderRegistry registry;

private List<ValuePair> options = new ArrayList<>();

@PostConstruct
protected void init() {
if(StringUtils.isBlank(optionsProvider)) {
return;
}

DropdownOptionsProvider provider = registry.getProvider(optionsProvider);
if(provider == null) {
return;
}

I18n i18n = getI18n(); // take a look at http://www.flexibledesigns.rs/getting-localization-messages-in-sling-models
List<Option> availableOptions = provider.getOptions(i18n);

if(CollectionUtils.isNotEmpty(availableOptions)) {
availableOptions.sort(Comparator.comparing(Option::getName));
options.addAll(availableOptions);
}

}

// getters ...
}