Quantcast
Channel: Experiencing Adobe Experience Manager (AEM, CQ)
Viewing all articles
Browse latest Browse all 526

AEM 6 SP2 - Touch UI ( Coral UI ) Nested Mutlifield ( Multi Multifield )

$
0
0

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

Demo | Package Install


Nested Multifield





Stored as JSON in CRX





Component Rendering





Dialog in CRX






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-fieldCheck 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>



Viewing all articles
Browse latest Browse all 526

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>