March 30, 2021
Estimated Post Reading Time ~

A practical guide to building SPAs on AEM using React

This guide is a bit outdated. If you are looking for a pure SPA solution (i.e. React-only components), I would recommend looking at Developing SPAs for AEM by Adobe. The guide below is now for scenarios where you want to have HTL-based components live next to React-based components.

When I talked about modern front-end productivity on AEM last year, I lamented how there wasn't a good solution for Javascript templating (really, Single Page Applications).

Templating... templating is a challenging one that I don't think has been solved sufficiently. Handlebars is a natural fit, but it misses the mark in certain contexts. If I'm doing a new AEM project today, I'm using HBS, but I'm yearning for something better.

The dust has settled a bit since that post:

  • Sling Model Exporters have been shipping for a year. 
  • Content Fragments are now naturally exposed as JSON.
  • Core Components 2.0 are now backed by Sling Model Exporters.
  • React has gone MIT.

We're still in a bit of a holding pattern, though. We likely still need to rely on HTL (server-side rendering) in 2018. Especially if we want 100% global SEO coverage. So what do we want from an SPA framework?

Ideal SPA Requirements

  1. 100% global SEO coverage
  2. Little-to-no Flashes Of Unstyled Content (FUOC)
  3. Authorable
  4. Front-end developer friendly... community momentum, documentation, hire-ability, etc.
  5. Mix and match with existing HTL components

Pick two? Maybe?

Enter React DOM Components

In short, React DOM Components allow a developer to build Javascript data models with regular old DOM. This DOM could be built using PHP, .NET, or in our case, Java (HTL). Want to build your Javascript model with data attributes? Child nodes? Child Arrays? Text Content? The API allows for simple or complex models to be built so data can be passed from server-side DOM to a React Component.

How does this look in AEM land?

At a high level, like this:

  1. The Sling Model (with Exporter) powers both server-side and client-side rendering.
  2. The HTL component is built the same way as always (but with a custom element name for efficiency)
  3. A DOM Model is built to define the attributes, nodes, and content to be used from DOM.
  4. A DOM Component is built to find the custom element, build the data model, and pass that data to a React Component.
  5. The React Component is just a vanilla React Component.
  6. A DAO class is built to fetch data from the Sling Model Exporter. Click a button? The React Component tells the DAO to fetch the data and update the state.

The big down-side to this process is that we're essentially building two models and two views. It's not ideal, but it lets each team (FE & BE) continue to build in ways they are familiar with. A back-end developer can still work in Java. A front-end developer who has never used AEM can work in almost 100% pure React the day they start the job.

React DOM Components are also AEM-friendly. They know when editor.html has mutated a page for authoring and pushes the state to the React Component automatically.

Sample Code

Step 1 - Sling Model

package org.millr.core.models;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
@Model(
adaptables = { SlingHttpServletRequest.class },
resourceType = "pugranch/components/content/tabControl",
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
@Exporter(name = "jackson", extensions = "json")
public class TabControl {
@ValueMapValue
private String title;
@SlingObject
private SlingHttpServletRequest request;
@SlingObject
private ResourceResolver resolver;
@SlingObject
private Resource resource;
public String getTitle() {
return title;
}
public String getSort() {
return request.getParameter("sort");
}
public List<Page> getChildren() {
List<Page> children = new ArrayList<Page>();
PageManager pageManager = resolver.adaptTo(PageManager.class);
if (pageManager != null) {
Page currentPage = pageManager.getContainingPage(resource);
Iterator<Page> childPages = currentPage.listChildren();
childPages.forEachRemaining(children::add);
if ("ZA".equals(getSort())) {
Collections.reverse(children);
}
return children;
}
return null;
}
}

Step 2 - TabControl HTL Component

<tab-control data-sly-use.model="org.millr.core.models.TabControl"
data-sort-by="${model.sortBy}"
data-model="${'{0}.model.json' @ format = resource.path}"
data-title="${model.title || 'testing 123'}">
<h2>${properties.subTitle || 'Sub Title'}</h2>
<sly data-sly-repeat.child="${model.children}">
<tab-item>${child.title}</tab-item>
</sly>
</tab-control>

Step 3 & 4 - TabControlDOM & TabControlModel

import { DOMModel, DOMComponent } from 'react-dom-components';
import TabControl from './TabControl';
import { TabItemModel } from '../tabItem/TabItemDOM';
class SubTitleModel extends DOMModel {
constructor(element) {
super(element);
this.getTextContent();
}
}
class TabControlModel extends DOMModel {
constructor(element) {
super(element);
this.getDataAttribute('title');
this.getDataAttribute('model');
this.getChildComponent('h2', SubTitleModel);
this.getChildComponentArray('tab-item', TabItemModel);
}
}
export default class TabControlDOM extends DOMComponent {
static get nodeName() { return 'tab-control'; }
static get model() { return TabControlModel; }
static get component() { return TabControl; }
}

Step 5 & 6 - TabControl React Component with DAO

/* global fetch */
import React from 'react';
import TabItem from '../tabItem/TabItem';
export default class TabControl extends React.Component {
constructor(props) {
super(props);
this.props = props;
this.state = {};
this.state = { sort: 'AZ', tabItems: [] };
this.props['tab-item'].map(tabItem => this.state.tabItems.push(tabItem.props.text));
// Gross React Stuff
this.handleClick = this.handleClick.bind(this);
this.getData = this.getData.bind(this);
}
getData() {
const fetchStr = `${this.props.model}?sort=${this.state.sort}`;
const responsePromise = fetch(fetchStr, { credentials: 'same-origin' })
.then(response => response.json());
return responsePromise;
}
handleClick() {
if (this.state.sort === 'AZ') {
this.state.sort = 'ZA';
} else {
this.state.sort = 'AZ';
}
this.getData()
.then((data) => {
this.state.tabItems = [];
data.children.forEach(child => this.state.tabItems.push(child.title));
this.setState({
tabItems: this.state.tabItems,
});
});
}
render() {
return (
<React.Fragment>
<h2>{this.props.title}</h2>
<h3 className="test-sub-title">{this.props.h2.props.text}</h3>
<button onClick={this.handleClick}>A-Z</button>
{this.state.tabItems.map(tabItem =>
<TabItem key={tabItem} text={tabItem} />)}
</React.Fragment>
);
}
}

The React Component and Data Access Object (DAO) have been combined for demonstration simplicity.

Prior Art

What is outlined above is not the only way to integrate AEM with React. There is a separate project using Typescript and Nashorn to achieve something similar. For my purposes, this is a little more opinionated and heavy handed than I like, hence the process above.

A Full Project

All the source with a sample implementation can be found on GitHub. This project is what I'm calling "Pug Ranch 2018" which is a reference implementation of AEM Archetype 12 plus a few front-end niceties to create a "tab-control" component using HTL and React.



By aem4beginner

No comments:

Post a Comment

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