AEM 6550 - SPA Editor Refresh Component (ReRender) on Dialog Update

Goal

Refresh & Render the SPA component with updated model data when a user adds new content in dialog (the component does not have a sling model exporter, so product REFRESH with call .model.json returns 404)...

Other ways to solve this is...

               1) by adding a filter server side, checking for incoming .model.json requests for specific components with no sling model exporter and return .json selector content...

               2) solution 2 below, adds a generic sling model exporter for component resource types, fetching node content as JSON (same as .json selector output)

Demo | Package Install | Github




Solution

1) Updating the component is a 2 step process...

                                            a) Trigger a custom event from the SPA editor with latest dialog data...

                                            b)  Listen to the event in component render script and update...


Trigger Custom Event

1) Add a client library to extend editable action EditableActions.REFRESH and pass the updated model in a custom event eaem-spa-component-refresh-event. Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-sites-spa-how-to-react/clientlibs


2) Create node /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-extensions of type cq:ClientLibraryFolder, add String[] property categories with value [cq.authoring.editor], String[] property dependencies with value lodash.

3) Create file (nt:file) /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-extensions/js.txt, add

                        refresh-component.js

4) Create file (nt:file) /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-extensions/refresh-component.js, add the following code

(function($, $document){
    var EAEM_COMPONENTS = "eaem-sites-spa-how-to-react/",
        EAEM_SPA_COMP_REFRESH_EVENT = "eaem-spa-component-refresh-event";

    $document.on("cq-editables-loaded", overrideSPAImageCompRefresh);

    function overrideSPAImageCompRefresh(){
        var _origExec = Granite.author.edit.EditableActions.REFRESH.execute;

        Granite.author.edit.EditableActions.REFRESH.execute = function(editable, config){
            if(editable.type.startsWith(EAEM_COMPONENTS)){
                $.ajax(editable.slingPath).done(function(compData){
                    sendComponentRefreshEvent(editable, compData);
                });
            }

            return _origExec.call(this, editable, config);
        };
    }

    function sendComponentRefreshEvent(editable, compData){
        let event = new CustomEvent(EAEM_SPA_COMP_REFRESH_EVENT, {
            detail: {
                type: editable.type,
                path: editable.path,
                slingPath: editable.slingPath,
                data: compData
            }
        });

        window.dispatchEvent(event);
    }
}(jQuery, jQuery(document)));



Add Custom Event Listener

5) In the React render script add a listener in componentDidMount() function to check if the event is for this component, update this.props with  latest model data and call this.forceUpdate() to update the component display (without refreshing page...)

import { MapTo } from '@adobe/cq-react-editable-components';
import DOMPurify from 'dompurify';
import React, { Component } from 'react';
import {Link} from "react-router-dom";

const ImageEditConfig = {
    emptyLabel: 'Image - Experience AEM',

    isEmpty: function (props) {
        return (!props || !props.fileReference || (props.fileReference.trim().length < 1));
    }
};

class Image extends Component {
    componentDidMount() {
        //todo check for wcmmode
        window.parent.addEventListener("eaem-spa-component-refresh-event", (event => {
            if( !event.detail || (event.detail.type !== this.props.cqType)){
                return;
            }

            Object.assign(this.props, event.detail.data);

            this.forceUpdate();
        }).bind(this));
    }

    get imageHTML() {
        const imgStyles = {
            "display": 'block',
            "margin-left": 'auto',
            "margin-right": 'auto'
        };

        return (
            <div>
                <Link to={this.props.imageLink}>
                    <img src={this.props.fileReference} style={imgStyles}/>
                </Link>
            </div>
        );
    }

    render() {
        return this.imageHTML;
    }
}

export default MapTo('eaem-sites-spa-how-to-react/components/image')(Image,ImageEditConfig);

Solution 2


Another way to achieve component-refresh-on-edit, is by adding a Generic Sling Exporter for components that do not need a specific model exporter...

1) Add a Generic Sling Exporter com.eaem.core.models.EAEMGenericComponentSlingExporter adding all Resource Types it should handle and return plain properties in the JSON....

package com.eaem.core.models;

import com.adobe.cq.export.json.ComponentExporter;
import com.adobe.cq.export.json.ExporterConstants;
import com.adobe.cq.wcm.core.components.models.Image;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;

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

@Model(
        adaptables = {SlingHttpServletRequest.class},
        adapters = {ComponentExporter.class},
        resourceType = {
                "eaem-sites-spa-how-to-react/components/image",
                "eaem-sites-spa-how-to-react/components/component-1",
                "eaem-sites-spa-how-to-react/components/component-2"
        }
)
@Exporter(
        name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
        extensions = ExporterConstants.SLING_MODEL_EXTENSION
)
public class EAEMGenericComponentSlingExporter implements ComponentExporter {
    private static Logger log = LoggerFactory.getLogger(EAEMGenericComponentSlingExporter.class);

    @Inject
    private Resource resource;

    @PostConstruct
    protected void initModel() {
    }

    public ValueMap getEaemData(){
        log.debug("EAEM Generic sling exporter handling - " + resource.getPath());

        Map<String, Object> rhMap = new LinkedHashMap<String, Object>();

        try{
            rhMap = getComponentDataMap(resource);

            addChildComponentDataMap(resource, rhMap);
        }catch(Exception e){
            log.error("Error getting component data in generic model exporter for resource - " + resource.getPath(),  e);
        }

        return rhMap;
    }

    private void addChildComponentDataMap(Resource resource, Map<String, Object> rhMap){
        Iterator<Resource> itr = resource.listChildren();
        Resource child = null;

        Map<String, Object> childMap;

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

            childMap = getComponentDataMap(child);

            rhMap.put(child.getName(), childMap);

            addChildComponentDataMap(child, childMap);
        }
    }

    private Map<String, Object> getComponentDataMap(Resource resource){
        ValueMap vm = resource.getValueMap();
        Map<String, Object> rhMap = new LinkedHashMap<String, Object>();

        for(Map.Entry<String, Object> entry : vm.entrySet()) {
            String key = entry.getKey();

            if(key.startsWith("jcr:") || key.startsWith("sling")){
                continue;
            }

            rhMap.put(key, entry.getValue());
        }

        return rhMap;
    }

    @Override
    public String getExportedType() {
        return resource.getResourceType();
    }
}

2) Adding the above model returns JSON Object with eaemData as key, which the SPA components do NOT understand....




3) To remove the eaemData key add a filter com.eaem.core.models.EAEMDefaultModelJSONFilter...

package com.eaem.core.models;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.wrappers.SlingHttpServletResponseWrapper;
import org.json.JSONObject;
import org.osgi.service.component.annotations.Component;
import org.osgi.framework.Constants;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import java.io.*;
import java.util.Iterator;

@Component(
        service = Filter.class,
        immediate = true,
        name = "Experience AEM Default Sling Model Response Modifier Servlet Filter",
        property = {
                Constants.SERVICE_RANKING + ":Integer=-99",
                "sling.filter.scope=COMPONENT",
                "sling.filter.pattern=.*.model.json"
        }
)
public class EAEMDefaultModelJSONFilter implements Filter {
    private static Logger log = LoggerFactory.getLogger(EAEMDefaultModelJSONFilter.class);

    public static String EAEM_DATA = "eaemData";

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        SlingHttpServletRequest slingRequest = (SlingHttpServletRequest)request;

        String uri = slingRequest.getRequestURI();

        if(!uri.endsWith(".model.json")){
            chain.doFilter(request, response);
            return;
        }

        SlingHttpServletResponse modelResponse = new DefaultSlingModelResponseWrapper((SlingHttpServletResponse)response);

        chain.doFilter(slingRequest, modelResponse);

        PrintWriter responseWriter = response.getWriter();

        responseWriter.write(getModifiedContent(modelResponse.toString()));
    }

    private String getModifiedContent(String origContent){
        String modifiedContent = origContent;

        try{
            JSONObject model = new JSONObject(origContent);

            model = (JSONObject) replaceEaemDataObject(model);

            modifiedContent = model.toString();
        }catch(Exception e){
            log.error("Error modifying model JSON content", e);
            modifiedContent = origContent;
        }

        return modifiedContent;
    }

    private Object replaceEaemDataObject(JSONObject jsonObject) throws Exception{
        Iterator<String> itr = jsonObject.keys();
        String key;

        JSONObject modJSONObj = new JSONObject();
        Object jsonValue = null;

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

            if(key.equals(EAEM_DATA)){
                JSONObject eaemData = (JSONObject)jsonObject.get(EAEM_DATA);

                eaemData.put(":type" , jsonObject.get(":type"));

                return eaemData;
            }else{
                jsonValue = jsonObject.get(key);

                if(JSONObject.class.isInstance(jsonValue)){
                    modJSONObj.put(key, replaceEaemDataObject((JSONObject)jsonValue));
                }else{
                    modJSONObj.put(key, jsonValue);
                }
            }
        }

        return modJSONObj;
    }

    @Override
    public void destroy() {
    }

    private class DefaultSlingModelResponseWrapper extends SlingHttpServletResponseWrapper {
        private CharArrayWriter writer;

        public DefaultSlingModelResponseWrapper (final SlingHttpServletResponse response) {
            super(response);
            writer = new CharArrayWriter();
        }

        public PrintWriter getWriter() throws IOException {
            return new PrintWriter(writer);
        }

        public String toString() {
            return writer.toString();
        }
    }
}

4) Adding the above filter removes eaemData key and merges the properties into parent object (format the SPA component rendering expects...)




2 comments:

  1. so the live reload part when you update an specific component is working fine,
    but when i refresh the page and i get the full model for the entire page the eaemSata is not being flatted and is not rendering correctly my component, are you facing that issue too?

    ReplyDelete
    Replies
    1. looks like there is some crazy bug in bundle "org.json" when there are nested components (throws a duplicate key exception, when there is an exception, original content is returned), had to switch to org.apache.sling.commons.json

      Delete