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>
Hi Srikanth,
ReplyDeleteOn 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
Hi Srikanth,
ReplyDeleteThe 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.
i am not able to add country field ...please suggest
ReplyDeleteThe issue is replicating after re authoring the dialog to expand the Super multifield. It is expanding the submultifield instead of super multifield.
ReplyDeleteBro if u have that code send me kalyankmr901@gmail.com
ReplyDelete