Build a Custom Component in AEM as a Cloud Service: HTL, Sling Model and Touch UI Dialog

Building a custom component was one of the first things I wrote about on this blog. In AEM: Create Custom Component in CQ5 with Custom Dialog the component was a JSP with a Classic UI dialog, and later posts like Develop a Component in Touch UI and Create a Basic Sightly Component in AEM moved to Touch UI and HTL. Those posts still get readers, but on AEM as a Cloud Service a few things have changed.

The Classic UI is removed, so cq:Dialog ExtJS dialogs are gone. The HTL JavaScript Use-API was officially deprecated in release 2024.10.0. Code reaches /apps only through Cloud Manager (or a Rapid Development Environment), and Cloud Manager checks your unit test coverage. The modern recipe is a component node, a Granite UI (Coral 3) dialog, a Sling Model and an HTL script, with a unit test next to it.

In this tutorial I will build an Event Card component from scratch: a title, a location, a level, a link and an agenda that authors fill with a composite multifield. Let’s see it step by step.

Table of contents


Why this matters

Core Components cover most of what a site needs, but every project ends up with a few components that are specific to the business: an event card, a product spec table, a store locator teaser. If you build those the old way, with JSP logic and scriptlets, they are hard to test, and untested code is exactly what the Cloud Manager code quality step flags. Adobe’s code quality rules mark coverage below 50% as an “Important” issue, which pauses the pipeline until someone reviews it.

A component built from HTL, a Sling Model and a Granite UI dialog keeps markup, logic and authoring separate. The model is plain Java that you can test without an AEM instance, the HTL stays readable for front-end developers, and the dialog is declarative XML.

Who should read this

This post is for developers who know basic AEM concepts and want the current way of building components on AEMaaCS, and for anyone maintaining JSP or Classic UI components in a project that is moving to the cloud.

Prerequisites

  • A project generated from the AEM Project Archetype with aemVersion=cloud. The examples use the app ID mysite and the package com.mysite.core.
  • A local AEM SDK author on http://localhost:4502, Java 21 and Maven. See Set Up AEMaaCS Local Development.
  • Basic Sling Model and HTL knowledge. My older post Use Sling Model with Sightly in AEM covers the basics.
  • Optional: access to a Rapid Development Environment (RDE) and the aio CLI with the RDE plugin.

How it worked in CQ5 and AEM 6.x vs AEMaaCS

PartCQ5 / early AEM 6.xAEM as a Cloud Service
Rendering scriptJSP with scriptletsHTL
LogicJSP, WCMUsePojo or the JavaScript Use-APISling Models (the JavaScript Use-API is deprecated since 2024.10.0)
DialogClassic UI cq:Dialog (ExtJS), later Coral 2Granite UI Coral 3 cq:dialog (granite/ui/components/coral/foundation/*)
Design settingsDesign mode and design dialogsContent policies on editable templates
DeploymentPackage Manager, CRXDE or VLT straight into /appsCloud Manager pipeline; RDE for fast iterations; /apps is immutable at runtime
Build JDKJava 8 or 11JDK 21 preferred (17 and 11 also listed for builds); Java 21 runtime

Anatomy of a component on AEMaaCS

A custom component is spread over two Maven modules. This is the complete list of files we will create:

ui.apps/src/main/content/jcr_root/apps/mysite/components/eventcard/
├── .content.xml              (component definition)
├── eventcard.html            (HTL script)
└── _cq_dialog/
    └── .content.xml          (Touch UI edit dialog)

core/src/main/java/com/mysite/core/models/
├── EventCard.java            (model interface)
├── AgendaItem.java           (model for one multifield entry)
└── impl/
    └── EventCardImpl.java    (Sling Model implementation)

core/src/test/java/com/mysite/core/models/impl/EventCardImplTest.java
core/src/test/resources/com/mysite/core/models/impl/EventCardImplTest.json

The component definition tells AEM the component exists. The dialog defines what authors can enter. The Sling Model reads and prepares the values. HTL renders them. That separation is the whole idea.


Hands-on: build the Event Card component step by step

Step 1 : Create the component definition

Create the folder ui.apps/src/main/content/jcr_root/apps/mysite/components/eventcard with this .content.xml:

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0"
          xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="cq:Component"
    jcr:title="Event Card"
    jcr:description="Shows an event with its location, level, agenda and a link."
    componentGroup="MySite - Content"/>

componentGroup decides where the component appears in the side panel and in the Allowed Components list of a template policy. A group that starts with a dot is hidden from the UI, which is what you want for helper components but not here. If you were extending a Core Component, you would also add sling:resourceSuperType; Adobe’s WKND Byline component, for example, extends core/wcm/components/image/v2/image.

Step 2 : Build the Touch UI dialog

Create _cq_dialog/.content.xml. The root uses cq/gui/components/authoring/dialog, and every field uses a Granite UI Coral 3 resource type. The agenda is a composite multifield, so each entry is saved as its own child node with a time and a topic property.

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
          xmlns:jcr="http://www.jcp.org/jcr/1.0"
          xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:primaryType="nt:unstructured"
    jcr:title="Event Card"
    sling:resourceType="cq/gui/components/authoring/dialog">
    <content
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/coral/foundation/container">
        <items jcr:primaryType="nt:unstructured">
            <tabs
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/tabs"
                maximized="{Boolean}true">
                <items jcr:primaryType="nt:unstructured">
                    <properties
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Properties"
                        sling:resourceType="granite/ui/components/coral/foundation/container"
                        margin="{Boolean}true">
                        <items jcr:primaryType="nt:unstructured">
                            <title
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                fieldLabel="Title"
                                name="./title"
                                required="{Boolean}true"/>
                            <location
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                fieldLabel="Location"
                                name="./location"/>
                            <level
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/coral/foundation/form/select"
                                fieldLabel="Level"
                                name="./level">
                                <items jcr:primaryType="nt:unstructured">
                                    <beginner jcr:primaryType="nt:unstructured" text="Beginner" value="beginner"/>
                                    <intermediate jcr:primaryType="nt:unstructured" text="Intermediate" value="intermediate"/>
                                    <advanced jcr:primaryType="nt:unstructured" text="Advanced" value="advanced"/>
                                </items>
                            </level>
                            <link
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
                                fieldLabel="Event page"
                                name="./link"
                                rootPath="/content/mysite"/>
                            <openInNewTab
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
                                text="Open the link in a new tab"
                                name="./openInNewTab"
                                value="true"
                                uncheckedValue="false"/>
                        </items>
                    </properties>
                    <agenda
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Agenda"
                        sling:resourceType="granite/ui/components/coral/foundation/container"
                        margin="{Boolean}true">
                        <items jcr:primaryType="nt:unstructured">
                            <agenda
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
                                composite="{Boolean}true"
                                fieldLabel="Agenda items">
                                <field
                                    jcr:primaryType="nt:unstructured"
                                    sling:resourceType="granite/ui/components/coral/foundation/container"
                                    name="./agenda">
                                    <items jcr:primaryType="nt:unstructured">
                                        <time
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                            fieldLabel="Time"
                                            name="./time"/>
                                        <topic
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                            fieldLabel="Topic"
                                            name="./topic"/>
                                    </items>
                                </field>
                            </agenda>
                        </items>
                    </agenda>
                </items>
            </tabs>
        </items>
    </content>
</jcr:root>

After an author saves the dialog, the content looks like this in CRXDE:

eventcard                      (sling:resourceType = mysite/components/eventcard)
  title        = AEM Meetup Bengaluru
  location     = Bengaluru
  level        = beginner
  link         = /content/mysite/us/en/events
  openInNewTab = true
  agenda
    item0      (time = 10:00, topic = Welcome and introductions)
    item1      (time = 10:30, topic = Editable templates in AEMaaCS)
CRXDE Lite showing the eventcard node under the events page with an agenda child holding item0 to item2, and the title, location, level, link and openInNewTab properties
What the dialog saved. The composite multifield creates one child node per agenda row under agenda.

Note:- Every name must start with ./, otherwise the value is not stored on the component node. This is still the most common reason for “my dialog does not save” questions I get.

Step 3 : Write the Sling Model interface

I always start with an interface. HTL and tests depend only on it, and the implementation can change freely.

package com.mysite.core.models;

import java.util.List;

import com.adobe.cq.export.json.ComponentExporter;

public interface EventCard extends ComponentExporter {

    String getTitle();

    String getLocation();

    String getLevel();

    String getLink();

    boolean isOpenInNewTab();

    List<AgendaItem> getAgenda();

    boolean isEmpty();
}

Extending ComponentExporter is optional. I add it because it costs nothing and gives you JSON output later (see the exporter section). Each agenda entry gets a small model of its own:

package com.mysite.core.models;

import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

@Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class AgendaItem {

    @ValueMapValue
    private String time;

    @ValueMapValue
    private String topic;

    public String getTime() {
        return time;
    }

    public String getTopic() {
        return topic;
    }
}

Step 4 : Implement the Sling Model

The implementation follows the same pattern as Adobe’s WKND Byline model: adaptable from the request, registered for the component’s resource type, optional injection by default, and a @PostConstruct method for anything that needs code.

package com.mysite.core.models.impl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.annotation.PostConstruct;

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.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.ExporterConstants;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

import com.adobe.cq.export.json.ComponentExporter;
import com.mysite.core.models.AgendaItem;
import com.mysite.core.models.EventCard;

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

    static final String RESOURCE_TYPE = "mysite/components/eventcard";

    @SlingObject
    private Resource resource;

    @ValueMapValue
    private String title;

    @ValueMapValue
    private String location;

    @ValueMapValue
    private String level;

    @ValueMapValue
    private String link;

    @ValueMapValue
    private boolean openInNewTab;

    private final List<AgendaItem> agenda = new ArrayList<>();

    @PostConstruct
    protected void init() {
        Resource agendaRoot = resource.getChild("agenda");
        if (agendaRoot != null) {
            for (Resource entry : agendaRoot.getChildren()) {
                AgendaItem item = entry.adaptTo(AgendaItem.class);
                if (item != null && StringUtils.isNotBlank(item.getTopic())) {
                    agenda.add(item);
                }
            }
        }
    }

    @Override
    public String getTitle() {
        return title;
    }

    @Override
    public String getLocation() {
        return location;
    }

    @Override
    public String getLevel() {
        return level;
    }

    @Override
    public String getLink() {
        // internal page paths need the .html extension, external URLs stay as they are
        if (StringUtils.startsWith(link, "/content/")) {
            return link + ".html";
        }
        return link;
    }

    @Override
    public boolean isOpenInNewTab() {
        return openInNewTab;
    }

    @Override
    public List<AgendaItem> getAgenda() {
        return Collections.unmodifiableList(agenda);
    }

    @Override
    public boolean isEmpty() {
        return StringUtils.isBlank(title);
    }

    @Override
    public String getExportedType() {
        return RESOURCE_TYPE;
    }
}

A few choices worth explaining:

  • Request as adaptable. Adobe’s examples adapt from SlingHttpServletRequest, which gives you access to request-scoped objects later (for example when you delegate to a Core Component model with ModelFactory, as the WKND Byline does).
  • Injector-specific annotations. @ValueMapValue and @SlingObject say exactly where a value comes from, which is easier to read than the generic @Inject.
  • Agenda in @PostConstruct. I read the child nodes explicitly and skip empty entries, so the HTL never has to deal with half-filled rows.
  • An isEmpty() method. The HTL uses it to decide between real output and the author placeholder.

Note:- The Apache Sling Models documentation now also lists the Jakarta Servlet request (SlingJakartaHttpServletRequest) as an adaptable. Adobe’s AEMaaCS documentation and the WKND tutorial still use SlingHttpServletRequest, so that is what I use here. Keep an eye on the AEMaaCS release notes before you switch.

Step 5 : Write the HTL script

Create eventcard.html. It uses the Core Components placeholder template, which only renders in edit and preview mode when the component is empty, so authors see a box they can click instead of nothing.

<sly data-sly-use.card="com.mysite.core.models.EventCard"
     data-sly-use.placeholderTemplate="core/wcm/components/commons/v1/templates.html"></sly>
<div class="cmp-eventcard" data-sly-test="${!card.empty}">
    <h3 class="cmp-eventcard__title">${card.title}</h3>
    <p class="cmp-eventcard__meta" data-sly-test="${card.location || card.level}">
        <span class="cmp-eventcard__location" data-sly-test="${card.location}">${card.location}</span>
        <span class="cmp-eventcard__level cmp-eventcard__level--${card.level}" data-sly-test="${card.level}">${card.level}</span>
    </p>
    <ul class="cmp-eventcard__agenda" data-sly-test="${card.agenda}" data-sly-list.item="${card.agenda}">
        <li class="cmp-eventcard__agenda-item"><strong>${item.time}</strong> ${item.topic}</li>
    </ul>
    <a class="cmp-eventcard__link"
       data-sly-test="${card.link}"
       href="${card.link}"
       data-sly-attribute.target="${card.openInNewTab ? '_blank' : ''}"
       data-sly-attribute.rel="${card.openInNewTab ? 'noopener noreferrer' : ''}">View event</a>
</div>
<sly data-sly-call="${placeholderTemplate.placeholder @ isEmpty=card.empty, classAppend='cmp-eventcard'}"></sly>

HTL escapes every expression for its context automatically: the title as HTML text, the link as a URI, the level inside the class attribute. That is one of the main reasons Adobe prefers HTL over JSP. Notice there is no Java logic in the script; if you find yourself writing a long expression, move it into the model.

For the styling, add the CSS to the ui.frontend module next to the other component styles. The archetype compiles it into the clientlib-site client library (category mysite.site), which is served from /etc.clientlibs because the library sets allowProxy.

.cmp-eventcard {
    border: 1px solid #d1d5db;
    border-radius: 8px;
    padding: 1.25rem;
}
.cmp-eventcard__level--beginner { color: #047857; }
.cmp-eventcard__level--advanced { color: #b91c1c; }
.cmp-eventcard__agenda { list-style: none; padding: 0; }

Step 6 : Add a unit test

AEM Mocks (from wcm.io) is already part of the archetype’s core module. Put a small content tree in a JSON file:

{
  "eventcard": {
    "jcr:primaryType": "nt:unstructured",
    "sling:resourceType": "mysite/components/eventcard",
    "title": "AEM Meetup Bengaluru",
    "location": "Bengaluru",
    "level": "beginner",
    "link": "/content/mysite/us/en/events",
    "openInNewTab": true,
    "agenda": {
      "jcr:primaryType": "nt:unstructured",
      "item0": { "jcr:primaryType": "nt:unstructured", "time": "10:00", "topic": "Welcome and introductions" },
      "item1": { "jcr:primaryType": "nt:unstructured", "time": "10:30", "topic": "Editable templates in AEMaaCS" },
      "item2": { "jcr:primaryType": "nt:unstructured", "time": "11:00" }
    }
  },
  "empty": {
    "jcr:primaryType": "nt:unstructured",
    "sling:resourceType": "mysite/components/eventcard"
  }
}

Then test the model through the same adaptation path that HTL uses:

package com.mysite.core.models.impl;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import com.mysite.core.models.AgendaItem;
import com.mysite.core.models.EventCard;

import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;

@ExtendWith(AemContextExtension.class)
class EventCardImplTest {

    private final AemContext ctx = new AemContext();

    @BeforeEach
    void setUp() {
        ctx.addModelsForClasses(EventCardImpl.class, AgendaItem.class);
        ctx.load().json("/com/mysite/core/models/impl/EventCardImplTest.json", "/content/test");
    }

    @Test
    void readsDialogValues() {
        ctx.currentResource("/content/test/eventcard");
        EventCard card = ctx.request().adaptTo(EventCard.class);

        assertEquals("AEM Meetup Bengaluru", card.getTitle());
        assertEquals("/content/mysite/us/en/events.html", card.getLink());
        assertTrue(card.isOpenInNewTab());
        assertEquals(2, card.getAgenda().size()); // the entry without a topic is skipped
        assertFalse(card.isEmpty());
    }

    @Test
    void isEmptyWithoutTitle() {
        ctx.currentResource("/content/test/empty");
        EventCard card = ctx.request().adaptTo(EventCard.class);

        assertTrue(card.isEmpty());
        assertTrue(card.getAgenda().isEmpty());
    }
}

Run mvn clean install in core. Two small tests like these cover nearly every line of the model, which keeps you well above the 50% coverage threshold that Cloud Manager checks.

Step 7 : Deploy locally or to an RDE

Build and deploy the whole project to the local SDK:

mvn clean install -PautoInstallSinglePackage

autoInstallSinglePackage deploys the all package; the autoInstallPackage profile deploys one module only. To try the component on a Rapid Development Environment, install the same package with the RDE plugin of the aio CLI:

aio plugins:install @adobe/aio-cli-plugin-aem-rde
aio aem:rde:install all/target/mysite.all-1.0.0-SNAPSHOT.zip

For Dev, Stage and Production the only way in is the Cloud Manager pipeline. Cloud Manager builds with the JDK named in .cloudmanager/java-version at the root of your repository; use 21 for new projects.

Step 8 : Allow the component in a template and test it

A new component does not appear on pages until a template allows it. Open Tools > General > Templates > MySite > Content Page, select the main Layout Container in Structure mode, open its policy and tick Event Card under MySite – Content on the Allowed Components tab. The companion post Editable Templates, Policies and the Style System in AEM as a Cloud Service explains policies in detail.

Now open a page, drag the Event Card in, check that the placeholder shows, fill the dialog and add two agenda rows. View the page with ?wcmmode=disabled to see the published markup.

Events page viewed with wcmmode=disabled showing two Event Cards with title, location, colour-coded level, agenda rows and a View event link
The finished component on a page, viewed with ?wcmmode=disabled. Two Event Cards, same code, different dialog values.

Optional: JSON output with the Sling Model Exporter

Because the model has an @Exporter annotation and implements ComponentExporter, Sling can serialize it to JSON with the model selector. That is handy for debugging, and for any front end that wants the component data without the markup:

curl -u admin:admin "http://localhost:4502/content/mysite/us/en/jcr:content/root/container/container/eventcard.model.json"

The path depends on where the component sits on your page; copy it from CRXDE. The JSON contains one entry per getter, plus :type with the resource type from getExportedType(). Add @JsonIgnore to getters you do not want to expose.

Extend a Core Component or build your own?

SituationBetter choiceWhy
You need a Title, Teaser or Image with a small tweakProxy component, optionally with the Style SystemYou inherit features, accessibility fixes and upgrades from Core Components
You need extra fields on an existing componentExtend it with sling:resourceSuperType and delegate in your modelAdobe’s WKND Byline extends the Core Image this way
The data and markup are specific to your businessCustom component like Event CardNothing to inherit; keep it small and tested

If you have not created proxy components yet, start with Create Proxy Component in AEM. Most projects need far fewer custom components than they think.

Common mistakes and troubleshooting

SymptomLikely causeFix
The component is not in the side panelNot allowed in the Layout Container policy, or componentGroup starts with a dotAllow it in the template policy; use a visible group
Dialog values are not savedField name without the ./ prefixUse name="./title"
Agenda is always emptyMultifield without composite="{Boolean}true", so values are stored as one string arraySet composite and put name="./agenda" on the inner container
HTL renders nothing and the log shows the model can’t be adaptedThe core bundle is not active, or the model package is not registeredCheck the bundle on http://localhost:4502/system/console/bundles (local SDK only); rebuild core
CSS works on author but returns 404 on publishClient library without allowProxySet allowProxy="{Boolean}true" and reference it through /etc.clientlibs
Pipeline pauses at code qualityCoverage below 50% or other Important issuesAdd AEM Mocks tests for the new models

Hands-on challenge

Add a Speakers tab to the dialog with a second composite multifield (name and role), expose it as List<Speaker> in the model, render it in HTL and extend the unit test. Then give the component a design dialog with a Styles tab and a “Compact” style. Success means the test passes, the JSON from .model.json shows the speakers, and the style changes the card without touching the HTL.

Key takeaways

  • A component on AEMaaCS is a component node, a Granite UI Coral 3 dialog, a Sling Model and an HTL script.
  • Classic UI dialogs are removed and the JavaScript Use-API is deprecated; use Sling Models with HTL.
  • Use a composite multifield for repeating groups of fields and read the child nodes in the model.
  • Keep logic in Java and test it with AEM Mocks; Cloud Manager flags coverage below 50%.
  • Deploy with autoInstallSinglePackage locally, aio aem:rde:install on an RDE, and Cloud Manager for every other environment.
  • A component is only available to authors after a template policy allows it.

Knowledge check

1. Which resource type does the root node of a Touch UI dialog use?

cq/gui/components/authoring/dialog.

2. Why does the agenda multifield need composite="{Boolean}true"?

Without it a multifield stores one property with an array of values. Composite mode stores each entry as a child node, so one entry can hold several fields.

3. What does DefaultInjectionStrategy.OPTIONAL change?

Missing properties no longer make the adaptation fail; the fields simply stay null or their default value.

4. When does the Core Components placeholder template render?

Only in edit or preview mode, and only when isEmpty is true.

5. How can you deploy the component to Production?

Only through a Cloud Manager pipeline. /apps is immutable at runtime, and Package Manager cannot install code there.

Frequently asked questions

Do I need a cq:editConfig for my component?

Not for a component with a dialog; Adobe’s reference guide says it appears in the components browser even without one. Add it when you need drop targets or listeners such as afteredit="REFRESH_PAGE".

Can I still use WCMUsePojo or the JavaScript Use-API?

The JavaScript Use-API was officially deprecated in release 2024.10.0, with the Java Use-API as the replacement, so move those scripts to Java. WCMUsePojo is a Java Use-API class and still works, but for new code use Sling Models: they are easier to test and are what Adobe’s examples use.

Should my model adapt from Resource or from the request?

Adobe’s component examples adapt from SlingHttpServletRequest. Use Resource for small helper models like AgendaItem, which only read properties.

How do I test the dialog itself?

Unit tests cover the model. For the dialog, open it on the local SDK or an RDE, and consider UI tests in the archetype’s ui.tests module for critical components.

Can I build the same component for Edge Delivery Services?

Not with this code. Edge Delivery Services uses blocks written in plain JavaScript and CSS. HTL components like this one are for AEM Sites pages rendered by AEM.

Continue your learning

References

Spread the love

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.