Tuesday, May 3, 2016

AEM 6.1 Tab(ular) Multi Field for touch ui authoring dialog


Disclaimer : Ideas/Implementions provided in this blog express my personal view. Adobe or Me will not be held responsible for damage caused on your system because of information. Please don't try any suggestion in production system without proper testing. This is not an Adobe supported blog


AEM OOTB provides a simple multi field widget at /libs/granite/ui/components/foundation/form/multifield. But this widget can't handle the multiple fields.

Objective:

Create new composite multi field widget which displays each field(set) in a separate tab. The tabular separation of field sets helps to organize the fields easily rather than showing each field set one after the other vertically

You can download the package here

The component looks like:



The dialog structure looks like :



The content after authoring looks like:



  1. Create the path /apps/authoring/touchui/widgets/tabular-multifield, with 'tabular-multifield' being sling:Folder node type
    • On tabular-multifild, set sling:resourceSuperType to 'granite/ui/components/foundation/form/field'
    • Create 'render.jsp' in 'tabular-mutlifield' with the following content
<%@include file="/libs/granite/ui/global.jsp" %><%
%><%@page session="false"
          import="java.util.HashMap,
                  java.util.Iterator,
      java.util.List,
         java.util.ArrayList,
      org.apache.sling.api.resource.ResourceResolver,
                  org.apache.sling.api.wrappers.ValueMapDecorator,
                  com.adobe.granite.ui.components.AttrBuilder,
                  com.adobe.granite.ui.components.ComponentHelper,
                  com.adobe.granite.ui.components.ComponentHelper.Options,
                  com.adobe.granite.ui.components.Config,
                  com.adobe.granite.ui.components.Field,
                  com.adobe.granite.ui.components.Tag,
                  com.adobe.granite.ui.components.Value" %><%--###
Example::
   
      + myinput
        - name = "myTabularMultiField"
        - sling:resourceType = "/apps/authoring/touchui/widgets/tabular-multifield"
        + field
          - sling:resourceType = "granite/ui/components/foundation/container"
          + items
              + pathfield
                 - sling:resourceType = "granite/ui/components/foundation/form/pathbrowser"
                 - name = "./one"
              + textfield
                 - sling:resourceType = "granite/ui/components/foundation/form/textfield"
                 - name = "./two"

###--%>
<%
Config cfg = cmp.getConfig();
ValueMap vm = (ValueMap) request.getAttribute(Field.class.getName());

Tag tag = cmp.consumeTag();
AttrBuilder attrs = tag.getAttrs();

attrs.add("id", cfg.get("id", String.class));
attrs.addRel(cfg.get("rel", String.class));
attrs.addClass(cfg.get("class", String.class));
attrs.add("title", i18n.getVar(cfg.get("title", String.class)));
String name = cfg.get("name", String.class);
attrs.add("name", name);
attrs.addClass("tabular-Multifield");
attrs.add("data-init", "tabularmultifield");

attrs.addOthers(cfg.getProperties(), "id", "rel", "name", "class", "title", "fieldLabel", "fieldDescription", "renderReadOnly");

Resource field = cfg.getChild("field");

List<TabInfo> tabsInfo = getTabsInfo(request, resourceResolver, name);
%><div <%= attrs.build() %>><%
    if (cfg.get("deleteHint", true)) {
     for (TabInfo tabInfo : tabsInfo) {
            %><input type="hidden" name="./<%= name != null ? name + "/" : "" + tabInfo.getNodeName() %>@Delete"><%
     }
 }
    %>    
    <div class="coral-TabPanel" data-init="tabs">
        <nav class="coral-TabPanel-navigation">
            <%for (TabInfo tabInfo : tabsInfo) {%>            
            <a class="coral-TabPanel-tab" href="#"  data-toggle="tab">
                <span><%=tabInfo.getName()%><span>
                    <i class="close-tab-button js-tabular-Multifield-remove coral-Icon coral-Icon--close coral-Icon--sizeXS"></i>
                    </span></span>
            </a>
            <%}%>
            <a class="coral-TabPanel-tab js-tabular-Multifield-add" href="#" data-target="#field-add" data-toggle="tab">
                <i class="coral-Icon coral-Icon--add coral-Icon--sizeXS"></i>
            </a>
        </nav>
        <div class="coral-TabPanel-content">
            <%for (TabInfo tabInfo : tabsInfo) {%>            
            <section class="coral-TabPanel-pane" role="tabpanel">
                <% include(field, tabInfo.getValues(), cmp, request); %>
            </section>
            <%}%>
            <section class="dummy-section coral-TabPanel-pane" style="display:none">
            </section>
        </div>
    </div>
    <script class="js-tabular-Multifield-input-template" type="text/html"><% include(field, cmp, request); %></script>
</div><%!

private void include(Resource field, ComponentHelper cmp, HttpServletRequest request) throws Exception {
    // include the field with no value set at all
    
    ValueMap existingVM = (ValueMap) request.getAttribute(Value.FORM_VALUESS_ATTRIBUTE);
    String existingPath = (String) request.getAttribute(Value.CONTENTPATH_ATTRIBUTE);
    
    request.removeAttribute(Value.FORM_VALUESS_ATTRIBUTE);
    request.removeAttribute(Value.CONTENTPATH_ATTRIBUTE);
    
    cmp.include(field, new Options().rootField(false));

    request.setAttribute(Value.FORM_VALUESS_ATTRIBUTE, existingVM);
    request.setAttribute(Value.CONTENTPATH_ATTRIBUTE, existingPath);
}

private void include(Resource field, ValueMap map, ComponentHelper cmp, HttpServletRequest request) throws Exception {    
    ValueMap existing = (ValueMap) request.getAttribute(Value.FORM_VALUESS_ATTRIBUTE);
    request.setAttribute(Value.FORM_VALUESS_ATTRIBUTE, map);

    cmp.include(field, new Options().rootField(false));

    request.setAttribute(Value.FORM_VALUESS_ATTRIBUTE, existing);
}
private List<TabInfo> getTabsInfo(HttpServletRequest request, ResourceResolver resolver, String name) throws Exception {
    List<TabInfo> tabsInfo = new ArrayList<TabInfo>();
    String contentPath = (String) request.getAttribute(Value.CONTENTPATH_ATTRIBUTE);
    if (name != null) {
        contentPath = contentPath + "/" + name;
    }
    Resource contentRes = resolver.getResource(contentPath);
    if (contentRes != null) {
        for (Iterator<Resource> it = contentRes.listChildren(); it.hasNext(); ) {
            Resource r = it.next();
            String rName = r.getName();
            if (rName.startsWith("tab")) {
                String tabIndex = rName.replace("tab", "");
                String tabName = "Tab " + tabIndex;
                ValueMap vm = r.adaptTo(ValueMap.class);
                TabInfo tabInfo = new TabInfo(tabName, rName, vm);
                tabsInfo.add(tabInfo);
            }
        }
    }
    return tabsInfo;
}

class TabInfo {
    private String name;
    private String nodeName;
    private ValueMap values;
    TabInfo(String name, String nodeName, ValueMap values) {
        this.name = name;
        this.nodeName = nodeName;
        this.values = values;
    }
    String getName() {
        return name;
    }
    String getNodeName() {
        return nodeName;
    }
    ValueMap getValues() {
        return values;
    }
}
%>
2. Create "cq.authoring.dialog" clientlibs in /apps/authoring/touchui/widgets/tabular-multifield/clientlibs
  • create js.txt in the clientlibs folder with the following content
#base=js
tabular-multi-field.js
  • create css.txt in the clientlibs folder with the following content
#base=css
tabular-multi-field.css
  • create tabular-multi-field.js file at /apps/authoring/touchui/widgets/tabular-multifield/clientlibs/js/tabular-multi-field.js with the following content
(function ($, console) {
  "use strict";

  var listTemplate =
    "<div class=\"coral-TabPanel\" data-init=\"tabs\"><nav class=\"coral-TabPanel-navigation\"></nav><div class=\"coral-TabPanel-content\"></div></div>";




  CUI.TabularMultifield = new Class({
    toString: "TabularMultifield",
    extend: CUI.Widget,

    /**
     @extends CUI.Widget

     @classdesc A composite field that allows you to add/remove multiple instances of a component.
     The component is added based on a template defined in a <code>&lt;script type=&quot;text/html&quot;&gt;</code> tag.
     The current added components are managed inside a <code>div.tabular-Multifield</code> element.

     @desc Creates a TabularMultifield component.
     @constructs
     */
    construct: function (options) {
      this.script = this.$element.find(".js-tabular-Multifield-input-template");
      this.tabsContainer = this.$element.find(".coral-TabPanel").data("tabs");
      var prevTabIndex = 0;
      var tabs = this.tabsContainer._getTabs();
      tabs.each(function() {
          var $this = $(this);
          var i = $this.text().trim().replace("Tab ", "");
          if (!isNaN(i)) {
              var index = parseInt(i);
              if (index > prevTabIndex) {
                  prevTabIndex = index;
              }
          }
      });
      this.tabIndex = prevTabIndex === 0 ? 1: prevTabIndex + 1;

      if (this.tabsContainer.length === 0) {
        this.tabsContainer = $(listTemplate).prependTo(this.$element).find(".coral-TabPanel").data("tabs");
      }
      var tabularCompositeFieldName = this.$element.attr("name") || "";
      if (tabularCompositeFieldName) {
          tabularCompositeFieldName+= "/";
      }

      var panels = this.tabsContainer._getPanels();
      var self = this;
      panels.each(function(){
        var $this = $(this);
        var panelIndex = $this.index();
        var tabNumber = $(tabs[panelIndex]).text().trim().replace("Tab ", "");
        self.adjustInputNames($this, tabularCompositeFieldName + "tab" + tabNumber);
        self._handleRadioButtons($this);
      });

      this._adjustMarkup();
      this._addListeners();
    },

    _handleRadioButtons: function(item) {
        item.find("input[type='radio']").each(function(){
           var $this = $(this);
           if ($this.attr("checked") == "checked") {
               $this.attr("aria-selected",  true);
               $this.prop("checked", "checked");
           }
        });
    },

    /**
     * Enhances the markup required for multifield.
     * @private
     */
    _adjustMarkup: function () {
      this.$element.addClass("coral-Multifield");
    },

    /**
     * Initializes listeners.
     * @private
     */
    _addListeners: function () {
      var self = this;
      var tabularCompositeFieldName = self.$element.attr("name") || "";
      if (tabularCompositeFieldName) {
          tabularCompositeFieldName+= "/";
      }

      //add
      this.$element.on("click", ".js-tabular-Multifield-add", function (e) {
          var item = $(self.script.html().trim());
          self.adjustInputNames(item, tabularCompositeFieldName + "tab" + self.tabIndex);
          self.handleCheckBoxesDefaultValue(item);
          var tabCount = self.tabsContainer._getTabs().length;
          var tabContent = "<span>Tab " + self.tabIndex + "<span> <i class=\"close-tab-button js-tabular-Multifield-remove coral-Icon coral-Icon--close coral-Icon--sizeXS\"></i>"
          self.tabsContainer.addItem({tabContent:tabContent, panelContent:item, index:tabCount-1});
          self.tabIndex++;
          $(self.$element).trigger("cui-contentloaded");

      });
      //remove
      this.$element.on("click", ".js-tabular-Multifield-remove", function (e) {
          var $this = $(this);
          var tabIndex = $(this).closest(".coral-TabPanel-tab.is-active").index();
          self.tabsContainer.removeItem(tabIndex);
          var tabCount = self.tabsContainer._getTabs().length;
          if (tabCount === 1) {
              self.tabIndex = 1;
          }
      });
    },
    handleCheckBoxesDefaultValue: function(item) {
        item.find("input[type='checkbox'][data-check-in-multifield='true']").each(function(){
            var $this = $(this);
            $this.prop("checked", true);
        });
    },
    adjustInputNames: function (item, prefix) {
        item.find("[name]").each(function(){
            var $this = $(this);
            var name = $this.attr("name");
            $this.attr("name", "./" + prefix + name.substr(1));
        });
    }
  });

  CUI.Widget.registry.register("tabularmultifield", CUI.TabularMultifield);

  if (CUI.options.dataAPI) {
    $(document).on("cui-contentloaded.data-api", function (e) {
      CUI.TabularMultifield.init($("[data-init~=tabularmultifield]", e.target));
    });
  }
})(jQuery, window.console);
  • create tabular-multi-field.css at /apps/authoring/touchui/widgets/tabular-multifield/clientlibs/css/tabular-multi-field.css with the following content
.tabular-Multifield a[data-toggle="tab"] i {
    font-size: 0.5rem;
    padding-right: 1rem;
    padding-left: 0.5rem;
}

.tabular-Multifield a[data-toggle="tab"] {
    text-transform: none;
}
Now you can use this multi field widget in authoring dialog, with the following example structure:

                          <tabular_multifield

                                jcr:primaryType="nt:unstructured"

                                sling:resourceType="/apps/authoring/touchui/widgets/tabular-multifield"

                                fieldLabel="My Tabular Multi Field"

                                name="tabularFields">

                                <field

                                    jcr:primaryType="nt:unstructured"

                                    sling:resourceType="granite/ui/components/foundation/container">

                                    <items jcr:primaryType="nt:unstructured">

                                        <one

                                            jcr:primaryType="nt:unstructured"

                                            sling:resourceType="granite/ui/components/foundation/form/pathbrowser"

                                            fieldLabel="path browser"

                                            name="./path"

                                            rootPath="/content"/>

                                        <two

                                            jcr:primaryType="nt:unstructured"

                                            sling:resourceType="granite/ui/components/foundation/form/textfield"

                                            fieldLabel="text field"

                                            name="./text"/>

                                        <position

                                            jcr:primaryType="nt:unstructured"

                                            sling:resourceType="granite/ui/components/foundation/form/radiogroup"

                                            fieldDescription="alignment"

                                            fieldLabel="Choose"

                                            text="Alignment">

                                            <items jcr:primaryType="nt:unstructured">

                                                <left

                                                    jcr:primaryType="nt:unstructured"

                                                    sling:resourceType="granite/ui/components/foundation/form/radio"

                                                    class="slide-item"

                                                    name="./overlayPosition"

                                                    text="Left"

                                                    value="left"/>

                                                <center

                                                    jcr:primaryType="nt:unstructured"

                                                    sling:resourceType="granite/ui/components/foundation/form/radio"

                                                    checked="true"

                                                    class="slide-item"

                                                    name="./overlayPosition"

                                                    text="Center"

                                                    value="center"/>

                                                <right

                                                    jcr:primaryType="nt:unstructured"

                                                    sling:resourceType="granite/ui/components/foundation/form/radio"

                                                    class="slide-item"

                                                    name="./overlayPosition"

                                                    text="Right"

                                                    value="right"/>

                                            </items>

                                        </position>

                                        <checkbox

                                            jcr:primaryType="nt:unstructured"

                                            sling:resourceType="granite/ui/components/foundation/form/checkbox"

                                            check-in-multifield="{Boolean}true"

                                            fieldDescription="My Checkbox"

                                            fieldLabel="checkbox field"

                                            name="./mycheckBox"

                                            text="My Checkbox"

                                            value="checked"/>

                                    </items>

                                </field>

                            </tabular_multifield>

1 comment:

  1. Thank for shareing the valuable information. For more information visit our website.
    Uipath Training Institute In Ameerpet

    ReplyDelete