ADOBE EXPERIENCE MANAGER (AEM) COMPONENTS
Message Component
AEM tutorial for development of a message component to allow an AEM author to add message banners to a page. Features include a field to set how many days before the message will reappear after it has been closed and a toggle switch to enable or disable the message.
Getting Started
Create the project using the AEM Project Archetype which is a Maven template that creates a minimal, best-practices-based Adobe Experience Manager (AEM) project. For the message component, I’m using the latest version which is 23 as of this writing.
For new components, typically I will start off by copying the helloworld component included with the archetype. e.g.,
cd ui.apps/src/main/content/jcr_root/apps/myproject
cp components/helloworld components/message
After updating our helloworld copy, renaming properties, etc. for the new message component, here is what we have.
message/.content.xml
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root 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="Message"
componentGroup="My Project - Content"/>
message/message.html
<div class="cmp-message" data-cmp-is="message">
</div>
<sly data-sly-use.templates="/apps/core/wcm/components/commons/v1/templates.html"
data-sly-call="${templates.placeholder @ isEmpty = !properties.text}"></sly>
We added a placeholder template call at the bottom so the component can be selected by an author for editing when it does not have any content.
Dialog
We’re going to want tabs in our message component dialog. To add these, replace the column
element with a tabs
element. e.g.,
message/_cq_dialog/.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" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="Properties"
sling:resourceType="cq/gui/components/authoring/dialog">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns">
<items jcr:primaryType="nt:unstructured">
<tabs
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/tabs"
maximized="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<text
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Text"
name="./text"/>
</items>
</tabs>
</items>
</content>
</jcr:root>
Our message dialog is going to use the v2 text dialog element to give the author a rich text editing experience for the message content.
Replace the existing message text
element with a copy of the text
element in core/wcm/components/text/v2/text/_cq_dialog/.content.xml
Then give the tab a title by adding a jcr:title
to the text
element. e.g.,
<text
jcr:primaryType="nt:unstructured"
jcr:title="Text"
While we’re at it, let’s also change the title of the dialog itself from Properties to Message. In the jcr:root
element at the top of the .content.xml
, update the jcr:title
with “Message”. e.g.,
<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="nt:unstructured"
jcr:title="Message"
sling:resourceType="cq/gui/components/authoring/dialog">
HTL
Now let’s update our template to display the text content that is entered by an author.
message/message.html
<div class="cmp-message" data-cmp-is="message">
<div>${properties.text @ context='html'}</div>
</div>
...
Install
Install our new component from the archetype project into the running AEM instance using Maven.
You can use mvn clean install to build and deploy it. e.g.,
mvn -PautoInstallPackage clean install
After dropping the component onto a page, add some content. e.g.,
Next, let’s provide a button so the use can dismiss the message. Also, add a data-cmp-hook-message="text"
data attribute to the div
containing the message text. The component JavaScript will use this as a selector.
message/message.html
<div class="cmp-message" data-cmp-is="message">
<div data-cmp-hook-message="text">${properties.text @ context='html'}</div>
<button>close</button>
</div>
...
Toggle Switch
The last step for the dialog is to add the Enable tab. Directly after the text element, add the enable
element from this message/_cq_dialog/.content.xml. The properties element is the next tab for the toggle switch that enables rendering and the inout for the number of days to hide.
Here is how the completed message dialog .content.xml
should look:
message/_cq_dialog/.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" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="Message"
sling:resourceType="cq/gui/components/authoring/dialog">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns">
<items jcr:primaryType="nt:unstructured">
<tabs
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/tabs"
maximized="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<text
jcr:primaryType="nt:unstructured"
jcr:title="Text"
sling:resourceType="cq/gui/components/authoring/dialog/richtext"
name="./text"
useFixedInlineToolbar="{Boolean}true">
<rtePlugins jcr:primaryType="nt:unstructured">
<format
jcr:primaryType="nt:unstructured"
features="bold,italic"/>
<justify
jcr:primaryType="nt:unstructured"
features="-"/>
<links
jcr:primaryType="nt:unstructured"
features="modifylink,unlink"/>
<lists
jcr:primaryType="nt:unstructured"
features="*"/>
<misctools jcr:primaryType="nt:unstructured">
<specialCharsConfig jcr:primaryType="nt:unstructured">
<chars jcr:primaryType="nt:unstructured">
<default_copyright
jcr:primaryType="nt:unstructured"
entity="&copy;"
name="copyright"/>
<default_euro
jcr:primaryType="nt:unstructured"
entity="&euro;"
name="euro"/>
<default_registered
jcr:primaryType="nt:unstructured"
entity="&reg;"
name="registered"/>
<default_trademark
jcr:primaryType="nt:unstructured"
entity="&trade;"
name="trademark"/>
</chars>
</specialCharsConfig>
</misctools>
<paraformat
jcr:primaryType="nt:unstructured"
features="*">
<formats jcr:primaryType="nt:unstructured">
<default_p
jcr:primaryType="nt:unstructured"
description="Paragraph"
tag="p"/>
<default_h1
jcr:primaryType="nt:unstructured"
description="Heading 1"
tag="h1"/>
<default_h2
jcr:primaryType="nt:unstructured"
description="Heading 2"
tag="h2"/>
<default_h3
jcr:primaryType="nt:unstructured"
description="Heading 3"
tag="h3"/>
<default_h4
jcr:primaryType="nt:unstructured"
description="Heading 4"
tag="h4"/>
<default_h5
jcr:primaryType="nt:unstructured"
description="Heading 5"
tag="h5"/>
<default_h6
jcr:primaryType="nt:unstructured"
description="Heading 6"
tag="h6"/>
<default_blockquote
jcr:primaryType="nt:unstructured"
description="Quote"
tag="blockquote"/>
<default_pre
jcr:primaryType="nt:unstructured"
description="Preformatted"
tag="pre"/>
</formats>
</paraformat>
<table
jcr:primaryType="nt:unstructured"
features="-">
<hiddenHeaderConfig
jcr:primaryType="nt:unstructured"
hiddenHeaderClassName="cq-wcm-foundation-aria-visuallyhidden"
hiddenHeaderEditingCSS="cq-RichText-hiddenHeader--editing"/>
</table>
<tracklinks
jcr:primaryType="nt:unstructured"
features="*"/>
</rtePlugins>
<uiSettings jcr:primaryType="nt:unstructured">
<cui jcr:primaryType="nt:unstructured">
<inline
jcr:primaryType="nt:unstructured"
toolbar="[format#bold,format#italic,format#underline,#justify,#lists,links#modifylink,links#unlink,#paraformat]">
<popovers jcr:primaryType="nt:unstructured">
<justify
jcr:primaryType="nt:unstructured"
items="[justify#justifyleft,justify#justifycenter,justify#justifyright]"
ref="justify"/>
<lists
jcr:primaryType="nt:unstructured"
items="[lists#unordered,lists#ordered,lists#outdent,lists#indent]"
ref="lists"/>
<paraformat
jcr:primaryType="nt:unstructured"
items="paraformat:getFormats:paraformat-pulldown"
ref="paraformat"/>
</popovers>
</inline>
<tableEditOptions
jcr:primaryType="nt:unstructured"
toolbar="[table#insertcolumn-before,table#insertcolumn-after,table#removecolumn,-,table#insertrow-before,table#insertrow-after,table#removerow,-,table#mergecells-right,table#mergecells-down,table#mergecells,table#splitcell-horizontal,table#splitcell-vertical,-,table#selectrow,table#selectcolumn,-,table#ensureparagraph,-,table#modifytableandcell,table#removetable,-,undo#undo,undo#redo,-,table#exitTableEditing,-]">
</tableEditOptions>
</cui>
</uiSettings>
</text>
<enable
jcr:primaryType="nt:unstructured"
jcr:title="Enable"
sling:resourceType="granite/ui/components/coral/foundation/container"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<enableMessage
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/switch"
fieldLabel="Enable Message"
name="./enableMessage"
uncheckedValue="false"
value="true" />
<daysHidden
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/numberfield"
fieldDescription="Number of days the current message will stay hidden after it is closed."
fieldLabel="Number of Days Hidden"
name="./daysHidden"
min="0"
max="365"
value="3" />
</items>
</enable>
</items>
</tabs>
</items>
</content>
</jcr:root>
Update the message template for the toggle switch enableMessage
property.
- Add
data-sly-test="${properties.enableMessage == 'true'}"
to the outer div. - Update the placeholder template call so the component will be rendered in edit mode when the toggle switch has it disabled.
Here is how the completed message.html
template should look after all of these updates:
message/message.html
<div data-sly-test="${properties.enableMessage == 'true'}"
class="cmp-message ${classCollapse}"
data-cmp-is="message"
data-cmp-id="message${resource.path.hashCode}"
data-days-hidden="${properties.daysHidden}">
<div data-cmp-hook-message="text">${properties.text @ context='html'}</div>
<button>close</button>
</div>
<sly data-sly-use.templates="/apps/core/wcm/components/commons/v1/templates.html"
data-sly-call="${templates.placeholder @ isEmpty = !properties.text || properties.enableMessage == 'false',
emptyTextAppend = properties.enableMessage == 'false' ? 'Disabled' : ''}"></sly>
Frontend UI
At this point the close button doesn’t do anything. We need to add some CSS and JavaScript to give our message component the close and reappear functionality.
To hide the the message when the button is clicked, we will use a CSS class named collapse
. However, when the author is editing the component, this class cannot be applied. Use the wcmmode.edit
variable to control removing or adding the class. e.g.,
<sly data-sly-test.classCollapse="${wcmmode.edit || wcmmode.preview ? '' : 'collapse'}" />
<div class="cmp-message ${classCollapse}" data-cmp-is="message">...
Note that we’re preventing the collapse class from being rendered by testing for either edit or preview modes with the
||
operator.
We will also need to expose some component resource data and additional dialog content from the server for the message component JavaScript to use. This will be handled by data attributes in the template as follows.
<sly data-sly-test.classCollapse="${wcmmode.edit || wcmmode.preview ? '' : 'collapse'}" />
<div class="cmp-message ${classCollapse}"
data-cmp-is="message"
data-cmp-id="message${resource.path.hashCode}"
data-days-hidden="${properties.daysHidden}">
<div data-cmp-hook-message="text">${properties.text @ context='html'}</div>
<button>close</button>
</div>
...
CSS
Component CSS and JavaScript is located in the ui.frontend
folder of the project. Under /src/main/webpack/components
, create a Sass file named _message.scss
. e.g.,
_message.scss
.cmp-message {
transition: all .3s ease;
&.collapse {
height: 0;
visibility: collapse;
}
}
JavaScript
Under /src/main/webpack/components
, create a JavaScript file named _message.js
. You could also copy and modify _helloworld.js
. e.g.,
_message.js
import { getCookie, setCookie } from './modules/cookie';
// Example of how a component should be initialized via JavaScript
(function() {
"use strict";
// Best practice:
// For a good separation of concerns, don't rely on the DOM structure or CSS selectors,
// but use dedicated data attributes to identify all elements that the script needs to
// interact with.
var selectors = {
self: '[data-cmp-is="message"]',
text: '[data-cmp-hook-message="text"]'
};
function Message(config) {
const COLLAPSE_CLASS = 'collapse';
config.element.text = config.element.querySelector(selectors.text);
config.element.close = config.element.querySelector('button');
const COOKIE = config.element.text.textContent.trim();
const COOKIE_NAME = config.element.getAttribute('data-cmp-id');
const CURRENT_COOKIE = getCookie(COOKIE_NAME);
function close() {
const days = parseInt(config.element.getAttribute('data-days-hidden')) || 3;
setCookie(COOKIE_NAME, COOKIE, days);
config.element.classList.add(COLLAPSE_CLASS);
}
function init(config) {
// Best practice:
// To prevents multiple initialization, remove the main data attribute that
// identified the component.
config.element.removeAttribute("data-cmp-is");
if (console && console.log) {
console.log(
"Message component is loaded"
);
}
if (!CURRENT_COOKIE || CURRENT_COOKIE !== COOKIE) {
config.element.classList.remove(COLLAPSE_CLASS);
}
config.element.close.addEventListener("click", close);
}
if (config && config.element) {
init(config);
}
}
// Best practice:
// Use a method like this mutation obeserver to also properly initialize the component
// when an author drops it onto the page or modified it with the dialog.
function onDocumentReady() {
var elements = document.querySelectorAll(selectors.self);
for (var i = 0; i < elements.length; i++) {
new Message({ element: elements[i] });
}
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
var body = document.querySelector("body");
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
// needed for IE
var nodesArray = [].slice.call(mutation.addedNodes);
if (nodesArray.length > 0) {
nodesArray.forEach(function(addedNode) {
if (addedNode.querySelectorAll) {
var elementsArray = [].slice.call(addedNode.querySelectorAll(selectors.self));
elementsArray.forEach(function(element) {
new Message({ element: element });
});
}
});
}
});
});
observer.observe(body, {
subtree: true,
childList: true,
characterData: true
});
}
if (document.readyState !== "loading") {
onDocumentReady();
} else {
document.addEventListener("DOMContentLoaded", onDocumentReady);
}
}());
Notice the cookie module import at the very top of our _message.js
file. Let’s add that module as follows. Create a modules
folder under components
. e.g.,
cd ui.frontend
mkdir -p src/main/webpack/components/modules
Then create the cookie module.
modules/cookie.js
function getCookie(name) {
var val = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)');
return val ? val[2] : null;
}
function setCookie(name, value, days) {
var date = new Date;
date.setTime(date.getTime() + 24*60*60*1000*days);
document.cookie = name + "=" + value + ";path=/;expires=" + date.toGMTString();
}
export {
getCookie,
setCookie
};
Frontend Build
As of this writing, the frontend build entry-point did not have a components
import path.
In order to have the frontend build include the component JavaScript files, update the import path in the ui.frontend/src/main/webpack/site/main.ts
entry-point file. e.g.,
main.ts
// builds component files into site.js
import "../components/*.js";
Now we’re ready to build the client-side libraries using the ui.frontend
build process.
cd ui.frontend
npm run dev
When this process has completed, the projects clientlib-site/js/site.js
file will be built using the import paths from ui.frontend/src/main/webpack/site/main.ts
. Since we ran the dev
build, source maps are included.
The ui.frontend
build process README contains all the details.
Tip for updating the static HTML template for use with the Webpack dev server: use
wget
as shown in the example below.
cd ui.frontend
wget --user=admin --password=admin -O src/main/webpack/static/index.html http://localhost:4502/content/myproject/us/en.html\?wcmmode\=disabled
npm run start
Follow the instructions in the aem-project-archetype for building and deploying the project files to AEM.
I like using the AEM Repo tool for incremental file updates since Maven builds take some time. For example, to push updated client libraries into the AEM server, use
repo put
:
cd ui.apps/src/main/content/jcr_root
repo put apps/myproject/clientlibs/clientlib-site
Follow Up
This tutorial get’s you started and the styling is very basic. Customize the CSS of the component as needed in the _message.scss
Sass file. You could even absolute position the message like a modal, adding a max-width, border and/or drop shadow.
Source Code
Part 3 of 5 in the AEM Component Dev series.