Goal
Create a Composite Multifield Component (CQ.form.MultiField) with panel field config to store data as json, in Classic UI. For Touch UI multifield check this post
Logic for AEM 60 and AEM 56 is same, i just didn't want to remove the link to 56 code
To store multi field values as child nodes (not json) check this post
AEM 60
Demo | Package Install
Bug Fixes
Reordering items overrides empty fields, Using CQ.form.Selection.PATH_PLACEHOLDER like $PATH/countries.options.json doesn't load options of a selection drop down in multi-field-panel - Demo | Package Install
LoadContent event does not fire on multifield items; Drag and drop to pathfield (in multifield) doesn't work; Widgets added in multifield in a tab other than the visible one, shrink; supporting allowBlank on widgets added in multifield - Demo | Package Install
AEM 56
Source code (not package install) and Demo
Prerequisites
If you are new to CQ
1) Read this post on how to create a sample page component
2) Here is another blog post on multifield fieldConfig customization
Create Component
1) Create component /apps/multifieldpanel/multifieldpanel with the following properties
2) Create clientlib /apps/multifieldpanel/multifieldpanel/clientlib of type cq:ClientLibraryFolder with the following properties
3) Create file /apps/multifieldpanel/multifieldpanel/clientlib/multipanel.js and add the following code. Here we are creating a panel extension and registering it as xtype mymultipanel
var MyClientLib = MyClientLib || {}; MyClientLib.MyMultiPanel = CQ.Ext.extend(CQ.Ext.Panel, { panelValue: '', constructor: function(config){ config = config || {}; MyClientLib.MyMultiPanel.superclass.constructor.call(this, config); }, initComponent: function () { MyClientLib.MyMultiPanel.superclass.initComponent.call(this); this.panelValue = new CQ.Ext.form.Hidden({ name: this.name }); this.add(this.panelValue); var dialog = this.findParentByType('dialog'); dialog.on('beforesubmit', function(){ var value = this.getValue(); if(value){ this.panelValue.setValue(value); } },this); }, getValue: function () { var pData = {}; this.items.each(function(i){ if(i.xtype == "label" || i.xtype == "hidden" || !i.hasOwnProperty("dName")){ return; } pData[i.dName] = i.getValue(); }); return $.isEmptyObject(pData) ? "" : JSON.stringify(pData); }, setValue: function (value) { this.panelValue.setValue(value); var pData = JSON.parse(value); this.items.each(function(i){ if(i.xtype == "label" || i.xtype == "hidden" || !i.hasOwnProperty("dName")){ return; } if(!pData[i.dName]){ return; } i.setValue(pData[i.dName]); }); }, validate: function(){ return true; }, getName: function(){ return this.name; } }); CQ.Ext.reg("mymultipanel", MyClientLib.MyMultiPanel);
4) Create file /apps/multifieldpanel/multifieldpanel/clientlib/js.txt and add the following code
multipanel.js
5) Create dialog /apps/multifieldpanel/multifieldpanel/dialog for component above with the following properties. Here,
a) The node /apps/multifieldpanel/multifieldpanel/dialog/items/items/tab1/items/map/fieldConfig has xtype mymultipanel
b) Each widget in the multipanel (eg. /apps/multifieldpanel/multifieldpanel/dialog/items/items/tab1/items/map/fieldConfig/items/product-year-value) should have a dName property, which is read by multi panel to store value entered by author for the field
<?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"> <items jcr:primaryType="cq:Widget" xtype="tabpanel"> <items jcr:primaryType="cq:WidgetCollection"> <tab1 jcr:primaryType="cq:Panel" title="Add"> <items jcr:primaryType="cq:WidgetCollection"> <map jcr:primaryType="cq:Widget" hideLabel="false" name="./map" title="Map" xtype="multifield"> <fieldConfig jcr:primaryType="cq:Widget" border="true" hideLabel="true" layout="form" padding="10px" width="1000" xtype="mymultipanel"> <items jcr:primaryType="cq:WidgetCollection"> <product-year-value jcr:primaryType="cq:Widget" dName="year" fieldLabel="Year" width="60" xtype="textfield"/> <product-price-value jcr:primaryType="cq:Widget" dName="price" fieldLabel="Price" width="60" xtype="textfield"/> <product-version-value jcr:primaryType="cq:Widget" dName="version" fieldLabel="Path to Version" xtype="pathfield"/> <product-lowStock-value jcr:primaryType="cq:Widget" dName="lowStock" fieldLabel="Low Stock ?" width="25" xtype="checkbox"/> </items> </fieldConfig> </map> </items> </tab1> </items> </items> </jcr:root>
6) Add the following code in /apps/multifieldpanel/multifieldpanel/multifieldpanel.jsp
<%@ page import="org.apache.sling.commons.json.JSONObject" %> <%@ page import="java.io.PrintWriter" %> <%@include file="/libs/foundation/global.jsp" %> <%@page session="false" %> <div style="display: block; border-style: solid; border-width: 1px; margin: 10px; padding: 10px"> <b>Multi Field Sample</b> <% try { Property property = null; if(currentNode.hasProperty("map")){ property = currentNode.getProperty("map"); } if (property != null) { JSONObject obj = null; Value[] values = null; if(property.isMultiple()){ values = property.getValues(); }else{ values = new Value[1]; values[0] = property.getValue(); } for (Value val : values) { obj = new JSONObject(val.getString()); %> Year : <b><%= obj.get("year") %></b>, Price : <b><%= obj.get("price") %></b>, Version : <b><%= obj.get("version")%></b>, Low Stock : <b><%=obj.get("lowStock")%></b> <% } } else { %> Add values in dialog <% } } catch (Exception e) { e.printStackTrace(new PrintWriter(out)); } %> </div>