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...
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:
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;
}
}