Using the ACS AEM Commons Nested Multifield
Creating dialogs in Adobe Experience Manager, or AEM, is key to granting content authors the ability to create dynamic, fully featured sites within a CMS framework. With AEM’s move to the Touch UI, authors now have a more modern and robust environment to create content.
Developers are able to tap into the power of the Touch UI to construct more powerful and dynamic functionality in order to enhance the authoring experience. Despite the expanded feature set, AEM’s new UI still lacks a handful of decidedly useful features.
One of the biggest sources of frustration that developers face is the multifield
resource type. Out of the box, this Granite resource type is only able to contain a single field. In order to add multiple fields, a developer would need to create multiple multifields, each containing a single field, then write a bunch of logic to keep each of those in sync. Quickly, that will become a development nightmare, and an even bigger nightmare to support or enhance later on.
Thankfully, AEM’s open source community has come to the rescue. One of the additions in ACS AEM Commons provides for a nested multifield that allows developers to create a multifield
of a fieldset
. The rest of this post will go into how to configure a dialog to utilize the acs-commons-nested property and read in the JSON value saved to the JCR.
This guide was written using AEM 6.1 with Service Pack 1 installed and has ACS-Commons version 2.2.4 installed.
It is Just a Property
Adding this functionality to a dialog couldn’t be simpler. All you need to do is add the property acs-commons-nested
to a fieldset
within a multifield
. Let’s look at the snippet below.
<example-multifield
jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/form/multifield"
fieldLabel="Example Multifield with Long Label">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/fieldset"
acs-commons-nested=""
name="./example">
<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">
<examplePath
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/pathbrowser"
fieldLabel="Example Path"
name="./examplePath"/>
<exampleText
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
fieldLabel="Example Text"
name="./exampleText"/>
</items>
</column>
</items>
</field>
</example-multifield>
First, we have the example-multifield
node with its sling:resourceType
set to be the standard Granite multifield
. Nothing unusual so far. That node then contains the field
node, which has its sling:resourceType
set to be Granite’s fieldset
. Contained within this node is the property called acs-commons-nested=””
, which is what ACS Commons is looking to enable the multi-multifield functionality.
From there, the rest is just your standard container of fields. In this example, I’ve added a path browser
and a textfield
.
The multifield on the dialog ends up functioning just like the standard AEM version, with the Add field button, along with the reordering and delete controls.
Make it Look Better
While the previous section did result in a functional dialog, the usability of the various form fields is quite low. Thankfully, this can be easily resolved by adding a couple of classes in three different locations in your XML.
The classes we will add are foundation-layout-util-maximized-alt
and long-label
. They will extend the length of the fieldLabel
properties when they render in the dialog, as well as place the fieldLabel
above the form fields.
Below is what the nodes look like in the XML with classes added to them:
<example-multifield
class="foundation-layout-util-maximized-alt long-label"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/multifield"
fieldLabel="Example Multifield with Long Label">
<examplePath
class="foundation-layout-util-maximized-alt long-label"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/pathbrowser"
fieldLabel="Example Path"
name="./examplePath"/>
<exampleText
class="foundation-layout-util-maximized-alt long-label"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
fieldLabel="Example Text"
name="./exampleText"/>
And this is the much more author-friendly dialog that will now display:
SaveAs JSONArray
When an author fills out the new nested multifield dialog, the data entered ends up being saved into the JCR as a String array, with the value of each array being a JSON object. If you were to view the example.infinity.json page for this node, you would see the following JSON returned:
{
"example": [
"{\"examplePath\":\"/content/dam/geometrixx\",\"exampleText\":\"Example Text Field Text\"}",
"{\"examplePath\":\"/content/geometrixx/en\",\"exampleText\":\"Second Example Text Field\"}"
]
}
And if you were to look at it in CRX/DE, the values look like the following:
Read In the JSON
I feel this guide wouldn’t be complete if I didn’t include how to actually read in the JSON from the JCR. This example is going to do this all within the JSP so that you can have a self contained, a fully functional component that you can use in the Geometrixx demo site. In practice, however, I have used custom JSP Tags that utilize reusable utility classes to handle the retrieval of the node’s properties and create JSONObject
or JSONArray
objects.
First, we will need a simple POJO to hold our data.
class ExamplePojo {
private String examplePath;
private String exampleText;
public ExamplePojo(String examplePath, String exampleText) {
this.examplePath = examplePath;
this.exampleText = exampleText;
}
public String getExamplePath() {
return this.examplePath;
}
public void setExamplePath(String examplePath) {
this.examplePath = examplePath;
}
public String getExampleText() {
return this.exampleText;
}
public void setExampleText(String exampleText) {
this.exampleText = exampleText;
}
}
Next would be to read in the property from the component’s node and convert that into a JSONObject
. From there, we can use the key/value pairs to create an ExamplePojo
object, and then add that to an ArrayList
. It should be noted that there is not a way to deserialize the JSONObject
directly into your POJO, which is why ExamplePojo
’s constructor is used to create the object.
Value[] values = new Value[]{};
PropertyIterator propItr = resource.adaptTo(Node.class).getProperties("example");
if (propItr.hasNext()) {
Property prop = propItr.nextProperty();
if (prop.isMultiple()) {
values = prop.getValues();
} else {
values = (new Value[]{prop.getValue()});
}
}
List<ExamplePojo> exampleList = new ArrayList<ExamplePojo>();
for (Value value : values) {
JSONObject jsonObj = new JSONObject(
new JSONTokener(value.getString()));
ExamplePojo pojo = new ExamplePojo(
jsonObj.getString("examplePath"),
jsonObj.getString("exampleText"));
exampleList.add(pojo);
}
You can drop this component onto any Geometrixx page after it is added to the dropzone via Design Mode.
Hopefully, this guide has been helpful and has expanded your ability to develop dialogs in AEM’s Touch UI.