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.
sling:resourceSuperType
.In this chapter, a new Card
component is created. The Card
component extends the Image Core Component adding additional content fields like a Title and a Call To Action button to perform the role of a teaser for other content within the SPA.
In a real-world implementation it may be more appropriate to simply use the Teaser Component than extending the Image Core Component to make a Card
component depending on project requirements. It is always recommended to use Core Components directly when possible.
Review the required tooling and instructions for setting up a local development environment.
Download the starting point for this tutorial via Git:
$ git clone git@github.com:adobe/aem-guides-wknd-spa.git
$ cd aem-guides-wknd-spa
$ git checkout Angular/extend-component-start
Deploy the code base to a local AEM instance using Maven:
$ mvn clean install -PautoInstallSinglePackage
If using AEM 6.x add the classic
profile:
$ mvn clean install -PautoInstallSinglePackage -Pclassic
Install the finished package for the traditional WKND reference site. The images provided by WKND reference site is reused on the WKND SPA. The package can be installed using AEM’s Package Manager.
You can always view the finished code on GitHub or check out the code locally by switching to the branch Angular/extend-component-solution
.
An initial Card Component has been provided by the chapter starter code. Inspect the starting point for the Card implementation.
In the IDE of your choice open the ui.apps
module.
Navigate to ui.apps/src/main/content/jcr_root/apps/wknd-spa-angular/components/card
and view the .content.xml
file.
<?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="Card"
sling:resourceSuperType="wknd-spa-angular/components/image"
componentGroup="WKND SPA Angular - Content"/>
The property sling:resourceSuperType
points to wknd-spa-angular/components/image
indicating that the Card
component inherits the functionality from the WKND SPA Image component.
Inspect the file ui.apps/src/main/content/jcr_root/apps/wknd-spa-angular/components/image/.content.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"
jcr:primaryType="cq:Component"
jcr:title="Image"
sling:resourceSuperType="core/wcm/components/image/v2/image"
componentGroup="WKND SPA Angular - Content"/>
Notice that the sling:resourceSuperType
points to core/wcm/components/image/v2/image
. This indicates that the WKND SPA Image component inherits the functionality from the Core Component Image.
Also known as the Proxy pattern Sling resource inheritance is a powerful design pattern for allowing child components to inherit functionality and extend/override behavior when desired. Sling inheritance supports multiple levels of inheritance, so ultimately the new Card
component inherits functionality of the Core Component Image.
Many development teams strive to be D.R.Y. (don’t repeat yourself). Sling inheritance makes this possible with AEM.
Beneath the card
folder, open the file _cq_dialog/.content.xml
.
This file is the Component Dialog definition for the Card
component. If using Sling inheritance, it is possible to 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.
Properties like sling:orderBefore
allow a developer to choose where to insert new tabs or form fields. In this case, the Text
tab is inserted before the asset
tab. To make full use of the Sling Resource Merger, it is important to know the original dialog node structure for the Image component dialog.
Beneath the card
folder, open the file _cq_editConfig.xml
. This 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. Review the <parameters>
node:
<parameters
jcr:primaryType="nt:unstructured"
sling:resourceType="wknd-spa-angular/components/card"
imageCrop=""
imageMap=""
imageRotate=""/>
Most components do not require a cq:editConfig
, the Image, and child descendents of the Image component are exceptions.
In the IDE switch to the ui.frontend
module, navigating to ui.frontend/src/app/components/card
:
Inspect the file card.component.ts
.
The component has already been stubbed out to map to the AEM Card
Component using the standard MapTo
function.
MapTo('wknd-spa-angular/components/card')(CardComponent, CardEditConfig);
Review the three @Input
parameters in the class for src
, alt
, and title
. These are expected JSON values from the AEM component that are mapped to the Angular component.
Open the file card.component.html
:
<div class="card" *ngIf="hasContent">
<app-image class="card__image" [src]="src" [alt]="alt" [title]="title"></app-image>
</div>
In this example we chose to reuse the existing Angular Image component app-image
by simply passing the @Input
parameters from card.component.ts
. Later in the tutorial, additional properties are added and displayed.
With this initial Card
implementation review the functionality in the AEM SPA Editor. To see the initial Card
component an update to the Template policy is needed.
Deploy the starter code to a local instance of AEM, if you haven’t already:
$ cd aem-guides-wknd-spa
$ mvn clean install -PautoInstallSinglePackage
Navigate to the SPA Page Template at http://localhost:4502/editor.html/conf/wknd-spa-angular/settings/wcm/templates/spa-page-template/structure.html.
Update the Layout Container’s policy to add the new Card
component as an allowed component:
Save the changes to the policy, and observe the Card
component as an allowed component:
Next, author the Card
component using the AEM SPA Editor.
Navigate to http://localhost:4502/editor.html/content/wknd-spa-angular/us/en/home.html.
In Edit
mode, add the Card
component to the Layout Container
:
Drag and drop an image from the Asset finder onto the Card
component:
Open the Card
component dialog and notice the addition of a Text Tab.
Enter the following values on the Text tab:
Card Path - choose a page beneath the SPA homepage.
CTA Text - “Read More”
Card Title - leave blank
Get title from linked page - check the checkbox to indicate true.
Update the Asset Metadata tab to add values for Alternative Text and Caption.
Currently no additional changes appear after updating the dialog. To expose the new fields to the Angular Component, we need to update the Sling Model for the Card
component.
Open a new tab and navigate to CRXDE-Lite. Inspect the content nodes beneath /content/wknd-spa-angular/us/en/home/jcr:content/root/responsivegrid
to find the Card
component content.
Observe that properties cardPath
, ctaText
, titleFromPage
are persisted by the dialog.
To ultimately expose the values from the component dialog to the Angular component, we need to update the Sling Model that populates the JSON for the Card
component. We also have the opportunity to implement two pieces of business logic:
titleFromPage
to true, return the title of the page specified by cardPath
otherwise return the value of cardTitle
textfield.cardPath
.Return to the IDE of your choice and open the core
module.
Open the file Card.java
at core/src/main/java/com/adobe/aem/guides/wknd/spa/angular/core/models/Card.java
.
Observe that the Card
interface currently extends com.adobe.cq.wcm.core.components.models.Image
and therefore inherits the methods of the Image
interface. The Image
interface already extends the ComponentExporter
interface which allows the Sling Model to be exported as JSON and mapped by the SPA editor. Therefore we do not need to explicitly extend ComponentExporter
interface like we did in the Custom Component chapter.
Add the following methods to the interface:
@ProviderType
public interface Card extends Image {
/***
* The URL to populate the CTA button as part of the card.
* The link should be based on the cardPath property that points to a page.
* @return String URL
*/
public String getCtaLinkURL();
/***
* The text to display on the CTA button of the card.
* @return String CTA text
*/
public String getCtaText();
/***
* The date to be displayed as part of the card.
* This is based on the last modified date of the page specified by the cardPath
* @return
*/
public Calendar getCardLastModified();
/**
* Return the title of the page specified by cardPath if `titleFromPage` is set to true.
* Otherwise return the value of `cardTitle`
* @return
*/
public String getCardTitle();
}
These methods are exposed via the JSON model API and passed to the Angular component.
Open CardImpl.java
. This is the implementation of Card.java
interface. This implementation has been partially stubbed out to accelerate the tutorial. Notice the use of the @Model
and @Exporter
annotations to ensure that the Sling Model is able to be serialized as JSON via the Sling Model Exporter.
CardImpl.java
also uses the Delegation pattern for Sling Models to avoid rewriting the logic from the Image Core Component.
Observe the following lines:
@Self
@Via(type = ResourceSuperType.class)
private Image image;
The above annotation instantiates an Image object named image
based on the sling:resourceSuperType
inheritance of the Card
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()
.
Next, implement the initModel()
method to initiate a private variable cardPage
based on the value of cardPath
@PostConstruct
public void initModel() {
if(StringUtils.isNotBlank(cardPath) && pageManager != null) {
cardPage = pageManager.getPage(this.cardPath);
}
}
The @PostConstruct initModel()
is called when the Sling Model is initialized, therefore it is a good opportunity to initialize objects that may be used by other methods in the model. The pageManager
is one of several Java™ backed global objects made available to Sling Models via the @ScriptVariable
annotation. The getPage method takes in a path and returns an AEM Page object or null if the path doesn’t point to a valid page.
This initializes the cardPage
variable, which is used by the other new methods to return data about the underlying linked page.
Review the global variables already mapped to the JCR properties saved the author dialog. The @ValueMapValue
annotation is used to automatically perform the mapping.
@ValueMapValue
private String cardPath;
@ValueMapValue
private String ctaText;
@ValueMapValue
private boolean titleFromPage;
@ValueMapValue
private String cardTitle;
These variables are used to implement the additional methods for the Card.java
interface.
Implement the additional methods defined in the Card.java
interface:
@Override
public String getCtaLinkURL() {
if(cardPage != null) {
return cardPage.getPath() + ".html";
}
return null;
}
@Override
public String getCtaText() {
return ctaText;
}
@Override
public Calendar getCardLastModified() {
if(cardPage != null) {
return cardPage.getLastModified();
}
return null;
}
@Override
public String getCardTitle() {
if(titleFromPage) {
return cardPage != null ? cardPage.getTitle() : null;
}
return cardTitle;
}
You can view the finished CardImpl.java here.
Open a terminal window and deploy just the updates to the core
module using the Maven autoInstallBundle
profile from the core
directory.
$ cd core/
$ mvn clean install -PautoInstallBundle
If using AEM 6.x add the classic
profile.
View the JSON model response at: http://localhost:4502/content/wknd-spa-angular/us/en.model.json and search for the wknd-spa-angular/components/card
:
"card": {
"ctaText": "Read More",
"cardTitle": "Page 1",
"title": "Woman chillaxing with river views in Australian bushland",
"src": "/content/wknd-spa-angular/us/en/home/_jcr_content/root/responsivegrid/card.coreimg.jpeg/1595190732886/adobestock-216674449.jpeg",
"alt": "Female sitting on a large rock relaxing in afternoon dappled light the Australian bushland with views over the river",
"cardLastModified": 1591360492414,
"ctaLinkURL": "/content/wknd-spa-angular/us/en/home/page-1.html",
":type": "wknd-spa-angular/components/card"
}
Notice that the JSON model is updated with additional key/value pairs after updating the methods in the CardImpl
Sling Model.
Now that the JSON model is populated with new properties for ctaLinkURL
, ctaText
, cardTitle
, and cardLastModified
we can update the Angular component to display these.
Return to the IDE and open the ui.frontend
module. Optionally, start the webpack dev server from a new terminal window to see the changes in real time:
$ cd ui.frontend
$ npm install
$ npm start
Open card.component.ts
at ui.frontend/src/app/components/card/card.component.ts
. Add the additional @Input
annotations to capture the new model:
export class CardComponent implements OnInit {
@Input() src: string;
@Input() alt: string;
@Input() title: string;
+ @Input() cardTitle: string;
+ @Input() cardLastModified: number;
+ @Input() ctaLinkURL: string;
+ @Input() ctaText: string;
Add methods for checking if the Call to Action is ready and for returning a date/time string based on the cardLastModified
input:
export class CardComponent implements OnInit {
...
get hasCTA(): boolean {
return this.ctaLinkURL && this.ctaLinkURL.trim().length > 0 && this.ctaText && this.ctaText.trim().length > 0;
}
get lastModifiedDate(): string {
const lastModifiedDate = this.cardLastModified ? new Date(this.cardLastModified) : null;
if (lastModifiedDate) {
return lastModifiedDate.toLocaleDateString();
}
return null;
}
...
}
Open card.component.html
and add the following markup to display the title, call to action and last modified date:
<div class="card" *ngIf="hasContent">
<app-image class="card__image" [src]="src" [alt]="alt" [title]="title"></app-image>
<div class="card__content">
<h2 class="card__title">
{{cardTitle}}
<span class="card__lastmod" *ngIf="lastModifiedDate">{{lastModifiedDate}}</span>
</h2>
<div class="card__action-container" *ngIf="hasCTA">
<a [routerLink]="ctaLinkURL" class="card__action-link" [title]="ctaText">
{{ctaText}}
</a>
</div>
</div>
</div>
Sass rules have already been added at card.component.scss
to style the title, call to action and last modified date.
You can view the finished Angular card component code here.
Deploy the full changes to AEM from the root of the project using Maven:
$ cd aem-guides-wknd-spa
$ mvn clean install -PautoInstallSinglePackage
Navigate to http://localhost:4502/editor.html/content/wknd-spa-angular/us/en/home.html to see the updated component:
You should be able to reauthor the existing content to create a page similar to the following:
Congratulations, you learned how to extend an AEM component and how Sling Models and dialogs work with the JSON model.
You can always view the finished code on GitHub or check out the code locally by switching to the branch Angular/extend-component-solution
.