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 = true

Important: 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:

  1. ${resource.children} - Iterates over all children of the current resource
  2. ${child.name == 'links'} - Finds the multifield node (name from dialog XML)
  3. ${child.children} - Iterates over item nodes (item0, item1, item2, ...)
  4. ${link.properties.title} - Access properties of each item node
  5. ${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 @ context='html'}
            </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 as link.title
  • Composite with resource.children → access as link.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: &lt;p&gt;Text&lt;/p&gt; (shows HTML tags as text) -->

<!-- CORRECT - use context='html' -->
<div>${bullet.properties.bulletPoint @ context='html'}</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

  1. Choose the right type: Simple for single values, Composite for objects
  2. Consistent naming: Always use name="./fieldName" with ./
  3. Required fields: Add required="{Boolean}true" to mandatory fields
  4. Null checking: Always check null in HTL and Java
  5. @ChildResource: Perfect for composite multifields in Sling Models
  6. Inner classes: Use inner class to represent composite items
  7. Resource.children: Most robust approach for HTL from 6.2
  8. 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 @ context='unsafe'}
</pre>

<!-- Debug composite multifield -->
<pre data-sly-unwrap="${wcmmode.disabled}">
  Links: ${properties.links @ context='unsafe'}
</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! 🚀