AEM Cloud Service - Content Fragment RTE Plugin for Dynamic Variables

Goal

Adobe Experience Manager 2021.6.5586.20210628T210726Z-210600 (June 28, 2021)

Create a Content Fragment RTE Plugin (RichTextEditor) for Dynamic Variables. Variable is resolved with a value, when the CF is added on a page, fetched from Page Properties. As an example consider Credit Cards and Interest Rates. Interest Rate content is added in AEM as a Content Fragment and the actual interest rate is replaced with value when the CF is added on a specific Card page...

Demo | Package Install | CF Model | Github


Add Dynamic Variable


CF with Dynamic Variables


Dynamic Variable Unresolved


Dynamic Variable Value entered in Page Properties


Dynamic Variable Resolved with Value



Solution

1) Add the plugin /apps/eaem-cs-cf-rte-dyn-var/cfm-dyn-var-plugin/clientlib with categories=[dam.cfm.authoring.contenteditor.v2, eaem-cfm.rte.plugin] and dependencies=eaem.lodash. Add the plugin logic JS file /apps/eaem-cs-cf-rte-dyn-var/cfm-dyn-var-plugin/clientlib/dyn-var-plugin.js with following code...

(function ($, $document) {
    var EAEM_PLUGIN_ID = "eaem-dyn-var",
        EAEM_TEXT_DYN_VAR_FEATURE = "eaemDynVar",
        EAEM_DYN_VAR_ICON = EAEM_PLUGIN_ID + "#" + EAEM_TEXT_DYN_VAR_FEATURE,
        CANCEL_CSS = "[data-foundation-wizard-control-action='cancel']",
        DYN_VAR_SELECTOR_URL = "/apps/eaem-cs-cf-rte-dyn-var/cfm-dyn-var-plugin/dyn-var-selector.html",
        SENDER = "experience-aem", REQUESTER = "requester", $eaemDynVarPicker,
        url = document.location.pathname;

    if( (url.indexOf("/editor.html") == 0)
            || ( url.indexOf("/mnt/overlay/dam/cfm/admin/content/v2/fragment-editor.html") == 0) ){
        extendStyledTextEditor();
        registerPlugin();
    }else if(url.indexOf(DYN_VAR_SELECTOR_URL) == 0){
        handlePicker();
    }

    function handlePicker(){
        $document.on("click", CANCEL_CSS, sendCancelMessage);

        $document.submit(sendSelectedVars);
    }

    function sendSelectedVars(){
        var message = {
            sender: SENDER,
            action: "submit",
            data: {}
        }, $form = $("form"), $field;

        _.each($form.find("[name^='./']"), function(field){
            if(!field.checked || (field.tagName !== "CORAL-CHECKBOX")){
                return;
            }

            $field = $(field);
            message.data[$field.attr("name").substr(2)] = $field.val();
        });

        parent.postMessage(JSON.stringify(message), "*");
    }

    function sendCancelMessage(){
        var message = {
            sender: SENDER,
            action: "cancel"
        };

        getParent().postMessage(JSON.stringify(message), "*");
    }

    function getParent() {
        if (window.opener) {
            return window.opener;
        }

        return parent;
    }

    function closePicker(event){
        event = event.originalEvent || {};

        if (_.isEmpty(event.data)) {
            return;
        }

        var message, action;

        try{
            message = JSON.parse(event.data);
        }catch(err){
            return;
        }

        if (!message || message.sender !== SENDER) {
            return;
        }

        action = message.action;

        if(action === "submit"){
            $eaemDynVarPicker.eaemFontPlugin.editorKernel.execCmd(EAEM_TEXT_DYN_VAR_FEATURE, message.data);
        }

        var modal = $eaemDynVarPicker.data('modal');
        modal.hide();
        modal.$element.remove();
    }

    function extendStyledTextEditor(){
        var origFn = Dam.CFM.StyledTextEditor.prototype._start;

        Dam.CFM.StyledTextEditor.prototype._start = function(){
            addDynVarPluginSettings(this);
            origFn.call(this);
        }
    }

    function addDynVarPluginSettings(editor){
        var config = editor.$editable.data("config");

        config.rtePlugins[EAEM_PLUGIN_ID] = {
            features: "*"
        };

        config.uiSettings.cui.multieditorFullscreen.toolbar.push(EAEM_DYN_VAR_ICON);
        config.uiSettings.cui.inline.toolbar.push(EAEM_DYN_VAR_ICON);
    }

    function registerPlugin(){
        var EAEM_CFM_DYN_VAR_PLUGIN = new Class({
            toString: "eaemCFMDynVarPlugin",

            extend: CUI.rte.plugins.Plugin,

            textFontUI:  null,

            getFeatures: function () {
                return [ EAEM_TEXT_DYN_VAR_FEATURE ];
            },

            notifyPluginConfig: function (pluginConfig) {
                var defaults = {
                    tooltips: {}
                };

                defaults.tooltips[EAEM_TEXT_DYN_VAR_FEATURE] = {
                    title: "Select Dynamic Variable..."
                };

                CUI.rte.Utils.applyDefaults(pluginConfig, defaults);

                this.config = pluginConfig;
            },

            initializeUI: function (tbGenerator) {
                if (!this.isFeatureEnabled(EAEM_TEXT_DYN_VAR_FEATURE)) {
                    return;
                }

                this.textFontUI = new tbGenerator.createElement(EAEM_TEXT_DYN_VAR_FEATURE, this, false,
                                        this.config.tooltips[EAEM_TEXT_DYN_VAR_FEATURE]);

                tbGenerator.addElement(EAEM_TEXT_DYN_VAR_FEATURE, 999, this.textFontUI, 999);

                if (tbGenerator.registerIcon) {
                    tbGenerator.registerIcon(EAEM_DYN_VAR_ICON, "brackets");
                }

                $(window).off('message', closePicker).on('message', closePicker);
            },

            isValidSelection: function(){
                var winSel = window.getSelection();
                return winSel && winSel.rangeCount == 1 && winSel.getRangeAt(0).toString().length > 0;
            },

            execute: function (pluginCommand, value, envOptions) {
                if (pluginCommand != EAEM_TEXT_DYN_VAR_FEATURE) {
                    return;
                }

                this.showFontModal(this.getPickerIFrameUrl());
            },

            showFontModal: function(url){
                var self = this, $iframe = $('<iframe>'),
                    $modal = $('<div>').addClass('eaem-cfm-font-size coral-Modal');

                $iframe.attr('src', url).appendTo($modal);

                $modal.appendTo('body').modal({
                    type: 'default',
                    buttons: [],
                    visible: true
                });

                $eaemDynVarPicker = $modal;

                $eaemDynVarPicker.eaemFontPlugin = self;

                $modal.nextAll(".coral-Modal-backdrop").addClass("cfm-coral2-backdrop");
            },

            getPickerIFrameUrl: function(){
                return Granite.HTTP.externalize(DYN_VAR_SELECTOR_URL) + "?" + REQUESTER + "=" + SENDER;
            }
        });

        var EAEM_CFM_DYN_VAR_CMD = new Class({
            toString: "eaemDynVarCmd",

            extend: CUI.rte.commands.Command,

            isCommand: function (cmdStr) {
                return (cmdStr.toLowerCase() == EAEM_TEXT_DYN_VAR_FEATURE);
            },

            getProcessingOptions: function () {
                var cmd = CUI.rte.commands.Command;
                return cmd.PO_SELECTION | cmd.PO_BOOKMARK | cmd.PO_NODELIST;
            },

            execute: function (execDef) {
                execDef.value = Object.values(execDef.value).join(" ");

                CUI.rte.commands.InsertHtml().execute(execDef);
            },

            queryState: function(selectionDef, cmd) {
                return false;
            }
        });

        CUI.rte.plugins.PluginRegistry.register(EAEM_PLUGIN_ID, EAEM_CFM_DYN_VAR_PLUGIN);

        CUI.rte.commands.CommandRegistry.register(EAEM_TEXT_DYN_VAR_FEATURE, EAEM_CFM_DYN_VAR_CMD);
    }
}(jQuery, jQuery(document)));


2) Create the plugin modal page /apps/eaem-cs-cf-rte-dyn-var/cfm-dyn-var-plugin/dyn-var-selector with Dynamic Variables configuration...

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/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="cq:Page">
    <jcr:content
        jcr:mixinTypes="[sling:VanityPath]"
        jcr:primaryType="nt:unstructured"
        jcr:title="Dyn Variable Selector"
        sling:resourceType="granite/ui/components/coral/foundation/page">
        <head jcr:primaryType="nt:unstructured">
            <favicon
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/page/favicon"/>
            <viewport
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/admin/page/viewport"/>
            <clientlibs
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/includeclientlibs"
                categories="[coralui3,granite.ui.coral.foundation,granite.ui.shell,dam.gui.admin.coral, eaem-cfm.rte.plugin]"/>
        </head>
        <body
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/coral/foundation/page/body">
            <items jcr:primaryType="nt:unstructured">
                <form
                    jcr:primaryType="nt:unstructured"
                    sling:resourceType="granite/ui/components/coral/foundation/form"
                    class="foundation-form content-container"
                    maximized="{Boolean}true"
                    style="vertical">
                    <items jcr:primaryType="nt:unstructured">
                        <wizard
                            jcr:primaryType="nt:unstructured"
                            jcr:title="Select the Dynamic Variable..."
                            sling:resourceType="granite/ui/components/coral/foundation/wizard">
                            <items jcr:primaryType="nt:unstructured">
                                <text
                                    jcr:primaryType="nt:unstructured"
                                    jcr:title="Select the Dynamic Variable..."
                                    sling:resourceType="granite/ui/components/coral/foundation/container">
                                    <items jcr:primaryType="nt:unstructured">
                                        <accordion
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/coral/foundation/accordion"
                                            margin="{Boolean}true"
                                            variant="quiet">
                                            <items jcr:primaryType="nt:unstructured">
                                                <fees
                                                    jcr:primaryType="nt:unstructured"
                                                    jcr:title="Fees"
                                                    sling:resourceType="granite/ui/components/coral/foundation/container">
                                                    <items jcr:primaryType="nt:unstructured">
                                                        <membershipFee
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
                                                            name="./membershipFee"
                                                            text="\{{membershipFee}}"
                                                            value="\{{membershipFee}}"/>
                                                        <balanceTransferMinFee
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
                                                            name="./balanceTransferMinFee"
                                                            text="\{{balanceTransferMinFee}}"
                                                            value="\{{balanceTransferMinFee}}"/>
                                                        <cashAdvanceMinFee
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
                                                            name="./cashAdvanceMinFee"
                                                            text="\{{cashAdvanceMinFee}}"
                                                            value="\{{cashAdvanceMinFee}}"/>
                                                    </items>
                                                </fees>
                                            </items>
                                        </accordion>
                                    </items>
                                    <parentConfig jcr:primaryType="nt:unstructured">
                                        <prev
                                            granite:class="foundation-wizard-control"
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/coral/foundation/anchorbutton"
                                            text="Cancel">
                                            <granite:data
                                                jcr:primaryType="nt:unstructured"
                                                foundation-wizard-control-action="cancel"/>
                                        </prev>
                                        <next
                                            granite:class="foundation-wizard-control"
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/coral/foundation/button"
                                            disabled="{Boolean}true"
                                            text="Insert"
                                            type="submit"
                                            variant="primary">
                                            <granite:data
                                                jcr:primaryType="nt:unstructured"
                                                foundation-wizard-control-action="next"/>
                                        </next>
                                    </parentConfig>
                                </text>
                            </items>
                        </wizard>
                    </items>
                </form>
            </items>
        </body>
    </jcr:content>
</jcr:root>


3) Add a simple Static Template Component in project for testing purposes /apps/eaem-cs-cf-rte-dyn-var/components/basic-htl-page-component

<div  style="margin: 10px 25px 10px 25px">
    <h2 style="text-align: center">Experience AEM CF Dynamic Variables Demo</h2>
    <div data-sly-resource="${'content' @ resourceType='wcm/foundation/components/parsys'}"></div>
</div>


4) Add necessary properties for entering Dynamic Variable values in Page Dialog configuration /apps/eaem-cs-cf-rte-dyn-var/components/basic-htl-page-component/cq:dialog

<?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">
    <content jcr:primaryType="nt:unstructured">
        <items jcr:primaryType="nt:unstructured">
            <tabs jcr:primaryType="nt:unstructured">
                <items jcr:primaryType="nt:unstructured">
                    <eaem
                            cq:showOnCreate="{Boolean}true"
                            jcr:primaryType="nt:unstructured"
                            jcr:title="Experience AEM"
                            sling:orderBefore="socialmedia"
                            sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns">
                        <items jcr:primaryType="nt:unstructured">
                            <column
                                    jcr:primaryType="nt:unstructured"
                                    sling:resourceType="granite/ui/components/coral/foundation/container">
                                <items jcr:primaryType="nt:unstructured">
                                    <accordion
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/coral/foundation/accordion"
                                            margin="{Boolean}true"
                                            variant="quiet">
                                        <items jcr:primaryType="nt:unstructured">
                                            <fees
                                                    jcr:primaryType="nt:unstructured"
                                                    jcr:title="Fees"
                                                    sling:resourceType="granite/ui/components/coral/foundation/container">
                                                <items jcr:primaryType="nt:unstructured">
                                                    <membershipFee
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                                            name="./dynVarmembershipFee"
                                                            fieldLabel="\{{membershipFee}}"/>
                                                    <balanceTransferMinFee
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                                            name="./dynVarbalanceTransferMinFee"
                                                            fieldLabel="\{{balanceTransferMinFee}}"/>
                                                    <cashAdvanceMinFee
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                                            name="./dynVarcashAdvanceMinFee"
                                                            fieldLabel="\{{cashAdvanceMinFee}}"/>
                                                    <foreignTransactionFee
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                                            name="./dynVarforeignTransactionFee"
                                                            fieldLabel="\{{foreignTransactionFee}}"/>
                                                </items>
                                            </fees>
                                        </items>
                                    </accordion>
                                </items>
                            </column>
                        </items>
                    </eaem>
                </items>
            </tabs>
        </items>
    </content>
</jcr:root>


5) Add a Content Fragment Component for selecting the content fragment, resolving Dynamic Variables and rendering the html /apps/eaem-cs-cf-rte-dyn-var/components/dyn-vars-cf

<div style="width: 100%; border: 1px solid; padding: 20px"
     data-sly-use.model="apps.experienceaem.assets.core.models.DynVarsCFModel"
     data-sly-test="${model.modalData}">
        <div style="color: red">
                ${model.modalData.eaemHeader}
        </div>
        <div style="margin-top: 10px">
                ${model.modalData.eaemContent  @context='html'}
        </div>
</div>
<div style="width: 100%; height: 30px; margin-top: 30px"
     data-sly-use.model="apps.experienceaem.assets.core.models.DynVarsCFModel" data-sly-test="${!model.cfSelectedFrom && wcmmode.edit}">
        Content Fragment not configured
</div>


6) Add a sling model apps.experienceaem.assets.core.models.DynVarsCFModel for resolving the Dynamic Variables with Values from Page Properties

package apps.experienceaem.assets.core.models;

import com.adobe.cq.dam.cfm.ContentElement;
import com.adobe.cq.dam.cfm.ContentFragment;
import com.adobe.cq.dam.cfm.FragmentData;
import com.day.cq.wcm.api.Page;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Optional;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.util.*;

@Model(
        adaptables = {SlingHttpServletRequest.class}
)
public class DynVarsCFModel {
    private static Logger log = LoggerFactory.getLogger(DynVarsCFModel.class);

    @Inject
    SlingHttpServletRequest request;

    @Inject
    Page currentPage;

    @ValueMapValue
    @Optional
    private String fragmentPath;

    @ValueMapValue
    @Optional
    private String cfSelectedFrom;

    private String variation;
    private Map<String,Object> modalData = new HashMap<String, Object>();

    @PostConstruct
    protected void init() {
        SlingHttpServletRequest slingRequest = (SlingHttpServletRequest)request;
        ResourceResolver resolver = slingRequest.getResourceResolver();

        Resource cfResource = null;
        variation = slingRequest.getParameter("variation");

        if(StringUtils.isEmpty(variation)){
            variation = "master";
        }

        if("URL".equals(cfSelectedFrom)){
            cfResource = slingRequest.getRequestPathInfo().getSuffixResource();
        }else if(StringUtils.isNotEmpty(fragmentPath)){
            cfResource = resolver.getResource(fragmentPath);
        }

        if(cfResource == null){
            return;
        }

        modalData = getCFData(cfResource.adaptTo(ContentFragment.class), resolver, currentPage.getProperties());
    }

    private Map<String,Object> getCFData(ContentFragment cf, ResourceResolver resolver, ValueMap pageProps){
        Map<String,Object> cfData = new HashMap<String, Object>();

        Iterator<ContentElement> cfElementsItr = cf.getElements();

        while(cfElementsItr.hasNext()){
            ContentElement cfElement = cfElementsItr.next();

            if(cfElement == null ){
                continue;
            }

            Object fragValue = getVariationValue(cfElement, variation).getValue();

            if(fragValue == null){
                continue;
            }else if(isMultiCF(cfElement)){
                List<Object> multis = new ArrayList<Object>();

                for(String linkPath : (String[])fragValue){
                    multis.add(getCFData(resolver.getResource(linkPath).adaptTo(ContentFragment.class), resolver, pageProps));
                }

                cfData.put(cfElement.getName(), multis);
            }else{
                cfData.put(cfElement.getName(), replaceDynVars(String.valueOf(fragValue), pageProps));
            }
        }

        return cfData;
    }

    private String replaceDynVars(String fragValue, ValueMap pageProps){
        Iterator<String> itr = pageProps.keySet().iterator();
        String key;

        while(itr.hasNext()){
            key = itr.next();

            if(!key.startsWith("dynVar")){
                continue;
            }

            fragValue = fragValue.replace("{{" + key.substring("dynVar".length()) + "}}", String.valueOf(pageProps.get(key)));
        }

        return fragValue;
    }

    private boolean isMultiCF(ContentElement cfElement){
        return cfElement.getValue().getDataType().isMultiValue();
    }

    public FragmentData getVariationValue(ContentElement cfElement, String variationName){
        if(StringUtils.isEmpty(variationName) || "master".equals(variationName)){
            return cfElement.getValue();
        }

        return cfElement.getVariation(variation).getValue();
    }

    public String getCfSelectedFrom() {
        return cfSelectedFrom;
    }

    public Map<String,Object> getModalData(){
        return modalData;
    }
}


7) Add Design for configuring the Parsys component Allowed Components in /apps/settings/wcm/designs/experience-aem (In Cloud Services for static templates design always comes from configuration in /apps; there is no Design option like in AEM 65 or CS SDK to configure allowed components and stored in for eg. /libs/settings/wcm/designs/default/jcr:content/basic-htl-page-component)

<?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="cq:Page">
    <jcr:content
        cq:lastModified="{Date}2017-07-20T15:17:11.670+01:00"
        cq:lastModifiedBy="admin"
        jcr:primaryType="nt:unstructured"
        jcr:title="Experience AEM Design"
        sling:resourceType="wcm/core/components/designer">
        <basic-htl-page-component jcr:primaryType="nt:unstructured">
            <content
                jcr:lastModified="{Date}2021-06-30T16:00:11.535-05:00"
                jcr:lastModifiedBy="admin"
                jcr:primaryType="nt:unstructured"
                sling:resourceType="wcm/foundation/components/parsys"
                components="[/apps/eaem-cs-cf-rte-dyn-var/components/dyn-vars-cf]">
                <section jcr:primaryType="nt:unstructured"/>
            </content>
        </basic-htl-page-component>
    </jcr:content>
</jcr:root>


8) Select the design /apps/settings/wcm/designs/experience-aem in Page Properties > Advanced


No comments:

Post a Comment