AEM 62 - Touch UI Nested ( Multi-Multi ) Composite Multifield storing data as Child Nodes

Goal


Create a 62 Touch UI Nested Composite Multifield aka Multi-Multi Field storing the entered data as Child Nodes. Package Install contains a sample component using this multifield extension

For storing the nested multifield data as json - check this post

PS: If you are using any other Touch UI multifield extension the re-registering of multifield using CUI.Widget.registry.register("multifield"CUI.Multifield); may cause issues

Demo | Package Install | Github


Nested Multifield



Stored as Child Nodes



Sample Dialog XML

#45 eaem-nested=NODE_STORE makes the multifield widget, nested composite multifield

<?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="EAEM TouchUI Nested Multifield"
    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"/>
                                    <countries
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/foundation/form/multifield"
                                        class="full-width"
                                        fieldDescription="Click '+' to add a new page"
                                        fieldLabel="Countries">
                                        <field
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/foundation/form/fieldset"
                                            eaem-nested="NODE_STORE"
                                            name="./countries">
                                            <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">
                                                        <country
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/foundation/form/textfield"
                                                            fieldDescription="Name of Country"
                                                            fieldLabel="Country Name"
                                                            name="./country"/>
                                                        <states
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/foundation/form/multifield"
                                                            class="full-width"
                                                            fieldDescription="Click '+' to add a new page"
                                                            fieldLabel="States">
                                                            <field
                                                                jcr:primaryType="nt:unstructured"
                                                                sling:resourceType="granite/ui/components/foundation/form/fieldset"
                                                                name="./states">
                                                                <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">
                                                                            <state
                                                                                jcr:primaryType="nt:unstructured"
                                                                                sling:resourceType="granite/ui/components/foundation/form/textfield"
                                                                                fieldDescription="Name of State"
                                                                                fieldLabel="State Name"
                                                                                name="./state"/>
                                                                            <path
                                                                                jcr:primaryType="nt:unstructured"
                                                                                sling:resourceType="granite/ui/components/foundation/form/pathbrowser"
                                                                                fieldDescription="Select Path"
                                                                                fieldLabel="Path"
                                                                                name="./path"
                                                                                rootPath="/content"/>
                                                                            <startDate
                                                                                jcr:primaryType="nt:unstructured"
                                                                                sling:resourceType="granite/ui/components/foundation/form/datepicker"
                                                                                class="field"
                                                                                displayedFormat="YYYY-MM-DD HH:mm"
                                                                                fieldLabel="Start Date"
                                                                                name="./startDate"
                                                                                type="datetime"/>
                                                                            <show
                                                                                jcr:primaryType="nt:unstructured"
                                                                                sling:resourceType="granite/ui/components/foundation/form/checkbox"
                                                                                name="./show"
                                                                                text="Show?"
                                                                                value="yes"/>
                                                                            <type
                                                                                jcr:primaryType="nt:unstructured"
                                                                                sling:resourceType="granite/ui/components/foundation/form/select"
                                                                                fieldDescription="Select Size"
                                                                                fieldLabel="Size"
                                                                                name="./size">
                                                                                <items jcr:primaryType="nt:unstructured">
                                                                                    <def
                                                                                        jcr:primaryType="nt:unstructured"
                                                                                        text="Select Size"
                                                                                        value=""/>
                                                                                    <small
                                                                                        jcr:primaryType="nt:unstructured"
                                                                                        text="Small"
                                                                                        value="small"/>
                                                                                    <medium
                                                                                        jcr:primaryType="nt:unstructured"
                                                                                        text="Medium"
                                                                                        value="medium"/>
                                                                                    <large
                                                                                        jcr:primaryType="nt:unstructured"
                                                                                        text="Large"
                                                                                        value="large"/>
                                                                                </items>
                                                                            </type>
                                                                            <tags
                                                                                jcr:primaryType="nt:unstructured"
                                                                                sling:resourceType="cq/gui/components/common/tagspicker"
                                                                                allowCreate="{Boolean}true"
                                                                                fieldLabel="Tags"
                                                                                name="./tags"/>
                                                                        </items>
                                                                    </column>
                                                                </items>
                                                            </field>
                                                        </states>
                                                    </items>
                                                </column>
                                            </items>
                                        </field>
                                    </countries>
                                </items>
                            </column>
                        </items>
                    </fieldset>
                </items>
            </column>
        </items>
    </content>
</jcr:root>


Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-touch-ui-nested-multi-field-node-store

2) Create node /apps/eaem-touch-ui-nested-multi-field-node-store/clientlib of type cq:ClientLibraryFolder, add String property categories with value cq.authoring.dialog, String property dependencies with value underscore

3) Create file (nt:file) /apps/eaem-touch-ui-nested-multi-field-node-store/clientlib/js.txt, add

                       nested-multifield.js

4) Create file (nt:file) /apps/eaem-touch-ui-nested-multi-field-node-store/clientlib/nested-multifield.js, add the following code

(function ($, $document) {
    var EAEM_NESTED = "eaem-nested",
        DATA_EAEM_NESTED = "data-" + EAEM_NESTED,
        CFFW = ".coral-Form-fieldwrapper",
        NODE_STORE = "NODE_STORE";

    function isNodeStoreMultifield(type) {
        return (type === NODE_STORE);
    }

    function isSelectOne($field) {
        return !_.isEmpty($field) && ($field.prop("type") === "select-one");
    }

    function setSelectOne($field, value) {
        var select = $field.closest(".coral-Select").data("select");

        if (select) {
            select.setValue(value);
        }
    }

    function isCheckbox($field) {
        return !_.isEmpty($field) && ($field.prop("type") === "checkbox");
    }

    function setCheckBox($field, value) {
        $field.prop("checked", $field.attr("value") === value);
    }

    function isDateField($field) {
        return !_.isEmpty($field) && $field.parent().hasClass("coral-DatePicker");
    }

    function setDateField($field, value) {
        var date = moment(new Date(value)),
            $parent = $field.parent();

        $parent.find("input.coral-Textfield").val(date.format($parent.data("displayed-format")));

        $field.val(date.format($parent.data("stored-format")));
    }

    function isTagsField($fieldWrapper) {
        return !_.isEmpty($fieldWrapper) && ($fieldWrapper.children(".js-cq-TagsPickerField").length > 0);
    }

    function getTagsFieldName($fieldWrapper) {
        return $fieldWrapper.children(".js-cq-TagsPickerField").data("property-path").substr(2);
    }

    function getTagObject(tag){
        var tagPath = "/etc/tags/" + tag.replace(":", "/");
        return $.get(tagPath + ".tag.json");
    }

    function setTagsField($fieldWrapper, tags) {
        if(_.isEmpty(tags)){
            return;
        }

        var cuiTagList = $fieldWrapper.find(".coral-TagList").data("tagList");

        _.each(tags, function(tag){
            getTagObject(tag).done(function(data){
                cuiTagList._appendItem( { value: data.tagID, display: data.titlePath} );
            });
        });
    }

    function isMultifield($formFieldWrapper){
        return ($formFieldWrapper.children("[data-init='multifield']").length > 0);
    }

    function setWidgetValue($field, value) {
        if (_.isEmpty($field)) {
            return;
        }

        if(isSelectOne($field)) {
            setSelectOne($field, value);
        }else if(isCheckbox($field)) {
            setCheckBox($field, value);
        }else if(isDateField($field)) {
            setDateField($field, value);
        }else {
            $field.val(value);
        }
    }

    function getMultifields($formField, isInner){
        var mNames = {}, mName, $multifield, $template,
            $multiTemplates = $formField.find(".js-coral-Multifield-input-template");

        $multiTemplates.each(function (i, template) {
            $template = $(template);
            $multifield = $($template.html());

            if(!isInner && !isNodeStoreMultifield($multifield.data(EAEM_NESTED))){
                return;
            }

            mName = $multifield.data("name").substring(2);

            mNames[mName] = $template.closest(".coral-Multifield");
        });

        return mNames;
    }

    function buildMultifield(data, $multifield, mName){
        var $formFieldWrapper, $field, $fieldSet, name,
            innerMultifields;

        _.each(data, function (value, key) {
            if(key.indexOf("jcr:") === 0){
                return;
            }

            $multifield.children(".js-coral-Multifield-add").click();

            $fieldSet = $multifield.find(".coral-Form-fieldset").last();

            _.each($fieldSet.find(CFFW), function (formFieldWrapper) {
                $formFieldWrapper = $(formFieldWrapper);

                if(isMultifield($formFieldWrapper)){
                    innerMultifields = getMultifields($formFieldWrapper, true);

                    _.each(innerMultifields, function($innerMultifield, nName){
                        buildMultifield(value[nName], $innerMultifield, nName);
                    });

                    return;
                }else if(isTagsField($formFieldWrapper)){
                    setTagsField($formFieldWrapper, value[getTagsFieldName($formFieldWrapper)]);
                    return;
                }

                $field = $formFieldWrapper.find("[name]");

                if(_.isEmpty($field)){
                    return;
                }

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

                if(_.isEmpty(value[name])){
                    return;
                }

                setWidgetValue($field, value[name]);
            });
        })
    }

    function addDataInFields() {
        $document.on("dialog-ready", dlgReadyHandler);

        function dlgReadyHandler() {
            var outerMultifields = getMultifields($(this), false),
                $form = $("form.cq-dialog"),
                actionUrl = $form.attr("action") + ".infinity.json";

            $.ajax(actionUrl).done(postProcess);

            function postProcess(data){
                _.each(outerMultifields, function($outerMultifield, mName){
                    buildMultifield(data[mName], $outerMultifield, mName);
                });
            }
        }
    }

    function fillValue($form, fieldSetName, $field, counter){
        var name = $field.attr("name"), value;

        if (!name) {
            return;
        }

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

        value = $field.val();

        if (isCheckbox($field)) {
            value = $field.prop("checked") ? $field.val() : "";
        }

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

        $('<input />').attr('type', 'hidden')
            .attr('name', fieldSetName + "/" + counter + "/" + name)
            .attr('value', value)
            .appendTo($form);
    }

    function addNestedMultifieldData($form, outerMultiName, $nestedMultiField){
        var $fieldSets = $nestedMultiField.find("[class='coral-Form-fieldset']"),
            nName = $fieldSets.data("name"), $fields;

        if(!nName){
            return;
        }

        nName = outerMultiName + "/" + nName.substring(2);

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

            $fields.each(function (counter, field) {
                fillValue($form, nName, $(field), (iCounter + 1));
            });
        });
    }

    function collectDataFromFields(){
        $document.on("click", ".cq-dialog-submit", collectHandler);

        function collectHandler() {
            var $form = $(this).closest("form.foundation-form"),
                mName = $("[" + DATA_EAEM_NESTED + "]").data("name"),
                $fieldSets = $("[" + DATA_EAEM_NESTED + "][class='coral-Form-fieldset']");

            var $fields, $field, name, $nestedMultiField;

            $fieldSets.each(function (oCounter, fieldSet) {
                $fields = $(fieldSet).children().children(CFFW);

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

                    //may be a nested multifield
                    $nestedMultiField = $field.find("[data-init='multifield']");

                    if($nestedMultiField.length == 0){
                        fillValue($form, mName, $(field).find("[name]"), (oCounter + 1));
                    }else{
                        addNestedMultifieldData($form, mName + "/" + (oCounter + 1) , $nestedMultiField);
                    }
                });
            });
        }
    }

    $document.ready(function () {
        addDataInFields();
        collectDataFromFields();
    });

    //extend otb multifield for adjusting event propagation when there are nested multifields
    //for working around the nested multifield add and reorder
    CUI.Multifield = new Class({
        toString: "Multifield",
        extend: CUI.Multifield,

        construct: function () {
            this.script = this.$element.find(".js-coral-Multifield-input-template:last");
        },

        _addListeners: function () {
            this.superClass._addListeners.call(this);

            //otb coral event handler is added on selector .js-coral-Multifield-add
            //any nested multifield add click events are propagated to the parent multifield
            //to prevent adding a new composite field in both nested multifield and parent multifield
            //when user clicks on add of nested multifield, stop the event propagation to parent multifield
            this.$element.on("click", ".js-coral-Multifield-add", function (e) {
                e.stopPropagation();
            });

            this.$element.on("drop", function (e) {
                e.stopPropagation();
            });
        }
    });

    CUI.Widget.registry.register("multifield", CUI.Multifield);
})(jQuery, jQuery(document));


5) Create file (nt:file) /apps/eaem-touch-ui-nested-multi-field-node-store/sample-nested-multi-field/sample-nested-multi-field.jsp for rendering the stored multifield data

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

<div style="display: block; border-style: solid; border-width: 1px; margin: 10px; padding: 10px">
    <b>Countries and States</b>

<%
        try {
            if (currentNode.hasNode("countries")) {
                Node countriesNode = currentNode.getNode("countries"), cNode;
                int counter = 1; PropertyIterator itr = null; Property property;

                while(true){
                    if(!countriesNode.hasNode(String.valueOf(counter))){
                        break;
                    }

                    cNode = countriesNode.getNode(String.valueOf(counter));

                    itr = cNode.getProperties();

                    while(itr.hasNext()){
                        property = itr.nextProperty();

                        if(property.getName().equals("jcr:primaryType")){
                            continue;
                        }
%>
                        <%=property.getName()%> : <b><%=property.getString()%></b>
<%
                    }

                    if(cNode.hasNode("states")){
                        Node statesNode = cNode.getNode("states"), sNode;
                        int sCounter = 1; PropertyIterator sTtr = null; Property sProperty;

                        while(true){
                            if(!statesNode.hasNode(String.valueOf(sCounter))){
                                break;
                            }

                            sNode = statesNode.getNode(String.valueOf(sCounter));

                            itr = sNode.getProperties();

                            while(itr.hasNext()){
                                sProperty = itr.nextProperty();

                                if(sProperty.getName().equals("jcr:primaryType")){
                                    continue;
                                }

                                String value = null;

                                if (sProperty.isMultiple()) {
                                    Value[] values = sProperty.getValues();
                                    value = StringUtils.join(values, ",");
                                } else {
                                    value = sProperty.getString();
                                }

%>
                                <div style="margin-left:30px">
                                        <%=sProperty.getName()%> : <b><%=value%></b>
                                </div>
<%
                            }

%>

<%

                            sCounter = sCounter + 1;
                        }
                }

                counter = counter + 1;
        }
    } else {
%>
    Add countries and states in dialog</b>
<%
            }
        } catch (Exception e) {
            e.printStackTrace(new PrintWriter(out));
        }
%>
</div>

7 comments:

  1. One of the biggest challenges that organizations face today is having inaccurate data and being unresponsive to the needs of the Adobe CQ5 CMS Email List organization.

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

    ReplyDelete
  3. Is anyone else seeing issues with this when you have two (or more) composite multifield side by side (i.e. not nested).

    I am seeing all the node created under the one parent.

    e.g.
    Composite Multifield Component C1 has two text fields, C1-T1 and C1-T2.
    Composite Multifield Component C2 has three text fields, C2-T,1 C2-T2, C2-T3.

    If I use the dialog to create two sets of values under each Composite Multifield component save this and then look is CRXDE Lite (or re-open the dialog) instead of seeing the C1 node with child nodes of '1' and '2', and the C2 node with child nodes of '1' and '2' I am only seeing the C1 node which has child nodes of '1', '2', '3' and '4' under it.

    ReplyDelete
    Replies
    1. I have fixed this by changing line #240 of the JS file from:

      fillValue($form, mName, $(field).find("[name]"), (oCounter + 1));

      to:

      fillValue($form, $(field).closest("[" + DATA_EAEM_NESTED + "]").data("name"), $(field).find("[name]"), (oCounter + 1));

      This fills my purpose but someone might want to take a closer look to make this more robust...

      Delete
  4. I found perfect code here : https://aemblogger.wordpress.com/2017/03/14/aem-touchui-multifield-nested-nodes/

    ReplyDelete
  5. is anyone getting same issue as me, when i open the dialog again for editing, the value for multifield is not populated in touch UI dialog.

    ReplyDelete
    Replies
    1. Yes,Because of conflict between ACS commons client libs apps.acs-commons.touchui-widgets.composite-multifield and this one.

      Delete