Map SPA components to AEM components map-components
Learn how to map Angular components to Adobe Experience Manager (AEM) components with the AEM SPA Editor JS SDK. Component mapping enables users to make dynamic updates to SPA components within the AEM SPA Editor, similar to traditional AEM authoring.
This chapter takes a deeper-dive into the AEM JSON model API and how the JSON content exposed by an AEM component can be automatically injected into a Angular component as props.
Objective
- Learn how to map AEM components to SPA Components.
- Understand the difference between Container components and Content components.
- Create a new Angular component that maps to an existing AEM component.
What you will build
This chapter will inspect how the provided Text
SPA component is mapped to the AEM Text
component. A new Image
SPA component is created that can be used in the SPA and authored in AEM. Out of the box features of the Layout Container and Template Editor policies will also be used to create a view that is a little more varied in appearance.
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/map-components-start
-
Deploy the code base to a local AEM instance using Maven:
code language-shell $ mvn clean install -PautoInstallSinglePackage
If using AEM 6.x add the
classic
profile:code language-shell $ mvn clean install -PautoInstallSinglePackage -Pclassic
You can always view the finished code on GitHub or check the code out locally by switching to the branch Angular/map-components-solution
.
Mapping Approach
The basic concept is to map a SPA Component to an AEM Component. AEM components, run server-side, export content as part of the JSON model API. The JSON content is consumed by the SPA, running client-side in the browser. A 1:1 mapping between SPA components and an AEM component is created.
High-level overview of mapping an AEM Component to a Angular Component
Inspect the Text Component
The AEM Project Archetype provides a Text
component that is mapped to the AEM Text component. This is an example of a content component, in that it renders content from AEM.
Let’s see how the component works.
Inspect the JSON model
-
Before jumping into the SPA code, it is important to understand the JSON model that AEM provides. Navigate to the Core Component Library and view the page for the Text component. The Core Component Library provides examples of all the AEM Core Components.
-
Select the JSON tab for one of the examples:
You should see three properties:
text
,richText
, and:type
.:type
is a reserved property that lists thesling:resourceType
(or path) of the AEM Component. The value of:type
is what is used to map the AEM component to the SPA component.text
andrichText
are additional properties that are exposed to the SPA component.
Inspect the Text component
-
Open a new terminal and navigate to the
ui.frontend
folder inside the project. Runnpm install
and thennpm start
to start the webpack dev server:code language-shell $ cd ui.frontend $ npm run start:mock
The
ui.frontend
module is currently set up to use the mock JSON model. -
You should see a new browser window open to http://localhost:4200/content/wknd-spa-angular/us/en/home.html
-
In the IDE of your choice open up the AEM Project for the WKND SPA. Expand the
ui.frontend
module and open the file text.component.ts underui.frontend/src/app/components/text/text.component.ts
: -
The first area to inspect is the
class TextComponent
at ~line 35:code language-js export class TextComponent { @Input() richText: boolean; @Input() text: string; @Input() itemName: string; @HostBinding('innerHtml') get content() { return this.richText ? this.sanitizer.bypassSecurityTrustHtml(this.text) : this.text; } @HostBinding('attr.data-rte-editelement') editAttribute = true; constructor(private sanitizer: DomSanitizer) {} }
@Input() decorator is used to declare fields who’s values are set via the mapped JSON object, reviewed earlier.
@HostBinding('innerHtml') get content()
is a method that exposes the authored text content from the value ofthis.text
. In the case that the content is rich text (determined by thethis.richText
flag) Angular’s built-in security is bypassed. Angular’s DomSanitizer is used to “scrub” the raw HTML and prevent Cross Site Scripting vulnerabilities. The method is bound to theinnerHtml
property using the @HostBinding decorator. -
Next inspect the
TextEditConfig
at ~line 24:code language-js const TextEditConfig = { emptyLabel: 'Text', isEmpty: cqModel => !cqModel || !cqModel.text || cqModel.text.trim().length < 1 };
The above code is responsible for determining when to render the placeholder in the AEM author environment. If the
isEmpty
method returns true then the placeholder is rendered. -
Finally take a look at the
MapTo
call at ~line 53:code language-js MapTo('wknd-spa-angular/components/text')(TextComponent, TextEditConfig );
MapTo is provided by the AEM SPA Editor JS SDK (
@adobe/cq-angular-editable-components
). The pathwknd-spa-angular/components/text
represents thesling:resourceType
of the AEM component. This path gets matched with the:type
exposed by the JSON model observed earlier. MapTo parses the JSON model response and passes the correct values to the@Input()
variables of the SPA component.You can find the AEM
Text
component definition atui.apps/src/main/content/jcr_root/apps/wknd-spa-angular/components/text
. -
Experiment by modifying the en.model.json file at
ui.frontend/src/mocks/json/en.model.json
.At ~line 62 update the first
Text
value to use anH1
andu
tags:code language-json "text": { "text": "<h1><u>Hello World!</u></h1>", "richText": true, ":type": "wknd-spa-angular/components/text" }
Return to the browser to see the effects served by the webpack dev server:
Try toggling the
richText
property between true / false to see the render logic in action. -
Inspect text.component.html at
ui.frontend/src/app/components/text/text.component.html
.This file is empty since the entire contents of the component is set by the
innerHTML
property. -
Inspect the app.module.ts at
ui.frontend/src/app/app.module.ts
.code language-js @NgModule({ imports: [ BrowserModule, SpaAngularEditableComponentsModule, AppRoutingModule ], providers: [ModelManagerService, { provide: APP_BASE_HREF, useValue: '/' }], declarations: [AppComponent, TextComponent, PageComponent, HeaderComponent], entryComponents: [TextComponent, PageComponent], bootstrap: [AppComponent] }) export class AppModule {}
The TextComponent is not explicitly included, but rather dynamically via AEMResponsiveGridComponent provided by the AEM SPA Editor JS SDK. Therefore must be listed in the app.module.ts’ entryComponents array.
Create the Image Component
Next, create an Image
Angular component that is mapped to the AEM Image component. The Image
component is another example of a content component.
Inspect the JSON
Before jumping into the SPA code, inspect the JSON model provided by AEM.
-
Navigate to the Image examples in the Core Component library.
Properties of
src
,alt
, andtitle
are used to populate the SPAImage
component.note note NOTE There are other Image properties exposed ( lazyEnabled
,widths
) that allow a developer to create an adaptive and lazy-loading component. The component built in this tutorial is simple and does not use these advanced properties. -
Return to your IDE and open up the
en.model.json
atui.frontend/src/mocks/json/en.model.json
. Since this is a net-new component for our project we need to “mock” the Image JSON.At ~line 70 add a JSON entry for the
image
model (don’t forget about the trailing comma,
after the secondtext_386303036
) and update the:itemsOrder
array.code language-json ... ":items": { ... "text_386303036": { "text": "<p>A new text component.</p>\r\n", "richText": true, ":type": "wknd-spa-angular/components/text" }, "image": { "alt": "Rock Climber in New Zealand", "title": "Rock Climber in New Zealand", "src": "/mocks/images/adobestock-140634652.jpeg", ":type": "wknd-spa-angular/components/image" } }, ":itemsOrder": [ "text", "text_386303036", "image" ],
The project includes a sample image at
/mock-content/adobestock-140634652.jpeg
that is used with the webpack dev server.You can view the full en.model.json here.
-
Add a stock photo to be displayed by the component.
Create a new folder named images beneath
ui.frontend/src/mocks
. Download adobestock-140634652.jpeg and place it in the newly created images folder. Feel free to use your own image, if desired.
Implement the Image component
-
Stop the webpack dev server if started.
-
Create a new Image component by running the Angular CLI
ng generate component
command from withinui.frontend
folder:code language-shell $ ng generate component components/image
-
In the IDE, open image.component.ts at
ui.frontend/src/app/components/image/image.component.ts
and update as follows:code language-js import {Component, Input, OnInit} from '@angular/core'; import {MapTo} from '@adobe/cq-angular-editable-components'; const ImageEditConfig = { emptyLabel: 'Image', isEmpty: cqModel => !cqModel || !cqModel.src || cqModel.src.trim().length < 1 }; @Component({ selector: 'app-image', templateUrl: './image.component.html', styleUrls: ['./image.component.scss'] }) export class ImageComponent implements OnInit { @Input() src: string; @Input() alt: string; @Input() title: string; constructor() { } get hasImage() { return this.src && this.src.trim().length > 0; } ngOnInit() { } } MapTo('wknd-spa-angular/components/image')(ImageComponent, ImageEditConfig);
ImageEditConfig
is the configuration to determine whether to render the author placeholder in AEM, based on if thesrc
property is populated.@Input()
ofsrc
,alt
, andtitle
are the properties mapped from the JSON API.hasImage()
is a method that will determine if the image should be rendered.MapTo
maps the SPA component to the AEM component located atui.apps/src/main/content/jcr_root/apps/wknd-spa-angular/components/image
. -
Open image.component.html and update it as follows:
code language-html <ng-container *ngIf="hasImage"> <img class="image" [src]="src" [alt]="alt" [title]="title"/> </ng-container>
This will render the
<img>
element ifhasImage
returns true. -
Open image.component.scss and update it as follows:
code language-scss :host-context { display: block; } .image { margin: 1rem 0; width: 100%; border: 0; }
note note NOTE The :host-context
rule is critical for the AEM SPA editor placeholder to function correctly. All SPA components that are intended to be authored in the AEM page editor will need this rule at a minimum. -
Open
app.module.ts
and add theImageComponent
to theentryComponents
array:code language-js entryComponents: [TextComponent, PageComponent, ImageComponent],
Like the
TextComponent
, theImageComponent
is dynamically loaded, and must be included in theentryComponents
array. -
Start the webpack dev server to see the
ImageComponent
render.code language-shell $ npm run start:mock
Image added to the SPA
note note NOTE Bonus challenge: Implement a new method to display the value of title
as a caption beneath the image.
Update Policies in AEM
The ImageComponent
component is only visible in the webpack dev server. Next, deploy the updated SPA to AEM and update the template policies.
-
Stop the webpack dev server and from the root of the project, deploy the changes to AEM using your Maven skills:
code language-shell $ cd aem-guides-wknd-spa $ mvn clean install -PautoInstallSinglePackage
-
From the AEM Start screen navigate to Tools > Templates > WKND SPA Angular.
Select and edit the SPA Page:
-
Select the Layout Container and click it’s policy icon to edit the policy:
-
Under Allowed Components > WKND SPA Angular - Content > check the Image component:
Under Default Components > Add mapping and choose the Image - WKND SPA Angular - Content component:
Enter a mime type of
image/*
.Click Done to save the policy updates.
-
In the Layout Container click the policy icon for the Text component:
Create a new policy named WKND SPA Text. Under Plugins > Formatting > check all the boxes to enable additional formatting options:
Under Plugins > Paragraph Styles > check the box to Enable paragraph styles:
Click Done to save the policy update.
-
Navigate to the Homepage http://localhost:4502/editor.html/content/wknd-spa-angular/us/en/home.html.
You should also be able to edit the
Text
component and add additional paragraph styles in full-screen mode. -
You should also be able to drag+drop an image from the Asset finder:
-
Add your own images via AEM Assets or install the finished code base for the standard WKND reference site. The WKND reference site includes many images that can be re-used on the WKND SPA. The package can be installed using AEM’s Package Manager.
Inspect the Layout Container
Support for the Layout Container is automatically provided by the AEM SPA Editor SDK. The Layout Container, as indicated by the name, is a container component. Container components are components that accept JSON structures which represent other components and dynamically instantiate them.
Let’s inspect the Layout Container further.
-
In the IDE open responsive-grid.component.ts at
ui.frontend/src/app/components/responsive-grid
:code language-js import { AEMResponsiveGridComponent,MapTo } from '@adobe/cq-angular-editable-components'; MapTo('wcm/foundation/components/responsivegrid')(AEMResponsiveGridComponent);
The
AEMResponsiveGridComponent
is implemented as part of the AEM SPA Editor SDK and is included in the project viaimport-components
. -
In a browser navigate to http://localhost:4502/content/wknd-spa-angular/us/en.model.json
The Layout Container component has a
sling:resourceType
ofwcm/foundation/components/responsivegrid
and is recognized by the SPA Editor using the:type
property, just like theText
andImage
components.The same capabilities of re-sizing a component using Layout Mode are available with the SPA Editor.
-
Return to http://localhost:4502/editor.html/content/wknd-spa-angular/us/en/home.html. Add additional Image components and try re-sizing them using the Layout option:
-
Re-open the JSON model http://localhost:4502/content/wknd-spa-angular/us/en.model.json and observe the
columnClassNames
as part of the JSON:The class name
aem-GridColumn--default--4
indicates the component should be 4 columns wide based on a 12 column grid. More details about the responsive grid can be found here. -
Return to the IDE and in the
ui.apps
module there is a client-side library defined atui.apps/src/main/content/jcr_root/apps/wknd-spa-angular/clientlibs/clientlib-grid
. Open the fileless/grid.less
.This file determines the breakpoints (
default
,tablet
, andphone
) used by the Layout Container. This file is intended to be customized per project specifications. Currently the breakpoints are set to1200px
and650px
. -
You should be able to use the responsive capabilities and the updated rich text policies of the
Text
component to author a view like the following:
Congratulations! congratulations
Congratulations, you learned how to map SPA components to AEM Components and you implemented a new Image
component. You also got a chance to explore the responsive capabilities of the Layout Container.
You can always view the finished code on GitHub or check the code out locally by switching to the branch Angular/map-components-solution
.
Next Steps next-steps
Navigation and Routing - Learn how multiple views in the SPA can be supported by mapping to AEM Pages with the SPA Editor SDK. Dynamic navigation is implemented using Angular Router and added to an existing Header component.
Bonus - Persist configurations to source control bonus
In many cases, especially at the beginning of an AEM project it is valuable to persist configurations, like templates and related content policies, to source control. This ensures that all developers are working against the same set of content and configurations and can ensure additional consistency between environments. Once a project reaches a certain level of maturity, the practice of managing templates can be turned over to a special group of power users.
The next few steps will take place using the Visual Studio Code IDE and VSCode AEM Sync but could be doing using any tool and any IDE that you have configured to pull or import content from a local instance of AEM.
-
In the Visual Studio Code IDE, ensure that you have VSCode AEM Sync installed via the Marketplace extension:
-
Expand the ui.content module in the Project explorer and navigate to
/conf/wknd-spa-angular/settings/wcm/templates
. -
Right+Click the
templates
folder and select Import from AEM Server: -
Repeat the steps to import content but select the policies folder located at
/conf/wknd-spa-angular/settings/wcm/policies
. -
Inspect the
filter.xml
file located atui.content/src/main/content/META-INF/vault/filter.xml
.code language-xml <!--ui.content filter.xml--> <?xml version="1.0" encoding="UTF-8"?> <workspaceFilter version="1.0"> <filter root="/conf/wknd-spa-angular" mode="merge"/> <filter root="/content/wknd-spa-angular" mode="merge"/> <filter root="/content/dam/wknd-spa-angular" mode="merge"/> <filter root="/content/experience-fragments/wknd-spa-angular" mode="merge"/> </workspaceFilter>
The
filter.xml
file is responsible for identifying the paths of nodes that are installed with the package. Notice themode="merge"
on each of the filters which indicates that existing content will not be modified, only new content is added. Since content authors may be updating these paths, it is important that a code deployment does not overwrite content. See the FileVault documentation for more details on working with filter elements.Compare
ui.content/src/main/content/META-INF/vault/filter.xml
andui.apps/src/main/content/META-INF/vault/filter.xml
to understand the different nodes managed by each module.