ADOBE EXPERIENCE MANAGER (AEM) CONTENT FRAGMENTS
AEM Content Fragment Composite Multifield
This example demonstrates how to create a composite Coral.Multifield for Content Fragments and store the data in the JCR as JSON. This example was created and tested with Adobe Experience Manager 6.5.7.0
Using CRXDE, create the client library that will load when the Content Fragments are being authored. Right click on the clientlibs
folder for your project and select Create › Create Node.
Enter clientlib-cfm-composite-multifield
for the node name and select cq:ClientLibraryFolder
for the type.
In CRXDE, select the clientlib-cfm-composite-multifield
to open its properties in the panel on the right.
As shown in the following image, at the bottom of the Properties panel, use the fields to add a new property named categories
with value dam.cfm.authoring.contenteditor.v2
. Be sure the type is set to String and the Multi button is enabled in order to modify the type to String[]
when added.
Even though the folder is created within your project, the library category,
dam.cfm.authoring.contenteditor.v2
is global. Therefore any clientlib with this category will be loaded for any site.
I’m going to use Visual Studio Code to create the cfm-composite-multifield.js
and js.txt
clientlib files. If you prefer, you can continue in CRXDE to create these files within the clientlib-cfm-composite-multifield cq:ClientLibraryFolder
.
To follow along using VS Code, you should have the AEM repo tool installed and a tasks.json configured within your project per the repo tool Visual Studio Code integration instructions.
In your projects clientlibs
folder, e.g., myproject/ui.apps/src/main/content/jcr_root/apps/myproject/clientlibs
create a clientlib-cfm-composite-multifield
folder which will be the same clientlib cq:ClientLibraryFolder
we created in CRXDE. In this new folder, create an empty file named .content.xml
for the folder properties we will import from the JCR.
The Get File Task
This step presumes that the AEM repo tool VS Code setup is completed.
The task will run on either the active open file or the folder it is in.
With the empty .content.xml
as the active file in your VS Code editor, invoke the Command Palette, Ctrl+Shift+p and select Tasks: Run Task.
Then select get file to run the repo get
command that will import the JCR content as .content.xml
.
Your .content.xml
file should now look like this.
.content.xml
<?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:ClientLibraryFolder"
categories="[dam.cfm.authoring.contenteditor.v2]"/>
Create this JavaScript file named cfm-composite-multifield.js
in the same folder. e.g.,
cfm-composite-multifield.js
(function ($) {
var CFM = window.Dam.CFM,
COMPOSITE_ITEM_VALUE = 'data-composite-item-value',
DEFAULT_VARIATION = 'master',
MF_NAME_ATTR = 'data-granite-coral-multifield-name';
var config = {};
config.form = document.querySelector('.content-fragment-editor');
config.multiComposites = config.form.querySelectorAll('[data-granite-coral-multifield-composite]');
/**
* EXIT when composite mulltifields do not exist
**/
if (config.multiComposites.length === 0) {
return;
}
CFM.Core.registerReadyHandler(getMultifieldsContent);
extendRequestSave();
function extendRequestSave() {
var origFn = CFM.editor.Page.requestSave;
CFM.editor.Page.requestSave = requestSave;
function extend() {
var extended = {},
i = 0;
// merge the object into the extended object
function merge(obj) {
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
extended[prop] = obj[prop];
}
}
};
for (; i < arguments.length; i++) {
merge(arguments[i]);
}
return extended;
}
function requestSave(callback, options) {
origFn.call(this, callback, options);
var mfData = getMultifieldData();
if (!mfData) {
return;
}
var url = CFM.EditSession.fragment.urlBase + '.cfm.content.json',
variation = getVariation(),
createNewVersion = (options && !!options.newVersion) || false;
var data = {
':type': 'multiple',
':newVersion': createNewVersion,
'_charset_': 'utf-8'
};
if (variation !== DEFAULT_VARIATION) {
data[':variation'] = variation;
}
var request = {
url: url,
method: 'post',
dataType: 'json',
data: extend(data, mfData),
cache: false
};
CFM.RequestManager.schedule({
request: request,
type: CFM.RequestManager.REQ_BLOCKING,
condition: CFM.RequestManager.COND_EDITSESSION,
ui: (options && options.ui)
})
}
}
function getMultifieldsContent() {
$.ajax(`${CFM.EditSession.fragment.urlBase}/jcr:content/data.2.json`)
.done(loadContentIntoMultiFields);
}
function getMultifieldData() {
let value,
values,
data = {};
config.multiComposites.forEach(function (el) {
values = [];
let fields,
items = el.items.getAll();
items.forEach(function (item) {
value = {};
fields = item.content.querySelectorAll('[name]');
fields.forEach(function (field) {
if (canSkip(field)) {
return;
}
value[getNameDotSlashRemoved(field.getAttribute('name'))] = getFieldValue(field);
});
values.push(JSON.stringify(value));
});
data[getNameDotSlashRemoved((el.getAttribute(MF_NAME_ATTR)))] = values;
});
return data;
}
function loadContentIntoMultiFields(data) {
var mfValArr, mfAdd,
vData = data[getVariation()], lastItem;
if (!vData) {
return;
}
config.multiComposites.forEach(function (el) {
mfValArr = vData[getNameDotSlashRemoved((el.getAttribute(MF_NAME_ATTR)))];
if (!mfValArr) {
return;
}
mfAdd = el.querySelector('[coral-multifield-add]');
mfValArr.forEach(function (item) {
mfAdd.click();
$lastItem = $(el).find('coral-multifield-item').last();
$lastItem.attr(COMPOSITE_ITEM_VALUE, item);
Coral.commons.ready($lastItem[0], function (component) {
fillMultifieldItems(component);
});
});
});
}
function fillMultifieldItems(mfItem) {
if (mfItem == null) {
return;
}
var mfMap = mfItem.getAttribute(COMPOSITE_ITEM_VALUE);
if (!mfMap) {
return;
}
mfMap = JSON.parse(mfMap);
for (const key in mfMap) {
let field = mfItem.querySelector(`[name$='${key}']`);
setFieldValue(field, mfMap[key]);
}
}
function canSkip(field) {
switch (field.type) {
case 'checkbox':
case 'hidden':
return true;
break;
default:
return false;
}
}
function getFieldValue(field){
var value;
if (field.tagName == 'CORAL-CHECKBOX') {
value = field.checked ? field.getAttribute('value') : '';
} else {
value = field.value;
}
return value;
}
function setFieldValue(field, value) {
if (field.tagName == 'CORAL-CHECKBOX') {
field.checked = (field.getAttribute('value') == value);
} else {
field.value = value;
}
}
function getNameDotSlashRemoved(name) {
if (!name) {
return;
}
let parts = name.split('/');
return parts[parts.length-1];
}
function getVariation() {
let variation = config.form.dataset.variation;
variation = variation || DEFAULT_VARIATION;
return variation;
}
}(jQuery));
Since the
dam.cfm.authoring.contenteditor.v2
clientlib category is global, we will not want this CFM dialog modification to initiate for other sites on the AEM instance.
Update the JavaScript at the top of the cfm-composite-multifield.js
file to exit when the Content Fragment being edited is not in our projects DAM path. Given that our project is named myproject
, you would update the beginning of the client library as follows.
Note the CF_BASEPATH
constant is set to the allowed project path, e.g., /content/dam/myproject/
.
(function ($) {
var CFM = window.Dam.CFM,
CF_BASEPATH = '/content/dam/myproject/',
COMPOSITE_ITEM_VALUE = 'data-composite-item-value',
DEFAULT_VARIATION = 'master',
MF_NAME_ATTR = 'data-granite-coral-multifield-name';
var config = {};
config.form = document.querySelector('.content-fragment-editor');
config.multiComposites = config.form.querySelectorAll('[data-granite-coral-multifield-composite]');
/**
* EXIT when composite mulltifields do not exist
* or specified CF_BASEPATH doesn't match.
**/
if (config.multiComposites.length === 0 || (CF_BASEPATH.length > 0
&& config.form.dataset.fragment.indexOf(CF_BASEPATH) === -1)) {
return;
}
...
Lastly a js.txt
file is needed. Alongside our .content.xml
and cfm-composite-multifield.js
files in the clientlib-cfm-composite-multifield
folder, create a js.txt
clientlib manifest with just the relative path to the cfm-composite-multifield.js
file. Since our js
file is not in a sub folder, the path is only the filename, for example:
js.txt
cfm-composite-multifield.js
The Put Folder Task
Using the repo tool again, we’re going to put these new files into the JCR using the “put folder” task.
Note that the task will fire without a confirmation prompt. Therefore it is important that you are mindful of the file and folders you are performing a task on to be sure unintended overwrites do not occur. For reference, the integrated VS Code terminal will contain task output.
You should have one of the files we’re transferring open within the editor so “put folder” will use its path when performing the task per the repo put -f ${fileDirname}
command.
Using the Command Palette again, Ctrl+Shift+p and select Tasks: Run Task then put folder.
Verify that your files were successfully transferred in CRXDE.
Content Fragment Model
Create the “Multifield Demo” Content Fragment Model in AEM. Under Update the CFM Dialog further down, we will modify the model so it will contain a composite multifield of products and their options.
Navigate to Tools › Assets › Content Fragment Models
Open the project folder, e.g., MyProject
.
Select the Create button.
For the Title, enter Multifield Demo
Add a single line text field and enter Products
for the Label.
Set the field to Render As multifield, and enter products
for the Property Name.
In CRXDE, navigate to the multifield-demo
CFM we just created and expand it to show the properties for the Products field.
At the bottom pf the Properties panel, add a property named composite
and set its value to true
as shown below.
The Get Folder Task
Now we are ready to pull down our multifield-demo
CFM into our VS Code editor to add the composite multifields.
Like we did earlier for clientlibs
, create an empty .content.xml
to set the path. In myproject/ui.content/src/main/content/jcr_root/conf/myproject/settings/dam/cfm/models/.content.xml
, create an empty file named .content.xml
for the folder properties and content to import from the JCR.
With the new .content.xml
open and active, use the Command Pallet Ctrl+Shift+p and select Tasks: Run Task then get folder.
Update the CFM Dialog
Now we can add additional fields to the Products composite multifield.
In VS Code, open the .content.xml
for the multifield-demo
CFM. For example, myproject/ui.content/src/main/content/jcr_root/conf/myproject/settings/dam/cfm/models/multifield-demo/.content.xml
We need to update the field we added earlier in AEM with the Content Fragment Model editor. We’re going to modify the field, so it’s a container for the other fields within the composite multifield. Replace the field
node with the following XML.
multifield-demo/.content.xml
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container"
name="./products">
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<product
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldDescription="Name of Product"
fieldLabel="Product Name"
name="./product"/>
<path
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathbrowser"
fieldDescription="Select Path"
fieldLabel="Path"
name="./path"
rootPath="/content"/>
<show
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
name="./show"
text="Show?"
value="yes"/>
<type
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/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>
</items>
</column>
</items>
</field>
The Put File Task
Using the repo tool, put the updated .content.xml
file into the JCR using the “put file” task.
Open the Command Palette, Ctrl+Shift+p and select Tasks: Run Task then put file.
Verify that the file was successfully transferred in CRXDE and that the dialog composite multifield nodes exist.
Content Fragment
In AEM, navigate to Assets › Files - e.g., http://localhost:4502/assets.html/content/dam
In the DAM, preferably in a project subfolder, select the Create button.
Select Content Fragment from the Create dropdown.
In the New Content Fragment Panel, select the “Multifield Demo” Template.
Next, for Properties, Give it a Title. We’re entering test
in this example. Select the Create button again.
Open the saved fragment and Add some Products so we can validate the content structure in the JCR in the following step.
Verify the saved content fragment data in CRXDE.
The content fragment products data is stored in JSON format.