AEM Cloud Service - Extend React SPA Sling Model for Page Style System support

Goal

AEM Cloud Version : 2021.5.5257.20210505T101930Z-210429 (May 05, 2021)

Extend the JSON model eg. /content/eaem-cs-spa-style-system/us/en.model.json for supporting Page Style System in a React SPA. The following extension adds page level css classes configured via Style System in the model's children cssClassNames property eg.spa page basicpage eaempage--background-aero and necessary logic on the react side to apply it...  

Demo | Package Install | Content Package | Github


Style System in Template Policy


Styles on Page


Style CSS in Model



Solution

1) Create the project using following maven archetype command

mvn -B archetype:generate -D archetypeGroupId=com.adobe.aem -D archetypeArtifactId=aem-project-archetype 
-D archetypeVersion=24 -D aemVersion=cloud -D appTitle="Experience AEM SPA Style System" -D appId="eaem-cs-spa-style-system" 
-D groupId="apps.experienceaem.sites.spa" -D frontendModule=react  -D includeExamples=n -D includeDispatcherConfig=n


2) Create the CSS file for Page Style System classes /apps/eaem-cs-spa-style-system/clientlibs/clientlib-base/main.css with the following code, add it in /apps/eaem-cs-spa-style-system/clientlibs/clientlib-base/css.txt

.eaempage--background-gray{
    background-color: #f1f1f1;
}

.eaempage--background-white{
    background-color: #ffffff;
}

.eaempage--background-black{
    background-color: #000000;
}

.eaempage--background-beige{
    background-color: #EEE1C6;
}

.eaempage--background-aero{
    background-color: #CAF1DE;
}


3) Add a filter apps.experienceaem.sites.spa.core.filters.EAEMDefaultModelJSONFilter for intercepting model.json requests, iterate child pages and add the configured style classes in property cssClassNames 

package apps.experienceaem.sites.spa.core.filters;

import com.day.cq.search.PredicateGroup;
import com.day.cq.search.Query;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.Hit;
import com.day.cq.search.result.SearchResult;
import com.day.cq.wcm.api.policies.ContentPolicy;
import com.day.cq.wcm.api.policies.ContentPolicyManager;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
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.api.wrappers.SlingHttpServletResponseWrapper;
import org.apache.sling.commons.json.JSONArray;
import org.json.JSONObject;
import org.osgi.service.component.annotations.Component;
import org.osgi.framework.Constants;

import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Session;
import javax.servlet.*;
import java.io.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

@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";
    private static final String SLING_VANITYPATH = "sling:vanityPath";
    private static final String CQ_STYLE_IDS = "cq:styleIds";
    private static final String SLING_VANITYPATH_JSON_PROP = "slingVanityPath";
    private static final String CSS_CLASS_NAMES = "cssClassNames";
    private static final String CHILDREN = ":children";

    @Reference
    private QueryBuilder builder;

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

    private String getModifiedContent(String origContent, SlingHttpServletRequest slingRequest){
        String modifiedContent = origContent;

        try{
            JSONObject model = new JSONObject(origContent);

            addAddnPropertiesInPageModel(model, slingRequest);

            model = (JSONObject) replaceEaemDataObject(model);

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

        return modifiedContent;
    }

    private void addAddnPropertiesInPageModel(JSONObject model, SlingHttpServletRequest slingRequest) throws Exception{
        if(!model.has(CHILDREN)){
            return;
        }

        JSONObject childrenModel = model.getJSONObject(CHILDREN);
        Iterator<String> childrenItr = childrenModel.keys();
        ResourceResolver resolver = slingRequest.getResourceResolver();
        Resource pageContent;

        while(childrenItr.hasNext()) {
            String pagePath = childrenItr.next();
            JSONObject childData = childrenModel.getJSONObject(pagePath);

            pageContent = resolver.getResource(pagePath + "/jcr:content");

            if(pageContent == null){
                continue;
            }

            ValueMap vm = pageContent.getValueMap();

            String[] slingVanityPaths = vm.get(SLING_VANITYPATH, String[].class);

            if(ArrayUtils.isNotEmpty(slingVanityPaths)){
                JSONArray vanityPaths = new JSONArray();

                Arrays.stream(slingVanityPaths).forEach(vanityPaths::put);

                childData.put(SLING_VANITYPATH_JSON_PROP, vanityPaths);
            }

            if(!childData.has(CSS_CLASS_NAMES)){
                continue;
            }

            String styles = childData.getString(CSS_CLASS_NAMES);
            String addnClasses = getCssClasses(resolver, pagePath, vm.get(CQ_STYLE_IDS, String[].class));

            if(!styles.contains(addnClasses)){
                childData.put(CSS_CLASS_NAMES, styles + " " + addnClasses);
            }
        }
    }

    private String getCssClasses(ResourceResolver resolver, String pagePath, String[] styleIds) throws Exception{
        ContentPolicyManager policyManager = resolver.adaptTo(ContentPolicyManager.class);

        Resource contentPolicyResource = getContentPolicyResource(resolver, resolver.getResource(pagePath));

        if( (contentPolicyResource == null) || ArrayUtils.isEmpty(styleIds)){
            return "";
        }

        String styleClasses = "";

        for(String styleId : styleIds){
            Query query = builder.createQuery(PredicateGroup.create(getStyleQueryPredicateMap(contentPolicyResource.getPath(), styleId)),
                    resolver.adaptTo(Session.class));

            SearchResult result = query.getResult();

            for (Hit hit : result.getHits()) {
                styleClasses = styleClasses + hit.getProperties().get("cq:styleClasses") + " ";
            }

        }

        return styleClasses.trim();
    }

    private Resource getContentPolicyResource(ResourceResolver resolver, Resource pageRes) {
        ContentPolicyManager policyManager = resolver.adaptTo(ContentPolicyManager.class);

        if (policyManager == null) {
            return null;
        }

        ContentPolicy policy = policyManager.getPolicy(pageRes);

        if (policy == null) {
            return null;
        }

        return policy.adaptTo(Resource.class);
    }

    private static Map<String, String> getStyleQueryPredicateMap(String stylePath, String styleId) {
        Map<String, String> map = new HashMap<>();
        map.put("path", stylePath);
        map.put("1_property","cq:styleId");
        map.put("1_property.value",styleId);
        map.put("p.hits","selective");
        map.put("p.properties","cq:styleClasses");

        return map;
    }

    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) Apply the cssClassNames (line #8) in eaem-cs-spa-style-system\ui.frontend\src\App.js

import { Page, withModel } from '@adobe/cq-react-editable-components';
import React from 'react';

// This component is the application entry point
class App extends Page {
  render() {
    return (
      <div className={this.props.cssClassNames}>
        {this.childComponents}
        {this.childPages}
      </div>
    );
  }
}

export default withModel(App);


No comments:

Post a Comment