HTL Tutorial #13: Multifield in AEM - Managing Data Lists
HTL Tutorial #13: Multifield in AEM - Managing Data Lists
What is a Multifield?
A multifield is an AEM dialog component that allows authors to add multiple instances of the same field or set of fields.
Practical examples:
- List of tags or keywords
- List of links with title and URL
- Image gallery with captions
- List of features with icon and description
Two Types of Multifield
AEM offers two types of multifield with completely different behaviors:
| Type | Configuration | JCR Structure | Use Case |
|---|---|---|---|
| Simple | Without composite |
String array | Single value per item (tags, keywords, URLs) |
| Composite | composite="{Boolean}true" |
Nodes with properties | Multiple values per item (link with title+url, image with src+alt) |
1. Simple Multifield
The simple multifield is ideal for lists of single values (strings, numbers, dates).
When to Use It?
✅ Use simple multifield when:
- You need only one value per item
- Lists of tags, keywords, categories
- Lists of simple URLs
- Lists of names or IDs
❌ Don't use simple multifield when:
- You need more than one field per item (e.g., title + URL)
- You need a complex structure
Dialog XML - Simple Multifield
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:granite="http://www.adobe.com/jcr/granite/1.0"
xmlns:cq="http://www.day.com/jcr/cq/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="Tags Component"
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">
<!-- SIMPLE MULTIFIELD -->
<tags
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
fieldLabel="Tags">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
name="./tags"/>
</tags>
</items>
</content>
</jcr:root>⚠️ Note: NO composite="{Boolean}true" here!
Resulting JCR Structure - Simple
When the author adds 3 tags, AEM creates a string array:
/content/mysite/page/jcr:content/tagscomponent
└─ tags = ["AEM", "HTL", "Sling Models"] (String[])Important: It's a string array, NOT a structure with nodes!
Retrieving Data from HTL - Simple Multifield
<!-- Check if there are tags -->
<div data-sly-test="${properties.tags}" class="tag-list">
<h3>Tags:</h3>
<ul>
<!-- Iterate over the string array -->
<li data-sly-list.tag="${properties.tags}">
<span class="tag-badge">
${tag}
</span>
<span class="tag-index">#${tagList.count}</span>
</li>
</ul>
</div>HTML Output:
<div class="tag-list">
<h3>Tags:</h3>
<ul>
<li>
<span class="tag-badge">AEM</span>
<span class="tag-index">#1</span>
</li>
<li>
<span class="tag-badge">HTL</span>
<span class="tag-index">#2</span>
</li>
<li>
<span class="tag-badge">Sling Models</span>
<span class="tag-index">#3</span>
</li>
</ul>
</div>Retrieving Data from Java - Simple Multifield
package com.mysite.models;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import java.util.Arrays;
import java.util.List;
import java.util.Collections;
@Model(adaptables = Resource.class)
public class TagsModel {
@ValueMapValue
private String[] tags;
/**
* Returns the list of tags
*/
public List<String> getTags() {
if (tags != null && tags.length > 0) {
return Arrays.asList(tags);
}
return Collections.emptyList();
}
/**
* Checks if there are tags
*/
public boolean hasTags() {
return tags != null && tags.length > 0;
}
/**
* Counts the tags
*/
public int getTagsCount() {
return tags != null ? tags.length : 0;
}
}HTL with Model:
<div data-sly-use.model="com.mysite.models.TagsModel"
data-sly-test="${model.hasTags}"
class="tag-cloud">
<h3>Tags (${model.tagsCount})</h3>
<ul class="tag-list">
<li data-sly-list.tag="${model.tags}">
<a href="/search?tag=${tag @ uriencode}" class="tag">
${tag}
</a>
</li>
</ul>
</div>⚠️ HTL Note: HTL DOES NOT support method calls with parameters like ${model.getFirstNTags(3)}.
You can only call getters without parameters (${model.tags}, ${model.tagsCount}, etc.).
2. Composite Multifield
The composite multifield is ideal for lists of complex objects with multiple properties.
Advantages and When to Use It?
✅ Use composite multifield when:
- Each item has more than one field (e.g., link with title + URL + target)
- You need a complex structure
- You need hierarchical organization in JCR
- You want to access specific properties easily
Advantages:
- 📦 Each item is a separate JCR node
- 🎯 Granular access to properties
- 🔍 Easier JCR queries
- 🏗️ Cleaner and more maintainable structure
- 📝 Support for rich text, pathfield, checkbox, etc.
Dialog XML - Composite Multifield
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:granite="http://www.adobe.com/jcr/granite/1.0"
xmlns:cq="http://www.day.com/jcr/cq/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="Link List Component"
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">
<!-- COMPOSITE MULTIFIELD -->
<links
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
fieldLabel="Link Items"
composite="{Boolean}true">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container"
name="./links">
<items jcr:primaryType="nt:unstructured">
<!-- Field 1: Title -->
<title
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Title"
name="./title"
required="{Boolean}true"/>
<!-- Field 2: URL -->
<url
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
fieldLabel="URL"
name="./url"
rootPath="/content"/>
<!-- Field 3: Open in new tab -->
<newTab
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
fieldLabel="Open in new tab"
name="./newTab"
text="Open link in new tab"
value="{Boolean}true"
uncheckedValue="{Boolean}false"/>
</items>
</field>
</links>
</items>
</content>
</jcr:root>🔑 Key Property: composite="{Boolean}true" - This creates nodes instead of arrays!
Resulting JCR Structure - Composite
When the author adds 3 links, AEM creates a node structure:
/content/mysite/page/jcr:content/linklistcomponent
└─ links (nt:unstructured)
├─ item0 (nt:unstructured)
│ ├─ title = "Homepage"
│ ├─ url = "/content/mysite/home"
│ └─ newTab = false
├─ item1 (nt:unstructured)
│ ├─ title = "About Us"
│ ├─ url = "/content/mysite/about"
│ └─ newTab = false
└─ item2 (nt:unstructured)
├─ title = "Contact"
├─ url = "/content/mysite/contact"
└─ newTab = trueImportant: Each item is a separate node with its own properties!
Retrieving Data from HTL - Composite Multifield
Approach 1: Simplified Access (AEM 6.3+)
Works if the node is directly under properties:
<nav data-sly-test="${properties.links}">
<ul class="link-list">
<li data-sly-list.link="${properties.links}">
<a href="${link.url}"
data-sly-attribute.target="${link.newTab ? '_blank' : ''}"
data-sly-attribute.rel="${link.newTab ? 'noopener noreferrer' : ''}">
${link.title}
</a>
</li>
</ul>
</nav>Approach 2: Resource.children (AEM 6.2+, Recommended)
This is the most robust approach:
<sly data-sly-list.child="${resource.children}">
<sly data-sly-test="${child.name == 'links'}">
<nav class="navigation">
<ul class="link-list">
<sly data-sly-list.link="${child.children}">
<li class="link-item">
<span class="link-number">${linkList.count}</span>
<a href="${link.properties.url}"
data-sly-attribute.target="${link.properties.newTab ? '_blank' : ''}"
data-sly-attribute.rel="${link.properties.newTab ? 'noopener noreferrer' : ''}">
${link.properties.title}
</a>
<!-- Badge for first/last -->
<span data-sly-test="${linkList.first}" class="badge">First</span>
<span data-sly-test="${linkList.last}" class="badge">Last</span>
</li>
</sly>
</ul>
</nav>
</sly>
</sly>How it works:
${resource.children}- Iterates over all children of the current resource${child.name == 'links'}- Finds the multifield node (name from dialog XML)${child.children}- Iterates over item nodes (item0, item1, item2, ...)${link.properties.title}- Access properties of each item node${linkList.count}- Use iteration variables (count, index, first, last, odd, even)
✅ Advantages of this approach:
- Works from 6.2, very stable
- Explicit and clear JCR navigation
- Full access to all iteration variables
- Works with any JCR structure, not just multifields
Retrieving Data from Java - Composite Multifield
package com.mysite.models;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.ChildResource;
import java.util.ArrayList;
import java.util.List;
import java.util.Collections;
@Model(adaptables = Resource.class)
public class LinkListModel {
/**
* Automatic injection of multifield child nodes
* @ChildResource looks for a child node called "links"
*/
@ChildResource
private List<Resource> links;
/**
* Returns the list of LinkItems
*/
public List<LinkItem> getLinkItems() {
List<LinkItem> items = new ArrayList<>();
if (links != null) {
for (Resource link : links) {
items.add(new LinkItem(link));
}
}
return items;
}
/**
* Checks if there are links
*/
public boolean hasLinks() {
return links != null && !links.isEmpty();
}
/**
* Counts the links
*/
public int getLinksCount() {
return links != null ? links.size() : 0;
}
/**
* Inner class to represent a single link
*/
public static class LinkItem {
private String title;
private String url;
private boolean newTab;
public LinkItem(Resource resource) {
this.title = resource.getValueMap().get("title", String.class);
this.url = resource.getValueMap().get("url", String.class);
this.newTab = resource.getValueMap().get("newTab", false);
}
public String getTitle() {
return title;
}
public String getUrl() {
return url;
}
public boolean isNewTab() {
return newTab;
}
/**
* Helper for target attribute
*/
public String getTarget() {
return newTab ? "_blank" : null;
}
/**
* Helper for rel attribute
*/
public String getRel() {
return newTab ? "noopener noreferrer" : null;
}
}
}HTL with Model:
<nav data-sly-use.model="com.mysite.models.LinkListModel"
data-sly-test="${model.hasLinks}">
<h3>Navigation (${model.linksCount} links)</h3>
<ul class="link-list">
<li data-sly-list.link="${model.linkItems}">
<a href="${link.url}"
data-sly-attribute.target="${link.target}"
data-sly-attribute.rel="${link.rel}">
${link.title}
</a>
</li>
</ul>
</nav>Complete Example: Bullet Points with RichText
Dialog XML
<bullets
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
fieldLabel="Bullet Points"
composite="{Boolean}true">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container"
name="./bullets">
<items jcr:primaryType="nt:unstructured">
<bulletPoint
jcr:primaryType="nt:unstructured"
sling:resourceType="cq/gui/components/authoring/dialog/richtext"
fieldLabel="Bullet Point"
name="./bulletPoint"
useFixedInlineToolbar="{Boolean}true"/>
</items>
</field>
</bullets>JCR Structure
/content/mysite/page/jcr:content/component
└─ bullets (nt:unstructured)
├─ item0 (nt:unstructured)
│ └─ bulletPoint = "<p>First important point</p>"
├─ item1 (nt:unstructured)
│ └─ bulletPoint = "<p>Second point to remember</p>"
└─ item2 (nt:unstructured)
└─ bulletPoint = "<p>Third concluding point</p>"HTL with Resource.children
<div class="bullet-list" data-sly-test="${resource.children}">
<sly data-sly-list.child="${resource.children}">
<sly data-sly-test="${child.name == 'bullets'}">
<ul>
<sly data-sly-list.bullet="${child.children}">
<li class="bullet-item">
<span class="bullet-number">${bulletList.count}</span>
<div class="bullet-content">
${bullet.properties.bulletPoint }
</div>
<!-- Badges -->
<span data-sly-test="${bulletList.first}" class="first-badge">First</span>
<span data-sly-test="${bulletList.last}" class="last-badge">Last</span>
</li>
</sly>
</ul>
</sly>
</sly>
</div>Comparison Table: Simple vs Composite
| Aspect | Simple Multifield | Composite Multifield |
|---|---|---|
| Configuration | No composite |
composite="{Boolean}true" |
| JCR Structure | String array | Nodes with properties |
| Fields per item | 1 field only | Multiple fields per item |
| Java Injection | @ValueMapValue String[] tags |
@ChildResource List<Resource> items |
| HTL Access | ${properties.tags} (array) |
${properties.links} or ${resource.children} |
| Use Case | Tags, keywords, simple lists | Links, features, galleries |
| Complexity | Low | Medium |
| Flexibility | Limited | High |
Common Multifield Problems
❌ Problem 1: Forgetting composite="{Boolean}true"
<!-- WRONG for complex data -->
<multifield
sling:resourceType="granite/ui/components/coral/foundation/form/multifield">
<!-- This creates an ARRAY, not nodes! -->
<!-- CORRECT for complex data -->
<multifield
composite="{Boolean}true">
<!-- This creates NODES with properties -->Symptom: In CRXDE you see an array instead of nodes, unable to access link.title in HTL.
Solution: Always add composite="{Boolean}true" if you have multiple fields.
❌ Problem 2: Wrong field name
<!-- WRONG - missing ./ -->
<field name="links">
<!-- CORRECT -->
<field name="./links">Symptom: Data is not saved in JCR.
Solution: Always use name="./fieldName" with ./ prefix.
❌ Problem 3: Not handling null/empty in HTL
<!-- RISKY - crash if links is null or empty -->
<ul data-sly-list.link="${properties.links}">
<!-- SAFE - check first -->
<ul data-sly-test="${properties.links}"
data-sly-list.link="${properties.links}">Symptom: Blank page or HTL error if multifield is empty.
Solution: Always use data-sly-test before iterating.
❌ Problem 4: Confusing access path for simple vs composite
<!-- SIMPLE MULTIFIELD (array) -->
<span data-sly-list.tag="${properties.tags}">
${tag} <!-- tag is a string -->
</span>
<!-- COMPOSITE MULTIFIELD (nodes) with properties.x -->
<span data-sly-list.link="${properties.links}">
${link.title} <!-- link is an object with properties -->
</span>
<!-- COMPOSITE MULTIFIELD (nodes) with resource.children -->
<sly data-sly-list.child="${resource.children}">
<sly data-sly-test="${child.name == 'links'}">
<sly data-sly-list.link="${child.children}">
${link.properties.title} <!-- MUST use .properties here! -->
</sly>
</sly>
</sly>Symptom: HTL doesn't show data or displays [object Object].
Solution:
- Simple → access value directly
- Composite with
properties.x→ access aslink.title - Composite with
resource.children→ access aslink.properties.title
❌ Problem 5: @ChildResource with wrong name
// WRONG - name doesn't match dialog
@ChildResource(name = "items") // but in dialog it's "links"!
private List<Resource> links;
// CORRECT - name matches dialog
@ChildResource // automatically looks for "links"
private List<Resource> links;
// OR explicitly
@ChildResource(name = "links")
private List<Resource> links;Symptom: List is always empty in Java, even if there's data in JCR.
Solution: The Java field name must match the multifield name in dialog XML.
❌ Problem 6: Not checking null in Models
// RISKY - NullPointerException if links is null
public int getLinksCount() {
return links.size(); // ❌ CRASH!
}
// SAFE
public int getLinksCount() {
return links != null ? links.size() : 0; // ✅ OK
}Symptom: NullPointerException in AEM logs.
Solution: Always check null before using collections.
❌ Problem 7: Incorrect XSS context for RichText
<!-- WRONG - rich text gets escaped -->
<div>${bullet.properties.bulletPoint}</div>
<!-- Output: <p>Text</p> (shows HTML tags as text) -->
<!-- CORRECT - use context='html' -->
<div>${bullet.properties.bulletPoint }</div>
<!-- Output: <p>Text</p> (correct HTML rendering) -->Symptom: HTML is displayed as text instead of being rendered.
Solution: Always use @ context='html' for RichText fields.
Final Best Practices
- ✅ Choose the right type: Simple for single values, Composite for objects
- ✅ Consistent naming: Always use
name="./fieldName"with./ - ✅ Required fields: Add
required="{Boolean}true"to mandatory fields - ✅ Null checking: Always check null in HTL and Java
- ✅ @ChildResource: Perfect for composite multifields in Sling Models
- ✅ Inner classes: Use inner class to represent composite items
- ✅ Resource.children: Most robust approach for HTL from 6.2
- ✅ Context awareness: Use
@ context='html'for RichText
Debugging Multifields
In CRXDE Lite
Check the structure in /content/path/to/component:
Simple Multifield:
/content/mysite/page/jcr:content/component
└─ tags = ["tag1", "tag2", "tag3"] (String[])Composite Multifield:
/content/mysite/page/jcr:content/component
└─ links (nt:unstructured)
├─ item0 (nt:unstructured)
│ ├─ title (String)
│ └─ url (String)
└─ item1 (nt:unstructured)In HTL
<!-- Debug simple multifield -->
<pre data-sly-unwrap="${wcmmode.disabled}">
Tags: ${properties.tags }
</pre>
<!-- Debug composite multifield -->
<pre data-sly-unwrap="${wcmmode.disabled}">
Links: ${properties.links }
</pre>Next Lesson
This concludes the HTL tutorial series! You've learned:
- ✅ All HTL block statements (text, test, list, use, template, attribute, element, unwrap)
- ✅ Expressions, operators, XSS contexts
- ✅ Simple and composite multifields in AEM
Final resources:
Lesson #13 of the HTL Tutorial series - Final Lesson! ← Previous lesson
Thank you for following this series! Happy coding with HTL! 🚀