*This article was modeled from the current Adobe React SPA documentation.
Learn how to extend an existing Core Component to be used with the AEM SPA Editor. Understanding how to extend an existing component is a powerful technique to customize and expand the capabilities of an AEM SPA Editor implementation.
Objective
- Extend an existing Core Component with additional properties and content.
- Understand the basic of Component Inheritance with the use of
sling:resourceSuperType
. - Learn how to leverage the Delegation Pattern for Sling Models to re-use existing logic and functionality.
What you will build
This chapter illustrates the additional code needed to add an extra property to a standard Image
component to fulfill the requirements for a new Banner
component. The Banner
component contains all of the same properties as the standard Image
component but includes an additional property for users to populate the Banner Text.
Prerequisites
Review the required tooling and instructions for setting up a local development environment. It is assumed at this point in the tutorial users have a solid understanding of the AEM SPA Editor feature.
Inheritance with Sling Resource Super Type
To extend an existing component set a property named sling:resourceSuperType
on your component’s definition. sling:resourceSuperType
is a property that can be set on an AEM component’s definition that points to another component. This explicitly sets the component to inherit all functionality of the component identified as the sling:resourceSuperType
.
If we want to extend the Image
component at wknd-spa-vue/components/image
we need to update the code in the ui.apps
module.
- Create a new folder beneath the
ui.apps
module forbanner
atui.apps/src/main/content/jcr_root/apps/wknd-spa-vue/components/banner
. - Beneath
banner
create a Component definition (.content.xml
) like the following:
<?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" jcr:primaryType="cq:Component" jcr:title="Banner" sling:resourceSuperType="wknd-spa-vue/components/image" componentGroup="WKND SPA Vue - Content"/>
This sets wknd-spa-vue/components/banner
to inherit all functionality of wknd-spa-vue/components/image
.
cq:editConfig
The _cq_editConfig.xml
file dictates the drag and drop behavior in the AEM authoring UI. When extending the Image component it is important that the resource type matches the component itself.
- In the
ui.apps
module create another file beneathbanner
named_cq_editConfig.xml
. - Populate
_cq_editConfig.xml
with the following XML:
<?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="cq:EditConfig"> <cq:dropTargets jcr:primaryType="nt:unstructured"> <image jcr:primaryType="cq:DropTargetConfig" accept="[image/gif,image/jpeg,image/png,image/webp,image/tiff,image/svg\\+xml]" groups="[media]" propertyName="./fileReference"> <parameters jcr:primaryType="nt:unstructured" sling:resourceType="wknd-spa-vue/components/banner" imageCrop="" imageMap="" imageRotate=""/> </image> </cq:dropTargets> <cq:inplaceEditing jcr:primaryType="cq:InplaceEditingConfig" active="{Boolean}true" editorType="image"> <inplaceEditingConfig jcr:primaryType="nt:unstructured"> <plugins jcr:primaryType="nt:unstructured"> <crop jcr:primaryType="nt:unstructured" supportedMimeTypes="[image/jpeg,image/png,image/webp,image/tiff]" features="*"> <aspectRatios jcr:primaryType="nt:unstructured"> <wideLandscape jcr:primaryType="nt:unstructured" name="Wide Landscape" ratio="0.6180"/> <landscape jcr:primaryType="nt:unstructured" name="Landscape" ratio="0.8284"/> <square jcr:primaryType="nt:unstructured" name="Square" ratio="1"/> <portrait jcr:primaryType="nt:unstructured" name="Portrait" ratio="1.6180"/> </aspectRatios> </crop> <flip jcr:primaryType="nt:unstructured" supportedMimeTypes="[image/jpeg,image/png,image/webp,image/tiff]" features="-"/> <map jcr:primaryType="nt:unstructured" supportedMimeTypes="[image/jpeg,image/png,image/webp,image/tiff,image/svg+xml]" features="*"/> <rotate jcr:primaryType="nt:unstructured" supportedMimeTypes="[image/jpeg,image/png,image/webp,image/tiff]" features="*"/> <zoom jcr:primaryType="nt:unstructured" supportedMimeTypes="[image/jpeg,image/png,image/webp,image/tiff]" features="*"/> </plugins> <ui jcr:primaryType="nt:unstructured"> <inline jcr:primaryType="nt:unstructured" toolbar="[crop#launch,rotate#right,history#undo,history#redo,fullscreen#fullscreen,control#close,control#finish]"> <replacementToolbars jcr:primaryType="nt:unstructured" crop="[crop#identifier,crop#unlaunch,crop#confirm]"/> </inline> <fullscreen jcr:primaryType="nt:unstructured"> <toolbar jcr:primaryType="nt:unstructured" left="[crop#launchwithratio,rotate#right,flip#horizontal,flip#vertical,zoom#reset100,zoom#popupslider]" right="[history#undo,history#redo,fullscreen#fullscreenexit]"/> <replacementToolbars jcr:primaryType="nt:unstructured"> <crop jcr:primaryType="nt:unstructured" left="[crop#identifier]" right="[crop#unlaunch,crop#confirm]"/> <map jcr:primaryType="nt:unstructured" left="[map#rectangle,map#circle,map#polygon]" right="[map#unlaunch,map#confirm]"/> </replacementToolbars> </fullscreen> </ui> </inplaceEditingConfig> </cq:inplaceEditing> </jcr:root>
3. The unique aspect of the file is the <parameters>
node that sets the resourceType to wknd-spa-vue/components/banner
.
<parameters
jcr:primaryType="nt:unstructured"
sling:resourceType="wknd-spa-vue/components/banner"
imageCrop=""
imageMap=""
imageRotate=""/>
Most component’s do not require a _cq_editConfig
. Image components and descendants are the exception.
Extend the Dialog
Our Banner
component requires an extra text field in the dialog to capture the bannerText
. Since we are using Sling inheritance, we can use features of the Sling Resource Merger to override or extend portions of the dialog. In this sample a new tab has been added to the dialog to capture additional data from an author to populate the Card Component.
- In the
ui.apps
module, beneath thebanner
folder, create a folder named_cq_dialog
. - Beneath
_cq_dialog
create a Dialog definition file.content.xml
. Populate it with the following:
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/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"
jcr:title="Banner"
sling:resourceType="cq/gui/components/authoring/dialog">
<content jcr:primaryType="nt:unstructured">
<items jcr:primaryType="nt:unstructured">
<tabs jcr:primaryType="nt:unstructured">
<items jcr:primaryType="nt:unstructured">
<text
jcr:primaryType="nt:unstructured"
jcr:title="Text"
sling:orderBefore="asset"
sling:resourceType="granite/ui/components/coral/foundation/container"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<columns
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<textGroup
granite:hide="${cqDesign.titleHidden}"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/well">
<items jcr:primaryType="nt:unstructured">
<bannerText
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldDescription="Text to display on top of the banner."
fieldLabel="Banner Text"
name="./bannerText"/>
</items>
</textGroup>
</items>
</column>
</items>
</columns>
</items>
</text>
</items>
</tabs>
</items>
</content>
</jcr:root>
The above XML definition will create a new tab named Text and order it before the existing Asset tab. It will contain a single field Banner Text.
The dialog will look like the following:
Observe that we did not have to define the tabs for Asset or Metadata. These are inherited via the sling:resourceSuperType
property.
Before we can preview the dialog, we need to implement the SPA Component and the MapTo
function.
Implement SPA Component
In order to use the Banner component with the SPA Editor, a new SPA component must be created that will map to wknd-spa-vue/components/banner
. This will be done in the ui.frontend
module.
- In the
ui.frontend
module create a new folder forBanner
atui.frontend/src/components/Banner
. - Create a new file named
Banner.js
beneath theBanner
folder. Populate it with the following:
<template> <div class="Banner"> <h4>{{bannerText}}</h4> <div class="BannerImage"> <img class="Image-src" :src="src" :alt="alt" :title="title ? title : alt" /> </div> </div> </template> <script> export default { name: 'Banner', props: { src: { type: String }, alt: { type: String }, title: { type: String }, bannerText: { type: String } } } </script> <style scoped> </style>
Update map-components.js to include the Banner component:
MapTo('wknd-spa-vue/components/banner')(Banner, { emptyLabel: 'Banner', isEmpty: function (props) { return !props || !props.src || props.src.trim().length < 1 } })
- At this point the project can be deployed to AEM and the dialog can be tested:
$ cd aem-guides-wknd-spa-vue
$ mvn clean install -PautoInstallSinglePackage
- Update the SPA Template’s policy to add the
Banner
component as an allowed component. - Navigate to a SPA page and add the
Banner
component to one of the SPA pages
Add Java Interface
To ultimately expose the values from the component dialog to the Vue component we need to update the Sling Model that populates the JSON for the Banner
component. This will be done in the core
module that contains all of the Java code for our SPA project.
First we will create a new Java interface for Banner
that extends the Image
Java interface.
- In the
core
module create a new file namedBannerModel.java
atcore/src/main/java/com/adobe/aem/guides/wkndspa/vue/core/models
. - Populate
BannerModel.java
with the following:
package com.adobe.aem.guides.wknd.spa.vue.core.models;
import com.adobe.cq.wcm.core.components.models.Image;
import org.osgi.annotation.versioning.ProviderType;
@ProviderType
public interface BannerModel extends Image {
public String getBannerText();
}
This will inherit all of the methods from the Core Component Image
interface and add one new method getBannerText()
.
Implement Sling Model
Next, implement the Sling Model for the BannerModel
interface.
- In the
core
module create a new file namedBannerModelImpl.java
atcore/src/main/java/com/adobe/aem/guides/wknd/spa/vue/core/models/impl
. - Populate
BannerModelImpl.java
with the following:
package com.adobe.aem.guides.wknd.spa.vue.core.models.impl;
import com.adobe.aem.guides.wknd.spa.vue.core.models.BannerModel;
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.models.annotations.*;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.apache.sling.models.annotations.via.ResourceSuperType;
@Model(
adaptables = SlingHttpServletRequest.class,
adapters = { BannerModel.class,ComponentExporter.class},
resourceType = BannerModelImpl.RESOURCE_TYPE,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class BannerModelImpl implements BannerModel {
// points to the the component resource path in ui.apps
static final String RESOURCE_TYPE = "wknd-spa-vue/components/banner";
@Self
private SlingHttpServletRequest request;
// With sling inheritance (sling:resourceSuperType) we can adapt the current resource to the Image class
// this allows us to re-use all of the functionality of the Image class, without having to implement it ourself
// see https://github.com/adobe/aem-core-wcm-components/wiki/Delegation-Pattern-for-Sling-Models
@Self
@Via(type = ResourceSuperType.class)
private Image image;
// map the property saved by the dialog to a variable named `bannerText`
@ValueMapValue
private String bannerText;
// public getter to expose the value of `bannerText` via the Sling Model and JSON output
@Override
public String getBannerText() {
return bannerText;
}
// Re-use the Image class for all other methods:
@Override
public String getSrc() {
return null != image ? image.getSrc() : null;
}
@Override
public String getAlt() {
return null != image ? image.getAlt() : null;
}
@Override
public String getTitle() {
return null != image ? image.getTitle() : null;
}
// method required by `ComponentExporter` interface
// exposes a JSON property named `:type` with a value of `wknd-spa-vue/components/banner`
// required to map the JSON export to the SPA component props via the `MapTo`
@Override
public String getExportedType() {
return BannerModelImpl.RESOURCE_TYPE;
}
}
Notice the use of the @Model
and @Exporter
annotations to ensure the Sling Model is able to be serialized as JSON via the Sling Model Exporter.
BannerModelImpl.java
uses the Delegation pattern for Sling Models to avoid rewriting all of the logic from the Image core component.
Observe the following lines:
@Self
@Via(type = ResourceSuperType.class)
private Image image;
The above annotation will instantiate an Image object named image
based on the sling:resourceSuperType
inheritance of the Banner
component.
@Override
public String getSrc() {
return null != image ? image.getSrc() : null;
}
It is then possible to simply use the image
object to implement methods defined by the Image
interface, without having to write the logic ourselves. This technique is used for getSrc()
, getAlt()
and getTitle()
.
Open a terminal window and deploy just the updates to the core
module using the Maven autoInstallBundle
profile from the core
directory.
Putting it all together
- Return to AEM and open the SPA page that has the
Banner
component. - Update the
Banner
component to include Banner Text:
Populate the component with an image:
Save the dialog updates.
You should now see the rendered value of Banner Text
Congratulations!
Congratulations, you learned how to extend an AEM component using the and how Sling Models and dialogs work with the JSON model.