• Extend a Component

    *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

    1. Extend an existing Core Component with additional properties and content.
    2. Understand the basic of Component Inheritance with the use of sling:resourceSuperType.
    3. 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.

    Final authored banner component

    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:resourceSuperTypeis 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.

    1. Create a new folder beneath the ui.apps module for banner at ui.apps/src/main/content/jcr_root/apps/wknd-spa-vue/components/banner.
    2. 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.

    1. In the ui.apps module create another file beneath banner named _cq_editConfig.xml.
    2. 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.

    1. In the ui.apps module, beneath the banner folder, create a folder named _cq_dialog.
    2. 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.

    1. In the ui.frontend module create a new folder for Banner at ui.frontend/src/components/Banner.
    2. Create a new file named Banner.js beneath the Banner 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
      }
    })
    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
    2. Update the SPA Template’s policy to add the Banner component as an allowed component.
    3. 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.

    1. In the core module create a new file named BannerModel.java at core/src/main/java/com/adobe/aem/guides/wkndspa/vue/core/models.
    2. 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.

    1. In the core module create a new file named BannerModelImpl.java at core/src/main/java/com/adobe/aem/guides/wknd/spa/vue/core/models/impl.
    2. 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

    1. Return to AEM and open the SPA page that has the Banner component.
    2. 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.

  • Leave a Reply