Using Sling Models With Nested Composite Mulitifields in AEM 6.3+ | Perficient Digital

Using Sling Models With Nested Composite Mulitifields in AEM 6.3+

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:

companies-dialog

When submitting that dialog, it will be stored into the following structure:

companies-node-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 named companies 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 into class‘s, just make sure that if you nest classes they are public 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:

companies-display

Hope this example helps you model complex nested composite multifields.

 

That’s it for now, till the next one!

Leave a Reply