AEM Cloud Service - Digital OnBoarding Process, Post Lead Form Data to a AEM React SPA

Goal

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

Process detailed below explains a sample Digital OnBoarding Flow, where a potential customer enters basic information in a Lead Entry page setup on AEM Sites. Form data can be stored in AEM or Adobe Campaign or any external system (for email outreach incase user does not continue with the process) and the user is later POST-forwarded to a Digital OnBoarding AEM React Spa App which shows the basic information entered in lead entry page and more steps for converting the lead to a customer. This post does not discuss Analytics and Campaign pieces...

Demo | Package Install | Content Package | Github


The Flow



Lead Entry Form



Digital On-Boarding SPA Authoring


Digital On-Boarding SPA

Solution

1) Create the AEM React SPA project using archetype https://github.com/adobe/aem-project-archetype

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 read Post data" 
-D appId="eaem-cs-spa-read-post-data" -D groupId="apps.experienceaem.sites.spa" 
-D frontendModule=react  -D includeExamples=n -D includeDispatcherConfig=n


2) Create the Lead Entry Form Component /apps/eaem-cs-spa-read-post-data/components/steps extending the core/wcm/components/title/v2/title component. Add additional fields in dialog for enabling them in end user form...

<?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
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Experience AEM"
                        sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
                        margin="{Boolean}true">
                        <items jcr:primaryType="nt:unstructured">
                            <column
                                jcr:primaryType="nt:unstructured"
                                jcr:title="Enable the fields for this step"
                                sling:resourceType="granite/ui/components/coral/foundation/form/fieldset">
                                <items jcr:primaryType="nt:unstructured">
                                    <name
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
                                        name="./showName"
                                        text="Show Name"
                                        value="true"/>
                                    <email
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
                                        name="./showEmail"
                                        text="Show Email"
                                        value="true"/>
                                    <ssn
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
                                        name="./showSSN"
                                        text="Show SSN"
                                        value="true"/>
                                    <company
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
                                        name="./showCompany"
                                        text="Show Company"
                                        value="true"/>
                                    <previousLink
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
                                            fieldDescription="Select the Previous step"
                                            fieldLabel="Previous step"
                                            name="./previousLink"
                                            rootPath="/content/eaem-cs-spa-read-post-data/us/en"/>
                                    <nextLink
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
                                            fieldDescription="Select the Next step"
                                            fieldLabel="Next step"
                                            name="./nextLink"
                                            rootPath="/content/eaem-cs-spa-read-post-data/us/en"/>
                                </items>
                            </column>
                        </items>
                    </eaem>
                </items>
            </tabs>
        </items>
    </content>
</jcr:root>


3) Add the HTL script for rendering the lead entry form component /apps/eaem-cs-spa-read-post-data/components/steps/steps.html

<form method="post" action="/content/eaem-cs-spa-read-post-data/us/en/home.html">
    <div>
        <h1 style="text-align: center;">Lead Form</h1>

        <div class='eaem-info'>
            <span>Enter name</span>
            <input name='eaemName'/>
        </div>

        <div class='eaem-info'>
            <span>Enter email</span>
            <input name='eaemEmail'/>
        </div>
    </div>

    <div class="eaem-info"><button type="submit">submit</button></div>
</form>


4) Since the data in Lead entry form is POSTed to SPA app (for security, so the user info is not visible in URL as with GET) hosted on url /content/eaem-cs-spa-read-post-data/us/en/home.html, add the path in CSRF filter configuration eaem-cs-spa-read-post-data\ui.config\src\main\content\jcr_root\apps\eaem-cs-spa-read-post-data\osgiconfig\config\com.adobe.granite.csrf.impl.CSRFFilter.config


5) Add a filter apps.experienceaem.sites.spa.core.filters.OnBoardingPostModifierFilter to convert the POST request to a GET request, so AEM does not strictly treat it as a POST and try to modify the node /content/eaem-cs-spa-read-post-data/us/en/home

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

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.wrappers.SlingHttpServletRequestWrapper;
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.IOException;

@Component(
        service = Filter.class,
        immediate = true,
        name = "Experience AEM convert SPA home requests from POST to GET ",
        property = {
                Constants.SERVICE_RANKING + ":Integer=-99",
                "sling.filter.scope=COMPONENT",
                "sling.filter.pattern=(/content/eaem-cs-spa-read-post-data/us/en/home*)",
        }
)
public class OnBoardingPostModifierFilter implements Filter {
    private static final Logger log = LoggerFactory.getLogger(OnBoardingPostModifierFilter.class);

    @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;
        SlingHttpServletResponse slingResponse = (SlingHttpServletResponse) response;

        try {
            if(!slingRequest.getMethod().equals("POST")){
                chain.doFilter(request, response);
                return;
            }

            slingResponse.setHeader("Dispatcher", "no-cache");

            RequestDispatcher dp = request.getRequestDispatcher(slingRequest.getRequestPathInfo().getResourcePath() + ".html");

            dp.include(new GetSlingServletRequestWrapper(slingRequest), response);
        } catch (Exception e) {
            log.error("Error converting POST to GET of SPA home : " + slingRequest.getRequestURI());
        }
    }

    @Override
    public void destroy() {
    }

    private class GetSlingServletRequestWrapper extends SlingHttpServletRequestWrapper {
        public GetSlingServletRequestWrapper(final SlingHttpServletRequest request) {
            super(request);
        }

        public String getMethod() {
            return "GET";
        }
    }
}


6) For the SPA App to read POSTed form data from a JS object added in window.eaemInitialData create a model apps.experienceaem.sites.spa.core.models.PostParamsModel

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

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.Model;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.inject.Inject;

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

    @Inject
    SlingHttpServletRequest request;

    private String eaemInitialData;

    @PostConstruct
    protected void init() {
        String eaemName = request.getParameter("eaemName");
        String eaemEmail = request.getParameter("eaemEmail");

        JSONObject jsonObject = new JSONObject();

        try{
            jsonObject.put("eaemName", eaemName);
            jsonObject.put("eaemEmail", eaemEmail);
        }catch (Exception e){
            log.error("Error creating eaemInitialData from request",e);
        }

        eaemInitialData = jsonObject.toString();
    }

    /**
     * @return brand
     */
    public String getEaemInitialData() {
        return eaemInitialData;
    }
}


7) Initialize window.eaemInitialData object with POSTed form data in SPA App root page eaem-cs-spa-read-post-data\ui.apps\src\main\content\jcr_root\apps\eaem-cs-spa-read-post-data\components\page\body.html

<noscript>You need to enable JavaScript to run this app.</noscript>
<script data-sly-use.model="apps.experienceaem.sites.spa.core.models.PostParamsModel" data-sly-test="${model.eaemInitialData}">
    window.eaemInitialData = ${model.eaemInitialData @ context='unsafe'};
</script>
<div id="spa-root"></div>


9) Crate a sling model exporter apps.experienceaem.sites.spa.core.models.EAEMGenericComponentSlingExporter for the Steps component...

package apps.experienceaem.sites.spa.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-cs-spa-read-post-data/components/image",
                "eaem-cs-spa-read-post-data/components/steps"
        }
)
@Exporter(
        name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
        extensions = ExporterConstants.SLING_MODEL_EXTENSION
)
public class EAEMGenericComponentSlingExporter implements ComponentExporter {

    @Inject
    private Resource resource;

    @PostConstruct
    protected void initModel() {
    }

    public ValueMap getEaemData(){
        return resource.getValueMap();
    }

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


8) Create the React render script eaem-cs-spa-read-post-data\ui.frontend\src\components\Steps\Steps.tsx for component /apps/eaem-cs-spa-read-post-data/components/steps. It reads the data entered in lead form entry from window.eaemInitialData

import { MapTo } from "@adobe/aem-react-editable-components";
import React, { FC, useState, useEffect } from "react";
import {Link} from "react-router-dom";
import "./StepsStyles.css";

type StepsProps = {
    [x: string]: any
};

declare global {
    interface Window {
        eaemInitialData: any;
    }
  }

const StepsEditConfig = {
    emptyLabel: "Steps - Experience AEM",

    isEmpty: function (props: any) {
        return !props || !props["jcr:title"];
    }
};

const AEMSteps: FC<StepsProps> = props => {
    const eaemInitialData = window.eaemInitialData;

    return (
        <div>
            <h1 style={{ textAlign: "center", color: "maroon" }}>
                {props["jcr:title"]}
            </h1>

            {
                props.showName &&
                <div className='eaem-info'>
                    <span>Enter name</span>
                    <input name='eaemName' value={eaemInitialData.eaemName}></input>
                </div>
            }
            {
                props.showEmail &&
                <div className='eaem-info'>
                    <span>Enter email</span>
                    <input name='eaemEmail' value={eaemInitialData.eaemEmail}></input>
                </div>
            }
            {
                props.showSSN &&
                <div className='eaem-info'>
                    <span>Enter SSN</span>
                    <input name='eaemSSN' value={eaemInitialData.eaemSSN}></input>
                </div>
            }
            {
                props.showCompany &&
                <div className='eaem-info'>
                    <span>Enter company</span>
                    <input name='eaemCompany' value={eaemInitialData.eaemCompany}></input>
                </div>
            }

            <div className='eaem-info'>
                {
                    props.previousLink &&                  
                    <Link to={props.previousLink}>
                        <button type="button">Previous</button>
                    </Link>
                }
                {
                    props.nextLink &&                  
                    <Link to={props.nextLink}>
                        <button type="button">Next</button>
                    </Link>
                }
            </div>
        </div>
    );
};

export default MapTo("eaem-cs-spa-read-post-data/components/steps")(AEMSteps, StepsEditConfig);



No comments:

Post a Comment