AEM 6550 - React Type Script SPA Composite Positioning Container Component

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


1) Create the project structure (for both React SPA and MPA authoring) with the following command using maven archetype - https://github.com/adobe/aem-project-archetype

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

2) Remove all additional components created, except the following required for testing... (or download Package Install)

                                                          /apps/eaem-sites-spa-how-to-react/components/spa
                                                          /apps/eaem-sites-spa-how-to-react/components/page
                                                          /apps/eaem-sites-spa-how-to-react/components/text
 

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. 

                                                          http://localhost:4502/content/eaem-sites-spa-how-to-react/us/en/eaem-home/jcr:content/root/responsivegrid/positioning_containe.model.json



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