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 CreateCreate Node.

Enter clientlib-cfm-composite-multifield for the node name and select cq:ClientLibraryFolder for the type.

CRXDE dialog: create cq:ClientLibraryFolder node named cfm-composite-multifield

CRXDE Create Node dialog - cfm-composite-multifield with type of cq:ClientLibraryFolder

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.

CRXDE: add categories property to the cq:ClientLibraryFolder node named cfm-composite-multifield

CRXDE properties panel for the cq:ClientLibraryFolder folder, cfm-composite-multifield

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.

Visual Studio Code run custom task on cfm-composite-multifield/.content.xml

Run VS Code task on cfm-composite-multifield/.content.xml

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.

assets.html/content/dam folders in AEM's Touch UI interface

DAM Folders

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.

CRXDE cfm-composite-multifield cq:ClientLibraryFolder expanded to show contents

CRXDE cfm-composite-multifield cq:ClientLibraryFolder and files

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

Content Fragment Model Editor - Multifield Demo

CFM Editor - Products field set to Render As multifield

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.

CRXDE multifield-demo Content Fragment Model field properties

CRXDE multifield-demo CFM field properties

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.

CRXDE multifield-demo content fragment model dialog composite multifield nodes

CRXDE multifield-demo CFM dialog composite multifield nodes

Content Fragment

In AEM, navigate to AssetsFiles - 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.

Adding Products to the Content Fragment in AEM

Verify the saved content fragment data in CRXDE.

CRXDE content fragment properties

CRXDE /content/dam/myproject/test/jcr:content/data/master products

The content fragment products data is stored in JSON format.


Source Code

comments powered by Disqus