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 as repo.

In AEM, navigate to the Experience Fragments, e.g., localhost:4502/aem/experience-fragments.html/content/experience-fragments.

Locate the Site Footer, and edit.

AEM Site Footer

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.

AEM Site Footer Edit

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,

Copyright component dialog content update

Which renders as,

© 2012- 2020 , My Project. All Rights Reseved.

For example,

AEM Site Footer

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
      • components
        • copyright
          • _cq_dialog
            • .content.xml
          • .content.xml
          • copyright.html
          • copyright.js
          • placeholder.js

Source Code

Part 1 of 5 in the AEM Component Dev series.

Folding Panel Component

comments powered by Disqus