The Adobe-developed and recently-donated-to-Apache-Sling project Sightly project has been out for a little under a year now, alongside Adobe Experience Manager 6, and has slowly been amassing documentation and gaining presence. The server-side templating language aims to give a facelift to the web development facet of Java-based software stacks, Adobe's AEM chief among them.
This post will run the reader through a sample implementation of a site footer using Sightly, showcasing and describing a few of its features. It will also make use of Sling Models as the back-end tool to grab JCR data (nodes, properties) into useful class models.
What we want is an authorable footer serving all pages of a language branch of a content tree. Let's assume that our site is for Acme, the reputable maker of all things widgets, gadgets, and sprockets. Our site structure will be traditional and look like,
/content/acme/en
/content/acme/en/about-us
/content/acme/en/news
/content/acme/en/products
/content/acme/en/contact
and so on. Let's assume that the
en
page serves as the homepage for the English site, and similarly, the /content/acme/fr
will serve as the French homepage, with all French content pages under it. As such, each homepage will need to have an instance of footer data.
For the sake of simplicity this post does not consider Language Copy, Live Copy, or any kind of translation implementation or methodology. If you're interested in translating content directly within AEM, do take a look at our AEM-LingoTek Translation Connector!
Like everything in technology, there are many ways to go about solving this requirement. One approach might be to create a cq:Component with a traditional dialog, and place it in an inherited paragraph system (
iparsys
) located on the footer. This way, the component is instantiated once, and all child pages the implement the iparsys will inherit it. I've used this approach a few times with success.
Another approach is to manage the footer data not by a component, but as part of the page-component the homepage. Specfically, by defining an extra set of Page Properties only for the homepage page-component, such that the data lives at the homepage node, like the
en
page defined earlier. With this approach, we don't develop a full-blown cq:Component. Instead, we make use of a simple Sightly script that's part of the page-component for the homepage.From mockup to markup
To start, let's assume the footer looks like:
Here's some markup to represent it, without consideration for style or layout:
<footer>
<div>
<ul>
<li>
About Us
<ul>
<li><a href="#">Environment</a></li>
<li><a href="#">History</a></li>
<li><a href="#">Boards</a></li>
</ul>
</li>
<li>
Widgets
<ul>
<li><a href="#">Sprockets</a></li>
<li><a href="#">Lobarts</a></li>
<li><a href="#">Plackerts</a></li>
</ul>
</li>
<li>
News & events
<ul>
<li><a href="#">Upcoming</a></li>
<li><a href="#">Press</a></li>
</ul>
</li>
</ul>
</div>
<div>
<p>(c) 2015 Acme Inc.</p>
<ul>
<li><a href="#">Facebook</a></li>
<li><a href="#">Twitter</a></li>
<li><a href="#">LinkedIn</a></li>
</ul>
</div>
</footer>
Page properties
Assuming the following structure in our
apps
folder:/apps/acme/components/pages/base
/apps/acme/components/pages/home
/apps/acme/templates/base
/apps/acme/templates/home
then the above markup can live very simply inside
footer.html
as part of the base
page-component.
Next, let's define an extra tab in the page properties of the homepage. Either copy the file
dialog.xml
from the base
page-component or the foundation page component from which all page components should inherit (either directly or indirectly). Then simply an add an entry in <items>
like,<footer
jcr:primaryType="cq:Widget"
path="/apps/acme/components/pages/home/tab_footer.infinity.json"
xtype="cqinclude"/>
Then define the properties required like you would any component dialog. This exercise is left to the reader. Let's assume the file
tab_footer.xml
has all the required xtypes to handle the footer data defined earlier.Let's model
Thinking about data and abstracting it into useful class representations is a wonderful thing. It helps developers understand what the nature of the domain data, and how it should be manipulated. A site footer being a rather commonplace WCMS feature does not require much analysis, and the modelling is straightforward. It has
- 3 columns of links
- text for the copyright
- social media links
We need to represent a
link
, a column
, and a footer
. A link
is simply pairing a title and a URL. A column
is contains a list of link
s and has a header text. A footer
is the collection of the 3 column
s, the copyright text, and a list of link
s.
We can now write our models and place them inside the same package. We use the
@Model
annotation provided by Sling Models, turning the class into an Adaptable, one of the keystone features of Sling. We omit the imports for the sake of legibility.
Link.java
This model allows us to call
adaptTo
on a org.apache.sling.api.resource.Resource
that has a property name
and link
(both required). This allows us to think and use a link as a Link
.package com.acme.components.models;
@Model(adaptables=Resource.class)
public class Link {
@Inject
public String name;
@Inject
public String link;
}
FooterColumn.java
This model allows us to call
adaptTo
on a Resource
that has a String property header
and a child Resource
called links
. In the PostConstruct
method, we build the list of Link
s by iterating over the children of links
. Here I want to expose a public member variable links
which unfortunately collides with the property name links
. The @Named
annotation allows me to fix that by naming the injected variable differently (linksResource
).package com.acme.components.models;
@Model(adaptables=Resource.class)
public class FooterColumn {
@Inject
public String header;
@Inject @Named("links")
private Resource linksResource;
public List<Link> links;
@PostConstruct
protected void init() throws Exception {
links = new ArrayList<Link>();
if(linksResource != null) {
Iterator<Resource> linkResources = linksResource.listChildren();
while(linkResources.hasNext()) {
Link link = linkResources.next().adaptTo(Link.class);
if(link != null)
links.add(link);
}
}
}
}
Footer.java
This is the representation of a site footer making use of the above two models. It has three injected
Resource
objects that are the 3 columns: these are the Resource
objects that get saved to the JCR when authors edit the page properties of the homepage. The name of the property must match the name of the variable, or else be annotated with @Named("propertyName")
with another variable name. The three Resource
objects are private member variables, while the List
of FooterColumn
s is exposed and gets built in the init
method that executes after adapting the resource.
The same idea applies for the list of social
Link
s. The copyright text is a simple injected property of the resource being adapted.
We add null checks in the
init()
method because it's possible that a column resource does not exist, but we still want the footer to be constructed with whatever data is available. Read up on the @Optional
annotation which specifies whether an injected property is required during construction of the model.package com.acme.components.models;
@Model(adaptables=Resource.class)
public class Footer {
// Our 3 columns and exposed list of columns
@Inject @Optional
private Resource column1;
@Inject @Optional
private Resource column2;
@Inject @Optional
private Resource column3;
public List<FooterColumn> columns;
// Our copyright text
@Inject
public String copyright;
// Our list of social media links
@Inject @Optional
private Resource social;
public List<Links> socialLinks;
@PostConstruct
protected void init() throws Exception {
columns = new ArrayList<FooterColumn>();
if (column1 != null)
columns.add(column1.adaptTo(FooterColumn.class));
if (column2 != null)
columns.add(column2.adaptTo(FooterColumn.class));
if (column3 != null)
columns.add(column3.adaptTo(FooterColumn.class));
if(social != null) {
socialColumn = social.adaptTo(FooterColumn.class);
if(socialColumn != null)
socialLinks = socialColumn.links;
}
}
}
Sightly, back-end
The last piece of the back-end puzzle is to provided a Use class. The Java Use-API provided by AEM allows the separation of the presentation layer (the view, the markup) from the business logic. Part of that business logic was handled already by defining models to work with. As a result, our Use class will be straightforward!
The WCMUse class is a utility class in AEM that's part of the Sightly package, and it's a handy class indeed. It gives us shortcuts to get properties, get the page manager, the current Resource, and so on. Here we write a class that we call
FooterUse
that extends WCMUse
. This is going to be the link between the Sightly HTML file and the models above.
In this class, the
activate()
method executes when the Use class is invoked by a front-end component, which will define in the next and final step. In this method, we're looking upward to find the homepage, i.e. a page whose template matches the homepage template. When we find it, we get the footer
node under the page's content node (jcr:content
) and adapt it to our Sling model above. Our Use class then exposes a public Footer member variable, and therein lies the secret sauce! Now our HTML file can make full use of it to output all that it needs.
FooterUse.java
package com.acme.components.use;
public class FooterUse extends WCMUse {
public static final String HOME_PAGE_TEMPLATE_PATH = "/apps/acme/templates/home";
public Footer footer;
@Override
public void activate() {
Page homePage = getHomePage(getCurrentPage());
if(homePage != null) {
Resource footerResource = homePage.getContentResource("footer");
if(footerResource != null) {
footer = footerResource.adaptTo(Footer.class);
}
}
}
private Page getHomePage(Page current) {
try {
while (current != null) {
String pageTemplate = current.getProperties()
.get(NameConstants.PN_TEMPLATE, "");
if (HOME_PAGE_TEMPLATE_PATH.equals(pageTemplate)) {
return current;
}
// else keep going up
current = current.getParent();
}
} catch(Exception e) {
// log the error
}
return null;
}
}
Sightly, front-end
Now that we have the back-end models and the Use class squared away, we work our way toward the front end. We will take the markup defined before, and apply the Sightly directives.
footer.html
<footer data-sly-use.footerUse="com.acme.components.use.FooterUse">
<div>
<ul data-sly-list.footerColumn="${footerUse.footer.columns}">
<li>
${footerColumn.header}
<ul data-sly-list.link="${footerColumn.links}">
<li><a href="${link.link}">${link.name}</a></li>
</ul>
</li>
</ul>
</div>
<div>
<p>${footerUse.footer.copyright}</p>
<ul data-sly-list.link="${footerUse.footer.socialLinks}">
<li><a href="${link.link}">${link.name}</a></li>
</ul>
</div>
</footer>
We make use of Sightly's
data-sly-use
to invoke the FooterUse
class, which kicks off the back-end portion. Then, we use Sightly's data-sly-list
to iterate through items of the exposed Footer model as necessary.
I hope this post has been useful in demonstrating some of the techniques that are possible for creating components in Adobe AEM using Sightly and Sling models! Happy coding.
No comments :
Post a Comment