Saturday, July 21, 2018

AEM 62 Touch UI Nested Multi Multi Composite Multifield storing data as Child Nodes

AEM 62 Touch UI Nested Multi Multi Composite Multifield storing data as Child Nodes


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



visit link download