AEM Cloud Service - React SPA Runtime Include Pages (Fragments) by Merging Models

Goal

AEM Cloud Service 2021.10.5940.20211013T233531Z-210900

This post is on creating AEM React SPA Html fragments and including them in multiple SPA Pages. Experience Fragments (XF) is one way to author reusable Html blocks, however XF editor does not support React rendering and the goal here is to reuse render scripts written in React. By merging the models server side, App gets a unified model (replacing page includes) when a request is made to model json eg. https://author-p10961-e90064.adobeaemcloud.com/content/eaem-spa-page-include/us/en.model.json

Demo | Package Install | Github


Page Include


Model JSON



Solution

1) Create a component /apps/eaem-spa-page-include/components/spa-include with dialog field pagePath for selecting the page to be included...

2) Add the React render script ui.frontend\src\components\SPAInclude\index.tsx. This script is just a placeholder to give user instructions and also required by AEM SPA editor..

import {FC} from "react";
import loadable from "@loadable/component";
import {MappedComponentProperties, MapTo} from "@adobe/aem-react-editable-components";
import React from "react";

const SPAIncludeConfig = {
    emptyLabel: "SPA Include Component",

    isEmpty: function (props: any) {
        return !props || !props.pagePath;
    }
};

type SPAIncludeProps = MappedComponentProperties & {
    pagePath?: string | "";
};

const SPAIncludeLazy = loadable(() => import('./SPAIncludeLazy'), {fallback: <></>});

const AEMSPAInclude: FC<SPAIncludeProps> = props => {
    return (
        <SPAIncludeLazy {...props} />
    );
};

export default MapTo('eaem-spa-page-include/components/spa-include')(AEMSPAInclude, SPAIncludeConfig);


3) For code splitting (lazy loading) add the main component code in ui.frontend\src\components\SPAInclude\SPAIncludeLazy.tsx

import {FC} from "react";
import * as React from "react";

type SPAIncludeProps = {
    pagePath ?: string;
};

const SPAIncludeLazy: FC<SPAIncludeProps> = props => {
    const styles : React.CSSProperties = {
        padding: "40px",
        textAlign: "center"
    }

    let html = <div style={ styles }>Select the page path in dialog</div>

    if (props.pagePath) {
        html = <div style={ styles }>Browser refresh the page...</div>
    }

    return html;
};

export default SPAIncludeLazy;


4) Add a filter apps.experienceaem.sites.core.filters.SPAPageIncludeFilter to merge the model of included page in main model json

package apps.experienceaem.sites.core.filters;

import org.apache.jackrabbit.oak.spi.security.authentication.JaasLoginContext;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.wrappers.SlingHttpServletResponseWrapper;
import org.apache.sling.commons.json.JSONObject;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Iterator;

@Component(
        service = Filter.class,
        immediate = true,
        name = "Experience AEM - SPA Page Include Filter",
        property = {
                Constants.SERVICE_RANKING + ":Integer=-99",
                "sling.filter.scope=COMPONENT",
                "sling.filter.pattern=.*.model.json"
        }
)
public class SPAPageIncludeFilter implements Filter {
    private static Logger log = LoggerFactory.getLogger(SPAPageIncludeFilter.class);

    private static final String MODEL_INCLUDE_COMPONENT = "eaem-spa-page-include/components/spa-include";

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

    @Override
    public void destroy() {
    }

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

            String uri = slingRequest.getRequestURI();

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

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

            chain.doFilter(slingRequest, modelResponseWrapper);

            PrintWriter responseWriter = response.getWriter();

            String modelResponse = modelResponseWrapper.toString();

            try{
                modelResponse = lookThroughModelForSPAIncludes(new JSONObject(modelResponse), slingRequest,
                                        (SlingHttpServletResponse) response).toString();
            }catch(Exception me){
                log.warn("Error replacing SPA Page includes", me);
            }

            responseWriter.write(modelResponse);
        } catch (ServletException e) {
            throw new ServletException("Error including spa page models", e);
        }
    }

    private Object lookThroughModelForSPAIncludes(JSONObject jsonObject, SlingHttpServletRequest slingRequest,
                                         SlingHttpServletResponse slingResponse) 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(":type")) {
                String typeValue = jsonObject.getString(key);

                if(typeValue.equals(MODEL_INCLUDE_COMPONENT) && jsonObject.has("pagePath")){
                    return getModelOfIncludedPage(jsonObject.getString("pagePath"), slingRequest, slingResponse);
                }else{
                    modJSONObj.put(key, typeValue);
                }
            } else {
                jsonValue = jsonObject.get(key);

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

        return modJSONObj;
    }

    private JSONObject getModelOfIncludedPage(String includePageModelPath, SlingHttpServletRequest slingRequest,
                                              SlingHttpServletResponse slingResponse) throws Exception {
        RequestDispatcher dp = slingRequest.getRequestDispatcher(includePageModelPath + "/jcr:content/root/responsivegrid.model.json");

        SlingHttpServletResponse wrapperResponse = new DefaultSlingModelResponseWrapper(slingResponse);

        dp.include(slingRequest, wrapperResponse);

        String includedPageJSON = wrapperResponse.toString();

        return new JSONObject(includedPageJSON);
    }


    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();
        }
    }
}


No comments:

Post a Comment