AEM 6 SP2 - Touch UI Multi Field Component

Goal


Create a component with Touch UI (Coral UI) Dialog and Multifield (granite/ui/components/foundation/form/multifield) widget. Here we extend product multifield and create a composite multifield  /apps/touch-ui-multi-field-panel/multifield , configure it with two form fields, a Textfield (granite/ui/components/foundation/form/textfield), Pathbrowser  (granite/ui/components/foundation/form/pathbrowser). End result is a simple Dashboard Component

Classic UI Multifield is available here

Touch UI Multifield storing values as child nodes is available here

For Touch UI Image Multifield check this post

Nested multifield (or Multi multifield) configuration is available here

Demo | Package Install | Package Install with widget code added in clientlib


Bug Fixes

Adding Composite Multifield in multiple Tabs doesn't work - Demo | Package Install


Touch UI Dialog




Dialog Structure in CRX



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create a folder (nt:folder) /apps/touch-ui-multi-field-panel

2) Create a component (cq:Component) /apps/touch-ui-multi-field-panel/sample-multi-field. Check this post on how to create a CQ component

3) Add the following xml for /apps/touch-ui-multi-field-panel/sample-multi-field/dialog created. Ideally a cq:Dialog should be configured with necessary widgets for displaying dialog in Classic UI. This post is on Touch UI dialogs so keep it simple (node required for opening the touch ui dialog created in next step)

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="cq:Dialog"
    title="Multi Field"
    xtype="dialog"/>


4) Create a nt:unstructured node /apps/touch-ui-multi-field-panel/sample-multi-field/cq:dialog with following xml (the dialog UI as xml)

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/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="Multifield TouchUI Component"
    sling:resourceType="cq/gui/components/authoring/dialog"
    helpPath="en/cq/current/wcm/default_components.html#Text">
    <content
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/foundation/container">
        <layout
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
        <items jcr:primaryType="nt:unstructured">
            <column
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/foundation/container">
                <items jcr:primaryType="nt:unstructured">
                    <fieldset
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Sample Dashboard"
                        sling:resourceType="granite/ui/components/foundation/form/fieldset">
                        <layout
                            jcr:primaryType="nt:unstructured"
                            sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
                        <items jcr:primaryType="nt:unstructured">
                            <column
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/foundation/container">
                                <items jcr:primaryType="nt:unstructured">
                                    <dashboard
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/foundation/form/textfield"
                                        fieldDescription="Enter Dashboard name"
                                        fieldLabel="Dashboard name"
                                        name="./dashboard"/>
                                    <pages
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="/apps/touch-ui-multi-field-panel/multifield"
                                        class="full-width"
                                        fieldDescription="Click '+' to add a new page"
                                        fieldLabel="URLs">
                                        <field
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/foundation/form/fieldset"
                                            name="./items">
                                            <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">
                                                        <page
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/foundation/form/textfield"
                                                            fieldDescription="Enter Page Name"
                                                            fieldLabel="Page Name"
                                                            name="./page"/>
                                                        <path
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/foundation/form/pathbrowser"
                                                            fieldDescription="Select Path"
                                                            fieldLabel="Path"
                                                            name="./path"
                                                            rootPath="/content"/>
                                                    </items>
                                                </column>
                                            </items>
                                        </field>
                                    </pages>
                                </items>
                            </column>
                        </items>
                    </fieldset>
                </items>
            </column>
        </items>
    </content>
</jcr:root>


5) The composite multifield node /apps/touch-ui-multi-field-panel/sample-multi-field/cq:dialog/content/items/column/items/fieldset/items/column/items/pages is of extended multifield type /apps/touch-ui-multi-field-panel/multifield (created in next steps)

6) Different layouts can be used to structure the multifield, here we use granite/ui/components/foundation/layouts/fixedcolumns layout

7) Create folder (sling:Folder) /apps/touch-ui-multi-field-panel/multifield

8) Create file (nt:file) /apps/touch-ui-multi-field-panel/multifield/multifield.jsp (for rendering the widget) and add following code. Inline JS was added for simplicity, its always recommended to create a clientlib (cq:ClientLibraryFolder) for javascript code

<%@ page import="com.adobe.granite.ui.components.Config" %>
<%@ page import="org.slf4j.Logger" %>
<%@ page import="org.slf4j.LoggerFactory" %>
<%@ page import="com.adobe.granite.ui.components.Value" %>
<%@ page import="org.apache.commons.lang3.StringUtils" %>
<%@include file="/libs/granite/ui/global.jsp" %>

<%--include ootb multifield--%>
<sling:include resourceType="/libs/granite/ui/components/foundation/form/multifield"/>

<%!
    private final Logger mLog = LoggerFactory.getLogger(this.getClass());
%>

<%
    Config mCfg = cmp.getConfig();

    Resource mField = mCfg.getChild("field");

    if (mField == null) {
        mLog.warn("Field node doesn't exist");
        return;
    }

    ValueMap mVM = mField.adaptTo(ValueMap.class);

    String mName = mVM.get("name", "");

    if ("".equals(mName)) {
        mLog.warn("name property doesn't exist on field node");
        return;
    }

    Value mValue = ((ComponentHelper) cmp).getValue();

    //get the values added in multifield
    String[] mItems = mValue.get(mName, String[].class);
%>

<script>
    (function () {
        //function to add values into multifield widgets. The values are stored in CRX by collectDataFromFields() as json
        //eg. {"page":"English","path":"/content/geometrixx/en"}
        var addDataInFields = function () {
            var mValues = [ <%= StringUtils.join(mValue.get(mName, String[].class), ",") %> ],
                    mName = '<%=mName%>',
                    $fieldSets = $("[class='coral-Form-fieldset'][data-name='" + mName + "']");

            var record, $fields, $field, name;

            $fieldSets.each(function (i, fieldSet) {
                $fields = $(fieldSet).find("[name]");

                record = mValues[i];

                if (!record) {
                    return;
                }

                $fields.each(function (j, field) {
                    $field = $(field);

                    name = $field.attr("name");

                    if (!name) {
                        return;
                    }

                    //strip ./
                    if (name.indexOf("./") == 0) {
                        name = name.substring(2);
                    }

                    $field.val(record[name]);
                });
            });
        };

        //collect data from widgets in multifield and POST them to CRX as JSON
        var collectDataFromFields = function(){
            $(document).on("click", ".cq-dialog-submit", function () {
                var $form = $(this).closest("form.foundation-form"), mName = '<%=mName%>';

                //get all the input fields of multifield
                var $fieldSets = $("[class='coral-Form-fieldset'][data-name='" + mName + "']");

                var record, $fields, $field, name;

                $fieldSets.each(function (i, fieldSet) {
                    $fields = $(fieldSet).find("[name]");

                    record = {};

                    $fields.each(function (j, field) {
                        $field = $(field);

                        name = $field.attr("name");

                        if (!name) {
                            return;
                        }

                        //strip ./
                        if (name.indexOf("./") == 0) {
                            name = name.substring(2);
                        }

                        record[name] = $field.val();

                        //remove the field, so that individual values are not POSTed
                        $field.remove();
                    });

                    if ($.isEmptyObject(record)) {
                        return;
                    }

                    //add the record JSON in a hidden field as string
                    $('<input />').attr('type', 'hidden')
                            .attr('name', mName)
                            .attr('value', JSON.stringify(record))
                            .appendTo($form);
                });
            });
        };

        $(document).ready(function () {
            addDataInFields();
            collectDataFromFields();
        });
    })();
</script>


9) sling:include at #9 includes the product multifield  /libs/granite/ui/components/foundation/form/multifield after which the code necessary for creating a composite multifield is added

10) The function collectDataFromFields() at #80 registers a click listener on dialog submit, to collect the multifield form data, create a json to group it and add in a hidden field before data is POSTed to CRX. The function addDataInFields() at #44 reads json data added previously and fills the multifield fields, when dialog is reopened. The extension uses simple jquery calls to get and set data; based on the widget type more code may be necessary for supporting complex Granite UI widgets (granite/ui/components/foundation/form)

11) Create the component jsp /apps/touch-ui-multi-field-panel/sample-multi-field/sample-multi-field.jsp for rendering data entered in dialog

<%@ page import="org.apache.sling.commons.json.JSONObject" %>
<%@ page import="java.io.PrintWriter" %>
<%@include file="/libs/foundation/global.jsp" %>
<%@page session="false" %>

<div style="display: block; border-style: solid; border-width: 1px; margin: 10px; padding: 10px">
    <b>Multi Field Sample Dashboard</b>

<%
    try {
        Property property = null;

        if (currentNode.hasProperty("items")) {
            property = currentNode.getProperty("items");
        }

        if (property != null) {
            JSONObject obj = null;
            Value[] values = null;

            if (property.isMultiple()) {
                values = property.getValues();
            } else {
                values = new Value[1];
                values[0] = property.getValue();
            }

            for (Value val : values) {
                obj = new JSONObject(val.getString());
%>
                Page : <b><%= obj.get("page") %></b>,
                URL : <b><a href="<%= obj.get("path") %>.html" target="_blank"><%= obj.get("path") %></a></b>

<%
            }
        } else {
%>
            Add values in dialog
<%
        }
    } catch (Exception e) {
        e.printStackTrace(new PrintWriter(out));
    }
%>

</div>


42 comments:

  1. Great example! It's very generic and suitable to any kind of needs I can imagine. And because of this, comes the unpleasant question: why this widget has not been included into the Service Pack 2 between the components of the foundation?
    Thanks,
    Alessandro

    ReplyDelete
  2. Hi Sreekanth ,
    i think i found a small bug during my tests.
    I am not able to delete the last element of a multifield.
    Can you confirm that?
    Thanks
    Axel

    ReplyDelete
    Replies
    1. hello Axel, thanks for testing it out, yeah last element or more precisely if there is only one element left in the multifield, deleting it wouldn't make it go away. save and open the dialog, it still exists... i think it's a product multifield bug, check page properties -> vanity url multifield

      Delete
    2. This comment has been removed by the author.

      Delete
    3. Hey guys, I found a workaround for this, just have a node under the items node (at the same level where you have the cutom multi field node) and add the following properties to it:
      name="./[name]@Delete"
      value="null"
      This did the trick for me.
      Happy Coding :)

      Delete
    4. Hi Vinay,

      I have the same problem and I'll try this out.

      Do you know if this is a 'hack' to get it to work, or is it an undocumented requirement?

      Delete
    5. Hi Chris,
      I had a similar situation with checkbox in general(outside of multifield) and found that the solution works. I found similar fix/implementation in many OOTB components(/libs/commerce/components/productpageproxy/cq:dialog/content/items/columns/items/tabs/items/advanced/items/cug/items/deleteenable) and tried this on multifield too and it worked well for me; seems like some js in the framework reads for @Delete names and removes corresponding property when saving dialog values.
      Since its available in OOTB components, i am not sure if I can call it a "hack" :)

      Delete
  3. Hello Sreekanth,

    I was creating multifield today. My use case was that i had to configure two tabs. Both the tabs have multifield and they are exactly the same but one tab configuration was for IOS and other for android. When i am entering and saving the values within both the tabs, they are combining into the first tab. The name parameter of the multifield is different for both the tabs and i was hoping to see two different properties on the content node.

    For example,

    the value are configured into iosItems or androidConfig whichever tab appears first in the list . Is it a product bug?

    Dialog File http://tempsend.com/B11E7E925B

    Screenshot for how data is being saved. -: http://tempsend.com/0B33D34C73

    Can you please help? You will have to change the tab configuration, to point to your local project for testing.

    ReplyDelete
    Replies
    1. Rahul, i see that the non-clientlib version works fine for multiple tabs, demo here - https://drive.google.com/file/d/0B4d6KmbLkAumWEdGZUU2a1pMQkk/view?usp=sharing

      There was a bug in clientlib one, fixed (check the demo and package install in "Bug Fixes" section above)

      Delete
  4. Hey Sreekanth,

    That did resolve the issue. Thanks a ton :)

    Cheers

    ReplyDelete
  5. Hi,
    I am facing issue in custom multifield component.
    can i have source code for both touch ui and classic ui and they both should sync up in case of custom multifield

    ReplyDelete
  6. This is great! However, I'm having issues getting the demo to work in 6.1. Getting the 'Uncaught RangeError: Maximum call stack size exceeded'. Same is happening with nested multifield demo [0]. Any ideas? Thanks in advance.

    [0] http://experience-aem.blogspot.com/2015/03/aem-6-sp2-touch-ui-coral-ui-nested-multi-multifield.html

    ReplyDelete
    Replies
    1. Nevermind. Got this working by removing the previously installed nested event and composite ones, then clearing /var/clientlibs and browser cache

      Delete
  7. Hi Sreekanth,
    How to make rich text mandatory or required in touch ui of AEM? Need help.

    ReplyDelete
    Replies
    1. Hello satya, check this http://experience-aem.blogspot.com/2015/08/aem-61-support-required-property-on-richtext-editor-rte-of-touchui.html

      Delete
    2. I have rich text in touch ui which has an option of required plugins in full view,but how can i get those plugins rendering in touch ui rich text multifield?

      Delete
  8. Hi Sreekanth, Thanks for the article, I am trying to move the custom multifield widget into touch ui cq:dialog from the dialog. Could you please provide an example for converting custom multifield widget into touch ui dialog. Also if you can provide me more details about the richtext editor in the dialog as I am not able to convert this as well into the granite touch ui dialog. Thanks

    ReplyDelete
    Replies
    1. Sandy, check the dialog conversion tool..

      http://docs.adobe.com/docs/en/aem/6-1/develop/dev-tools/dialog-conversion.html
      https://github.com/Adobe-Consulting-Services/acs-aem-commons/blob/master/bundle/src/main/java/com/adobe/acs/commons/util/impl/MultifieldPanelDialogRewriteRule.java

      Delete
  9. Thanks, this is helpful. I'm trying to set a default value in the .content.xml, and it's not working - the multifield just defaults to empty. Is that something that should work with this code (so I'm doing something wrong), or would I need to tweak it?

    ReplyDelete
  10. Awesome..Followed the same steps as you mentioned in the post. Working fine. Thanks alot.

    I got another requirement, instead of saving the multi composite data as json object I need to save it as string arrays.
    Example: empName: John, Peter, White and location: India, UK, US.
    The empName and location should be stored as string arrays and as two different properties in content node.

    Please help me in achieving this.

    Thanks,
    Sravani

    ReplyDelete
    Replies
    1. Hi Sreekanth,

      Any update on this? Waiting for your reply.

      Thanks in Advance.

      Sravani Vagicharla.

      Delete
  11. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. try this http://experience-aem.blogspot.com/2015/11/aem-61-sample-granite-widget-in-touch-ui-extending-multifield-to-limit-items.html

      Delete
  12. Hi Sreekanth,

    Created new Touch UI Workflow Dialog, which has multifield widget in it, it has select box as child, When clicking on "Add Field" button in the "multi field" widget, it's not loading select box,but throwing following exception/warning in console.

    "An instance of Select is already attached to the specified target element. Future versions of CoralUI will throw an exception at this point."

    I tried adding text box in the multi field, resulting in same problem, Does the workflow dialog support multi field widget or Am i doing any thing wrong?

    Posted my question in AEM forums ,if you need more info please have a look
    http://help-forums.adobe.com/content/adobeforums/en/experience-manager-forum/adobe-experience-manager.topic.html/forum__ez9u-aem_6_1_createda.html

    Thanks,
    Chandra

    ReplyDelete
    Replies
    1. Chandra, you'll find a sample of select in multifield here (check the bug fixes section) - http://experience-aem.blogspot.com/2015/06/aem-61-touch-ui-composite-multifield-store-values-as-child-nodes.html

      Can you upload a package of your workflow dialog to dropbox and share the url here....

      Delete
  13. This is not working for check box.

    ReplyDelete
    Replies
    1. Vasim, try this out:
      in the addDataInFields method of the javascript code replace line 74 with:
      if ($field[0].className == "coral-Checkbox-input" && record[name]) {
      $(field).prop('checked', true);
      } else {
      $field.val(record[name]);
      }

      and line 108 (collectDataFromFields function) with:
      if ($field[0].className == "coral-Checkbox-input") {
      if ($(field).is(':checked')) {
      record[name] = $(field).val();
      } else {
      record[name] = "";
      }
      } else {
      record[name] = $field.val();
      }

      Delete
  14. This comment has been removed by the author.

    ReplyDelete
  15. This comment has been removed by the author.

    ReplyDelete
  16. Hi! Followed all the steps given in the post, but how do I test it? there no site in the content tab in aem( I have no idea how AEM works)

    ReplyDelete
  17. This comment has been removed by the author.

    ReplyDelete
  18. Hi Sreekanth, I am using Bug fix version in my application. I am facing strange issue

    Issue: When First time dialog open then data entries not filled in form (because dialog-ready event not trigger) but when I open dialog again then everything works fine.

    Can you please help why dialog is not in ready state at first time and how to resolve this issue ?

    ReplyDelete
    Replies
    1. Gourav,

      I am facing the same issue. Did you get a solution for this problem.

      Delete
  19. Hi ,
    In my case in classic ui dialog content is stored in string format as below
    name - raul, ram
    place - delhi, noida
    but in touch ui dialog using sling resource type - granite/multifield data is store as above
    but while editing dialog its not gettig pre-populated with stored value
    so what js we can write to prepoulate from string array.
    Above example is to store content and prepoulate from JSON, but I need from String Array

    ReplyDelete
  20. Hello,
    I Created multifield in touch ui dialog, but some times my content is getting cleared. Can anyone help with this situation

    ReplyDelete
  21. Hello,
    I Created multifield in touch ui dialog, but some times my content is getting cleared. Can anyone help with this situation

    ReplyDelete
  22. Hi,

    Can we have multifield within a multifield?

    ReplyDelete
  23. Hello, Am facing problem when i use this multifield for page properties dialog. Can someone tell me how can i resolve this issue.

    ReplyDelete
  24. Sreekanth, Can you please provide a component which works in AEM 6.0, where a composite multifield stores a value in JSON string. It stores as individual array values.

    POST on dialog close does not send the value of json string along with form data.

    ReplyDelete
  25. Hello Sreekanth ,

    I want to use both Touch UI Multifield storing values as child nodes and Touch UI Multifield in my project . But problem is both js has same name and categories is also kept as same . So how to tackle this

    ReplyDelete
  26. Hi Sreekanth,

    I have a requirement to add mandatory field. I tried multiple things to amend the code but nothing worked. Can you assist and provide a script to make attributes mandatory?

    ReplyDelete