Goal
Create a 62 Touch UI Nested Composite Multifield aka Multi-Multi Field storing the entered data as Child Nodes. Package Install contains a sample component using this multifield extension
For storing the nested multifield data as json - 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
For AEM 63 nested composite multifield using Coral 2 check this post
Demo | Package Install | Github
Nested Multifield
Stored as Child Nodes
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, 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"; 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/sample-nested-multi-field/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>
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.
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteIs anyone else seeing issues with this when you have two (or more) composite multifield side by side (i.e. not nested).
ReplyDeleteI am seeing all the node created under the one parent.
e.g.
Composite Multifield Component C1 has two text fields, C1-T1 and C1-T2.
Composite Multifield Component C2 has three text fields, C2-T,1 C2-T2, C2-T3.
If I use the dialog to create two sets of values under each Composite Multifield component save this and then look is CRXDE Lite (or re-open the dialog) instead of seeing the C1 node with child nodes of '1' and '2', and the C2 node with child nodes of '1' and '2' I am only seeing the C1 node which has child nodes of '1', '2', '3' and '4' under it.
I have fixed this by changing line #240 of the JS file from:
DeletefillValue($form, mName, $(field).find("[name]"), (oCounter + 1));
to:
fillValue($form, $(field).closest("[" + DATA_EAEM_NESTED + "]").data("name"), $(field).find("[name]"), (oCounter + 1));
This fills my purpose but someone might want to take a closer look to make this more robust...
I found perfect code here : https://aemblogger.wordpress.com/2017/03/14/aem-touchui-multifield-nested-nodes/
ReplyDeleteis anyone getting same issue as me, when i open the dialog again for editing, the value for multifield is not populated in touch UI dialog.
ReplyDeleteYes,Because of conflict between ACS commons client libs apps.acs-commons.touchui-widgets.composite-multifield and this one.
DeleteCould you find Any workaround for this problem ?
DeleteI replaced in the js
Deleteline 256: CUI.Multifields = new Class({
line 261: CUI.Widget.registry.register("multifield", CUI.Multifields);
Hello, great site!
ReplyDeleteAnyone has worked on adding the Image functionality to this nested multifield?
Hi Sreekanth
ReplyDeleteThanks for this multifield widget. I am facing issues when the outer multi-field has tags. It saves only one value as a String and doesn't pre populate the tags in dialog. I have attached dialog.xml file and also a screenshot for your reference. If you have some time can you please take a look at this. This is turning out to be a show stopper for me.
Dialog Structure https://imgur.com/UbiAopt
Have shared the component at https://drive.google.com/open?id=0B_Ps1thLZ9ziZ2hYTnp2REttTlE
Screenshot of content structure -- https://imgur.com/a/RTN7p
I tried to edit in nested-multifield.js but couldn't crack it.