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.
title "AEM Dialogs…" linkUrl /content/… showCta true {Boolean}
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)
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
01 Container 02 Items 03 Tabs 04 Fields — every dialog is a variation of these trees.
A new component, from an empty folder. Every dialog node by hand — no copying.
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.
Full reference — every field, every property. Link in the description.
More exist — numberfield, datepicker, switch. Same pattern; pick them up from the reference docs.
+ description - sling:resourceType = "cq/gui/…/dialog/richtext" + rtePlugins + format - features = "bold,italic" + lists - features = "*" + links - features = "modifylink,unlink"
+ style - sling:resourceType = "granite/ui/…/form/select" + items + default - text · value · selected + highlight - text · value + dark - text · value
+ showCta - sling:resourceType = "granite/ui/…/form/checkbox" - text = "Show call-to-action button" - value = true {Boolean} - uncheckedValue = false {Boolean}
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.
<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>
HTL escapes everything by default. This says: trusted rich text — sanitize, then render. RTE output only.
Checkbox ticked AND a link present — otherwise no button at all.
Empty style → default. Empty button label → Read more.
Every line maps to one dialog field. Same names, both sides — that's the whole contract.
.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; }
/* 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); }
Everything sits under .aemi-teaser — nothing leaks into the rest of the page.
The select's value becomes a CSS class. One dropdown, three looks — live in the demo.
Fine for learning — real projects ship CSS through clientlibs. Later in the series.
+ highlights - sling:resourceType = "granite/ui/components/coral/foundation/form/multifield" + field - sling:resourceType = "granite/ui/components/coral/foundation/form/textfield" - name = "./highlights"
+ teaser - highlights = [ "Built from scratch", "No copy-paste", "Validation included" ]
<ul data-sly-list.item=
"${properties.highlights}">
<li>${item}</li>
</ul>
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.
+ 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"
+ 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
<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>
properties can't read child nodes. So we walk the component's children and pick the one named highlights.
Each item is a node. valueMap reads its properties: text, link. Link present → link. Otherwise plain text.
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.
The tab holding the broken field gets flagged too — authors are pointed straight at the problem.
Subscribe — and drop a comment. I read all of them.