AEM 63 - Touch UI Nested ( Multi-Multi ) Coral 2 Composite Multifield

Goal


AEM 63 Touch UI Nested Composite Multifield (Multi-Multi Field) storing the entered data as Child Nodes.

This implementation uses Coral 2 multifield/libs/granite/ui/components/foundation/form/multifield; AEM 63 provides otb implementation of composite multifield (NOT nested composite multifield) using Coral 3 widget - /libs/granite/ui/components/coral/foundation/form/multifield

This post is on nested composite multifield; For composite multifield use otb widget /libs/granite/ui/components/coral/foundation/form/multifield (set property composite=true).

For AEM 62 check this post

Demo | Package Install | Github


Nested Composite Multifield



Stored as Child Nodes



Sample Dialog 



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.all, 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";

    if(CUI.Multifield.eaemNMFExtended){
        return;
    }

    CUI.Multifield.eaemNMFExtended = true;

    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/eaem-sample-nested-multi-field/eaem-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>

5 comments:

  1. Hi Srikanth,

    On new 6.3 instance the package provided by worked well. But after installed https://github.com/Adobe-Consulting-Services/acs-aem-commons/releases/tag/acs-aem-commons-3.9.0 it stopped working . When I click on Add Field nothing happens. Is there a way to fix this ?

    Thanks
    Karthik

    ReplyDelete
  2. Hi Srikanth,

    The package provided here worked in 6.3, but if I try to add any new fields after the multifield element in the dialog, it is not rendering correctly.
    For example, I have added a new text field "country2" after the "states" element and dialog does not render correctly. Please let us know if there is any fix for this issue.

    ReplyDelete
  3. i am not able to add country field ...please suggest

    ReplyDelete
  4. The issue is replicating after re authoring the dialog to expand the Super multifield. It is expanding the submultifield instead of super multifield.

    ReplyDelete
  5. Bro if u have that code send me kalyankmr901@gmail.com

    ReplyDelete