Skip to main content
Level 2
April 29, 2026
Solved

TouchUI dialog not saving multifield data properly in editor mode but working fine in view as publish

  • April 29, 2026
  • 2 replies
  • 100 views

Hi everyone
dialog.xml

<?xml version="1.0" encoding="UTF-8"?>

<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/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"

xmlns:granite="http://www.adobe.com/jcr/granite/1.0"

jcr:primaryType="nt:unstructured"

jcr:title="Contact Cards Configuration"

sling:resourceType="cq/gui/components/authoring/dialog">

<content

jcr:primaryType="nt:unstructured"

sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns">

<items jcr:primaryType="nt:unstructured">

<column

jcr:primaryType="nt:unstructured"

sling:resourceType="granite/ui/components/coral/foundation/container">

<items jcr:primaryType="nt:unstructured">

<contacts

jcr:primaryType="nt:unstructured"

sling:resourceType="granite/ui/components/coral/foundation/form/multifield"

fieldLabel="Add Contact Fragments"

composite="{Boolean}false"

deleteHint="{Boolean}true"

typeHint="String[]">

<field

jcr:primaryType="nt:unstructured"

sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"

name="./contactFragmentPaths"

rootPath="/content/dam"

required="{Boolean}true"

nodeTypes="dam:Asset">

<granite:data

jcr:primaryType="nt:unstructured"

filter="hierarchyNotFile"

mimeTypes="[text/html]"/>

</field>

</contacts>

</items>

</column>

</items>

</content>

</jcr:root>

My model file
 

package com.mysite.aem.core.models;

 

import com.adobe.cq.dam.cfm.ContentElement;

import com.adobe.cq.dam.cfm.ContentFragment;

import org.apache.sling.api.resource.Resource;

import org.apache.sling.api.resource.ResourceResolver;

import org.apache.sling.models.annotations.DefaultInjectionStrategy;

import org.apache.sling.models.annotations.Model;

import org.apache.sling.models.annotations.injectorspecific.SlingObject;

import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

 

import javax.annotation.PostConstruct;

import java.util.ArrayList;

import java.util.List;

import java.util.Optional;

 

@Model(adaptables = { Resource.class},

adapters = ContactCardsModel.class,

defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)

public class ContactCardsModel {

 

private static final Logger LOG = LoggerFactory.getLogger(ContactCardsModel.class);

 

@SlingObject

private ResourceResolver resourceResolver;

 

// Use Object to prevent injection failure if the JCR stores a single string vs an array

@ValueMapValue(name = "contactFragmentPaths")

private Object contactPathsObject;

 

private List<Contact> contacts = new ArrayList<>();

 

@PostConstruct

protected void init() {

String[] paths = getPathsAsArray();

if (paths != null && resourceResolver != null) {

for (String path : paths) {

Resource fragmentResource = resourceResolver.getResource(path);

if (fragmentResource != null) {

ContentFragment fragment = fragmentResource.adaptTo(ContentFragment.class);

if (fragment != null) {

contacts.add(new Contact(

getElementValue(fragment, "name"),

getElementValue(fragment, "role"),

getElementValue(fragment, "profileimage"),

getElementValue(fragment, "gmail"),

getElementValue(fragment, "phone"),

getElementValue(fragment, "country")

));

}

}

}

}

}

 

/**

* Helper to safely convert the multifield object to a String array.

*/

private String[] getPathsAsArray() {

if (contactPathsObject instanceof String[]) {

return (String[]) contactPathsObject;

} else if (contactPathsObject instanceof String) {

return new String[]{(String) contactPathsObject};

}

return null;

}

 

private String getElementValue(ContentFragment fragment, String elementName) {

return Optional.ofNullable(fragment.getElement(elementName))

.map(ContentElement::getContent)

.orElse("");

}

 

public List<Contact> getContacts() {

return contacts;

}

 

public static class Contact {

private final String name;

private final String role;

private final String profileimage;

private final String gmail;

private final String phone;

private final String country;

 

public Contact(String name, String role, String profileimage, String gmail, String phone, String country) {

this.name = name;

this.role = role;

this.profileimage = profileimage;

this.gmail = gmail;

this.phone = phone;

this.country = country;

}

 

public String getName() { return name; }

public String getRole() { return role; }

public String getProfileimage() { return profileimage; }

public String getGmail() { return gmail; }

public String getPhone() { return phone; }

public String getCountry() { return country; }

}

}

Whenever I’m trying to update the data in dialog the old data is missing.

Data is losing only in author mode not in publisher mode How to fix it?

Best answer by akhil_merupula

Hi laxman,

Good question and I’ve seen this exact issue come up a few times. Looking at your dialog.xml and the Sling Model, I can spot what’s going wrong.
The problem is in your multifield configuration. You have composite set to false but your field name is ./contactFragmentPaths. When composite is false, AEM stores the multifield values as a String array directly on the component node. The issue is that in author mode, when you reopen the dialog, AEM is not consistently reading back the stored array and repopulating the multifield correctly especially when only one item exists, because Sling stores a single value as a plain String, not a String array.
Your Sling Model actually already handles this correctly on the read side with your getPathsAsArray() method that checks instanceof String vs String[]. So publish works fine because the model reads it correctly. The problem is purely on the author dialog side not repopulating.
There are two ways to fix this.
The first option is to change composite to true in your dialog and wrap the pathfield in a composite multifield structure. This makes AEM store each entry as a child node instead of a flat array, which is more reliable for dialog repopulation and eliminates the single value vs array ambiguity entirely. The tradeoff is you’d need to update your model to read from child nodes instead of a flat property.
The second option which is simpler if you want to keep your current model is to keep composite false but add this to your multifield node: renderHidden set to true. This forces the dialog to re-render existing values properly when reopened. Also double check that your field name is exactly ./contactFragmentPaths with the dot slash prefix, because without it the value gets written to a different path than where AEM looks when repopulating.
Also one thing I noticed your nodeTypes is set to dam:Asset but you’re pointing to Content Fragments which are dam:Asset technically but filtered through hierarchyNotFile. That should be fine but worth verifying the paths being saved are actually the CF paths and not folder paths.
If neither fixes it, drop a CRXDE screenshot of what the saved node looks like after you fill the dialog and I can pinpoint exactly where the read/write mismatch is happening.​​​​​​​​​

2 replies

chaudharynick
Level 4
May 5, 2026

Hi ​@laxman 

This is happening as you are trying to have it stored in the form of String[], which is not a correct way to create a multifield

You can try adding these 2 hidden fields as a workaround

<items jcr:primaryType="nt:unstructured">
<!-- Your existing multifield -->
<contacts
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
fieldLabel="Add Contact Fragments">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
name="./contactFragmentPaths"
rootPath="/content/dam"
required="{Boolean}true"
nodeTypes="dam:Asset">
<granite:data
jcr:primaryType="nt:unstructured"
filter="hierarchyNotFile"
mimeTypes="[text/html]"/>
</field>
</contacts>

<!-- ADD THIS: Forces single items to save as String[] -->
<contactFragmentPathsTypeHint
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/hidden"
name="./contactFragmentPaths@TypeHint"
value="String[]" />

<!-- ADD THIS: Ensures the JCR property is deleted if the author removes all items -->
<contactFragmentPathsDeleteHint
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/hidden"
name="./contactFragmentPaths@Delete"
value="{Boolean}true" />
</items>

or create a multifield in the recommended format

<tablist
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
composite="{Boolean}true">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container"
name="./tabList">
<items jcr:primaryType="nt:unstructured">
<text jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Text"
name="./text"/>

</items>
</field>
</tablist>

 

laxmanAuthor
Level 2
May 6, 2026

Hi ​@chaudharynick I have added 3 contentfragments in the dialog.

in author mode it is not rendering all 3 cards but in view as publisher I can see all cards. why I can see this content discrepancy..!!! between the modes(author v/s publisher) and I have tried with your solutions still facing same issue.
Thanks for the response…!!!

my HLI code

<div class="contact-card-container" data-sly-use.model="com.mysite.aem.core.models.ContactCardsModel">

<div class="contact-grid" data-sly-list.contact="${model.contacts}">

<div class="contact-card">

<div class="card-image-wrapper" data-sly-test="${contact.profileimage}">

<img src="${contact.profileimage}" alt="${contact.name}" class="card-image"/>

</div>



<div class="card-details">

<h3 class="contact-name">${contact.name}</h3>

<p class="contact-title">${contact.role}</p>

<p class="contact-location">${contact.country}</p>

<div class="contact-actions">

<a href="mailto:${contact.gmail}" class="btn-email" title="Email ${contact.name}">

<span class="icon">✉</span> Send Email

</a>

<a href="tel:${contact.phone}" class="btn-phone" title="Call ${contact.name}">

<span class="icon">☎</span> ${contact.phone}

</a>

</div>

</div>

</div>



</div>

<div data-sly-test="${!model.contacts}" class="cq-placeholder" data-emptytext="Configure Contact Cards (Select Content Fragments)"></div>

</div>


css
 

.contact-grid {

display: grid;

grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));

gap: 30px;

padding: 20px 0;

}



.contact-card {

border: 1px solid #e1e1e1;

background: #fff;

transition: transform 0.2s ease;

}



.contact-card:hover {

box-shadow: 0 4px 15px rgba(0,0,0,0.1);

}



.card-image {

width: 100%;

height: 250px;

object-fit: cover; /* Professional crop */

}



.card-details {

padding: 20px;

}



.contact-name {

font-size: 1.2rem;

color: #007b78; /* Infineon Green-ish */

margin: 0 0 5px 0;

}



.contact-title {

font-weight: bold;

font-size: 0.9rem;

margin-bottom: 15px;

}



.contact-actions a {

display: block;

margin-top: 10px;

text-decoration: none;

color: #333;

font-size: 0.85rem;

}



.contact-actions a:hover {

text-decoration: underline;

}

Yes I’m saving the data in string[] format why because I just want to store the contentfragment links in it.But my actual question why the content is missing only in author mode.
Can you please suggest the best practice while dealing with multifield dialog and contentfragments
 

chaudharynick
Level 4
May 7, 2026

Hi ​@laxman 

Thanks for sharing your code! It makes troubleshooting much easier.

To answer your first question: Why are you seeing a discrepancy between Edit (Author) mode and View as Published?

This is a classic AEM authoring quirk. The cards are almost certainly not missing from the page; they are just visually collapsing or stacking on top of each other due to a CSS conflict with AEM's Editor UI.

1. Why the Discrepancy Happens

When you are in Edit mode, AEM’s JavaScript engine injects an absolute-positioned overlay layer (cq-Overlay) over your components to provide the blue/green editing borders and toolbars. To draw these boxes, AEM has to calculate the exact height, width, and position of your HTML elements.

Your CSS uses display: grid; with grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));. AEM's editor scripts notoriously struggle to calculate bounding boxes for dynamic CSS Grids. Because the editor cannot determine the explicit height/width of the grid tracks, it often forces the grid items to overlap, collapse to a height of 0, or push out of the visible container.

When you switch to View as Published (which appends ?wcmmode=disabled under the hood), AEM strips away all the authoring JS and cq-Overlay elements. Your browser natively renders the raw HTML and CSS Grid perfectly, which is why all 3 cards suddenly appear.

How to verify this: If you right-click and "Inspect" the DOM in Chrome while in Edit mode, you will likely see all 3 <div class="contact-card"> elements exist in the HTML, even though you can't see them on the screen.

 

for the best approach for multififeld

 

dialog

<tablist
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
composite="{Boolean}true">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container"
name="./tabList">
<items jcr:primaryType="nt:unstructured">
<text jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Text"
name="./text"/>
</items>
</field>
</tablist>

 

Sling model of component


@Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Getter
public class TabModel {

@ChildResource
private List<TabItem> tabList;


@PostConstruct
protected void init() {
if(tabList !=null){
// anything you want to do with the list
}
}
}

 

TabItem Resource


@Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Getter
public class TabItem {

@ValueMapValue
private String text;

}

 

 

In Sling model everything is a childResource and you can adaptTo that to a custom model and write the business logic according to that

akhil_merupulaAccepted solution
Level 3
May 10, 2026

Hi laxman,

Good question and I’ve seen this exact issue come up a few times. Looking at your dialog.xml and the Sling Model, I can spot what’s going wrong.
The problem is in your multifield configuration. You have composite set to false but your field name is ./contactFragmentPaths. When composite is false, AEM stores the multifield values as a String array directly on the component node. The issue is that in author mode, when you reopen the dialog, AEM is not consistently reading back the stored array and repopulating the multifield correctly especially when only one item exists, because Sling stores a single value as a plain String, not a String array.
Your Sling Model actually already handles this correctly on the read side with your getPathsAsArray() method that checks instanceof String vs String[]. So publish works fine because the model reads it correctly. The problem is purely on the author dialog side not repopulating.
There are two ways to fix this.
The first option is to change composite to true in your dialog and wrap the pathfield in a composite multifield structure. This makes AEM store each entry as a child node instead of a flat array, which is more reliable for dialog repopulation and eliminates the single value vs array ambiguity entirely. The tradeoff is you’d need to update your model to read from child nodes instead of a flat property.
The second option which is simpler if you want to keep your current model is to keep composite false but add this to your multifield node: renderHidden set to true. This forces the dialog to re-render existing values properly when reopened. Also double check that your field name is exactly ./contactFragmentPaths with the dot slash prefix, because without it the value gets written to a different path than where AEM looks when repopulating.
Also one thing I noticed your nodeTypes is set to dam:Asset but you’re pointing to Content Fragments which are dam:Asset technically but filtered through hierarchyNotFile. That should be fine but worth verifying the paths being saved are actually the CF paths and not folder paths.
If neither fixes it, drop a CRXDE screenshot of what the saved node looks like after you fill the dialog and I can pinpoint exactly where the read/write mismatch is happening.​​​​​​​​​

laxmanAuthor
Level 2
May 11, 2026

thanks alot..!!

laxmanAuthor
Level 2
May 11, 2026

@akhil_merupula I tried first approach it is working fine thankyou so much now I got to know where I went wrong. once  again thanks for share your solution. Now I didn’t see any discrepancy in b/w editor mode and publisher…!!!