May 3, 2020
Estimated Post Reading Time ~

Custom AEM ClientLibs Markup for HTML5

AEM clientlibs are extremely powerful. They allow you to produce client-side JavaScript and CSS libraries while controlling minification, concatenation, and dependency management. However, out-of-the-box, they don't allow you to customize the HTML output. While optimizing your website for speed, you may want to use the defer, async, and/or onload attributes on your script elements. Likewise, while managing your cross-origin resource sharing (CORS) HTTP requests, you may want to utilize the crossorigin attribute on your script and/or link elements for JavaScript and CSS.

Download project from GitHub

Using clientlibs in AEM with Sightly is demonstrated in the AEM documentation Using Client-Side Libraries, as well as Feike Visser's Sightly intro part 5: FAQ.
SightlyClientLibUsage.html
<head data-sly-use.clientLib="${'/libs/granite/sightly/templates/clientlib.html'}">

<!--/* for css+js */-->
<meta data-sly-call="${clientLib.all @ categories='your.clientlib'}" data-sly-unwrap></meta>

<!--/* only js */-->
<meta data-sly-call="${clientLib.js @ categories='your.clientlib'}" data-sly-unwrap></meta>

<!--/* only css */-->
<meta data-sly-call="${clientLib.css @ categories='your.clientlib'}" data-sly-unwrap></meta>


</head>


When we follow the data-sly-use block's expression value in our AEM instance, we find three files that work together with the HtmlLibraryManager to provide the final clientlib output. clientlib.html contains named Sightly template blocks ( js, css, all), which in turn set the proper Sightly expression option for mode and call the named Sightly templates in graniteClientLib.html. graniteClientLib.html then sets the proper Sightly expression options and delegates the more complex logic to a Java-Use API POJO. ClientLibUseObject.java then utilizes the HtmlLibraryManager in order to write out the final HTML markup.

All files associated with Sightly clientlibs are viewable in your AEM instance under the /libs/granite/sightly/templates node. To make any alterations, we simply copy all three files out of /libs into /apps, update the files, and update our Sightly components to point to the new clientlib component (usually headlibs.html or similar partial in the page component).
CustomSightlyClientLibUsage.html
<head data-sly-use.clientLib="${'/apps/clientlib-async/sightly/templates/clientlib.html'}">
<!--/* for css+js */-->
<meta data-sly-call="${clientLib.all @ categories='your.clientlib'}" data-sly-unwrap></meta>
</head>

In our case, we want to alter the HTML markup by adding additional attributes to the script and link elements. In order to do that, we first add additional Sightly expression options to all the Sightly templates. In this example, I'm using loading, onload, and crossorigin options.
CustomSightlyClientLib.html
<!--/*
For the entire file, see: https://github.com/nateyolles/aem-clientlib-async/blob/master/clientlib-async/apps/clientlib-async/sightly/templates/clientlib.html
*/-->
<template data-sly-template.all="${@ categories='Client Library categories',
loading='Accepts async and defer',
onload='JavaScript to run for async and defer',
crossorigin='Accepts anonymous and use-credentials'}">
<section data-sly-test="${request.getResourceResolver}"
data-sly-use.clientlib="${'/apps/clientlib-async/sightly/templates/graniteClientLib.html'}"
data-sly-call="${clientlib.include @ categories=categories, loading=loading, onload=onload, crossorigin=crossorigin}"
data-sly-unwrap>
</section>
</template>

<!--/*
For the entire file, see: https://github.com/nateyolles/aem-clientlib-async/blob/master/clientlib-async/apps/clientlib-async/sightly/templates/graniteClientlib.html
*/-->
<template data-sly-template.include="${@ categories='Client Library categories',
mode='optional: JS or CSS, case-insensitve',
loading='optional: JS async or defer',
onload='optional: JS to run for async and defer',
crossorigin='optional: accepts anonymous and use-credentials'}"
data-sly-use.clientlib="${'apps.clientlib_async.sightly.templates.ClientLibUseObject' @ categories=categories, mode=mode, loading=loading, onload=onload, crossorigin=crossorigin}">
${clientlib.include @ context='unsafe'}
</template>


Once the new Sightly expression options are set, the Java-Use POJO is updated to obtain those options from the provided bindings. AEM hands the responsibility of printing the final HTML markup to the HtmlLibraryManager's writeIncludes, writeCssInclude and writeJsInclude methods. However, since we want custom markup, we need to do that ourselves in the POJO.
ClientLibUseObject.java
package apps.clientlib_async.sightly.templates;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collection;
import java.util.List;
import java.util.ArrayList;

import javax.script.Bindings;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.scripting.SlingBindings;
import org.apache.sling.api.scripting.SlingScriptHelper;
import org.apache.sling.api.resource.Resource;

import com.day.cq.widget.ClientLibrary;
import com.day.cq.widget.HtmlLibraryManager;
import com.day.cq.widget.LibraryType;

import com.adobe.granite.xss.XSSAPI;

import org.slf4j.Logger;

import io.sightly.java.api.Use;

/**
* Sightly Clientlibs that can accept expression options for 'defer', 'async'
* 'onload' and 'crossorigin'.
*
* See: https://github.com/nateyolles/aem-clientlib-async
*
* This class is mostly code from /libs/granite/sightly/templates/ClientLibUseObjec.java,
* found in your local AEM instance. The differences are that this class gets
* the 'loading' and 'onload' attributes, gets the categories retrieved from
* {@link com.day.cq.widget.HtmlLibraryManager#getLibraries(String[], LibraryType, boolean, boolean)}
* and writes it's own HTML script elements rather than have the HtmlLibrary
* manger do it for us using {@link com.day.cq.widget.HtmlLibraryManager#writeIncludes(SlingHttpServletRequest, Writer, String...)}.
*
* @author Nate Yolles <yolles@adobe.com>
* @version 2.0.0
* @since 2015-03-19
* @see libs.granite.sightly.templates.ClientLibUseObject
* @see com.day.cq.widget.HtmlLibraryManager
*/
public class ClientLibUseObject implements Use {

private static final String BINDINGS_CATEGORIES = "categories";
private static final String BINDINGS_MODE = "mode";

/**
* Sightly parameter that becomes the script element void attribute such as
* 'defer' and 'async'. Valid values are listed in {@link #VALID_JS_ATTRIBUTES}.
*/
private static final String BINDINGS_LOADING = "loading";

/**
* Sightly parameter that becomes the javascript function value in the
* script element's 'onload' attribute.
*/
private static final String BINDINGS_ONLOAD = "onload";

/**
* Sightly parameter that becomes the value in the script and link elements'
* 'crossorigin' attribute.
*/
private static final String BINDINGS_CROSS_ORIGIN = "crossorigin";

/**
* HTML markup for javascript. Add 'type="text/javascript"' if you are not
* using HTML 5.
*/
private static final String TAG_JAVASCRIPT = "<script src=\"%s\"%s></script>";

/**
* HTML markup for stylesheets.
*/
private static final String TAG_STYLESHEET = "<link rel=\"stylesheet\" href=\"%s\"%s>";

/**
* HTML markup for onload attribute of script element.
*/
private static final String ONLOAD_ATTRIBUTE = " onload=\"%s\"";

/**
* HTML markup for crossorigin attribute of script and link elements.
*/
private static final String CROSS_ORIGIN_ATTRIBUTE = " crossorigin=\"%s\"";

/**
* Valid void attributes for HTML markup of script element.
*/
private static final List<String> VALID_JS_ATTRIBUTES = new ArrayList<String>(){{
add("async");
add("defer");
}};

/**
* Valid values for crossorigin attribute for HTML markup of script and link
* elements.
*/
private static final List<String> VALID_CROSS_ORIGIN_VALUES = new ArrayList<String>(){{
add("anonymous");
add("use-credentials");
}};

private HtmlLibraryManager htmlLibraryManager = null;
private String[] categories;
private String mode;
private String loadingAttribute;
private String onloadAttribute;
private String crossoriginAttribute;
private SlingHttpServletRequest request;
private PrintWriter out;
private Logger log;
private Resource resource;
private XSSAPI xssAPI;

/**
* Same as AEM provided method with the addition of getting the XSSAPI
* service and the two additional bindings for loading and onload.
*
* @see libs.granite.sightly.templates.ClientLibUseObject#init(Bindings)
*/
public void init(Bindings bindings) {
loadingAttribute = (String) bindings.get(BINDINGS_LOADING);
onloadAttribute = (String) bindings.get(BINDINGS_ONLOAD);
crossoriginAttribute = (String) bindings.get(BINDINGS_CROSS_ORIGIN);
resource = (Resource) bindings.get("resource");

Object categoriesObject = bindings.get(BINDINGS_CATEGORIES);
if (categoriesObject != null) {
if (categoriesObject instanceof Object[]) {
Object[] categoriesArray = (Object[]) categoriesObject;
categories = new String[categoriesArray.length];
int i = 0;
for (Object o : categoriesArray) {
if (o instanceof String) {
categories[i++] = ((String) o).trim();
}
}
} else if (categoriesObject instanceof String) {
categories = ((String) categoriesObject).split(",");
int i = 0;
for (String c : categories) {
categories[i++] = c.trim();
}
}
if (categories != null && categories.length > 0) {
mode = (String) bindings.get(BINDINGS_MODE);
request = (SlingHttpServletRequest) bindings.get(SlingBindings.REQUEST);
log = (Logger) bindings.get(SlingBindings.LOG);
SlingScriptHelper sling = (SlingScriptHelper) bindings.get(SlingBindings.SLING);
htmlLibraryManager = sling.getService(HtmlLibraryManager.class);
xssAPI = sling.getService(XSSAPI.class);
}
}
}

/**
* Essentially the same as the AEM provided method with the exception that
* the HtmlLibraryManger's writeIncludes methods have been replaced with
* calls to #includeLibraries.
*
* @see libs.granite.sightly.templates.ClientLibUseObject#include()
*/
public String include() {
StringWriter sw = new StringWriter();

if (categories == null || categories.length == 0) {
log.error("'categories' option might be missing from the invocation of the /apps/beagle/sightly/templates/clientlib.html" +
"client libraries template library. Please provide a CSV list or an array of categories to include.");
} else {
PrintWriter out = new PrintWriter(sw);
if ("js".equalsIgnoreCase(mode)) {
includeLibraries(out, LibraryType.JS);
} else if ("css".equalsIgnoreCase(mode)) {
includeLibraries(out, LibraryType.CSS);
} else {
includeLibraries(out, LibraryType.CSS);
includeLibraries(out, LibraryType.JS);
}
}

return sw.toString();
}

/**
* Construct the HTML markup for the script and link elements.
*
* @param out The PrintWriter object responsible for writing the HTML.
* @param LibraryType The library type either CSS or JS.
*/
private void includeLibraries(PrintWriter out, LibraryType libraryType) {
if (htmlLibraryManager != null && libraryType != null && xssAPI != null) {
Collection<ClientLibrary> libs = htmlLibraryManager.getLibraries(categories, libraryType, false, false);

String attribute = StringUtils.EMPTY;

if (libraryType.equals(LibraryType.JS)) {
if (StringUtils.isNotBlank(loadingAttribute) && VALID_JS_ATTRIBUTES.contains(loadingAttribute.toLowerCase())) {
attribute = " ".concat(loadingAttribute.toLowerCase());
}

if (StringUtils.isNotBlank(onloadAttribute)) {
String safeOnload = xssAPI.encodeForHTMLAttr(onloadAttribute);

if (StringUtils.isNotBlank(safeOnload)) {
attribute = attribute.concat(String.format(ONLOAD_ATTRIBUTE, safeOnload));
}
}
}

if (StringUtils.isNotBlank(crossoriginAttribute) && VALID_CROSS_ORIGIN_VALUES.contains(crossoriginAttribute.toLowerCase())) {
attribute = attribute.concat(String.format(CROSS_ORIGIN_ATTRIBUTE, crossoriginAttribute.toLowerCase()));
}

for (ClientLibrary lib : libs) {
out.write(String.format(libraryType.equals(LibraryType.JS) ? TAG_JAVASCRIPT : TAG_STYLESHEET, lib.getIncludePath(libraryType, htmlLibraryManager.isMinifyEnabled()), attribute));
}
}
}
}


I've written a demonstration AEM project as well as provided the code which can easily be installed into a single folder for use in your project. There is also an AEM package available for an easy install. View the AEM Clientlib Async project on GitHub.


By aem4beginner

No comments:

Post a Comment

If you have any doubts or questions, please let us know.