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

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

  1. ${resource.children} - Itera su tutti i child della risorsa corrente
  2. ${child.name == 'links'} - Trova il nodo del multifield (nome dal dialog XML)
  3. ${child.children} - Itera sui nodi item (item0, item1, item2, ...)
  4. ${link.properties.title} - Accedi alle proprietà di ogni nodo item
  5. ${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 @ context='html'}
            </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 come link.title
  • Composite con resource.children → accedi come link.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: &lt;p&gt;Testo&lt;/p&gt; (mostra tag HTML come testo) -->

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

  1. Scegli il tipo giusto: Semplice per valori singoli, Composite per oggetti
  2. Naming consistente: Usa sempre name="./nomeField" con ./
  3. Required fields: Aggiungi required="{Boolean}true" ai campi obbligatori
  4. Null checking: Controlla sempre null in HTL e Java
  5. @ChildResource: Perfetta per multifield composite in Sling Models
  6. Inner classes: Usa inner class per rappresentare item composite
  7. Resource.children: Approccio più robusto per HTL dalla 6.2
  8. 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 @ context='unsafe'}
</pre>

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