// AEM Insider · EP05

AEM Dialogs —
Deep Dive.

Build any dialog from scratch — fields, tabs, validation, and multifields inside multifields.
youtube.com/@aeminsider  ·  aeminsider.com  ·  aeminsider.substack.com
Teaser
EDIT DIALOG
Content Link & CTA Style
Title *
AEM Dialogs, demystified
Description
Bold, lists, links — rich text.
Show call-to-action button
Cancel Done
// Chapter 01

What is a dialog?

The form that opens when an author clicks Configure on a component — the wrench in the toolbar.

The author fills in fields and saves. The values are stored in the repository. The component reads them and renders the page.

No code for the author. No repository access. Just a form.

A dialog is not code. It's nodes describing a form.
DIALOG
Title *
 
Link to page
 
Show button
the author fills it in
JCR
title
  "AEM Dialogs…"
linkUrl
  /content/…
showCta
  true {Boolean}
values stored as properties
TEASER COMPONENT
Read more
HTL reads & renders
// Chapter 02

Anatomy of a dialog

With Tabs — Today's Teaser
cq:dialog ──────────────────── cq/gui/components/authoring/dialog
└─ content ─────────────────── granite/ui/components/coral/foundation/container
   └─ items
      └─ tabs ──────────────── granite/ui/components/coral/foundation/tabs
         └─ items
            ├─ Tab 1 ───────── granite/ui/components/coral/foundation/container
            │  └─ items
            │     ├─ field ─── granite/ui/components/coral/foundation/form/textfield
            │     └─ field ─── granite/ui/components/coral/foundation/form/select
            └─ Tab 2 ───────── (same shape inside)
// items nodes have no resource
// type — plain holders.
// no fixedcolumns layer needed —
// fields render in any container.
Without Tabs — EP04's Card
cq:dialog ──────────────────── cq/gui/components/authoring/dialog
└─ content ─────────────────── granite/ui/components/coral/foundation/fixedcolumns
   └─ items
      └─ column ────────────── granite/ui/components/coral/foundation/container
         └─ items
            ├─ title ────────── granite/ui/components/coral/foundation/form/textfield
            └─ description ──── granite/ui/components/coral/foundation/form/textarea
// the flat shortcut — fine for a few
// fields. Retrofitting tabs later is annoying.

01 Container   02 Items   03 Tabs   04 Fields   — every dialog is a variation of these trees.

// Demo

Build the Teaser.

A new component, from an empty folder. Every dialog node by hand — no copying.

4
Tabs
6
Field types
2
Levels of multifield
0
Lines of Java
// Chapter 03

Meet Granite UI

AEM's built-in library of form fields. They live under /libs, ready to use. One node, pointed at the right resource type — that's a field.

Textfield
Why AEM dialogs matter
Select
Highlight
Checkbox
Show call-to-action button
// one node + one property = a form field
sling:resourceType = "granite/ui/components/coral/foundation/form/textfield"

Full reference — every field, every property. Link in the description.

// Chapter 04

One property decides everything

name = "./title"
Dialog field
name="./title"
JCR property
title
HTL
${properties.title}
Must match exactly. Case sensitive. Most dialog bugs live in this one property.
./  means: save it right here, on this component's node.
// Chapter 05

Six fields cover most real projects

Textfield
granite/ui/components/coral/foundation/form/textfield
stores → Text
Rich text (RTE)
cq/gui/components/authoring/dialog/richtext
stores → HTML
Pathfield
granite/ui/components/coral/foundation/form/pathfield
stores → A path
Checkbox
granite/ui/components/coral/foundation/form/checkbox
stores → true / false
Select
granite/ui/components/coral/foundation/form/select
stores → One value from a list
Multifield
+ NESTED
granite/ui/components/coral/foundation/form/multifield
stores → A list of items

More exist — numberfield, datepicker, switch. Same pattern; pick them up from the reference docs.

// Chapter 05 · Continued

Why some fields need more nodes

Rich Text
+ description
  - sling:resourceType =
    "cq/gui/…/dialog/richtext"
  + rtePlugins
    + format  - features = "bold,italic"
    + lists   - features = "*"
    + links   - features = "modifylink,unlink"
Its features are structured config — so they live in child nodes.
Select
+ style
  - sling:resourceType =
    "granite/ui/…/form/select"
  + items
    + default    - text · value · selected
    + highlight  - text · value
    + dark       - text · value
Each dropdown option is its own child node: text + value.
Checkbox
+ showCta
  - sling:resourceType =
    "granite/ui/…/form/checkbox"
  - text = "Show call-to-action button"
  - value = true {Boolean}
  - uncheckedValue = false {Boolean}
No child nodes — but text instead of fieldLabel, and typed Boolean values.

The resource type decides which properties and child nodes a field understands. Simple fields are flat. Structured configuration becomes child nodes. Every field's options are in the reference docs.

// Chapter 06

The HTL — reading it back

teaser.html
<div class="aemi-teaser aemi-teaser--${properties.style || 'default'}">
  <h2>${properties.title}</h2>
  <div>${properties.description @ context='html'}</div>
  <a data-sly-test="${properties.showCta && properties.linkUrl}"
     href="${properties.linkUrl @ extension='html'}">
    ${properties.ctaLabel || 'Read more'}</a>
</div>
@ context='html'

HTL escapes everything by default. This says: trusted rich text — sanitize, then render. RTE output only.

data-sly-test

Checkbox ticked AND a link present — otherwise no button at all.

|| fallback

Empty style → default. Empty button label → Read more.

Every line maps to one dialog field. Same names, both sides — that's the whole contract.

// Chapter 06 · Continued

The styles — for today only

teaser.html — <style> · base
.aemi-teaser {
    max-width: 640px;
    padding: 24px;
    border: 1px solid #ddd;
    border-radius: 8px;
}
.aemi-teaser__title { margin: 0 0 12px; }
.aemi-teaser__description { color: #444; line-height: 1.5; }
.aemi-teaser__cta {
    display: inline-block;
    margin-top: 16px;
    padding: 10px 22px;
    background: #D32F2F;
    color: #fff;
    border-radius: 6px;
}
.aemi-teaser__highlights { margin: 16px 0 0; padding-left: 20px; }
.aemi-teaser__highlight a { color: #D32F2F; }
teaser.html — <style> · variants
/* the Style dropdown's value
   becomes one of these classes */
.aemi-teaser--highlight { border: 2px solid #D32F2F; background: #fdf3f3; }
.aemi-teaser--dark {
    background: #16171A;
    border-color: #16171A;
    color: #fff;
}
.aemi-teaser--dark .aemi-teaser__title { color: #fff; }

/* the RTE outputs its own <p> and <li> tags — the page's
   global styles color those directly. Reach them explicitly: */
.aemi-teaser--dark .aemi-teaser__description,
.aemi-teaser--dark .aemi-teaser__description p,
.aemi-teaser--dark .aemi-teaser__description li,
.aemi-teaser--dark .aemi-teaser__highlight {
    color: rgba(255,255,255,.8);
}
Scoped

Everything sits under .aemi-teaser — nothing leaks into the rest of the page.

--highlight / --dark

The select's value becomes a CSS class. One dropdown, three looks — live in the demo.

Inline for today

Fine for learning — real projects ship CSS through clientlibs. Later in the series.

// Chapter 07

The simple multifield

Dialog
+ highlights
  - sling:resourceType =
    "granite/ui/components/coral/foundation/form/multifield"
  + field
    - sling:resourceType =
      "granite/ui/components/coral/foundation/form/textfield"
    - name = "./highlights"
No composite. The field itself is the textfield — one per row.
JCR
+ teaser
  - highlights = [
      "Built from scratch",
      "No copy-paste",
      "Validation included"
    ]
No child nodes — the whole list is ONE multi-value property.
HTL
<ul data-sly-list.item=
        "${properties.highlights}">
    <li>${item}</li>
</ul>
A property — so plain properties reads it. No resource.children.

One field per row → one multi-value property → plain properties reads it. The moment a row needs text AND a link — or a nested list — a property can't hold it. That's the composite multifield. Next slide.

// Chapter 07 · Continued

How to configure a multifield

Dialog
+ highlights
  - sling:resourceType = "granite/ui/components/coral/foundation/form/multifield"
  - composite = true
  + field
    - sling:resourceType = "granite/ui/components/coral/foundation/container"
    - name = "./highlights"
    + items
      + text
        - sling:resourceType = "granite/ui/components/coral/foundation/form/textfield"
        - name = "text"
      + link
        - sling:resourceType = "granite/ui/components/coral/foundation/form/pathfield"
        - name = "link"
      + points
        - sling:resourceType = "granite/ui/components/coral/foundation/form/multifield"
        - composite = true
        + field
          - sling:resourceType = "granite/ui/components/coral/foundation/container"
          - name = "./points"
          + items
            + text
              - sling:resourceType = "granite/ui/components/coral/foundation/form/textfield"
              - name = "text"
JCR
+ teaser
  + highlights
    + item0
      - text
      - link
      + points
        + item0
          - text
        + item1
          - text
    + item1
      - text

name="./highlights" → the highlights node    name="./points" → a points node inside each item

// Chapter 07 · Continued

The multifield HTL

teaser.html — added after the CTA link
<sly data-sly-list.child="${resource.children}">
  <ul data-sly-test="${child.name == 'highlights'}"
      data-sly-list.item="${child.children}">
    <li>
      <a data-sly-test="${item.valueMap.link}"
         href="${item.valueMap.link @ extension='html'}">${item.valueMap.text}</a>
      <sly data-sly-test="${!item.valueMap.link}">${item.valueMap.text}</sly>
      <!-- nested multifield: same pattern, one level deeper -->
      <sly data-sly-list.grp="${item.children}">
        <ul class="aemi-teaser__points" data-sly-test="${grp.name == 'points'}"
            data-sly-list.point="${grp.children}">
          <li>${point.valueMap.text}</li>
        </ul>
      </sly>
    </li>
  </ul>
</sly>
resource.children

properties can't read child nodes. So we walk the component's children and pick the one named highlights.

item.valueMap

Each item is a node. valueMap reads its properties: text, link. Link present → link. Otherwise plain text.

Sling Model

The nested list is three loops deep. It works — but you can see where this is going. A Sling Model reads the whole tree in a few clean lines. Later in the series.

Paste it under the CTA link, refresh — the highlights render in the order the author chose.

// Chapter 08

Validation

Title * i
 
Please fill out this field

The tab holding the broken field gets flagged too — authors are pointed straight at the problem.

requiredCan't save while the field is empty. Red ring + flagged tab.
maxlengthA hard character limit. The author simply can't type past it.
emptyTextGrey placeholder inside an empty field. Nudges good content.
fieldDescriptionThe small info icon with a tooltip. Explains the rule.
Custom rules — regex, "required only if…" — need JavaScript, shipped through a clientlib. Coming later in the series.
// Next Episode

EP06 — Editable Templates.

How every AEM page gets its structure
Templates, policies & initial content
Why our Teaser was allowed on the page
Build a template in the template editor
YouTube
youtube.com/@aeminsider
Substack
aeminsider.substack.com
Website
aeminsider.com

Subscribe — and drop a comment. I read all of them.