Problem Statement:
How can I leverage component policies chosen at the template level to manage the dropdown-based selection?
Introduction:
AEM has integrated component policies as a pivotal element of the editable template feature. This functionality empowers both authors and developers to provide options for configuring the comprehensive behavior of fully-featured components, including deciding which fields to display, hide, or customize. This configuration is carried out at the template level, facilitating reuse across various templates or template-specific customizations.
Furthermore, AEM introduces a robust styling feature that empowers frontend developers to manage the visual appearance and user interface of components. This grants authors the capability to tailor the look and feel of the component.
Requirement:
In a previous discussion, we outlined a step-by-step process for Accessing Component Policies in AEM at the Component Dialog Level, Sightly code, and Sling model.
Now, consider a scenario where a company is selling various electronic devices in different countries. For example, a company like Brisket sells phones, tablets, and laptops in India, and only phones and tablets in the USA. However, if they plan to introduce laptops in the USA in the future, they’d like to make changes to the template policy. How can this be achieved at the dialog level?
Solution:
Step 1: L Create an “ElectronicsDS” component and add the design dialog with specific attributes:
Step 2: Create the dialog and add an extra granite node with a specific property:
Step 3: When you open the component dialog and inspect the field, you will see extra attributes, including “component-path” and “field-path”.
Step 4: Create a dialog-level listener that makes a resourceType based call to a Servlet using “field-path” and passes the attributes as shown.
(function($, $document, Granite, Coral) {
var flag = false;
var errorMessage = 'Error occurred during processing';
$(document).on("foundation-contentloaded", function(e) {
var value = $("coral-select[name='./country']").val();
var fieldPath = $("coral-select[name='./devices']").attr("data-field-path");
var compPath = $("coral-select[name='./devices']").attr("data-component-path");
populateItems(fieldPath, compPath, value);
});
var REGEX_SELECTOR = "dropdown.selector",
foundationReg = $(window).adaptTo("foundation-registry");
foundationReg.register("foundation.validation.validator", {
selector: "[data-foundation-validation='" + REGEX_SELECTOR + "']",
validate: function(el) {
if ($(el).is(":visible")) {
var value = $(el).val();
var fieldPath = $("coral-select[name='./devices']").attr("data-field-path");
var compPath = $("coral-select[name='./devices']").attr("data-component-path");
populateItems(fieldPath, compPath, value);
}
}
});
function populateItems(fieldPath, compPath, value) {
dsUrl = Granite.HTTP.externalize(fieldPath) + ".html?componentPath=" + compPath + "&dropdownValue=" + value;
$.ajax({
type: "GET",
async: false,
url: dsUrl,
success: function(result) {
if (result) {
var select = document.querySelector("coral-select[name='./devices']");
var newHtml = document.createElement("div");
newHtml.innerHTML = result;
var newResult = newHtml.querySelector("coral-select[name='./devices']").children;
Coral.commons.ready(select, function(component) {
component.items.clear();
[...newResult].forEach((e) => {
component.items.add(e);
})
});
} else {
flag = true;
}
}
});
if (flag) {
return errorMessage;
}
}
}(jQuery, $(document), Granite, Coral));
Step 5: Create a registered path-based Servlet that accepts the component path as a parameter, retrieves the component policy, and returns dropdown options.
package com.aem.operations.core.servlets;
import com.adobe.granite.ui.components.ExpressionHelper;
import com.day.cq.wcm.api.policies.ContentPolicy;
import com.day.cq.wcm.api.policies.ContentPolicyManager;
import com.day.crx.JcrConstants;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.*;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.jetbrains.annotations.NotNull;
import org.osgi.service.component.annotations.Component;
import com.adobe.granite.ui.components.Config;
import com.adobe.granite.ui.components.Value;
import com.adobe.granite.ui.components.ds.DataSource;
import com.adobe.granite.ui.components.ds.SimpleDataSource;
import com.adobe.granite.ui.components.ds.ValueMapResource;
import com.adobe.granite.ui.components.ExpressionResolver;
import org.osgi.service.component.annotations.Reference;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.*;
@Component(
service = {Servlet.class},
property = {
"sling.servlet.resourceTypes=" + ElectronicsDSServlet.RESOURCE_TYPE,
"sling.servlet.methods=GET",
"sling.servlet.extensions=html"
}
)
public class ElectronicsDSServlet extends SlingSafeMethodsServlet {
private static final long serialVersionUID = 1L;
static final String RESOURCE_TYPE = "aemoperations/components/electronics/alloweddevices";
@Reference
protected ExpressionResolver expressionResolver;
@Override
protected void doGet(@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response)
throws ServletException, IOException {
SimpleDataSource allowedDevicesDataSource = new SimpleDataSource(getAllowedDevices(request).iterator());
request.setAttribute(DataSource.class.getName(), allowedDevicesDataSource);
}
protected List<Resource> getAllowedDevices(@NotNull SlingHttpServletRequest request) {
List<Resource> colors = Collections.emptyList();
String contentPath = (String) request.getAttribute(Value.CONTENTPATH_ATTRIBUTE);
if(StringUtils.isEmpty(contentPath)){
contentPath = request.getParameter("componentPath");
}
String dropdownValue = request.getParameter("dropdownValue");
if(StringUtils.isEmpty(dropdownValue)){
Config config = getConfig(request);
ValueMap map = getComponentValueMap(config, request);
dropdownValue = getParameter(config, "dropdownValue", request);
if (StringUtils.isEmpty(dropdownValue)) {
dropdownValue = map != null ? map.get("country", String.class) : StringUtils.EMPTY;
}
}
ResourceResolver resolver = request.getResourceResolver();
ContentPolicy policy = null;
if (StringUtils.isNotEmpty(contentPath)) {
policy = getContentPolicy(contentPath, resolver);
}
if (StringUtils.isEmpty(contentPath) || policy == null) {
contentPath = request.getRequestPathInfo().getSuffix();
if (StringUtils.isNotEmpty(contentPath)) {
policy = getContentPolicy(contentPath, resolver);
}
}
if (policy != null) {
colors = populateDropdown(policy, resolver, dropdownValue);
}
return colors;
}
private ContentPolicy getContentPolicy(@NotNull String path, @NotNull ResourceResolver resolver) {
ContentPolicy policy = null;
ContentPolicyManager policyMgr = resolver.adaptTo(ContentPolicyManager.class);
Resource contentResource = resolver.getResource(path);
if (contentResource != null && policyMgr != null) {
policy = policyMgr.getPolicy(contentResource);
}
return policy;
}
private List<Resource> populateDropdown(@NotNull ContentPolicy policy, @NotNull ResourceResolver resolver, String dropdownValue) {
List<Resource> devices = new ArrayList<>();
ValueMap device = null;
Resource policyRes = resolver.resolve(policy.getPath());
Iterator<Resource> children = policyRes.listChildren();
while (children.hasNext()) {
final Resource child = children.next();
if (StringUtils.equalsIgnoreCase(child.getName(), "multifield")) {
Iterator < Resource > multiChild = child.listChildren();
while (multiChild.hasNext()) {
ValueMap valueMap = multiChild.next().adaptTo(ModifiableValueMap.class);
String[] type = valueMap.get("country",String[].class);
if(ArrayUtils.contains(type, dropdownValue)){
String[] allowedDevices = valueMap.get("devices",String[].class);
if (allowedDevices != null && allowedDevices.length > 0) {
for (String allowedDevice : allowedDevices) {
device = new ValueMapDecorator(new HashMap<>());
device.put("text", allowedDevice.toUpperCase());
device.put("value", allowedDevice);
devices.add(new ValueMapResource(resolver, new ResourceMetadata(), JcrConstants.NT_UNSTRUCTURED,
device));
}
}
}
}
}
}
return devices;
}
Config getConfig(SlingHttpServletRequest request) {
// get datasource configuration
Resource datasource = request.getResource().getChild(Config.DATASOURCE);
if (datasource == null) {
return null;
}
return new Config(datasource);
}
protected String getParameter(@NotNull Config config, @NotNull String name,
@NotNull SlingHttpServletRequest request) {
String value = config.get(name, String.class);
if (value == null) {
return null;
}
ExpressionHelper expressionHelper = new ExpressionHelper(expressionResolver, request);
return expressionHelper.getString(value);
}
ValueMap getComponentValueMap(Config config, SlingHttpServletRequest request) {
if (config == null) {
return null;
}
String componentPath = getParameter(config, "componentPath", request);
if (componentPath == null) {
return null;
}
// get component resource
Resource component = request.getResourceResolver().getResource(componentPath);
if (component == null) {
return null;
}
return component.getValueMap();
}
}
By accessing the component policy and selecting country and electronics, you can dynamically control the available dropdown options. For example, selecting “Country A” displays phone, tablet, and laptop options, while choosing “Country B” shows only phone and tablet options.
Working code for this can be found in the repository: Link to Repository