HTL Tutorial #13: Multifield in AEM - Gestire Liste di Dati
HTL Tutorial #13: Multifield in AEM - Gestire Liste di Dati
Cos'è un Multifield?
Un multifield è un componente dialog di AEM che permette agli autori di aggiungere multiple istanze dello stesso campo o set di campi.
Esempi pratici:
- Lista di tag o keywords
- Lista di link con titolo e URL
- Gallery di immagini con caption
- Lista di features con icona e descrizione
Due Tipi di Multifield
AEM offre due tipi di multifield con comportamenti completamente diversi:
| Tipo | Configurazione | Struttura JCR | Caso d'uso |
|---|---|---|---|
| Semplice | Senza composite |
Array di stringhe | Un solo valore per item (tags, keywords, URLs) |
| Composite | composite="{Boolean}true" |
Nodi con properties | Più valori per item (link con title+url, image con src+alt) |
1. Multifield Semplice
Il multifield semplice è ideale per liste di valori singoli (stringhe, numeri, date).
Quando Usarlo?
✅ Usa multifield semplice quando:
- Hai bisogno di un solo valore per item
- Liste di tag, keywords, categories
- Liste di URL semplici
- Liste di nomi o ID
❌ Non usare multifield semplice quando:
- Serve più di un campo per item (es. titolo + URL)
- Hai bisogno di struttura complessa
Dialog XML - Multifield Semplice
<?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">
<!-- MULTIFIELD SEMPLICE -->
<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>⚠️ Nota: NESSUN composite="{Boolean}true" qui!
Struttura JCR Risultante - Semplice
Quando l'autore aggiunge 3 tag, AEM crea un array di stringhe:
/content/mysite/page/jcr:content/tagscomponent
└─ tags = ["AEM", "HTL", "Sling Models"] (String[])Importante: È un array di stringhe, NON una struttura con nodi!
Recupero Dati da HTL - Multifield Semplice
<!-- Verifica se ci sono tag -->
<div data-sly-test="${properties.tags}" class="tag-list">
<h3>Tags:</h3>
<ul>
<!-- Itera sull'array di stringhe -->
<li data-sly-list.tag="${properties.tags}">
<span class="tag-badge">
${tag}
</span>
<span class="tag-index">#${tagList.count}</span>
</li>
</ul>
</div>Output HTML:
<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>Recupero Dati da Java - Multifield Semplice
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;
/**
* Restituisce la lista di tag
*/
public List<String> getTags() {
if (tags != null && tags.length > 0) {
return Arrays.asList(tags);
}
return Collections.emptyList();
}
/**
* Verifica se ci sono tag
*/
public boolean hasTags() {
return tags != null && tags.length > 0;
}
/**
* Conta i tag
*/
public int getTagsCount() {
return tags != null ? tags.length : 0;
}
}HTL con 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>⚠️ Nota HTL: HTL NON supporta chiamate a metodi con parametri come ${model.getFirstNTags(3)}.
Puoi chiamare solo getter senza parametri (${model.tags}, ${model.tagsCount}, ecc.).
2. Multifield Composite
Il multifield composite è ideale per liste di oggetti complessi con più proprietà.
Vantaggi e Quando Usarlo?
✅ Usa multifield composite quando:
- Ogni item ha più di un campo (es. link con titolo + URL + target)
- Hai bisogno di struttura complessa
- Serve organizzazione gerarchica nel JCR
- Vuoi accedere a properties specifiche facilmente
Vantaggi:
- 📦 Ogni item è un nodo JCR separato
- 🎯 Accesso granulare alle properties
- 🔍 Query JCR più facili
- 🏗️ Struttura più pulita e mantenibile
- 📝 Supporto per rich text, pathfield, checkbox, etc.
Dialog XML - Multifield Composite
<?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">
<!-- MULTIFIELD COMPOSITE -->
<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">
<!-- Campo 1: Titolo -->
<title
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Title"
name="./title"
required="{Boolean}true"/>
<!-- Campo 2: URL -->
<url
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
fieldLabel="URL"
name="./url"
rootPath="/content"/>
<!-- Campo 3: Apri in nuova 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" - Questo crea nodi invece di array!
Struttura JCR Risultante - Composite
Quando l'autore aggiunge 3 link, AEM crea una struttura di nodi:
/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 = trueImportante: Ogni item è un nodo separato con le sue properties!
Recupero Dati da HTL - Multifield Composite
Approccio 1: Accesso Semplificato (AEM 6.3+)
Funziona se il nodo è direttamente sotto 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>Approccio 2: Resource.children (AEM 6.2+, Raccomandato)
Questo è l'approccio più robusto che hai condiviso:
<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 per primo/ultimo -->
<span data-sly-test="${linkList.first}" class="badge">Primo</span>
<span data-sly-test="${linkList.last}" class="badge">Ultimo</span>
</li>
</sly>
</ul>
</nav>
</sly>
</sly>Come funziona:
${resource.children}- Itera su tutti i child della risorsa corrente${child.name == 'links'}- Trova il nodo del multifield (nome dal dialog XML)${child.children}- Itera sui nodi item (item0, item1, item2, ...)${link.properties.title}- Accedi alle proprietà di ogni nodo item${linkList.count}- Usa le variabili di iterazione (count, index, first, last, odd, even)
✅ Vantaggi di questo approccio:
- Funziona dalla 6.2, molto stabile
- Navigazione JCR esplicita e chiara
- Accesso completo a tutte le variabili di iterazione
- Funziona con qualsiasi struttura JCR, non solo multifield
Recupero Dati da Java - Multifield Composite
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 {
/**
* Injection automatica dei child nodes del multifield
* @ChildResource cerca un child node chiamato "links"
*/
@ChildResource
private List<Resource> links;
/**
* Restituisce la lista di LinkItem
*/
public List<LinkItem> getLinkItems() {
List<LinkItem> items = new ArrayList<>();
if (links != null) {
for (Resource link : links) {
items.add(new LinkItem(link));
}
}
return items;
}
/**
* Verifica se ci sono link
*/
public boolean hasLinks() {
return links != null && !links.isEmpty();
}
/**
* Conta i link
*/
public int getLinksCount() {
return links != null ? links.size() : 0;
}
/**
* Inner class per rappresentare un singolo 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 per attributo target
*/
public String getTarget() {
return newTab ? "_blank" : null;
}
/**
* Helper per attributo rel
*/
public String getRel() {
return newTab ? "noopener noreferrer" : null;
}
}
}HTL con 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>Esempio Completo: Bullet Points con 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>Struttura JCR
/content/mysite/page/jcr:content/component
└─ bullets (nt:unstructured)
├─ item0 (nt:unstructured)
│ └─ bulletPoint = "<p>Primo punto importante</p>"
├─ item1 (nt:unstructured)
│ └─ bulletPoint = "<p>Secondo punto da ricordare</p>"
└─ item2 (nt:unstructured)
└─ bulletPoint = "<p>Terzo punto conclusivo</p>"HTL con 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>
<!-- Badge -->
<span data-sly-test="${bulletList.first}" class="first-badge">Primo</span>
<span data-sly-test="${bulletList.last}" class="last-badge">Ultimo</span>
</li>
</sly>
</ul>
</sly>
</sly>
</div>Tabella di Confronto: Semplice vs Composite
| Aspetto | Multifield Semplice | Multifield Composite |
|---|---|---|
| Configurazione | Nessun composite |
composite="{Boolean}true" |
| Struttura JCR | Array di stringhe | Nodi con properties |
| Numero campi per item | 1 solo campo | Più campi per item |
| Injection Java | @ValueMapValue String[] tags |
@ChildResource List<Resource> items |
| Accesso HTL | ${properties.tags} (array) |
${properties.links} o ${resource.children} |
| Caso d'uso | Tag, keywords, liste semplici | Link, features, gallery |
| Complessità | Bassa | Media |
| Flessibilità | Limitata | Alta |
Problemi Comuni con i Multifield
❌ Problema 1: Dimenticare composite="{Boolean}true"
<!-- SBAGLIATO per dati complessi -->
<multifield
sling:resourceType="granite/ui/components/coral/foundation/form/multifield">
<!-- Questo crea un ARRAY, non nodi! -->
<!-- CORRETTO per dati complessi -->
<multifield
composite="{Boolean}true">
<!-- Questo crea NODI con properties -->Sintomo: In CRXDE vedi un array invece di nodi, impossibile accedere a link.title in HTL.
Soluzione: Aggiungi sempre composite="{Boolean}true" se hai più campi.
❌ Problema 2: Nome field errato
<!-- SBAGLIATO - manca ./ -->
<field name="links">
<!-- CORRETTO -->
<field name="./links">Sintomo: I dati non vengono salvati nel JCR.
Soluzione: Usa sempre name="./nomeField" con ./ prefix.
❌ Problema 3: Non gestire null/empty in HTL
<!-- RISCHIOSO - crash se links è null o vuoto -->
<ul data-sly-list.link="${properties.links}">
<!-- SICURO - controlla prima -->
<ul data-sly-test="${properties.links}"
data-sly-list.link="${properties.links}">Sintomo: Pagina bianca o errore HTL se il multifield è vuoto.
Soluzione: Usa sempre data-sly-test prima di iterare.
❌ Problema 4: Confondere access path per semplice vs composite
<!-- MULTIFIELD SEMPLICE (array) -->
<span data-sly-list.tag="${properties.tags}">
${tag} <!-- tag è una stringa -->
</span>
<!-- MULTIFIELD COMPOSITE (nodi) con properties.x -->
<span data-sly-list.link="${properties.links}">
${link.title} <!-- link è un oggetto con properties -->
</span>
<!-- MULTIFIELD COMPOSITE (nodi) con 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} <!-- DEVI usare .properties qui! -->
</sly>
</sly>
</sly>Sintomo: HTL non mostra dati o mostra [object Object].
Soluzione:
- Semplice → accedi direttamente al valore
- Composite con
properties.x→ accedi comelink.title - Composite con
resource.children→ accedi comelink.properties.title
❌ Problema 5: @ChildResource con nome sbagliato
// SBAGLIATO - il nome non corrisponde al dialog
@ChildResource(name = "items") // ma nel dialog è "links"!
private List<Resource> links;
// CORRETTO - il nome corrisponde al dialog
@ChildResource // cerca automaticamente "links"
private List<Resource> links;
// OPPURE esplicitamente
@ChildResource(name = "links")
private List<Resource> links;Sintomo: La lista è sempre vuota in Java, anche se ci sono dati nel JCR.
Soluzione: Il nome del field Java deve corrispondere al name del multifield nel dialog XML.
❌ Problema 6: Non controllare null nei Model
// RISCHIOSO - NullPointerException se links è null
public int getLinksCount() {
return links.size(); // ❌ CRASH!
}
// SICURO
public int getLinksCount() {
return links != null ? links.size() : 0; // ✅ OK
}Sintomo: NullPointerException nei log AEM.
Soluzione: Controlla sempre null prima di usare collections.
❌ Problema 7: Context XSS scorretto per RichText
<!-- SBAGLIATO - il rich text viene escaped -->
<div>${bullet.properties.bulletPoint}</div>
<!-- Output: <p>Testo</p> (mostra tag HTML come testo) -->
<!-- CORRETTO - usa context='html' -->
<div>${bullet.properties.bulletPoint }</div>
<!-- Output: <p>Testo</p> (rendering HTML corretto) -->Sintomo: HTML viene mostrato come testo invece di essere renderizzato.
Soluzione: Usa sempre @ context='html' per campi RichText.
Best Practices Finali
- ✅ Scegli il tipo giusto: Semplice per valori singoli, Composite per oggetti
- ✅ Naming consistente: Usa sempre
name="./nomeField"con./ - ✅ Required fields: Aggiungi
required="{Boolean}true"ai campi obbligatori - ✅ Null checking: Controlla sempre null in HTL e Java
- ✅ @ChildResource: Perfetta per multifield composite in Sling Models
- ✅ Inner classes: Usa inner class per rappresentare item composite
- ✅ Resource.children: Approccio più robusto per HTL dalla 6.2
- ✅ Context awareness: Usa
@ context='html'per RichText
Debug Multifield
In CRXDE Lite
Verifica la struttura in /content/path/to/component:
Multifield Semplice:
/content/mysite/page/jcr:content/component
└─ tags = ["tag1", "tag2", "tag3"] (String[])Multifield Composite:
/content/mysite/page/jcr:content/component
└─ links (nt:unstructured)
├─ item0 (nt:unstructured)
│ ├─ title (String)
│ └─ url (String)
└─ item1 (nt:unstructured)In HTL
<!-- Debug multifield semplice -->
<pre data-sly-unwrap="${wcmmode.disabled}">
Tags: ${properties.tags }
</pre>
<!-- Debug multifield composite -->
<pre data-sly-unwrap="${wcmmode.disabled}">
Links: ${properties.links }
</pre>Prossima Lezione
Questa conclude la serie di tutorial HTL! Hai imparato:
- ✅ Tutti i block statements HTL (text, test, list, use, template, attribute, element, unwrap)
- ✅ Espressioni, operatori, context XSS
- ✅ Multifield semplice e composite in AEM
Risorse finali:
Lezione #13 della serie HTL Tutorial - Lezione Finale! ← Lezione precedente
Grazie per aver seguito questa serie! Buon coding con HTL! 🚀