Friday 17 February 2017

Creating a site footer in AEM 6 using Sightly and Sling Models

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 links and has a header text. A footer is the collection of the 3 columns, the copyright text, and a list of links.
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 Links 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 FooterColumns is exposed and gets built in the init method that executes after adapting the resource.
The same idea applies for the list of social Links. 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