Touch UI multifield

In this post, we will show you how use Touch UI multifield widget. Let’s build a simple Address Entries component. In order to simplify the things and demonstrate how to create and use multifield Touch UI widget, all data displayed by the component are provided via its configuration.

Every address entry has the following fields: street, city and country. For defining a row, which is for this case an (street, city, country) entry, we can reuse OOTB Touch UI multifield widget. When properly configured, this widget will render three fields (street, city and country) and a button, which allows an editor to add as many address entries as she/he would like to have.

That’s true if we’re working on AEM 6.3 project. For AEM versions 6.2 and below, there’s also multifield widget but the problem is that it allows us to have only one field. Luckily the solution is in ACS Commons extension for multifield widget and we will explain how to use it properly below. You can download ACS Commons package from this link.

In the component dialog, define Address Entries multifield:

<addressEntries
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/multifield"
class="foundation-layout-util-maximized-alt long-label"
fieldDescription="Click 'Add Field' button in order to add a new entry."
fieldLabel="Address Entries">
</addressEntries>

Then add a child with name fieldand type granite/ui/components/foundation/form/fieldset:

<addressEntries
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/multifield"
class="foundation-layout-util-maximized-alt long-label"
fieldDescription="Click 'Add Field' button in order to add a new entry."
fieldLabel="Entries">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/fieldset"
acs-commons-nested=""
name="./addressEntries">
</field>
</addressEntries>

The property in line 10 is the important one: it activates ACS Commons multifield extension. All data entered via multifield widget will be serialized in JSON format and saved into addressEntries JCR property.

However, another configuration is possible as well. When saving the data, instead of serializing it in JSON format, a JCR node will be created for each entry and the fields will be saved into the corresponding properties of that node. In this case acs-commons-nested property should have NODE_STORE value. In both cases, the widget and ACS Commons extension will handle data saving and loading.

The last thing is to add Address Entry fields:

<addressEntries
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/multifield"
class="foundation-layout-util-maximized-alt long-label"
fieldDescription="Click 'Add Field' button in order to add a new entry."
fieldLabel="Entries">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/fieldset"
acs-commons-nested=""
name="./addressEntries">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
method="absolute"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<street
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
class="foundation-layout-util-maximized-alt long-label"
fieldLabel="Street"
name="./street"/>
<city
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
class="foundation-layout-util-maximized-alt long-label"
fieldLabel="City"
name="./city"/>
<country
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
class="foundation-layout-util-maximized-alt long-label"
fieldLabel="Country"
name="./country"/>
</items>
</column>
</items>
</field>
</addressEntries>

Corresponding Sling model could look like this one:

package rs.flexibledesigns.multifielddemo.sling.model;

import rs.flexibledesigns.multifielddemo.model.AddressEntry;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.commons.json.JSONException;
import org.apache.sling.commons.json.JSONObject;
import org.apache.sling.commons.json.JSONTokener;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.jcr.*;
import java.util.ArrayList;
import java.util.List;

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

private static final Logger LOGGER = LoggerFactory.getLogger(AddressEntriesModel.class);
private static final String PROPERTY_ADDRESS_ENTRIES = "addressEntries";
private static final String PROPERTY_STREET = "street";
private static final String PROPERTY_CITY = "city";
private static final String PROPERTY_COUNTRY = "country";

@Self
private Resource resource;

@ValueMapValue(name = "addressEntries")
private String entriesAsJson;

private List<AddressEntry> entries = new ArrayList<>();

@PostConstruct
protected void init() {

if(StringUtils.isBlank(entriesAsJson)) {
return;
}

try {
Value[] values;
Property prop = resource.adaptTo(Node.class).getProperty(PROPERTY_ADDRESS_ENTRIES);
if(prop.isMultiple()) {
values = prop.getValues();
} else {
values = new Value[] {prop.getValue()};
}

for(Value value : values) {
JSONObject jsonObj = new JSONObject(new JSONTokener(value.getString()));
AddressEntry entry = new AddressEntry(jsonObj.getString(PROPERTY_STREET), jsonObj.getString(PROPERTY_CITY), jsonObj.getString(PROPERTY_COUNTRY));
entries.add(entry);
}
} catch (RepositoryException e) {
LOGGER.error("Cannot load address entries", e);
} catch (JSONException e) {
LOGGER.error("Cannot parse address entry data", e);
}

}

public List<AddressEntry> getEntries() {
return entries;
}

}

// AddressEntry class

public final class AddressEntry {
private final String street;
private final String city;
private final String country;

public AddressEntry(String street, String city, String country) {
this.street = street;
this.city = city;
this.country = country;
}

// getters

}

and the component script:

<sly data-sly-use.model="rs.flexibledesigns.multifielddemo.sling.model.AddressEntriesModel">
<table data-sly-test="${model.entries}">
<tr>
<th>Street</th>
<th>City</th>
<th>Country</th>
<tr>
<tr data-sly-repeat.entry="${model.entries}">
<th>${entry.street}</th>
<th>${entry.city}</th>
<th>${entry.country}</th>
<tr>
</sly>