Article summary

Summary

Discusses how to create a custom AEM component that uses a nested multi-field widget for collecting information from authors. 

A special thank you to Praveen Dubey a member of the AEM community for contributing AEM code that is used in this article. 

Digital Marketing Solution(s) Adobe Experience Manager (Adobe CQ)
Audience
Developer (intermediate)
Required Skills
Java, Maven, HTML
Tested On Adobe Experience Manager 5.5, 5.6
 

Note:

You can download an AEM package that contains code and the OSGi bundle that are used in this article. Download the package and deploy using package manager. The purpose of this code is to show the community these concepts in action. That is, it's to illustrate how to write an AEM custom component that uses a nested multifield. This community code is for teaching purposes only and not meant to go into production as is.

You can view the application by using the following URL: http://localhost:4502/cf#/content/NestedMultifield.html (assuming you deploy on author). 

Download

Introduction

You can develop a custom Adobe Experience Manager (AEM) component that uses a nested multi-field control located in a dialog. A nested multi-field control is an inner multi-field control within an outer multi-field control and lets an author dynamically enter data. For example, assume the AEM control lists developers and each developer has an unknown number of skills to display. That is, within the inner multi-field, the author enter details such as professional skill set. The outer multi-field determines how may developers to display.

Consider the nested multi-field control located in the following illustration. 

NestedMultifield

To create an AEM component that uses a nested multi-field, perform these tasks:

  1. Create a CQ application folder structure.
  2. Create a template on which the page component is based.
  3. Create the page component based on the template.
  4. Create an AEM  column component.
  5. Add a dialog to the component.
  6. Create a CQ web page that uses the new component.

Note:

If you deploy the package that is shown at the start of this artilce, you can skip these steps and read the article to understand the concepts. 

Create an Experience Manager application folder structure 

Create an Experience Manager application folder structure that contains templates, components, and pages by using CRXDE Lite. 

CQAppSetup

The following describes each application folder:

  • application name: contains all of the resources that an application uses. The resources can be templates, pages, components, and so on. 
  • components: contains components that your application uses. 
  • page: contains page components. A page component is a script such as a JSP file.
    global: contains global components that your application uses.
  • template: contains templates on which you base page components. 
  • src: contains source code that comprises an OSGi component (this development article does not create an OSGi bundle using this folder). 
  • install: contains a compiled OSGi bundles container.

To create an application folder structure:

  1. To view the CQ welcome page, enter the URL http://[host name]:[port] into a web browser. For example, http://localhost:4502.
  2. Select CRXDE Lite.
  3. Right-click the apps folder (or the parent folder), select Create, Create Folder.
  4. Enter the folder name into the Create Folder dialog box. Enter nested_multifield
  5. Repeat steps 1-4 for each folder specified in the previous illustration. 
  6. Click the Save All button.

 

Note:

You have to click the Save All button when working in CRXDELite for the changes to be made.

Create a template 

You can create a template by using CRXDE Lite. A CQ template enables you to define a consistent style for the pages in your application. A template comprises of nodes that specify the page structure. For more information about templates, see Templates.

To create a template, perform these tasks:

1. To view the CQ welcome page, enter the URL http://[host name]:[port] into a web browser. For example, http://localhost:4502.
2. Select CRXDE Lite.
3. Right-click the template folder (within your application), select Create, Create
Template.
4. Enter the following information into the Create Template dialog box:

  • Label: The name of the template to create. Enter templateMultifield
  • Title: The title that is assigned to the template.
  • Description: The description that is assigned to the template.
  • Resource Type: The component's path that is assigned to the template and copied to implementing pages. Enter /apps/nested_multifield/components/page/templateMultifield.
  • Ranking: The order (ascending) in which this template will appear in relation to other templates. Setting this value to 1 ensures that the template appears first in the list.

5. Add a path to Allowed Paths. Click on the plus sign and enter the following value: /content(/.*)?.
6. Click Next for Allowed Parents.
7. Select OK on Allowed Children.
 

Create a render component that uses the template

Components are re-usable modules that implement specific application logic to render the content of your web site. You can think of a component as a collection of scripts (for example, JSPs, Java servlets, and so on) that completely realize a specific function. In order to realize this functionality, it is your responsibility as a CQ developer to create scripts that perform specific functionality. For more information about components, see Components.

By default, a component has at least one default script, identical to the name of the component. To create a render component, perform these tasks:

1. To view the CQ welcome page, enter the URL http://[host name]:[port] into a web browser. For example, http://localhost:4502.

2. Select CRXDE Lite.

3. Right-click /apps/nested_multifield/components/page, then select
Create, Create Component.

4. Enter the following information into the Create Component dialog box:

  • Label: The name of the component to create. Enter templateMultifield
  • Title: The title that is assigned to the component.
  • Description: The description that is assigned to the template.
  • Super Type:foundation/components/page. 

5. Select Next for Advanced Component Settings and Allowed Parents.

6. Select OK on Allowed Children.

7. Open the templateMultifield.jsp located at: /apps/nested_multifield/components/page/templateMultifield/templateMultifield.jsp.

8. Enter the following JSP code.

<html>
<%@include file="/libs/foundation/global.jsp" %>
<cq:include script="/libs/wcm/core/components/init/init.jsp"/>
<body>
<h1>Here is where your custom AEM component will go</h1>
<cq:include path="par" resourceType="foundation/components/parsys" />
</body>
</html>

Create the AEM custom component that uses a nested multifield

After you setup the AEM folder structure, create the AEM custom multi-field component by performing these tasks:

1. Right click on /apps/nested_multifield/components and then select New, Component.

2. Enter the following information into the Create Component dialog box:

  • Label: The name of the component to create. Enter developer-profile-setup.
  • Title: The title that is assigned to the component. Enter developer-profile-setup.
  • Description: The description that is assigned to the template. Enter developer-profile-setup.
  • Super Resource Type: Enter foundation/components/parbase.
  • Group: The group in the side rail or side kick where the component appears. Enter Adobe. (The developers component is located under the Adobe heading in the Touch UI side rail. Also appears in Adobe in the classic view sidekick.)
  • Allowed parents: Enter */*parsys.

3. Click Ok.

Add a dialog to the AEM component  

A dialog lets an author click on the component in the Touch UI (or Classic UI) view during design time and enter values that are used by the component. The component created in this development article lets the AEM author specify a given number of developers and lists the skills of each one.  

Nested2

The following illustration shows the JCR nodes that represent the dialog created in this section.

dialog

To create the dialog, perform these tasks:

1.  Select /apps/nested_multifield/components/developer-profile-setup and select Create, Create Dialog.

2.  In the Title field, enter column.

3. Click Ok.

4.  Delete all nodes under /apps/nested_multifield/components/developer-profile-setup/dialog.

Create the Dialog tab

Perform these tasks.

1. Click on the following node: /apps/nested_multifield/components/developer-profile-setup/dialog.
2. Right click and select Create, Create Node
3. Enter the following values:
  • Name: items
  • Type: cq:Widget
4. Add the following property:
  • xtype (String) - tabpanel

5. Select the /apps/nested_multifield/components/developer-profile-setup/dialog/items node.

6. Right click and select Create, Create Node.

7. Enter the following values:

  • Name: items
  • Type: cq:WidgetCollection

8. Select the /apps/nested_multifield/components/developer-profile-setup/dialog/items/items node.

9. Right click and select Create, Create Node.
10. Enter the following values:
  • Name: developer
  • Type: cq:Widget
11. Add the following properties:
  • title (String) -Developer (title that appears on the tab)
  • xtype (String) - panel (defines the data type of this field.)
12. Click on the following node:/apps/nested_multifield/components/developer-profile-setup/dialog/items/items/developer.

13. Right click and select Create, Create Node.

14. Enter the following values:

  • Name: items
  • Type: cq:WidgetCollection

15. Select the Select the /apps/nested_multifield/components/developer-profile-setup/dialog/items/items/developer/items.

16. Right click and select Create, Create Node.
 
17. Enter the following values:
  • Name: developer
  • Type: cq:Widget

18. Add the following properties:

  • fieldLabel (String) - Developer's Data
  • name (String) - ./devdata
  • xtype (String) - multifield 

19 . Select the /apps/nested_multifield/components/developer-profile-setup/dialog/items/items/developer/items/developernode.

20. Right click and select Create, Create Node.

21. Enter the following values:

  • Name: fieldConfig
  • Type: cq:Widget

22.  Add the following properties:

  • xtype (String) - devprofile

Add the JavaScript that defines a custom xtype to a cq:ClientLibraryFolder node

Add a JavaScript file that defines a custom xtype to an AEM client library node. 

clientlib

dev-profile.js file

The dev-profile JavaScript file defines a custom xtype that contains these fields:

  • CQ.Ext.form.TextField whose fieldlabel value is Developer's Name
  • A CQ.Ext.form.TextArea whose fieldlabel value is About Developer
  • A CQ.form.MultiField whose fieldlabel value is Click '+' to add your Skills (this represents the inner multi-field)
The following code shows how the inner multi-field control is created. 
 
//define the inner multifield
this.skillSet = new CQ.form.MultiField({
    fieldLabel : "Add Skills",
    fieldDescription : "Click '+' to add your Skills",
    width : 400,
    fieldConfig: {
    "xtype" : "textfield",
    allowBlank: false,
    },
listeners: {
change: {
scope:this,
fn:this.updateHidden
}
}
});

The following JavaScript represents the entire dev-profile.js file. 

try {
    if (typeof HCL == 'undefined') {
        HCL = {}; // creating namespace
    }
    HCL.SitemapDatacollection = CQ.Ext.extend(CQ.form.CompositeField, {

    /**
     * @private
     * @type CQ.Ext.form.TextField
     */
    hiddenField : null,
    /**
     * @private
     * @type CQ.Ext.form.PathField
     */
    developerName : null,
    /**
     * @private
     * @type CQ.Ext.form.PathField
     */
    developerDesc : null,
    /**
     * @private
     * @type CQ.Ext.form.MultiField
     */
    replaceMulti : null,    
    /**
     * @private
     * @type CQ.Ext.form.CheckBox
     */
    lastMod : null,
    /**
     * @private
     * @type CQ.Ext.form.ComboBox
     */
    changeFreq : null,
    /**
     * @private
     * @type CQ.Ext.form.ComboBox
     */
    priority : null,
    /**
     * @private
     * @type CQ.Ext.form.MultiField
     */
    skillSet : null,    
    /**
     * @private
     * @type CQ.Ext.form.CheckBox
     */
    temporaryDisable : null,

    constructor : function(config) {
        config = config || {};
        var defaults = {
            "border" : true,
            "padding" : 10,
            "style" : "padding:10px 0 0 5px;",
            "layout" : "form",
            "labelWidth" : 200
        };
        config = CQ.Util.applyDefaults(config, defaults);
        HCL.SitemapDatacollection.superclass.constructor
                .call(this, config);
    },

    // overriding CQ.Ext.Component#initComponent
    initComponent : function() {
        HCL.SitemapDatacollection.superclass.initComponent.call(this);

        // Hidden field
        this.hiddenField = new CQ.Ext.form.Hidden({
            name : this.name
        });
        this.add(this.hiddenField);


        this.developerName = new CQ.Ext.form.TextField({
            fieldLabel : "Developer's Name",
            allowBlank: false,
            width : 400,
            listeners : {
                change : {
                    scope : this,
                    fn : this.updateHidden
                },
                dialogclose : {
                    scope : this,
                    fn : this.updateHidden
                }
            }
        });
        this.add(this.developerName);
        this.developerDesc = new CQ.Ext.form.TextArea({
            fieldLabel : "About Developer",
            fieldDescription: "Provide a detail description about developer",
            allowBlank: false,
            width : 400,
            listeners : {
                change : {
                    scope : this,
                    fn : this.updateHidden
                },
                dialogclose : {
                    scope : this,
                    fn : this.updateHidden
                },
            }
        });
        this.add(this.developerDesc);


		//define the inner multifield
        this.skillSet = new CQ.form.MultiField({
            fieldLabel : "Add Skills",
            fieldDescription : "Click '+' to add your Skills",
            width : 400,
            fieldConfig: {
                "xtype" : "textfield",
                allowBlank: false,
            },
            listeners: {
                change: {
                    scope:this,
                    fn:this.updateHidden
                }
            }
        });
        this.add(this.skillSet);
    },
    // overriding CQ.form.CompositeField#setValue
    setValue : function(value) {

        var readVal = '';
        var storeVal = '';
        var replaceMultiValues = '';
        var lastModVal = '';
        var changeFreqVal = '';
        var priorityVal = '';
        var temporaryDisableVal = '';
        var skillSetValues = '';        
        if (value) {
            var colValue = value.split('|');
            if (colValue.length > 0) {
                //temporaryDisableVal = colValue[0];
                readVal             = colValue[0];
                storeVal            = colValue[1];
                //replaceMultiValues  = colValue[3];
                //lastModVal          = colValue[4];
                ///changeFreqVal       = colValue[5];
                //priorityVal         = colValue[6];
                skillSetValues = colValue[2];
            }
        }
        this.developerName.setValue(readVal);
        this.developerDesc.setValue(storeVal);
       this.skillSet.setValue(skillSetValues.split(','));    
    },    
    // overriding CQ.form.CompositeField#getValue
    getValue : function() {
        return this.getRawValue();
    },
    getRawValue : function() {

        //var temporaryDisableVal = this.temporaryDisable.getValue() || "";
        var readVal             = this.developerName.getValue() || "";
        var storeVal            = this.developerDesc.getValue() || "";
        //var replaceMultiValues  = this.replaceMulti.getValue() || "";
       // var lastModVal          = this.lastMod.getValue() || "";
        //var changeFreqVal       = this.changeFreq.getValue() || "";
        //var priorityVal         = this.priority.getValue() || "";
        var skillSetValues = this.skillSet.getValue() || "";
       // if (temporaryDisableVal == '')
       //     temporaryDisableVal = " ";
        var value = readVal + "|" + storeVal + "|" + skillSetValues;
        this.hiddenField.setValue(value);
        return value;
    },
    updateHidden : function() {
        this.hiddenField.setValue(this.getValue());
    },
        destroyRichText : function(){
    this.el.dom={};
}
});
    CQ.Ext.reg('devprofile', HCL.SitemapDatacollection);
} catch (e) {
    // suppressing error.
    // error occurs for CQ.form.CompositeField in mobile devices.
}


try {
    if (typeof HCL == 'undefined') {
        HCL = {}; // creating namespace
    }
    HCL.SitemapDatacollectionReplaceMulti = CQ.Ext.extend(CQ.form.CompositeField, {

    /**
     * @private
     * @type CQ.Ext.form.TextField
     */
    hiddenReplaceMultiField : null,
    /**
     * @private
     * @type CQ.Ext.form.TextField
     */
    replaceWhat : null,    
    /**
     * @private
     * @type CQ.Ext.form.TextField
     */
    replaceWith : null,

    constructor : function(config) {
        config = config || {};
        var defaults = {
            "border" : true,
            "padding" : 10,
            "style" : "padding:10px 0 0 5px;",
            "layout" : "form",
        };
        config = CQ.Util.applyDefaults(config, defaults);
        HCL.SitemapDatacollectionReplaceMulti.superclass.constructor
                .call(this, config);
    },

    // overriding CQ.Ext.Component#initComponent
    initComponent : function() {
        HCL.SitemapDatacollectionReplaceMulti.superclass.initComponent.call(this);

        // Hidden field
        this.hiddenReplaceMultiField = new CQ.Ext.form.Hidden({
            name : this.name
        });
        this.add(this.hiddenReplaceMultiField);
        this.replaceWhat = new CQ.Ext.form.TextField({
            fieldLabel : "Skill",
            fieldDescription: "text that we want to be replaced in site map",
            width : 400,
            regex: /^[A-Za-z0-9-_\/\.]+$/,
            regexText: "Only alphanumeric with -, _ and / allowed.",
            listeners : {
                change : {
                    scope : this,
                    fn : this.updateHidden
                }
            }
       });
        this.add(this.replaceWhat);        
        this.replaceWith = new CQ.Ext.form.TextField({
            fieldLabel : "Replace with",
            fieldDescription: "text that we want to be replaced with in site map",
            width : 400,
            regex: /^[A-Za-z0-9-_\/:\.]+$/,
            regexText: "Only alphanumeric with -, _ and / allowed.",
            listeners : {
                change : {
                    scope : this,
                    fn : this.updateHidden
                }
            }
        });
        this.add(this.replaceWith);
    },
    // overriding CQ.form.CompositeField#setValue
    setValue : function(value) {
        var replaceWhatVal = '';
        var replaceWithVal = '';
        if (value) {
            var colValue = value.split('~');
            if (colValue.length > 0) {
                replaceWhatVal      = colValue[0];
                replaceWithVal      = colValue[1];
            }
        }
        this.replaceWhat.setValue(replaceWhatVal);
        this.replaceWith.setValue(replaceWithVal);
    },    
    // overriding CQ.form.CompositeField#getValue
    getValue : function() {
        return this.getRawValue();
    },
    getRawValue : function() {
        var replaceWhatVal      = this.replaceWhat.getValue() || "";
        var replaceWithVal      = this.replaceWith.getValue() || "";
        var value = replaceWhatVal + "~"  + replaceWithVal;
        this.hiddenReplaceMultiField.setValue(value);
        return value;
    },
    updateHidden : function() {
        this.hiddenReplaceMultiField.setValue(this.getValue());
    }
});
    CQ.Ext.reg('devprofile', HCL.SitemapDatacollectionReplaceMulti);
} catch (e) {
    // suppressing error.
    // error occurs for CQ.form.CompositeField in mobile devices.
}


Add the  JS file file to the ClientLibs folder:

  1. Right-click /apps/nested_multifield/components then select New, Node.
  2. Make sure that the node type is cq:ClientLibraryFolder and name the node clientlibs.
  3. Right click on clientlibs and select Properties. Add the categories property to this node. Specify aussie.custom.widget.devprofile and ensure the type is String[].
  4. Create a file named dev-profile.js to the clientlibs node. Add the code shown in this section to the file.  
  5. Add a TXT file to the clientlibs node named js.txt. Add dev-profile.js to this txt file.

developer-profile-setup JSP

The developer-profile-setup.jsp is the main JSP file for the component and is located at: 

/apps/nested_multifield/components/developer-profile-setup/developer-profile-setup.jsp

This application logic located in the developer-profile-setup.jsp defines the nested multi-field. For each multi-field filled in (outer multi-field), a developer profile is displayed in the AEM web page. The following JSP code is the developer-profile-setup.jsp file.  

<jsp:directive.include file="/libs/foundation/global.jsp" />
<cq:includeClientLib categories="aussie.custom.widget.devprofile" />
AEM Custom Nested Multifieid Component
<%@taglib prefix="sling" uri="http://sling.apache.org/taglibs/sling/1.0" %>
<%@taglib prefix="cq" uri="http://www.day.com/taglibs/cq/1.0" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

<%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<div id="wrapper" style="height:auto;border:1px solid grey">
    Developer's Data
    <c:forEach var="items" items="${properties.devdata}" varStatus="status">
                    <c:set var="listItem" value="${fn:split(items,'|')}" />
                    <c:set var="developerName" value="${fn:trim(listItem[0])}" />
                    <c:set var="about" value="${fn:trim(listItem[1])}" />
                    <c:set var="skills" value="${fn:trim(listItem[2])}" />
    
        <div class="dev-profile" style="background-color:#E0F8F7;height:auto;border:1px solid grey;margin-top:15px">
                Name : ${developerName} <br>
                About : ${about} <br>
                Skills : ${skills}
        </div>
    </c:forEach>
</div>

Create the EditConfig node

cq:EditConfig node defines the behaviour of the component. See Configuring with cq:EditConfig Properties.

Perform the following tasks:

1. Select /apps/nested_multifield/components/developer-profile-setup.

2. Right click and select Create, Create Node

3. Enter the following values:

  • Name: cq:editConfig
  • Type: cq:EditConfig

4. Add the following property:

  • cq:actions (String[]) - EDITANNOTATECOPYMOVEDELETEINSERT
  • cq:dialogMode (String) - floating
5. Select /apps/nested_multifield/components/developer-profile-setup/cq:editConfig.
6. Right click and select Create, Create Node.
7. Enter the following values:
  • Name: cq:listeners
  • Type: cq:EditListenersConfig
8. Add the following property:
  • afteredit (String) - REFRESH_PAGE
9. Select /apps/nested_multifield/components/developer-profile-setup/cq:editConfig.
10 Right click and select Create, Create Node.
11 Enter the following values:
  • Name: cq:dropTargets
  • Type: nt:unstructured
12. Select /apps/nested_multifield/components/developer-profile-setup/cq:editConfig/cq:dropTargets. Right click and select Create, Create Node.

13. Enter the following values:

  • Name: image
  • Type: cq:DropTargetConfig

14. Add the following properties: 

  • accept (String[]) - image/.*
  • groups (String[]) - media
  • propertyName (String) - ./fileReference

15. Select /apps/nested_multifield/components/developer-profile-setup/cq:editConfig/cq:dropTargets/image. Right click and select Create, Create Node.

16. Enter the following values:

  • Name: parameters
  • Type: nt:unstructured

17. Add the following properties: 

  • sling:resourceType (String[]) - foundation/components/image

 

 
afteredit
afteredit
propertyName
propertyName

Create a CQ web page that uses the nested multi-field component

The final task is to create an AEM page that uses the column component, that is shown in the followng illustration. 

 

webPage

Create an AEM page that displays the component.

  1. Go to the CQ Websites page at http://localhost:4502/siteadmin#/content.
  2. Select New Page.
  3. Specify the title of the page in the Title field. Enter NestedApp.
  4. Specify the name of the page in the Name field.
  5. Select templateMultifield from the template list that appears. This value represents the template that is created in this development article. If you do not see it, then repeat the steps in this development article. For example, if you made a typing mistake when entering in path information, the template will not show up in the New Page dialog box.
  6. Open the page by clicking the NestedApp page.
  7. The component will be under the Adobe heading in the sidekick. Drag this component onto the AEM page. 
  8. Open the dialog and enter values into the dialog fields.

Populate the AEM side kick

If the sidekick is empty, you can populate it by clicking the Design button located at the bottom of the sidekick. Next click the Edit button that appears and select Adobe from the list of categories. To view the sidekick with the Adobe category, click the down arrow icon that appears on the sidekick. Drag the component on the AEM web page. Double click on the component and you see the dialog that you created in this development article.  

The following illustration, shows the Adobe category where the component is located. 

Design

See also

Congratulations, you have just created an AEM custom column component that you can use in your projects. Please refer to the AEM community page for other articles that discuss how to build AEM services/applications.

This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License  Twitter™ and Facebook posts are not covered under the terms of Creative Commons.

Legal Notices   |   Online Privacy Policy