AEM in 40 Days
CalendarPhase 3 · Building with the Archetype

Day 21 of 40

WKND: A Custom Component

Granite UI dialogs, the Sling Model, and the HTL that binds them

~60 min read2 videos3 source pages

By the end of today you should be able to

  1. Build a custom component end to end: node, dialog, model, HTL
  2. Write a Granite UI dialog with several field types including a multifield
  3. Give the component a sensible empty state in the editor
  4. Add a component icon and make it presentable to authors

The full stack of a component

The WKND tutorial builds a Byline component: a contributor's name, a photo, and a list of occupations. It is small but exercises everything — dialog with a multifield, a Sling Model with real logic, HTL that handles an empty state. Here is the shape of the work.

1. The component node

/apps/wknd/components/byline/.content.xml
  jcr:primaryType = "cq:Component"
  jcr:title = "Byline"
  jcr:description = "Displays a contributor's name, photo and occupations"
  componentGroup = "WKND - Content"

No sling:resourceSuperType here — this one is genuinely custom rather than a proxy.

2. The dialog

Three fields: an image (via a pathfield), a name, and a multifield of occupations.

<name
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="Name"
    name="./name"
    required="{Boolean}true"/>

<occupations
    sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
    fieldLabel="Occupations"
    composite="{Boolean}true">
  <field
      sling:resourceType="granite/ui/components/coral/foundation/container"
      name="./occupations">
    <items>
      <occupation
          sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
          fieldLabel="Occupation"
          name="./occupation"/>
    </items>
  </field>
</occupations>

A composite multifield stores each entry as a child node with named properties, which is what you want as soon as an entry has more than one field. It produces occupations/item0/occupation, occupations/item1/occupation and so on.

3. The Sling Model

@Model(adaptables = SlingHttpServletRequest.class,
       adapters = { Byline.class },
       resourceType = "wknd/components/byline",
       defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class BylineImpl implements Byline {

    @ValueMapValue
    private String name;

    @ChildResource
    private List<Resource> occupations;

    @Override
    public List<String> getOccupations() {
        if (occupations == null) return Collections.emptyList();
        return occupations.stream()
                .map(r -> r.getValueMap().get("occupation", String.class))
                .filter(Objects::nonNull)
                .sorted()
                .collect(Collectors.toList());
    }

    @Override
    public boolean isEmpty() {
        return StringUtils.isBlank(name) || getOccupations().isEmpty();
    }
}

Note where the logic lives: null-handling, filtering and sorting are all in Java, where they are testable. HTL never has to know any of it.

4. The HTL, including the empty state

<sly data-sly-use.byline="com.adobe.aem.guides.wknd.core.models.Byline"/>

<sly data-sly-test="${byline.empty}"
     data-sly-call="${placeholderTemplate.placeholder @ isEmpty=true}"/>

<div class="cmp-byline" data-sly-test="${!byline.empty}">
  <h2 class="cmp-byline__name">${byline.name}</h2>
  <ul class="cmp-byline__occupations"
      data-sly-list.occupation="${byline.occupations}">
    <li>${occupation}</li>
  </ul>
</div>
Always handle the empty state

A component with no content should render a visible placeholder in the editor and nothing at all on publish. Without it, an author drops the component, sees nothing, and cannot click it to open the dialog. The Core Components placeholder template gives you the standard treatment for free.

5. The icon

Components appear in the author's component browser, and a distinguishable icon genuinely helps. Options, in order of preference:

  • cq:icon — the name of a Coral UI icon, e.g. textEdit. Simplest and consistent with the rest of the UI.
  • abbreviation — a two-character abbreviation, derived from jcr:title if unset.
  • A .svg file in the component folder, for a bespoke mark.

6. Allow it in a policy

The component will not appear until a template policy allows it. This is the step people forget after building everything else — the component is complete, correct, and invisible. Add it to the layout container's allowed components in the relevant template.

Today's takeaway

Node, dialog, model, HTL, icon, policy. Logic in the model, empty state in the HTL, and remember that the policy is what makes it real for authors.

Watch

Adobe's own videos for this topic. They load only when you press play.

Component Development
Component Icons

Read on Experience League

The primary sources these notes are drawn from.

Your notes

Saved automatically to this browser.

Check yourself

10 questions on today's material. 80% to pass.

Take the quiz