AEM 6560 - React SPA Dynamic Media Smart Crop Image Component

Goal

Create a React SPA Smart Crop Image component /apps/eaem-sites-spa-how-to-react/components/dm-image-smart-crop to show the dynamic crops for different breakpoints...

Demo | Package Install | Github


Setup DM Image Profiles


Configure Folder with DM Info


Smart Crops


Component Dialog


Desktop


Mobile



Solution

1) To get the smart crops of an image,  create the following nt:file /apps/eaem-sites-spa-how-to-react/smart-crop-renditions/smart-crop-renditions.jsp to return the dynamic crops as JSON (serves as a client side datasource for crops drop down, created in next steps...)


<%@include file="/libs/granite/ui/global.jsp"%>

<%@page session="false"
        import="java.util.Iterator,
                  org.apache.sling.commons.json.JSONObject,
                  com.adobe.granite.ui.components.Config"%>

<%
    Config cfg = cmp.getConfig();
    ValueMap dynVM = null;

    JSONObject dynRenditions = new JSONObject();
    Resource dynResource = null;

    response.setContentType("application/json");

    for (Iterator<Resource> items = cmp.getItemDataSource().iterator(); items.hasNext();) {
        JSONObject dynRendition = new JSONObject();

        dynResource = items.next();

        dynVM = dynResource.getValueMap();

        String name = String.valueOf(dynVM.get("breakpoint-name"));

        dynRendition.put("type", "IMAGE");
        dynRendition.put("name", name);
        dynRendition.put("url", dynVM.get("copyurl"));

        dynRenditions.put(name, dynRendition);
    }

    dynRenditions.write(response.getWriter());
%>


2) Create /apps/eaem-sites-spa-how-to-react/smart-crop-renditions/renditions and set the sling:resourceType to /apps/eaem-sites-spa-how-to-react/smart-crop-renditions. This is the content node for fetching renditions...

                                                        http://localhost:4502/apps/eaem-sites-spa-how-to-react/smart-crop-renditions/renditions.html/content/dam/home-assets/chaitra-birthday.JPG

3) Create node /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-extensions of type cq:ClientLibraryFolder, add String[] property categories with value [cq.authoring.dialog.all], String[] property dependencies with value lodash.

4) Create file (nt:file) /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-extensions/js.txt, add

                                                        dm-smart-crops.js

5) Create file (nt:file) /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-extensions/dm-smart-crops.js, add the following code...

(function ($, $document) {
    var DM_FILE_REF = "[name='./fileReference']",
        CROPS_MF = "[data-granite-coral-multifield-name='./crops']",
        SMART_CROPS_URL = "/apps/eaem-sites-spa-how-to-react/smart-crop-renditions/renditions.html",
        dynRenditions = {};

    $document.on('dialog-ready', loadSmartCrops);

    function loadSmartCrops() {
        var dialogPath;

        try {
            dialogPath = Granite.author.DialogFrame.currentDialog.editable.slingPath;
        } catch (err) {
            console.log("Error getting dialog path...", err);
        }

        if (!dialogPath) {
            return;
        }

        dialogPath = dialogPath.substring(0, dialogPath.lastIndexOf(".json"));

        $.ajax(dialogPath + ".2.json").done(handleCropsMF);
    }

    function handleCropsMF(dialogData) {
        var $cropsMF = $(CROPS_MF),
            mfName = $cropsMF.attr("data-granite-coral-multifield-name"),
            selectData = dialogData[mfName.substr(2)];

        $cropsMF.find("coral-select").each(function (index, cropSelect) {
            var $cropSelect = $(cropSelect), selUrl,
                name = $cropSelect.attr("name");

            name = name.substring(mfName.length + 1);
            name = name.substring(0,name.indexOf("/"));

            if(selectData[name]){
                selUrl = selectData[name]["url"];
            }

            loadCropsInSelect($cropSelect, selUrl);
        });

        $cropsMF.on("change", function () {
            var multifield = this;

            _.defer(function () {
                var justAddedItem = multifield.items.last(),
                    $cropSelect = $(justAddedItem).find("coral-select");

                loadCropsInSelect($cropSelect);
            });
        });

        $(DM_FILE_REF).closest("coral-fileupload").on("change", function(){
            dynRenditions = {};

            $cropsMF.find("coral-select").each(function (index, cropSelect) {
                var $cropSelect = $(cropSelect);

                $cropSelect[0].items.clear();

                loadCropsInSelect($cropSelect);
            });
        })
    }

    function getCoralSelectItem(text, value, selected) {
        return '<coral-select-item value="' + value + '" ' + selected + '>' + text + '</coral-select-item>';
    }

    function loadCropsInSelect($cropSelect, selectedValue) {
        var $fileRef = $(DM_FILE_REF),
            fileRef = $fileRef.val();

        if ( !fileRef || ($cropSelect[0].items.length > 1)) {
            return;
        }

        if (_.isEmpty(dynRenditions)) {
            $.ajax({url: SMART_CROPS_URL + fileRef, async: false}).done(function (renditions) {
                dynRenditions = renditions;
                addInSelect();
            });
        } else {
            addInSelect();
        }

        function addInSelect() {
            _.each(dynRenditions, function (rendition) {
                $cropSelect.append(getCoralSelectItem(rendition.name, rendition.url,
                    ((selectedValue == rendition.url) ? "selected" : "")));
            });
        }
    }
}(jQuery, jQuery(document)));


6) Create the component /apps/eaem-sites-spa-how-to-react/components/dm-image-smart-crop. In the next step we'd be creating the react render type script...


7) Add the component render script in eaem-sites-react-spa-dm-image\ui.frontend\src\components\DMSmartCropImage\DMSmartCropImage.tsx with the following code...

import { MapTo } from '@adobe/cq-react-editable-components';
import React, { Component } from 'react';
import { Link } from "react-router-dom";
import CSS from 'csstype';

function isObjectEmpty(obj) {
    return (Object.keys(obj).length == 0);
}

interface ImageComponentProps {
    smartCrops: object
    fileReference: string
    imageLink: string
}

interface ImageComponentState {
    imageSrc: string
}

const ImageEditConfig = {
    emptyLabel: 'Dynamic Media Smart Crop Image - Experience AEM',

    isEmpty: function (props) {
        return (!props || !props.fileReference || (props.fileReference.trim().length < 1));
    }
};

class Image extends React.Component<ImageComponentProps, ImageComponentState> {
    constructor(props: ImageComponentProps) {
        super(props);

        this.state = {
            imageSrc: this.imageUrl()
        }
    }

    componentDidMount() {
        window.addEventListener('resize', this.updateImage.bind(this));
    }

    componentDidUpdate(){
        console.log("in update");

        const currentSrc = this.state.imageSrc;
        const newSrc = this.imageUrl();

        if(currentSrc != newSrc){
            this.updateImage();
        }
    }

    componentWillUnmount() {
        window.removeEventListener('resize', this.updateImage);
    }

    updateImage(){
        this.setState({
            imageSrc: this.imageUrl()
        })
    }

    imageUrl() {
        const imageProps = this.props;
        let src = imageProps.fileReference;

        if (!isObjectEmpty(imageProps.smartCrops)) {
            const breakPoints = Object.keys(imageProps.smartCrops).sort((a: any, b: any) => b - a);

            for (const i in breakPoints) {
                let bp = parseInt(breakPoints[i]);

                if (bp < window.innerWidth) {
                    src = imageProps.smartCrops[bp];
                    break;
                }
            }
        }

        return src;
    }

    get imageHTML() {
        const imgStyles: CSS.Properties = {
            display : 'block',
            marginLeft: 'auto',
            marginRight: 'auto'
        };

        return (
            <Link to={this.props.imageLink}>
                <img src={this.state.imageSrc} style={imgStyles} />
            </Link>
        );
    }

    render() {
        return this.imageHTML;
    }
}

export default MapTo('eaem-sites-spa-how-to-react/components/dm-image-smart-crop')(Image, ImageEditConfig);


8) DMSmartCropImage was imported in ui.frontend\src\components\import-components.js

                           ...

                           import './DMSmartCropImage/DMSmartCropImage';

9) Create a Sling Model Exporter com.eaem.core.models.ImageComponentSlingExporter for exporting the component properties

                          SPA App Model Export: 

                          http://localhost:4502/content/eaem-sites-spa-how-to-react/us/en.model.json

                          Container Component Model Export: 

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


10) Add the following code in com.eaem.core.models.ImageComponentSlingExporter

package com.eaem.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.Optional;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

@Model(
        adaptables = {SlingHttpServletRequest.class},
        adapters = {ComponentExporter.class},
        resourceType = {
                "eaem-sites-spa-how-to-react/components/image",
                "eaem-sites-spa-how-to-react/components/dm-image-smart-crop"
        }
)
@Exporter(
        name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
        extensions = ExporterConstants.SLING_MODEL_EXTENSION
)
public class ImageComponentSlingExporter implements ComponentExporter {

    @Inject
    private Resource resource;

    @ValueMapValue
    @Optional
    private String imageLink;

    @ValueMapValue
    @Optional
    private String fileReference;

    @ValueMapValue
    @Optional
    private boolean openInNewWindow;

    private Map<String, String> smartCrops;

    @PostConstruct
    protected void initModel() {
        smartCrops = new LinkedHashMap<String, String>();

        Resource cropsRes = resource.getChild("crops");

        if(cropsRes == null){
            return;
        }

        Iterator<Resource> itr = cropsRes.listChildren();
        ValueMap vm = null;

        while(itr.hasNext()){
            vm = itr.next().getValueMap();
            smartCrops.put(vm.get("breakpoint", ""), vm.get("url", ""));
        }
    }

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

    public Map<String, String> getSmartCrops() {
        return smartCrops;
    }

    public String getImageLink() {
        return imageLink;
    }

    public void setImageLink(String imageLink) {
        this.imageLink = imageLink;
    }

    public String getFileReference() {
        return fileReference;
    }

    public void setFileReference(String fileReference) {
        this.fileReference = fileReference;
    }

    public boolean isOpenInNewWindow() {
        return openInNewWindow;
    }

    public void setOpenInNewWindow(boolean openInNewWindow) {
        this.openInNewWindow = openInNewWindow;
    }
}


No comments:

Post a Comment