Accessing Component Policies in AEM at the Component Dialog Level

How can I access component policies that were chosen at the template level to determine the visibility of specific fields within a component’s dialog?

AEM (Adobe Experience Manager) has integrated component policies as a central feature in the editable template system. This feature empowers both authors and developers to provide options for configuring the comprehensive behavior of fully featured components, including deciding which fields should be shown, hidden, or customized. This configuration can occur at the template level, allowing for reuse across various templates or template-specific customization.

Furthermore, AEM introduces a robust styling feature that empowers frontend developers to manage the visual appearance and user interface of components. This provides authors with the ability to configure the look and feel of components.

Let’s illustrate this with an example using one of AEM’s core components:

Requirement:

We want to prevent authors from editing the Pretitle field in the Teaser component.

Steps to Access the Policy:

Step 1: Navigate to a page in the WKND site via the Site Navigation. In our case, let’s open a magazine page.

WKND site

Step 2: Access the Teaser component, where you will see the Pretitle field enabled.

Teaser Component

Step 3: Now, go to the template editor to access the template structure.

Access Tempalte

Step 4: Click on the Teaser component’s policy.

Access Teaser Policy

Step 5: Select the checkbox to hide the Pretitle field.

Configure Policy

Step 6: Return to the page, refresh it, and open the Teaser component. You’ll notice that you can enable or disable fields using the powerful policy feature.

Teaser component without Pretitle

Now, let’s explore how this works from a backend perspective by accessing CRXDE:

Step 1: In CRXDE, access your project structure to locate your Teaser component.

Path: /apps/wknd/components/teaser
CRXDE: Teaser component

You’ll notice that the component doesn’t have any content, as it overlays the core Teaser component.

Step 2: Navigate to the core Teaser component to access the design dialog (Policy-level fields).

Path: /apps/core/wcm/components/teaser/v2/teaser/cq:design_dialog/content/items/tabs/items/main/items/elements/items/pretitle
CRXDE: Design dialog

You can see that Policy-level fields are located under the design dialog, which can be used to modify the component dialog or behavior. The powerful aspect of policies is that you can have different policies for different templates.

Step 3: Go to the component dialog and examine the Pretitle fields.

Path: /apps/core/wcm/components/teaser/v2/teaser/cq:dialog/content/items/tabs/items/text/items/columns/items/column/items/pretitle
CRXDE: Dialog

You’ll see that a property has been added to the component level using the OOTB (Out of the Box) Granite UI. This can be used for handling:

FilteringResourceWrapper
Render Condition

For more details, you can access these links for Granite UI and documentation.

However, to access any policy field at the component level, you can use the cqDesign implicit variable provided by ExpressionCustomizer by calling the property name as follows:

${cqDesign.[propertyname]} – general field name
For example: ${cqDesign.pretitleHidden} – pretitle field name

AEM Text component tracking – Adobe Client Datalayer

Problem Statement:

How to track Text component using ACDL? How to track the links inside a text component using ACDL? Can I add the custom ID to the links?

Requirement:

Track all the hyperlinks inside the text component using ACDL and add also provide a custom ID to track the links.

Introduction:

The goal of the Adobe Client Data Layer is to reduce the effort to instrument websites by providing a standardized method to expose and access any kind of data for any script.

The Adobe Client Data Layer is platform agnostic, but is fully integrated into the Core Components for use with AEM.

You can also learn more about the OOTB core text component here and also you can learn more about the Adobe Client data layer here. You can also learn about how to create tracking for the custom component here.

In the following example, we are going to use component delegation to delegate the OOTB text component and enable custom tracking on the text component, you can learn more about component delegation here.

Add a JSOUP and Lombok dependency to your project.

<dependency>
	<groupId>org.jsoup</groupId>
	<artifactId>jsoup</artifactId>
	<version>1.13.1</version>
	<scope>provided</scope>
</dependency>
<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<version>1.18.20</version>
	<scope>provided</scope>
</dependency>

Add a service called JSONConverter as shown below:

package com.adobe.aem.guides.wknd.core.service;

import com.fasterxml.jackson.databind.util.JSONPObject;

/**
 * Interface to deal with Json.
 */
public interface JSONConverter {

    /**
     * Convert Json Object to given object
     *
     * @param jsonpObject
     * @param clazz       type of class
     * @return @{@link Object}
     */
    @SuppressWarnings("rawtypes")
    Object convertToObject(JSONPObject jsonpObject, Class clazz);

    /**
     * Convert Json Object to given object
     *
     * @param jsonString
     * @param clazz      type of class
     * @return @{@link Object}
     */
    @SuppressWarnings("rawtypes")
    Object convertToObject(String jsonString, Class clazz);

    /**
     * Convert Json Object to given object
     *
     * @param object
     * @return @{@link String}
     */
    String convertToJsonString(Object object);

    /**
     * Convert Json Object to given object
     *
     * @param object
     * @return @{@link JSONPObject}
     */
    JSONPObject convertToJSONPObject(Object object);
}

Add a Service implementation JSONConverterImpl to convert object to JSON String using Object Mapper API

package com.adobe.aem.guides.wknd.core.service.impl;

import com.adobe.aem.guides.wknd.core.service.JSONConverter;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.util.JSONPObject;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;

@Component(service = JSONConverter.class)
public class JSONConverterImpl implements JSONConverter {

    private static final Logger LOG = LoggerFactory.getLogger(JSONConverterImpl.class);

    @SuppressWarnings("unchecked")
    @Override
    public Object convertToObject(JSONPObject jsonpObject, @SuppressWarnings("rawtypes") Class clazz) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.readValue(jsonpObject.toString(), clazz);
        } catch (IOException e) {
            LOG.debug("IOException while converting JSON to {} class {}", clazz.getName(), e.getMessage());
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Object convertToObject(String jsonString, @SuppressWarnings("rawtypes") Class clazz) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.readValue(jsonString, clazz);
        } catch (IOException e) {
            LOG.debug("IOException while converting JSON to {} class {}", clazz.getName(), e.getMessage());
        }
        return null;
    }

    @Override
    public String convertToJsonString(Object object) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.setSerializationInclusion(Include.NON_EMPTY).writerWithDefaultPrettyPrinter().writeValueAsString(object);
        } catch (IOException e) {
            LOG.debug("IOException while converting object {} to Json String {}", object.getClass().getName(),
                    e.getMessage());
        }
        return null;
    }

    @Override
    public JSONPObject convertToJSONPObject(Object object) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            String jsonString = mapper.writeValueAsString(object);
            return mapper.readValue(jsonString, JSONPObject.class);
        } catch (IOException e) {
            LOG.debug("IOException while converting object {} to Json String {}", object.getClass().getName(),
                    e.getMessage());
        }
        return null;
    }
}

Create TexModelImpl Sling model class which will be extending OOTB Text component and add delegate to override default getText() method.

Create a custom method called addLinkTracking and JSOUP API to read the text and get all the hyperlinks, once you have all the hyperlinks you can add custom tracking code by calling getLinkData method and this method should take care of custom ID tracking or generating default ID for all the links.

package com.adobe.aem.guides.wknd.core.models.impl;

import com.adobe.aem.guides.wknd.core.service.JSONConverter;
import com.adobe.cq.export.json.ComponentExporter;
import com.adobe.cq.export.json.ExporterConstants;
import com.adobe.cq.wcm.core.components.models.Text;
import com.adobe.cq.wcm.core.components.util.ComponentUtils;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.components.ComponentContext;
import lombok.experimental.Delegate;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.*;
import org.apache.sling.models.annotations.injectorspecific.*;
import org.apache.sling.models.annotations.via.ResourceSuperType;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

@Model(adaptables = SlingHttpServletRequest.class, adapters = {Text.class, ComponentExporter.class}, resourceType = TextModelImpl.RESOURCE_TYPE, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class TextModelImpl implements Text {

    public static final String RESOURCE_TYPE = "wknd/components/text";

    @SlingObject
    protected Resource resource;

    @ScriptVariable(injectionStrategy = InjectionStrategy.OPTIONAL)
    private ComponentContext componentContext;

    @ScriptVariable(injectionStrategy = InjectionStrategy.OPTIONAL)
    private Page currentPage;

    @ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
    @Default(values=StringUtils.EMPTY)
    protected String id;

    @OSGiService
    private JSONConverter jsonConverter;

    @Self
    @Via(type = ResourceSuperType.class)
    @Delegate(excludes = DelegationExclusion.class)
    private Text text;

    @Override
    public String getText() {
        return addLinkTracking(text.getText());
    }

    private String addLinkTracking(String text) {
        if(StringUtils.isNotEmpty(text) && (ComponentUtils.isDataLayerEnabled(resource) || resource.getPath().contains("/content/experience-fragments"))) {
            Document doc = Jsoup.parse(text);
            Elements anchors = doc.select("a");
            AtomicInteger counter = new AtomicInteger(1);
            anchors.stream().forEach(anch -> {
                anch.attr("data-cmp-clickable", true);
                anch.attr("data-cmp-data-layer", getLinkData(anch, counter.getAndIncrement()));
            });
            return doc.body().html();
        }
        return text;
    }

    public String getLinkData(Element anchor, int count) {
        //Create a map of properties we want to expose
        Map<String, Object> textLinkProperties = new HashMap<>();
        textLinkProperties.put("@type", resource.getResourceType()+"/link");
        textLinkProperties.put("dc:title", anchor.text());
        textLinkProperties.put("xdm:linkURL", anchor.attr("href"));

        //Use AEM Core Component utils to get a unique identifier for the Byline component (in case multiple are on the page)                        
        String textLinkID;
        if(StringUtils.isEmpty(id)) {
            textLinkID = ComponentUtils.getId(resource, this.currentPage, this.componentContext) + ComponentUtils.ID_SEPARATOR + ComponentUtils.generateId("link", resource.getPath()+anchor.text());
        } else {
            textLinkID = ComponentUtils.getId(resource, this.currentPage, this.componentContext) + ComponentUtils.ID_SEPARATOR + "link-" + count;
        }
        // Return the bylineProperties as a JSON String with a key of the bylineResource's ID
        return String.format("{\"%s\":%s}",
                textLinkID,
                jsonConverter.convertToJsonString(textLinkProperties));
    }

    private interface DelegationExclusion {
        String getText();
    }
}

Check the default tracking for hyperlinks inside the text component

auto generated link tracking

Add a custom ID to the component as shown below:

tracking ID field

In the below screenshot, we can see a custom tracking ID added to the link and each link will be called has 1, 2, 3 …

custom tracking ID for each link
click event getting captured

You can also learn more about
AEM ACDL (Adobe Client Data Layer) tracking – Core Component

AEM ACDL (Adobe Client Data Layer) tracking – Core Component

Problem Statement:

What is ACDL? How to use ACDL within AEM? How to add custom ID for component tracking

Requirement:

Enable ACDL on the project and track the events and add provide ID for component tracking instead of using auto generated HEX value

Introduction:

The Adobe Client Data Layer introduces a standard method to collect and store data about a visitors experience on a webpage and then make it easy to access this data. The Adobe Client Data Layer is platform agnostic but is fully integrated into the Core Components for use with AEM.

You can also visit the article to enable ACDL on your project, you can go through the document to understand its usage.

After enabling on the site you can see the datalayer for each component:

Enter adobeDataLayer.getState() in browser console

component datalayer

You can paste the following JS script on the console to test button clicks on the page

function bylineClickHandler(event) {
    var dataObject = window.adobeDataLayer.getState(event.eventInfo.path);
    if (dataObject != null && dataObject['@type'] === 'wknd/components/button') {  
        console.log("Component Clicked!");      
        console.log("Component Path: " + dataObject['xdm:linkURL']);        
        console.log("Component text: " + dataObject['dc:title']);
    }
}

window.adobeDataLayer.push(function (dl) {
     dl.addEventListener("cmp:click", bylineClickHandler);
});
tracking button click event

You can also change the component ID to a readable ID for tracking purposes by adding ID field details inside the component

ID field

Now you can see the readable ID added to the button component

button component with id details

You can go through the following document to track the events in Adobe Analytics