You probably already know that Adobe added support for composite multifield in 6.3. Adobe also added support for nested multifield and nested composite multifield in AEM 6.4.
Nested? Composite? Huh?
Take a look at the AEM 6.4 docs for multifield
Let’s define them
Multifiled:
Allows authors the ability to add a list of items, each item let’s call it a fieldset, has only one field. Example, a list of emails.
Composite Multifield
Same as a normal multifield
, but can handle multiple fields in the fieldset. Example, a list of addresses where each address has multiple fields: street, city, state and zip.
As of 6.3+ Composite multifields are denoted with
composite="true"
attribute
Nested Multifield
A composite multifield
that contains a multifield
as one of the items in the fieldset. Example, a list of users where each user has a name and a list of social media links.
Nested Composite Multified
A composite multified that contains another composite multifield as one of the items in the fieldset. Example, a list companies that contain a list of departments, where each department has a name and manager.
+ companies + company1 - name + department1 - name - manager + department2 - name - manager ... + departmentN + company2 - name + department1 - name - manager ... + departmentN ... +companyN
Building a Companies Component
Let’s look at how we can build a component that allows authoring and displaying the companies as described above:
Consider the following dialog XML:
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/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="Companies" 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"> <companies jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/multifield" fieldDescription="Click 'Add Field' to add a new company." composite="{Boolean}true"> <field jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/container" name="./companies"> <items jcr:primaryType="nt:unstructured"> <name jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/textfield" fieldLabel="Comp. Name" name="name"/> <departments jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/multifield" fieldDescription="Click 'Add Field' to add a new department." fieldLabel="Departments" composite="{Boolean}true"> <field jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/container" name="./departments"> <items jcr:primaryType="nt:unstructured"> <name jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/textfield" fieldLabel="Dep. Name" name="name"/> <manager jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/textfield" fieldLabel="Manager" name="manager"/> </items> </field> </departments> </items> </field> </companies> </items> </column> </items> </content> </jcr:root>
When using that dialog, you’ll see something like this:
When submitting that dialog, it will be stored into the following structure:
Let’s get to modelin’
How can we model this structure with sling models? You could write a model where you iterate over the resource tree and create some data structure to make it easier to use in HTL. But that seems like a lot of work, is there really no better way?
Yes! there is!
sling models can handle collections so we can write a model like the following:
package com.sample.models; import java.util.List; import javax.inject.Inject; import org.apache.sling.api.resource.Resource; import org.apache.sling.models.annotations.DefaultInjectionStrategy; import org.apache.sling.models.annotations.Model; @Model( adaptables = {Resource.class}, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL) public interface CompaniesModel { @Inject List<Company> getCompanies(); // the name `getCompanies` corresponds to the multifield name="./companies" /** * Company model * has a name and a list of departments */ @Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL) interface Company { @Inject String getName(); @Inject List<Department> getDepartments(); // the name `getDepartments` corresponds to the multifield name="./departments" } /** * Department model * has a name and a manager */ @Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL) interface Department { @Inject String getName(); @Inject String getManager(); } }
I think this is pretty self explanatory, and really easy to reason about.
The Gotchas
- Make sure the getter method names match the
name
properties in your dialog xml, see comments in the code. - When we do
@Inject List<Company> getCompanies();
we are basically telling sling, that under the component resource node, there is a node namedcompanies
which has child nodes that should be modeled after (adapted to)Company
model. - I am using
Interface
‘s here since there is no need to process any of the user inputs (no business logic), but you can switch that intoclass
‘s, just make sure that if you nest classes they arepublic
or don’t nest them and rather add them to their own class file.
Displaying the companies:
<sly data-sly-use.companiesModel="com.sample.models.CompaniesModel"/> <sly data-sly-test.empty="${!companiesModel.companies}" /> <div data-sly-test="${wcmmode.edit && empty}" class="cq-placeholder" data-emptytext="${component.title}"></div> <sly data-sly-test="${!empty}"> <div> <ul data-sly-list.company="${companiesModel.companies}"> <li>${company.name} <ul data-sly-list.department="${company.departments}"> <li> <b>Department:</b> ${department.name} <br/> <b>Manager</b>: ${department.manager} </li> </ul> </li> </ul> </div> </sly>
Which, after being authored, displays the following:
Hope this example helps you model complex nested composite multifields.
That’s it for now, till the next one!
Could you post an example if I want to use the classes instead of interfaces. I tried it but with classes its not working.
Hi Marsh,
Could you please provide an example that’s not working? Maybe open a stackoverflow question about it, include all the details and I’ll be happy to help.
I SEE SAM EISSUE, ITS NOT WORKING WITH CLASS
Could you please provide an example of what is not working for you? put it in a repo on github and I can take a look. Thanks!
Hello,
The second time when you open the dialog and when you click on outer most Add button it shows Dep. Name and Manager field instead of the Comp Name section.
Do you know how to resolve this ?
Could you please provide an approach for Unit test case for the above.
If you save and reopen the dialog, the 1st multifield element disappears.
Hi Yogesh, could you please provide more details? like the AEM version you are using, the dialog XML and any other related information?