Extend a Core Component extend-component
- The Universal Editor for visually editing headless content.
- The Content Fragment Editor for form-based editing of headless content.
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 use the Delegation Pattern for Sling Models to reuse existing logic and functionality.
What you will build
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.
Card component depending on project requirements. It is always recommended to use Core Components directly when possible.Prerequisites
Review the required tooling and instructions for setting up a local development environment.
Get the code
-
Download the starting point for this tutorial via Git:
code language-shell $ 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:
code language-shell $ mvn clean install -PautoInstallSinglePackageIf using AEM 6.x add the
classicprofile:code language-shell $ 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.
Inspect initial Card implementation
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.appsmodule. -
Navigate to
ui.apps/src/main/content/jcr_root/apps/wknd-spa-angular/components/cardand view the.content.xmlfile.
code language-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="Card" sling:resourceSuperType="wknd-spa-angular/components/image" componentGroup="WKND SPA Angular - Content"/>The property
sling:resourceSuperTypepoints towknd-spa-angular/components/imageindicating that theCardcomponent 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:code language-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:resourceSuperTypepoints tocore/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
Cardcomponent 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
cardfolder, open the file_cq_dialog/.content.xml.This file is the Component Dialog definition for the
Cardcomponent. 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:orderBeforeallow a developer to choose where to insert new tabs or form fields. In this case, theTexttab is inserted before theassettab. 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
cardfolder, 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:code language-xml <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.frontendmodule, navigating toui.frontend/src/app/components/card:
-
Inspect the file
card.component.ts.The component has already been stubbed out to map to the AEM
CardComponent using the standardMapTofunction.code language-js MapTo('wknd-spa-angular/components/card')(CardComponent, CardEditConfig);Review the three
@Inputparameters in the class forsrc,alt, andtitle. These are expected JSON values from the AEM component that are mapped to the Angular component. -
Open the file
card.component.html:code language-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-imageby simply passing the@Inputparameters fromcard.component.ts. Later in the tutorial, additional properties are added and displayed.
Update the Template Policy
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:
code language-shell $ 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
Cardcomponent as an allowed component:
Save the changes to the policy, and observe the
Cardcomponent as an allowed component:
Author initial Card 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
Editmode, add theCardcomponent to theLayout Container:
-
Drag and drop an image from the Asset finder onto the
Cardcomponent:
-
Open the
Cardcomponent 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
Cardcomponent. -
Open a new tab and navigate to CRXDE-Lite. Inspect the content nodes beneath
/content/wknd-spa-angular/us/en/home/jcr:content/root/responsivegridto find theCardcomponent content.
Observe that properties
cardPath,ctaText,titleFromPageare persisted by the dialog.
Update Card Sling Model
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:
- If
titleFromPageto true, return the title of the page specified bycardPathotherwise return the value ofcardTitletextfield. - Return the last modified date of the page specified by
cardPath.
Return to the IDE of your choice and open the core module.
-
Open the file
Card.javaatcore/src/main/java/com/adobe/aem/guides/wknd/spa/angular/core/models/Card.java.Observe that the
Cardinterface currently extendscom.adobe.cq.wcm.core.components.models.Imageand therefore inherits the methods of theImageinterface. TheImageinterface already extends theComponentExporterinterface which allows the Sling Model to be exported as JSON and mapped by the SPA editor. Therefore we do not need to explicitly extendComponentExporterinterface like we did in the Custom Component chapter. -
Add the following methods to the interface:
code language-java @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 ofCard.javainterface. This implementation has been partially stubbed out to accelerate the tutorial. Notice the use of the@Modeland@Exporterannotations to ensure that the Sling Model is able to be serialized as JSON via the Sling Model Exporter.CardImpl.javaalso uses the Delegation pattern for Sling Models to avoid rewriting the logic from the Image Core Component. -
Observe the following lines:
code language-java @Self @Via(type = ResourceSuperType.class) private Image image;The above annotation instantiates an Image object named
imagebased on thesling:resourceSuperTypeinheritance of theCardcomponent.code language-java @Override public String getSrc() { return null != image ? image.getSrc() : null; }It is then possible to simply use the
imageobject to implement methods defined by theImageinterface, without having to write the logic ourselves. This technique is used forgetSrc(),getAlt(), andgetTitle(). -
Next, implement the
initModel()method to initiate a private variablecardPagebased on the value ofcardPathcode language-java @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. ThepageManageris one of several Java™ backed global objects made available to Sling Models via the@ScriptVariableannotation. 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
cardPagevariable, 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
@ValueMapValueannotation is used to automatically perform the mapping.code language-java @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.javainterface. -
Implement the additional methods defined in the
Card.javainterface:code language-java @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; }note note NOTE You can view the finished CardImpl.java here. -
Open a terminal window and deploy just the updates to the
coremodule using the MavenautoInstallBundleprofile from thecoredirectory.code language-shell $ cd core/ $ mvn clean install -PautoInstallBundleIf using AEM 6.x add the
classicprofile. -
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:code language-json "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
CardImplSling Model.
Update Angular Component
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.frontendmodule. Optionally, start the webpack dev server from a new terminal window to see the changes in real time:code language-shell $ cd ui.frontend $ npm install $ npm start -
Open
card.component.tsatui.frontend/src/app/components/card/card.component.ts. Add the additional@Inputannotations to capture the new model:code language-diff 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
cardLastModifiedinput:code language-js 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.htmland add the following markup to display the title, call to action and last modified date:code language-html <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.scssto style the title, call to action and last modified date.note note NOTE You can view the finished Angular card component code here. -
Deploy the full changes to AEM from the root of the project using Maven:
code language-shell $ 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! congratulations
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.