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