AEM 63 - Touch UI Expandable Collapsible Composite Multifield

Goal


Extend the product Composite Multifield to provide Expand / Collapse feature, for better experience and easy reordering

Demo | Package Install | Github


All Items Collapsed



One Item Expanded

Easy Reordering

Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-touchui-collapsible-multifield

2) Sample dialog with composite multifield /apps/eaem-touchui-collapsible-multifield/sample-collapsible-multifield/cq:dialog
<?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="63 Collapsible Multifield"
    sling:resourceType="cq/gui/components/authoring/dialog">
    <content
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns">
        <items jcr:primaryType="nt:unstructured">
            <column
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/container">
                <items jcr:primaryType="nt:unstructured">
                    <products
                        jcr:primaryType="nt:unstructured"
                        sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
                        composite="{Boolean}true"
                        eaem-show-on-collapse="EAEM.showProductName"
                        fieldLabel="Products">
                        <field
                            jcr:primaryType="nt:unstructured"
                            sling:resourceType="granite/ui/components/coral/foundation/container"
                            name="./products">
                            <items jcr:primaryType="nt:unstructured">
                                <column
                                    jcr:primaryType="nt:unstructured"
                                    sling:resourceType="granite/ui/components/coral/foundation/container">
                                    <items jcr:primaryType="nt:unstructured">
                                        <product
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                            fieldDescription="Name of Product"
                                            fieldLabel="Product Name"
                                            name="./product"/>
                                        <path
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/coral/foundation/form/pathbrowser"
                                            fieldDescription="Select Path"
                                            fieldLabel="Path"
                                            name="./path"
                                            rootPath="/content"/>
                                        <startDate
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/coral/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/coral/foundation/form/checkbox"
                                            name="./show"
                                            text="Show?"
                                            value="yes"/>
                                        <type
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/coral/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>
                                    </items>
                                </column>
                            </items>
                        </field>
                    </products>
                </items>
            </column>
        </items>
    </content>
</jcr:root>


3) #14 eaem-show-on-collapse property is for the function creating summary of multifield items in collapsed mode; here it is EAEM.showProductName, returns the first field value in multifield item (if empty or error in creating summary Click to expand text is shown)

4) Create node /apps/eaem-touchui-collapsible-multifield/clientlib of type cq:ClientLibraryFolder, add String property categories with value cq.authoring.dialog.all, String property dependencies with value underscore

5) Create file (nt:file) /apps/eaem-touchui-collapsible-multifield/clientlib/js.txt, add

                       collapsible-multifield.js

6) Create file (nt:file) /apps/eaem-touchui-collapsible-multifield/clientlib/collapsible-multifield.js, add the following code

(function(){
    if(typeof EAEM === "undefined"){
        EAEM = {};
    }

    //the function executed when user clicks collapse; returns summary of multifield item data
    EAEM.showProductName = function(fields){
        return Object.values(fields)[0];
    }
}());

(function ($, $document, gAuthor) {
    var summaryCreators = {},
        CORAL_MULTIFIELD = "coral-multifield",
        CORAL_MULTIFIELD_ITEM = "coral-multifield-item",
        CORAL_MULTIFIELD_ITEM_CONTENT = "coral-multifield-item-content",
        EAEM_SHOW_ON_COLLAPSE = "eaem-show-on-collapse",
        RS_MULTIFIELD = "granite/ui/components/coral/foundation/form/multifield";

    $document.on("dialog-ready", addCollapsers);

    function addCollapsers(){
        var $multifields = $(CORAL_MULTIFIELD).css("padding-right", "2.5rem");

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

        $multifields.find(CORAL_MULTIFIELD_ITEM).each(handler);

        $multifields.on('change', function(){
            $multifields.find(CORAL_MULTIFIELD_ITEM).each(handler);
        });

        loadShowSummaryCreatorFunctions();

        addExpandCollapseAll($multifields);

        function handler(){
            var $mfItem = $(this);

            if(!_.isEmpty($mfItem.find("[icon=accordionUp]"))){
                return;
            }

            addAccordionIcons($mfItem);

            addSummarySection($mfItem);
        }
    }

    function addSummarySection($mfItem){
        var $summarySection = $("<div/>").insertAfter($mfItem.find(CORAL_MULTIFIELD_ITEM_CONTENT))
            .addClass("coral-Well").css("cursor", "pointer").hide();

        $summarySection.click(function(){
            $mfItem.find("[icon='accordionDown']").click();
        });
    }

    function addExpandCollapseAll($multifields){
        var $mfAdd, expandAll, collapseAll;

        $multifields.find("[coral-multifield-add]").each(handler);

        function handler(){
            $mfAdd = $(this);

            expandAll = new Coral.Button().set({
                variant: 'secondary',
                innerText: "Expand All"
            });

            expandAll.$.css("margin-left", "10px").click(function(){
                event.preventDefault();
                $(this).closest(CORAL_MULTIFIELD).find("[icon='accordionDown']").click();
            });

            collapseAll = new Coral.Button().set({
                variant: 'secondary',
                innerText: "Collapse All"
            });

            collapseAll.$.css("margin-left", "10px").click(function(){
                event.preventDefault();
                $(this).closest(CORAL_MULTIFIELD).find("[icon='accordionUp']").click();
            });

            $mfAdd.after(expandAll).after(collapseAll);
        }
    }

    function loadShowSummaryCreatorFunctions(){
        var editable = gAuthor.DialogFrame.currentDialog.editable;

        if(!editable){
            return;
        }

        $.ajax(editable.config.dialog + ".infinity.json").done(fillLabelCreatorFns);

        function fillLabelCreatorFns(obj){
            if(!_.isObject(obj) || _.isEmpty(obj)){
                return;
            }

            _.each(obj, function(value){
                if(value["sling:resourceType"] === RS_MULTIFIELD){
                    if(!_.isEmpty(value.field) && !_.isEmpty(value.field.name)) {
                        summaryCreators[value.field.name] = value[EAEM_SHOW_ON_COLLAPSE];
                    }
                }else{
                    if(_.isObject(value) && !_.isEmpty(value)){
                        fillLabelCreatorFns(value);
                    }
                }
            });
        }
    }

    function addAccordionIcons($mfItem){
        var up = new Coral.Button().set({
            variant: "quiet",
            icon: "accordionUp",
            title: "Collapse"
        });

        up.setAttribute('style', 'position:absolute; top: 0; right: -2.175rem');
        up.$.on('click', handler);

        $mfItem.append(up);

        var down = new Coral.Button().set({
            variant: "quiet",
            icon: "accordionDown",
            title: "Expand"
        });

        down.setAttribute('style', 'position:absolute; top: 0; right: -2.175rem');
        down.$.on('click', handler).hide();

        $mfItem.append(down);

        function handler(event){
            event.preventDefault();

            var mfName = $(this).closest(CORAL_MULTIFIELD).attr("data-granite-coral-multifield-name"),
                $mfItem = $(this).closest(CORAL_MULTIFIELD_ITEM),
                $summarySection = $mfItem.children("div");

            $summarySection.html(getSummary($mfItem, mfName));

            adjustUI.call(this, $summarySection);
        }

        function adjustUI($summarySection){
            var icon = $(this).find("coral-icon").attr("icon"),
                $content = $mfItem.find(CORAL_MULTIFIELD_ITEM_CONTENT);

            if(icon == "accordionUp"){
                if($summarySection.css("display") !== "none"){
                    return;
                }

                $summarySection.show();

                $content.slideToggle( "fast", function() {
                    $content.hide();
                });

                up.$.hide();
                down.$.show();
            }else{
                if($summarySection.css("display") === "none"){
                    return;
                }

                $summarySection.hide();

                $content.slideToggle( "fast", function() {
                    $content.show();
                });

                up.$.show();
                down.$.hide();
            }
        }
    }

    function getSummary($mfItem, mfName){
        var summary = "Click to expand";

        try{
            if(summaryCreators[mfName]){
                var fields = {};

                $mfItem.find("input").each(function(){
                    var $input = $(this);
                    fields[$input.attr("name")] = $input.val();
                });

                summary = eval(summaryCreators[mfName])(fields);
            }
        }catch(err){}

        if(!summary){
            summary = "Click to expand";
        }

        return summary;
    }
}(jQuery, jQuery(document), Granite.author));

3 comments:

  1. I guess we can skip the customization if we use the collapsible layout: https://docs.adobe.com/docs/en/aem/6-3/develop/ref/granite-ui/api/jcr_root/libs/granite/ui/components/foundation/layouts/collapsible/index.html In my previous project it worked for us to add collapsible feature to multifields and nested multifields.

    ReplyDelete
  2. could you please post multifield limit in AEM 6.3

    ReplyDelete
  3. hello ! pathbrowser in multifield don't click to Browse !
    I copy your code and run on localhost !

    ReplyDelete