Goal
Create a Touch UI Nested multifield (or Multi multifield). The nested one is configured with two form fields, a Textfield (granite/ui/components/foundation/form/textfield), Pathbrowser (granite/ui/components/foundation/form/pathbrowser )
Remember, it's always recommended to minimize the number of UI extensions in projects, given a choice, a better (or simple) design of the dialog i guess would be without a nested multifield
A Composite Touch UI multifield is available here
For AEM 62 Nested Multifield storing data as Child Nodes check this post
Demo | Package Install
Nested Multifield
Component Rendering
Solution
1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create a folder (nt:folder) /apps/touch-ui-nested-multi-field-panel
2) Create a component (cq:Component) /apps/touch-ui-nested-multi-field-panel/sample-nested-multi-field. Check this post on how to create a CQ component
3) Add the following xml for /apps/touch-ui-nested-multi-field-panel/sample-nested-multi-field/dialog created. Ideally a cq:Dialog should be configured with necessary widgets for displaying dialog in Classic UI. This post is on Touch UI so keep it simple (node required for opening the touch ui dialog created in next step)
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" jcr:primaryType="cq:Dialog" title="Multi Field" xtype="dialog"/>
4) Create a nt:unstructured node /apps/touch-ui-nested-multi-field-panel/sample-nested-multi-field/cq:dialog with following xml (the dialog UI as xml)
<?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="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>
5) Line 45 in above dialog xml marks this multifield as nested by setting a no value flag eaem-nested
6) Different layouts can be used to structure the multifield, here we use granite/ui/components/foundation/layouts/fixedcolumns layout
7) Create a clientlib (cq:ClientLibraryFolder) /apps/touch-ui-nested-multi-field-panel/clientlib with categories property as String with value cq.authoring.dialog and dependencies as String[] with value underscore
8) Create file (nt:file) /apps/touch-ui-nested-multi-field-panel/clientlib/js.txt with
nested-multifield.js
9) Create file (nt:file) /apps/touch-ui-nested-multi-field-panel/clientlib/nested-multifield.js with following code
(function () { var DATA_EAEM_NESTED = "data-eaem-nested"; var CFFW = ".coral-Form-fieldwrapper"; //reads multifield data from server, creates the nested composite multifields and fills them var addDataInFields = function () { $(document).on("dialog-ready", function() { 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"); var actionUrl = $form.attr("action") + ".json"; var postProcess = function(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 var fillNestedFields = function($multifield, valueArr){ _.each(valueArr, function(record, index){ $multifield.find(".js-coral-Multifield-add").click(); //a setTimeout may be needed _.each(record, function(value, key){ var $field = $($multifield.find("[name='./" + key + "']")[index]); $field.val(value); }) }) }; $.ajax(actionUrl).done(postProcess); }); }; var fillValue = function($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 var getRecordFromMultiField = function($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 var collectDataFromFields = function(){ $(document).on("click", ".cq-dialog-submit", function () { var $form = $(this).closest("form.foundation-form"); var mName = $("[" + DATA_EAEM_NESTED + "]").data("name"); var $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); })();
10) The function collectDataFromFields() registers a click listener on dialog submit, to collect the multifield form data, create a json to group it and add in a hidden field before data is POSTed to CRX. The function addDataInFields() reads json data added previously and fills the multifield & nested multifield fields, when dialog is opened. Extension uses simple jquery & underscore js calls to get and set data; based on the widget type more code may be necessary for supporting complex Granite UI widgets (granite/ui/components/foundation/form)
11) Create the component jsp /apps/touch-ui-nested-multi-field-panel/sample-nested-multi-field/sample-nested-multi-field.jsp for rendering data entered in dialog
<%@ page import="org.apache.sling.commons.json.JSONObject" %> <%@ page import="java.io.PrintWriter" %> <%@ page import="org.apache.sling.commons.json.JSONArray" %> <%@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 { Property property = null; if (currentNode.hasProperty("countries")) { property = currentNode.getProperty("countries"); } if (property != null) { JSONObject country = null, state = null; Value[] values = null; if (property.isMultiple()) { values = property.getValues(); } else { values = new Value[1]; values[0] = property.getValue(); } for (Value val : values) { country = new JSONObject(val.getString()); %> Country : <b><%= country.get("country") %></b> <% if (country.has("states")) { JSONArray states = (JSONArray) country.get("states"); if (states != null) { for (int index = 0, length = states.length(); index < length; index++) { state = (JSONObject) states.get(index); %> <div style="padding-left: 25px"> <a href="<%= state.get("path") %>.html" target="_blank"> <%= state.get("state") %> - <%= state.get("path") %> </a> </div> <% } } } } } else { %> Add values in dialog <% } } catch (Exception e) { e.printStackTrace(new PrintWriter(out)); } %> </div>
Thanks Sreekanth , ain't you just awesome
ReplyDeleteHi Sreekanth, I am using both nested multifield and composite multifield components and they are behaving very weirdly together. I am getting Uncaught RangeError: Maximum call stack size exceeded (too much recursion).
ReplyDelete_addListeners: function () {
this.superClass._addListeners.call(this);
The dialogs are storing the values in the node but then at times aren't retrieving the values back when you reopen them. Any suggestions around how to get rid of the problem?
hi sahil, can you paste the dialog configuration here....
DeleteThis comment has been removed by the author.
DeleteThis comment has been removed by the author.
DeleteHey Sreekanth,
DeleteThanks, I tried uploading the file twice but i was unable to. The values of the dialog isn't being retained when you reopen it. The values do persists though.
I have attached it here http://temp-host.com/download.php?file=wi23zk
Cheers
Missing "./" in name property; so iconPath should be ./iconPath, omnitureButtonName shuld be ./omnitureButtonName etc
DeleteHi srikanth,
DeleteI want this for classic ui
Raghav, its the same as composite multifield (http://experience-aem.blogspot.com/2013/09/aem-cq-56-multifield-panel.html). Anyway i've added a post with sample dialog configuration etc. - http://experience-aem.blogspot.com/2015/06/aem-61-classic-ui-nested-composite-multifield-panel.html
Deletethanks a lot. :)
DeleteHey Sreekanth, That did resolve the issue. Thanks a lot.
ReplyDeleteHey Sreekanth,
ReplyDeleteI'm seeing an error thrown "CUI is not defined" at this point in the js file : CUI.Multifield = new Class({....
Any idea as to how this might be resolved?
Thanks
mah8473, what is the aem version? is the category of clientlib cq.authoring.dialog
DeleteSreekanth, I was on AEM 6 sp2, but have just upgraded to 6.1. The CUI error was evident in both versions. Yes the clientlib category is cq.authoring.dialog. I was curious if I might be missing a dependency? I note there is a dependency for underscore.
ReplyDeletebtw: This is actually the same dialog mentioned by sahil thadhani above. I've inherited this from him.
Thanks
Mark
Mark, can you upload the package and send url?
Deletehttps://www.dropbox.com/s/t86nrb4hgacc2z5/mypost.ui.apps-1.0-SNAPSHOT.zip?dl=0
Deletehttps://www.dropbox.com/s/hq8paziizm14vn6/mypost.ui.content-1.0-SNAPSHOT.zip?dl=0
Sreekanth, were those packages OK?
DeleteMark, installed packages, but the content page with dashboardSection component rendered with exceptions (i guess because of some missing jars).. havent explored further, can you create a package with just the component and added on geometrixx english page, resend it
DeleteSreehanth, apologies, i forgot the dashboard page used a property iterator in my core package
Deletehere are the additional jars you will need
https://www.dropbox.com/s/dq14zcr7a1ygao0/core.ui.apps-1.0-SNAPSHOT.zip?dl=0
https://www.dropbox.com/s/66roxnfuyx91i95/core.ui.content-1.0-SNAPSHOT.zip?dl=0
There is no jar in the packages attached, throwing - Cannot find class au.com.auspost.mypost.core.sightly.PropertyIterator
DeleteAlso i would suggest adding just the multifield component on geometrixx page and try
Sreenkanth, I've narrowed it done further. it looks like the category cq.authoring.dialog injects these in the wrong order
Delete/libs/cq/gui/components/common/tagspicker/clientlibs.js
etc/clientlibs/granite/coralui2keys.js
I'm going to raise a daycare ticket.
Hi.
ReplyDeleteIt's working. but if we add any fields (.ie textfield) right after nest-multifield, then the layout will break. :(
i.e in your example, I'm adding 1 more textfield right after countries (which is nested one). the layout after countries will be broken.
Hi, thank you for reporting, i see the issue.... workaround is to make sure the nested multifield is last widget of dialog, working on a fix
DeleteHi Sreekanth,
DeleteIs there a fix for the above issue. I have a similar issue where there are two multifields in a nested multifield. The fields after the first multified appear outside the parent multi-field.
Here is the crx package of the component: https://www.dropbox.com/sh/x8twgl4cx66l77w/AABlEaeya2--CNSujPaofJQSa?dl=0
Hi Sreekanth
DeleteIs there a fix for this issue?
I am unable to add elements after a nested multifield in a dialog?
Hi Sreekanth
DeleteIs there a fix for this issue?
I am unable to add elements after a nested multifield in a dialog?
Hi Sreekanth, I am using composite multifield component. I am getting Uncaught RangeError: Maximum call stack size exceeded (too much recursion).
ReplyDelete_addListeners: function () {
this.superClass._addListeners.call(this);
Can you please give me in this issue?
hi Syam, check the comments above (you might be hitting the same issue).. check if "./" is missing in name property
DeleteThis comment has been removed by the author.
ReplyDeleteHi Sreekanth
ReplyDeleteIt seems that it doesn't work on 6.1 , I tested your package on 6.1 and it throws Maximum call stack size exceeded on clientlib.js ,any update on that ?
/Parisa
Parisa, i remember testing the widget on 61; make sure you have "./" in name property.. check above comments
DeleteHi Sreekanth,
Delete./ is present on all name property, even then i am getting Maximum call stack size exceeded error, please advise
Hi,
ReplyDeleteI created a multifield as above. But I have a problem: The Multifield's fields can't set required. I checked source code and saw you removed the multifield's fields out form. So The Granite Validation don't see that fields and can't validate for that fields.
Could you please help me resolve that problem?
Thanks
This comment has been removed by the author.
ReplyDeleteHi Sreekanth,
ReplyDeleteI have a checkbox in the child multifield. The value is not changed on edit and is always persisted as default value.
Tried to update js to include your changes in https://github.com/schoudry/acs-aem-commons/commit/ac91c67707d0b2c5ae6230706ea02f1f4bdce891, but couldn't resolve it.
Could you please help with that?
Thanks!
Hi,
DeleteI am also having the same issue? If you were able to resolve it, please let me know how. Thanks.
Hi Sreekanth,
ReplyDeleteWhat are the changes required if I want to have different versions of nested-multifield.js in different components? Since the categories is same i.e. cq.authoring.dialog for all, they override the behavior of each other. To create two different versions, we changed the resourceType in component as "granite/ui/components/foundation/form/newmultifield" and changed the register logic in nested-multifield.js i.e.
CUI.NewMultifield = 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("newmultifield", CUI.NewMultifield);
})();
Do we need to do something else as well? This doesnt give any js error but doesnt show the multifield as well.
Hi Shelly Goel,
DeleteHave you got solution for your issue.Please let me know as I m also facing similar issue.
Hi Sreekanth,
ReplyDeleteI have modified the script little bit to fix issue "multifield blank entries added when dialog-ready trigger explicitly".
Changes description:
a) Remove 'js-coral-Multifield-add' click trigger from fillNestedFields and invoke before checking which field is active.
if(_.isArray(rValue) && !_.isEmpty(rValue)){
var $multifield = $($fieldSets[i]).find("[data-init='multifield']"),
//find active tab rValue after striping ./
activeKey = $multifield.closest('section[data-eaem-nested]').attr('data-name').substring(2);
/*Add the multifield only for active tab */
if(rKey == activeKey){
$multifield.find(".js-coral-Multifield-add").click();
}
fillNestedFields( $multifield, rValue);
}
2) Add $(document).trigger('dialog-ready'); (in cq.authoring.dialog scenario have analyzed that dialog-ready is not triggering at first attempt), Hope this will solve Shally problem as well.
you can give me file nested-multifield.js.
DeleteI integrated, it isn't work.
Hi Sreekanth,
ReplyDeleteIn case of touch UI, any solution for When Datepicker is a field in a multifield, the date is not loaded while editing again?
Hi, this was fixed in ACS commons - check https://github.com/Adobe-Consulting-Services/acs-aem-commons/pull/674/files
DeleteHi Sreekanth,
ReplyDeleteI'm facing also the problem that it is not working in 6.1 throwing the error clientlibs.js:177 Uncaught RangeError: Maximum call stack size exceeded. I use the example you provided, so there are ./ properties fields in there.
Can you help me out, really need a nested multifield solution
Hi Sreekanth,
ReplyDeleteMy requirement is to have 3 level nested multifield. I can see that the layout is breaking for dialog when 3rd level multifield is added. Also This scriipt is not working for 3 level multifield. Can you please help me out. It is a mandatory requirement for me.
Thanks
Hi Sreekanth,
ReplyDeleteI'm facing also the problem that it is not working in 6.1 throwing the error Uncaught RangeError: Maximum call stack size exceeded.
I use the example you provided, the ./ is in the name properties. Please advice what else i need to check on.
Hi Wayne/Sreekanth,
ReplyDeleteCan you please provide an update for Uncaught RangeError: Maximum call stack size exceeded. We are already using ./ in name properties.
Thanks,
Vijender
Hi.This nested-multifield.js stores the data as json.I want to store data as child node.How can i do that?
ReplyDelete@Wayne Pang, @Ishita Gandhi
ReplyDeleteUncaught RangeError: Maximum call stack size exceeded?
You are facing this error because
1. this.superClass._addListeners.call(this); goes for infinite loop call. The reason is this.superClass never gives Superclass of CUI.Multifield which is in CoralUI js but instead it is giving its same CUI.Multifield which is in custom multifield js.
2. Not sure what causing this issue. Becuase a fresh installtion of AEM 6.1 is not facing problem mentioned in point 1.
3. Install a fresh instance, and try applying this custom multifield package.
A solution to this is to create the child with a different name other than CUI.Multifield. I used CUI.Multifields and was successful.
DeleteHi Sreekanth,
ReplyDeleteI have customized siteadmin with new columns using https://docs.adobe.com/docs/en/cq/5-6-1/developing/customize_siteadmin.html
I observed roll out option is disabled for blueprint and live copy configured pages after customizing siteadmin.Could you please help me.
Can anyone please paste the .js file for the Sreekanth example which is not giving Uncaught RangeError: Maximum call stack size exceeded? error
ReplyDeleteHi Sreekanth,
ReplyDeleteThis solution is working fine for me in a dialog having only one tab. But once I add one more tab in my dialog, then the elements in the second tab is not showing at all or getting disappeared.
Do you know this problem?. Thanks
https://aemblogger.wordpress.com/2017/03/14/aem-touchui-multifield-nested-nodes/
ReplyDeletebest solution to implement multifield and save values in nested nodes
Hello,
ReplyDeleteThank you for all the valuable information shared in this blog, it has been very useful!
Does anyones has worked in adding the Image functionality in this nested multifield?
cq:dialog structure should be same as given or it will work for any cq:dialog with nested multifield????
ReplyDeleteI have created dialog with two column and in second column i need to add one multifield and inside that multifield i have put child multifield(nested).
This will work for that also?