Goal
Create a React SPA Positioning Container for Component Overlays. The container provides authoring dialog interface for adding backgrounds with color, image, video etc.. adding opacity, inner components alignment, positioning, width, height and various other features for creating a free form nested component layout....
Thank De Shauné Elder for design reference https://www.awesomescreenshot.com/video/832666?key=18f8d1f8822aa95ee43ce4c0b20bb837
Demo | Package Install | Github
Container Editing
Preview
Solution
mvn -B archetype:generate -D archetypeGroupId=com.adobe.granite.archetypes -D archetypeArtifactId=aem-project-archetype -D archetypeVersion=23 -D aemVersion=6.5.0 -D appTitle="Experience AEM SPA React" -D appId="eaem-sites-spa-how-to-react" -D groupId="com.eaem" -D frontendModule=react -D includeExamples=n -D includeErrorHandler=n -D includeDispatcherConfig=n
3) Create component /apps/eaem-sites-spa-how-to-react/components/positioning-container extending core/wcm/components/container/v1/container. Add the component formatting options in tabs Background, Format and Colors
4) Create the custom widget for opacity used in dialog - /apps/eaem-sites-spa-how-to-react/sites/extensions/slider
5) Add the following logic in file /apps/eaem-sites-spa-how-to-react/sites/extensions/slider/slider.jsp for opacity...
<%@include file="/libs/granite/ui/global.jsp" %> <%@page session="false" import="org.apache.commons.lang3.StringUtils, com.adobe.granite.ui.components.AttrBuilder, com.adobe.granite.ui.components.Config, com.adobe.granite.ui.components.Field, com.adobe.granite.ui.components.Tag" %> <%@ page import="org.apache.sling.api.SlingHttpServletRequest" %> <% Config cfg = cmp.getConfig(); SlingHttpServletRequest thisRequest = slingRequest; Resource dialog = thisRequest.getResourceResolver().getResource(thisRequest.getRequestPathInfo().getSuffix()); ValueMap vm = slingRequest.getResource().getValueMap(); String name = cfg.get("name", String.class); String sliderValue = dialog.getValueMap().get(name, "50"); Tag tag = cmp.consumeTag(); AttrBuilder attrs = tag.getAttrs(); cmp.populateCommonAttrs(attrs); attrs.add("name", name); attrs.add("value", sliderValue); attrs.add("min", cfg.get("min", Double.class)); attrs.add("max", cfg.get("max", Double.class)); attrs.add("step", cfg.get("step", String.class)); String fieldLabel = cfg.get("fieldLabel", String.class); String fieldDesc = cfg.get("fieldDescription", String.class); %> <div class="coral-Form-fieldwrapper"> <label class="coral-Form-fieldlabel"><%=fieldLabel%></label> <coral-slider style="width:100%; margin: 0" <%= attrs.build() %>></coral-slider> <coral-icon class="coral-Form-fieldinfo" icon="infoCircle" size="S"></coral-icon> <coral-tooltip target="_prev" placement="left" class="coral3-Tooltip" variant="info" role="tooltip" style="display: none;"> <coral-tooltip-content><%=fieldDesc%></coral-tooltip-content> </coral-tooltip> </div> <div class="eaem-dialog-slider"> <span><%=sliderValue%>%</span> </div>
6) Create the custom widget for content alignment used in dialog - /apps/eaem-sites-spa-how-to-react/sites/extensions/alignment
7) Add the following logic in file /apps/eaem-sites-spa-how-to-react/sites/extensions/alignment/alignment.jsp for alignment...
<%@include file="/libs/granite/ui/global.jsp" %> <%@page session="false" import="org.apache.commons.lang3.StringUtils, com.adobe.granite.ui.components.AttrBuilder, com.adobe.granite.ui.components.Config, com.adobe.granite.ui.components.Field, com.adobe.granite.ui.components.Tag" %> <%@ page import="org.apache.sling.api.SlingHttpServletRequest" %> <% Config cfg = cmp.getConfig(); SlingHttpServletRequest thisRequest = slingRequest; Resource dialog = thisRequest.getResourceResolver().getResource(thisRequest.getRequestPathInfo().getSuffix()); String name = cfg.get("name", String.class); ValueMap vm = dialog.getValueMap(); String value = vm.get(name, "Center"); String fieldLabel = cfg.get("fieldLabel", String.class); String fieldDesc = cfg.get("fieldDescription", String.class); %> <div class="coral-Form-fieldwrapper"> <label class="coral-Form-fieldlabel"><%=fieldLabel%></label> <div class="eaem-dialog-content-align"> <input type="hidden" name="<%=name%>" value="<%=value%>"/> <div>Center</div> <coral-icon icon="chevronUp" size="M" data-content-align="Top"></coral-icon> <coral-tooltip target="_prev" variant="info" role="tooltip" style="display: none;" placement="top">Top</coral-tooltip> <coral-icon icon="chevronDown" size="M" data-content-align="Bottom"></coral-icon> <coral-tooltip target="_prev" variant="info" role="tooltip" style="display: none;" placement="top">Bottom</coral-tooltip> <coral-icon icon="chevronDoubleLeft" size="M" data-content-align="Extreme Left"></coral-icon> <coral-tooltip target="_prev" variant="info" role="tooltip" style="display: none;" placement="top">Extreme Left</coral-tooltip> <coral-icon icon="chevronLeft" size="M" data-content-align="Left"></coral-icon> <coral-tooltip target="_prev" variant="info" role="tooltip" style="display: none;" placement="top">Left</coral-tooltip> <coral-icon icon="chevronRight" size="M" data-content-align="Right"></coral-icon> <coral-tooltip target="_prev" variant="info" role="tooltip" style="display: none;" placement="top">Right</coral-tooltip> <coral-icon icon="chevronDoubleRight" size="M" data-content-align="Extreme Right"></coral-icon> <coral-tooltip target="_prev" variant="info" role="tooltip" style="display: none;" placement="top">Extreme Right</coral-tooltip> <coral-icon icon="chevronUpDown" size="M" data-content-align="Center"></coral-icon> <coral-tooltip target="_prev" variant="info" role="tooltip" style="display: none;" placement="top">Center</coral-tooltip> </div> <coral-icon class="coral-Form-fieldinfo" icon="infoCircle" size="S"></coral-icon> <coral-tooltip target="_prev" placement="left" variant="info" role="tooltip" style="display: none;"> <coral-tooltip-content><%=fieldDesc%></coral-tooltip-content> </coral-tooltip> </div>
8) Create a client library /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-extensions with categories = [cq.authoring.editor] and dependencies lodash for the custom widgets clientside execution...
9) Create clientlib js file /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-extensions/js.txt with the following entry
positioning-container-dialog.js
10) Add the js logic in /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-extensions/positioning-container-dialog.js
(function($, $document){ var DIALOG_SLIDER = ".eaem-dialog-slider", DIALOG_CONTENT_ALIGN = ".eaem-dialog-content-align", DIALOG_FIELD_SELECTED = "eaem-dialog-content-selected"; $document.on("dialog-ready", initPositioningContainerDialog); function initPositioningContainerDialog(){ addSliderListener(); addContentAlignmentListener(); } function addSliderListener(){ var $sliders = $(DIALOG_SLIDER); $sliders.each(function(){ var $sliderValue = $(this), $slider = $sliderValue.prev(".coral-Form-fieldwrapper").find("coral-slider"); if(_.isEmpty($slider)){ return; } $slider.on("change", function(){ $sliderValue.html($(this).val() + "%"); }); }); } function addContentAlignmentListener(){ var $contentAlignContainer = $(DIALOG_CONTENT_ALIGN), $contentAlignDisplay = $contentAlignContainer.find("div"), $contentAlign = $("[name='./contentAlignment']"); addInitialPositions(); $contentAlignContainer.find("coral-icon").click(function(){ $(this).toggleClass(DIALOG_FIELD_SELECTED); calculatePositioning(); }); function addInitialPositions(){ var alignments = $contentAlign.val(); $contentAlignDisplay.html(alignments); _.each(alignments.split(","), function(alignment){ var $icon = $contentAlignContainer.find("[data-content-align='" + alignment.trim() + "']"); $icon.addClass(DIALOG_FIELD_SELECTED); }) } function calculatePositioning(){ var $alignIcons = $contentAlignContainer.find("coral-icon." + DIALOG_FIELD_SELECTED), position = ""; $alignIcons.each(function(){ position = position + $(this).data("content-align") + ", "; }); if(position.includes(",")){ position = position.substring(0, position.lastIndexOf(",")); } position = position.trim(); if(!position){ position = "Center"; } $contentAlignDisplay.html(position); $contentAlign.val(position); } } }(jQuery, jQuery(document)));
11) For the data required by SPA component interface add sling model com.eaem.core.models.EAEMPositioningContainerModelImpl with the following code, returning dialog data as plain JSON...
package com.eaem.core.models; import com.adobe.cq.export.json.ComponentExporter; import com.adobe.cq.export.json.ContainerExporter; import com.day.cq.wcm.foundation.model.responsivegrid.ResponsiveGrid; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonSerialize; 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.ScriptVariable; import javax.annotation.PostConstruct; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; @Model( adaptables = {SlingHttpServletRequest.class}, adapters = {ContainerExporter.class, ComponentExporter.class}, resourceType = {"eaem-sites-spa-how-to-react/components/positioning-container"} ) @Exporter( name = "jackson", extensions = {"json"} ) @JsonSerialize(as = EAEMPositioningContainerModel.class) public class EAEMPositioningContainerModelImpl extends ResponsiveGrid implements EAEMPositioningContainerModel{ @ScriptVariable private Resource resource; @PostConstruct protected void initModel() { super.initModel(); } public Map<String, Object> getBackgroundProps(){ Map<String, Object> backgroundDivProps = new LinkedHashMap<String, Object>(); ValueMap vm = resource.getValueMap(); String overlayOpacity = vm.get("overlayOpacity", "100"); backgroundDivProps.put("backgroundHeight", vm.get("backgroundHeight", "500px")); backgroundDivProps.put("backgroundWidth", vm.get("backgroundWidth", "INSET")); backgroundDivProps.put("overlayOpacity", Float.parseFloat(overlayOpacity) / 100); backgroundDivProps.put("backgroundType", vm.get("backgroundType", "NONE")); backgroundDivProps.put("backgroundImage", vm.get("backgroundImage", "")); backgroundDivProps.put("backgroundColor", vm.get("backgroundColor", "")); return backgroundDivProps; } public Map<String, Object> getSectionProps(){ Map<String, Object> sectionProps = new LinkedHashMap<String, Object>(); ValueMap vm = resource.getValueMap(); sectionProps.put("sectionHeight", vm.get("sectionHeight", "")); sectionProps.put("contentWidth", vm.get("contentWidth", "")); sectionProps.put("sectionBGColor", vm.get("sectionBGColor", "")); sectionProps.put("contentAlignment", vm.get("contentAlignment", "Center")); return sectionProps; } }
12) At this point you should be able to access the component dialog data using sling model url eg.
13) On the React front end side, add necessary configuration for typescript support in eaem-sites-react-spa-positioning-container\ui.frontend\package.json and run npm install
"dependencies": {
"@types/jest": "^26.0.0",
"@types/node": "^14.0.13",
"@types/react": "^16.9.38",
"@types/react-dom": "^16.9.8",
"typescript": "^3.9.5"
......
}
14) Add the TS file eaem-sites-react-spa-positioning-container\ui.frontend\src\components\PositioningContainer\PositioningContainer.tsx for necessary container component positioning logic...
import React from "react"; import CSS from 'csstype'; import { MapTo, Container } from "@adobe/cq-react-editable-components"; class EAEMPositioningContainer extends Container { OVERLAY_POSITION = { TOP: "10%", BOTTOM: "80%", LEFT: "20%", EXTREME_LEFT: "5%", RIGHT: "20%", EXTREME_RIGHT: "5%" }; constructor(props: any) { super(props); //@ts-ignore this.props = props; } get childComponents() { return super.childComponents; } get placeholderComponent() { return super.placeholderComponent; } get containerProps() { let containerProps = super.containerProps; //@ts-ignore let rhProps = this.props; rhProps.backgroundProps = rhProps.backgroundProps || {}; rhProps.sectionProps = rhProps.sectionProps || {}; let bgProps = rhProps.backgroundProps; const bgStyles: CSS.Properties = { zIndex: 0, position: "relative" }; bgStyles.width = "100%"; bgStyles.height = bgProps.backgroundHeight; bgStyles.backgroundColor = bgProps.backgroundColor; bgStyles.opacity = bgProps.overlayOpacity; if (bgProps.backgroundType == "IMAGE" && bgProps.backgroundImage) { bgStyles.backgroundImage = 'url("' + bgProps.backgroundImage + '")'; //bgStyles.backgroundRepeat = "no-repeat"; } containerProps.style = bgStyles; return containerProps; } get sectionStyles() { //@ts-ignore let rhProps = this.props; let sectionProps = rhProps.sectionProps; const sectionStyles: CSS.Properties = { zIndex: 1, position: "absolute" }; sectionStyles.backgroundColor = sectionProps.sectionBGColor || undefined; sectionStyles.height = sectionProps.sectionHeight || undefined; if (sectionProps.contentWidth) { sectionStyles.width = sectionProps.contentWidth; sectionStyles.textAlign = "center"; } let contentAlignment = sectionProps.contentAlignment || ""; if (contentAlignment == "Center") { sectionStyles.top = "50%"; sectionStyles.left = "50%"; sectionStyles.transform = "translate(-50%, -50%)"; } else { contentAlignment = contentAlignment.split(","); contentAlignment.map((alignment: string) => { alignment = alignment.trim(); if (alignment == "Top") { sectionStyles["top"] = this.OVERLAY_POSITION.TOP; } else if (alignment == "Bottom") { sectionStyles["top"] = this.OVERLAY_POSITION.BOTTOM; } else if (alignment == "Extreme Left") { sectionStyles["left"] = this.OVERLAY_POSITION.EXTREME_LEFT; } else if (alignment == "Left") { sectionStyles["left"] = this.OVERLAY_POSITION.LEFT; } else if (alignment == "Extreme Right") { sectionStyles["right"] = this.OVERLAY_POSITION.EXTREME_RIGHT; } else if (alignment == "Right") { sectionStyles["right"] = this.OVERLAY_POSITION.RIGHT; } }); } return sectionStyles; } render() { return ( <div {...this.containerProps}> <div style={this.sectionStyles}> {this.childComponents} {this.placeholderComponent} </div> </div> ); } } export default MapTo("eaem-sites-spa-how-to-react/components/positioning-container")( EAEMPositioningContainer );
15) Add the PositioningContainer.tsx path in eaem-65-extensions\eaem-sites-react-spa-positioning-container\ui.frontend\src\components\import-components.js
import './Page/Page';
import './Text/Text';
import './PositioningContainer/PositioningContainer';
No comments:
Post a Comment