ADOBE EXPERIENCE MANAGER (AEM) COMPONENTS
Copyright Component
An Adobe Experience Manager (AEM) example to demonstrate development of a proof of concept copyright component that optionally renders the current year from code. Maven AEM Project Archetype 23 and AEM version 6.5 are used to show this component within the footer experience fragment.
Features
- Current year data provided via Sling Model or JavaScript Use-API.
- Touch UI dialog event handler using clientlib script.
- Edit mode placeholder when the component is empty.
Getting Started
As with most AEM projects, we’re using a Maven project archetype for this. You could develop the component and clientlib directly in CRXDE Lite if you prefer since there will not be a Java code bundle dependency. However, If you do not have an existing AEM Maven project to work with, I highly recommend you use one. This AEM Maven Project series can guide you through the setup process.
Copying the existing
helloworld
component included with the archetype is an easy way to jump start development.
Component Folder
In the the components
folder of your project, e.g., ui.apps/src/main/content/jcr_root/apps/myproject/components
create a folder named copyright
.
In order for this to be a component, there are properties that need to be assigned to the copyright
folder. Within the folder, create a .content.xml
file as follows for this. Update the componentGroup
property as needed.
.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="Copyright"
componentGroup="myproject - Content"/>
Component Dialog
If you copied the helloworld
component, you should already have a dialog to start with. Otherwise, create a folder named _cq_dialog
in the copyright
folder.
In the _cq_dialog
folder, create a .content.xml
file as follows. At this point, the dialog is exactly the same as the helloworld
component and contains only a single text input. Ultimately, this input will be used to display the text that comes after the copyright year.
_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">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<text
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Text"
name="./text"/>
</items>
</column>
</items>
</content>
</jcr:root>
Component Use-API JavaScript
In the copyright
component folder, create a server-side copyright.js
JavaScript file that defines the use-class and utilizes the java.util.Calendar
class to return the current year to the copyright.html
template.
copyright.js
"use strict";
use( function() {
var data = {};
var Calendar = Packages.java.util.Calendar;
data.currentYear = Calendar.getInstance().get(Calendar.YEAR);
return data;
});
If you’re building the project with Maven, use the Component Sling Model below instead of the JavaScript Use-API to return the current year to the copyright component template.
Component Sling Model
For this example, I copied the HelloWorlModel.java
from core/src/main/java/com/myproject/core/models
and modified it as needed. e.g.,
cd core/src/main/java/com/myproject
cp core/models/HelloWorldModel.java core/models/CoprightModel.java
CopyrightModel.java
package com.myproject.core.models;
import static org.apache.sling.api.resource.ResourceResolver.PROPERTY_RESOURCE_TYPE;
import javax.annotation.PostConstruct;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.Default;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
import org.apache.sling.models.annotations.injectorspecific.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.apache.sling.settings.SlingSettingsService;
import java.util.Calendar;
@Model(adaptables = Resource.class)
public class CopyrightModel {
@ValueMapValue(name=PROPERTY_RESOURCE_TYPE, injectionStrategy=InjectionStrategy.OPTIONAL)
@Default(values="No resourceType")
protected String resourceType;
@OSGiService
private SlingSettingsService settings;
@SlingObject
private Resource currentResource;
@SlingObject
private ResourceResolver resourceResolver;
private int currentYear;
@PostConstruct
protected void init() {
currentYear = Calendar.getInstance().get(Calendar.YEAR);
}
public int getCurrentYear() {
return currentYear;
}
}
Component Template
In the the copyright
component folder, create an html
file with the same name, e.g., copyright.html
. The div
element containing the copyright content uses a data-sly-use.copyright
attribute to implement the use-API JavaScript file. The data returned by the copyright.js
file is available on the copyright
node defined by the attribute.
copyright.html
Using JavaScript Use-API
<div data-sly-use.copyright="${'copyright.js'}">
${copyright.currentYear}
${properties.text}
</div>
Using Sling Model
<div data-sly-use.model="com.myproject.core.models.CopyrightModel">
${model.currentYear}
${properties.text}
</div>
Install
Now would be a good time to deploy the component into AEM to verify the current state of progress.
You can use mvn clean install to build and deploy it. e.g.,
mvn -PautoInstallPackage clean install
Once the project is installed, you may decide to transfer files using the AEM repo tool, FileVault VLT or Eclipse. You could also choose to run the Maven build again. However, even with
-DskipTests
, it can be more time consuming than using tools such asrepo
.
Footer Experience Fragment
In AEM, navigate to the Experience Fragments, e.g., localhost:4502/aem/experience-fragments.html/content/experience-fragments.
Locate the Site Footer, and edit.
Drag or Add the copyright component into the Footer Experience Fragment. At this point, only the year is shown. The copyright notice that is shown above our copyright component is text with a static year and sample content using the text component.
The Maven AEM Project Archetype 23 used to build this proof of concept provides the structure and sample content such as the footer experience fragment and text component.
Component Dialog Fields
In the copyright/_cq_dialog/.content.xml
dialog content file, update these items/text
node properties:
fieldLabel="Text After"
fieldDescription="Text to display after the year."
Then, add these two fields as shown below in the dialog content file.
- Text Before:
prepend
- Year and Text Before:
year
_cq_dialog/.content.xml
<items jcr:primaryType="nt:unstructured">
<prepend
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Text Before"
fieldDescription="Text to display before the year."
name="./prepend"
value="©"/>
<text
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Text After"
fieldDescription="Text to display after the year."
name="./text"/>
<year
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/switch"
fieldLabel="Year and Text Before"
fieldDescription="Display the current year and allow Text Before it."
name="./renderYear"
value="true"
checked="true"
uncheckedValue="false"/>
</items>
With these fields, there are more copyright notice options utilizing the code driven current year. For example,
- Text Before field contains “Copyright ©”
- The end of Text Before field contains the first year of publication followed by a hyphen to show a range. e.g., “Copyright © 2012-”
- Switch off the display of the current year provided by the calendar class. In the following section, an event handler is added to this switch to disable/enable the Text Before field as well. For internationalization, many countries do not use a copyright notice and/or year on their pages.
copyright.html
Using JavaScript Use-API
<div data-sly-use.copyright="${'copyright.js'}">
<sly data-sly-test="${properties.renderYear}">
<span data-sly-test="${properties.prepend}">${properties.prepend}</span>
${copyright.currentYear}</sly>
${properties.text}
</div>
Using Sling Model
<div data-sly-use.model="com.myproject.core.models.CopyrightModel">
<sly data-sly-test="${properties.renderYear}">
<span data-sly-test="${properties.prepend}">${properties.prepend}</span>
${model.currentYear}</sly>
${properties.text}
</div>
Component Dialog Event Handler
Option 1, under the clientlibs
folder, e.g., apps/myproject/clientlibs
, create a clientlib-edit
folder.
Option 2, create a clientlib-edit
folder in the copyright
component folder, e.g., apps/myproject/components/copyright
.
In order for this to be a clientlibs
folder, there are properties that need to be assigned to it. In the clientlib-edit
folder, create a .content.xml
file as follows for this.
.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:ClientLibraryFolder"
categories="[cq.authoring.dialog]" />
You could also add the dependencies="[cq.jquery]"
property to the folder, however, it doesn’t seem to be needed in this project since jQuery is loaded by default for the authoring environment. You could also use vanilla JavaScript for the handler instead of jQuery. Typically, I will use jQuery for the cq.authoring.dialog
category which is consistent with AEM and vanilla JavaScript when using a frontend build process such as Webpack.
You can use these dialog events to perform UI actions as needed. e.g., $document.on("dialog-ready", ...
Event | Description |
---|---|
dialog-ready | the dialog has been opened |
dialog-success | the dialog content has been saved |
dialog-closed | the dialog has been closed |
Create a dialog.js
JavaScript file in the clientlib-edit
folder. Then, using jQuery, add a dialog-ready
on event handler. The following handler code runs when the dialog is opened using this event. Remove or comment out the console.log
statements as needed for testing. The year switch value determines if the Text Before input will be disabled or enabled.
dialog.js
(function ($, $document) {
console.log('--- dialog.js loaded ---');
$document.on("dialog-ready", function() {
console.log('--- dialog-ready ---');
var $renderYearSwitch = $('[name="./renderYear"]');
/**
* if renderYear switch control exists:
*/
if ($renderYearSwitch.length) {
var $prependText = $('[name="./prepend"]');
/**
* disable the prepend text input when
* the switch is off
*/
if (!$renderYearSwitch.prop('checked')) {
$prependText.prop('disabled', true);
}
$renderYearSwitch.on('change', function() {
if(this.checked) {
$prependText.prop('disabled', false);
} else {
$prependText.prop('disabled', true);
}
});
}
});
})($, $(document));
Create a js.txt
file in the clientlib-edit
folder. This manifest file lists each JavaScript file included in the folder on a single line that gets merged into the generated /libs/cq/gui/components/authoring/dialog/clientlibs/all.js
.
Be advised, at browser viewport widths less than 1024 pixels, the dialog will open in fullscreen as a cq-page-dialog. When this happens, the
"dialog-ready"
event does not fire.
js.txt
dialog.js
Use Cases
There are different styles and formats used for displaying a copyright notice. This Sample Copyright Notices page contains a good bit of informaton on the subject.
Spaces between the property output in the template do not allow a hyphen without a space directly before the year or a comma directly after the current year. For example, consider this dialog content,
Which renders as,
© 2012- 2020 , My Project. All Rights Reseved.
For example,
1. Update the copyright template as follows so there are not any spaces between the property values. This solution however, leaves it up to the author to add a Text Before trailing space, and/or a Text After leading space as needed.
<div data-sly-use.copyright="${'copyright.js'}">
<sly data-sly-test="${properties.renderYear}">
${properties.prepend}${copyright.currentYear}</sly>${properties.text}
</div>
2. To solve the manual space entry issue above, update the dialog.js
adding a function to the submit checkmark click event. When the form is saved, spaces are added around the year if there isn’t a hyphen before or a comma after.
dialog.js
(function ($, $document) {
$document.on("dialog-ready", function() {
var $renderYearSwitch = $('[name="./renderYear"]');
/**
* if renderYear switch control exists:
*/
if ($renderYearSwitch.length) {
var prepend,
text,
lastChar,
firstChar,
alphanumeric,
$prependInput = $('[name="./prepend"]'),
$textInput = $('[name="./text"]');
/**
* disable the prepend text input when
* the switch is off
*/
if (!$renderYearSwitch.prop('checked')) {
$prependInput.prop('disabled', true);
}
$renderYearSwitch.on('change', function() {
if(this.checked) {
$prependInput.prop('disabled', false);
} else {
$prependInput.prop('disabled', true);
}
});
/**
* when the dialog form is saved
*/
$('.cq-dialog-submit').on("click", function () {
prepend = $prependInput.val();
text = $textInput.val();
// last character of prepend
lastChar = prepend.slice(-1);
// first character of text
firstChar = text.charAt(0);
// regex to look for alpha-numeric char
alphanumeric = /^[0-9a-zA-Z]+$/;
/**
* check prepend input value on save
* and add trailing space as needed.
*/
if (lastChar.indexOf('-') === -1) {
prepend = prepend + ' ';
$prependInput.val(prepend);
}
/**
* check text input value on save
* and add leading space as needed.
*/
if (firstChar.match(alphanumeric)) {
text = ' ' + text;
$textInput.val(text);
}
});
}
});
})($, $(document));
Edit Mode Placeholder
If you were to clear the text inputs and toggle the year switch off. There would not be any content to render making the component not visible or selectable for editing. To fix this, we will add some HTL to the copyright template that implements a JavaScript Use API file for the logic.
Create this placeholder.js
file in the copyright
component folder as follows.
placeholder.js
"use strict";
use( function() {
var placeholder = false;
var renderYear = properties.get("renderYear");
var text = properties.get("text");
if (renderYear == null || renderYear == 'false' && text == null) {
placeholder = true;
}
return placeholder;
});
Update the copyright component template adding the placeholder Sightly (HTL) at the bottom. We’re only using the placeholder when wcmmode.edit
is true.
copyright.html
Using JavaScript Use-API
<div data-sly-use.copyright="${'copyright.js'}">
<sly data-sly-test="${properties.renderYear}">
${properties.prepend}${copyright.currentYear}</sly>${properties.text}
</div>
<!--/* edit mode placeholder */-->
<sly data-sly-test="${wcmmode.edit}" data-sly-use.placeholder="${'placeholder.js'}">
<div data-sly-test="${placeholder}"
style="min-height: 20px;"
data-emptytext="${component.properties.jcr:title}"></div>
</sly>
Using Sling Model
<div data-sly-use.model="com.myproject.core.models.CopyrightModel">
<sly data-sly-test="${properties.renderYear}">
${properties.prepend}${model.currentYear}</sly>${properties.text}
</div>
<!--/* edit mode placeholder */-->
<sly data-sly-test="${wcmmode.edit}" data-sly-use.placeholder="${'placeholder.js'}">
<div data-sly-test="${placeholder}"
style="min-height: 20px;"
data-emptytext="${component.properties.jcr:title}"></div>
</sly>
Apps File Structure
Here is the folder and file structure within the apps folder for the clientlibs and copyright component code created in this proof of concept.
- apps
- myproject
- clientlibs
- clientlib-edit
- .content.xml
- dialog.js
- js.txt
- clientlib-edit
- components
- copyright
- _cq_dialog
- .content.xml
- .content.xml
- copyright.html
- copyright.js
- placeholder.js
- _cq_dialog
- copyright
- clientlibs
- myproject
Source Code
Part 1 of 5 in the AEM Component Dev series.