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

Goal


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

For storing the nested multifield data as child nodes - 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


Nested Multifield



Stored as JSON



Sample Dialog XML

#45 empty valued flag eaem-nested 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 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"/>
                                    <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=""
                                            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"/>
                                                                        </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-panel

2) Create node /apps/eaem-touch-ui-nested-multi-field-panel/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-panel/clientlib/js.txt, add

                       nested-multifield.js

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

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

    //reads multifield data from server, creates the nested composite multifields and fills them
    function addDataInFields() {
        $document.on("dialog-ready", dlgReadyHandler);

        function dlgReadyHandler() {
            var mName = $("[" + DATA_EAEM_NESTED + "]").data("name");

            if(!mName){
                return;
            }

            //strip ./
            mName = mName.substring(2);

            var $fieldSets = $("[" + DATA_EAEM_NESTED + "][class='coral-Form-fieldset']"),
                $form = $fieldSets.closest("form.foundation-form"),
                actionUrl = $form.attr("action") + ".json";

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

            function postProcess(data){
                if(!data || !data[mName]){
                    return;
                }

                var mValues = data[mName], $field, name;

                if(_.isString(mValues)){
                    mValues = [ JSON.parse(mValues) ];
                }

                _.each(mValues, function (record, i) {
                    if (!record) {
                        return;
                    }

                    if(_.isString(record)){
                        record = JSON.parse(record);
                    }

                    _.each(record, function(rValue, rKey){
                        $field = $($fieldSets[i]).find("[name='./" + rKey + "']");

                        if(_.isArray(rValue) && !_.isEmpty(rValue)){
                            fillNestedFields( $($fieldSets[i]).find("[data-init='multifield']"), rValue);
                        }else{
                            $field.val(rValue);
                        }
                    });
                });
            }

            //creates & fills the nested multifield with data
            function fillNestedFields($multifield, valueArr){
                _.each(valueArr, function(record, index){
                    $multifield.find(".js-coral-Multifield-add").click();

                    _.each(record, function(value, key){
                        var $field = $($multifield.find("[name='./" + key + "']")[index]);
                        $field.val(value);
                    })
                })
            }
        }
    }

    function fillValue($field, record){
        var 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();
    }

    //for getting the nested multifield data as js objects
    function getRecordFromMultiField($multifield){
        var $fieldSets = $multifield.find("[class='coral-Form-fieldset']");

        var records = [], record, $fields, name;

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

            record = {};

            $fields.each(function (j, field) {
                fillValue($(field), record);
            });

            if(!$.isEmptyObject(record)){
                records.push(record)
            }
        });

        return records;
    }

    //collect data from widgets in multifield and POST them to CRX as JSON
    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 record, $fields, $field, name, $nestedMultiField;

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

                record = {};

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

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

                    if($nestedMultiField.length == 0){
                        fillValue($field.find("[name]"), record);
                    }else{
                        name = $nestedMultiField.find("[class='coral-Form-fieldset']").data("name");

                        if(!name){
                            return;
                        }

                        //strip ./
                        name = name.substring(2);

                        record[name] = getRecordFromMultiField($nestedMultiField);
                    }
                });

                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();
    });

    //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 (options) {
            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 comments:

  1. Hi Srikanth,

    I am trying to have a composite multifield in a scaffold form. Can you please guide me how i can have two textfields in a multifield for a scaffold form?

    ReplyDelete
  2. 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
  3. Perfect solution: https://aemblogger.wordpress.com/2017/03/14/aem-touchui-multifield-nested-nodes/

    ReplyDelete
  4. I am trying to use this nested multifield in multible tabs of same dialog. Its working on first tab only . Any solution for this ?

    ReplyDelete
  5. Hi Karthik
    I am also facing same issue. Have you got any solution?

    ReplyDelete