After our SUMMIT presentation on adobe.com's AEM implementation, I had several people ask about how we approach dialog complexity. 

Below are a few rules we go by when building dialogs.

1. Differentiate labels, descriptions, and help paths

  • Label (fieldLabel) - Short & concise
  • Description (fieldDescription) - More context & information
  • Dialog Help (helpPath) - Dense and verbose, full documentation with screenshots and examples.

Each should have a different meaning and purpose. On adobe.com, we optimize for the everyday use experience, not the first use experience. Sure, an author may need more instruction with a short label like, 'Border' during first-use, but once an author is trained on a feature, something more verbose like, 'Component Border Control' is redundant and un-necessary.

Examples

Label: Border

Description: Define the width, type, and color of the component's border.

Label: Enable Container

Description: Turn on a container to prevent the contents from expanding beyond a 1200px max width.


Personally, I also try to re-word concepts between label and description to prevent duplicating the same words. 'Enable Container' worded differently would be, 'Turn on a container'. This is to capture different ways authors may read and understand a particular field.

2. Remove complex and redundant words

The general idea is that most authors don't need overly complex labels and descriptions, this is the work they do every day. It's also important to remove redundant words. If you're in a table component, there's no sense in saying, 'Table Width'. The term, 'Table' is implied. 

Words we strictly prohibit that sound overly complex and typically do not bring value to labels:

  • Configuration - I.E. Border Configuration
  • Control - I.E. Background Control
  • Select - I.E. Select Type
  • Set - I.E. Set Type

These terms do not provide value. We're in a dialog; configuring, controlling, selecting, and setting things is the whole point we're there.


3. Add a little CSS

There's nothing saying you cannot add a clientlib to give Coral 3 a little help. There are several scenarios where having a top-to-bottom approach to all fields can have a negative impact on the authoring experience. 

Let's look at setting a custom width with OOTB Coral 3:


With a little CSS, we can clean things up and give better differentiation of the section:


Or how about a multifield:


Anyone who has worked with multifield knows it can get very complex very fast. Above, we have little differentiation of items and no easy way to identify each. With a little CSS, we can add an index and a separator:


4. Lean on mobile-first inheritance
Building mobile-first components is a no-brainer. However, providing tools inside the dialog to create an inheritance model can also reduce complexity. Below we have an example of how an author can set mobile / default colors, and inherit or override as needed.

 

5. Hide complexity until it's needed

On adobe.com, we have two powerful components used for a lot of general purpose layout. Unfortunately, with power comes complexity. We do everything possible to remove complexity until it's needed. We have two primary ways we achieve this:
  1. Using cq-dialog-dropdown-showhide-target we can show / hide containers.
  2. Using granite:renderconditions we can completely remove fields. - See point 6.

Below we have an area to define borders:


However, there might be a scenario where a design calls for a border on an individual side.

We can expose that complexity using cq-dialog-dropdown-showhide-target :


6. Leverage Editable Templates and Render Conditions

Sometimes the best UI is no UI. Combining the power of render conditions with the power of editable templates (and content policies) can allow you to selectively render parts of a component during page creation.

Here's a quick video on how this can work:

7. Use Custom Icons


Custom icons are a great way to give a little extra polish and help show authors some love.

You can use a file (_cq_icon.svg or _cq_icon.png) in the component folder or you choose one of the many coral icons names in your component's XML definition.

I've created a sample of a _cq_icon.svg on github free to download.


09/28/2019 Update

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

Step 2 - TabControl HTL Component

Step 3 & 4 - TabControlDOM & TabControlModel

Step 5 & 6 - TabControl React Component with DAO

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.

Front-ends for AEM pre-2017

If you've paid attention to the front-end world within AEM you've likely seen blogs to integrate grunt or gulp. These solutions are good starts, but don't solve many problems.

  • How do we write modern ES6 syntax?
  • What about linting?
  • How do we have good separation between authoring and publish clientlibs?
  • How do we support a multi-tennant lifestyle?
  • Why are we using tools that have overlap? i.e. NPM and Bower

My front-end proposal scales better than most solutions while supporting ultra-modern front-end standards.

Modern front-ends for AEM in 2017

Let's talk tools, the whys, and then about the hows.

Tools

  • Dependency management: NPM
  • Task Runner: Webpack
  • Bundler: Webpack
  • Transpiling: Babel
  • Linting: ESLint + Airbnb Style Guide
  • Syntax: ES6 & LESS
  • Templating: TBD

Whys

We're using NPM instead of Bower because there's no longer a need for Bower... most FEDs understand package.json these days.

We're using webpack because it's a decent task runner and an excellent bundler... let's not use Grunt / Gulp to then run webpack... one tool. Webpack also doesn't need to be installed globally which makes on-boarding easier.

We use Babel to transpile our ES6 into something browsers can understand. It's widely documented which, again, helps on-boarding.

We're using ESLint + Airbnb Javascript Style Guide so our code follows strict standards that are widely used and documented. Again, this helps on-boarding... the clearest path to having documentation for your own project is to leverage tools that already have exceptional docs.

ES6 is the future (technically the present), let's start using it. LESS is AEM friendly, so if we ever decide to use AEM's native LESS compiler, we're not having to re-write everything.

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.

Hows

So, how does all of this work?


The high level overview

  • We start with a simple folder for our area of concern... publish, author, theme, bu1, bu2, whatever.
  • It uses .content.xml, css.txt, and js.txt to create a proper clientlib.
  • It has src and dist folders in it.
  • Inside src are our source files to be run through eslint, babel, and webpack.
  • Our dist folder consumes the output.

About our clientlib definition

If you've never worked with AEM, you might just think it's OK to drop a script tag on your page and be done. Unfortunately, you will end up hobbling AEM functionality and make more work for your back-end developers. Wrapping your JS (minified or not) inside a proper clientlib is incredibly important and will reduce work in the long run. Certain granite views are built around consuming a namespaced clientlib, and if you wish to view the raw JS files on non-dispatched servers (i.e. 4502), BE winds up needing to write content disposition configs because AEM serves up raw JS as attachments when they're not proper clientlibs.

Proxied Clientlibs

We add a property to our clientlib definition to allow it to be proxied out to /etc.clientlibs/. This reduces folder traversal... we're no longer digging into /etc/clientlibs/ourproject and /apps/ourproject/components. Everything just sits in /apps/ourproject/clientlibs and /apps/ourproject/components.

The .content.xml

Our webpack config

The basics is that we have entries, outputs, rules, and plugins.

The entries exist in the src folder, outputs go in the dist folder, rules and plugins should be fairly readable...

With our three entries (clientlibs) we have a lot of ground covered while being separated into their areas of concern:

  1. Base Publish - All structure and base-level features consumed by all parties (authors, visitors, etc.)
  2. Base Author - All structure and base-level features that are only consumed by authors. Everything from TouchUI hacks to Dialog listeners.
  3. Base Theme - All LESS / JS that can be individualized based on a tenant's requirements (think font families, margin & padding sizes, etc.)

Developer on-boarding

With one maven command, everything is pulled in and setup to start working:

mvn clean install -PautoInstallPackage
Day-to-day development

Things differ a bit here for BE and FE. BE can continue to use the maven tools they're use to. FE can switch over to start using tools they're use to:

cd ui.apps
npm run aemlocal
<new terminal tab>

aemsync

By paring our aemlocal script (defined in package.json and basically calling webpack --watch) with aemsync, we can now build anything on the front-end without any maven cruft. Want to add a new dropdown to a dialog? No problem. Want to wire up a new some new javascript functionality? No problem. All of this syncs up seamlessly to AEM with perfect separation of concerns.

Source Code

Want to see how it all works? Go check the source code.

aemfront-endback-endtoolingwebpacknpmES6

If you ask any front-end developer what they need to be successful, they'll likely tell you they three things: 

  1. A rapid prototyping tool.
  2. A JSON end-point to grab data.
  3. A template language that makes sense.
  4. Don't tell me how to do my job.

If you ask any back-end developer what they need to be successful, they'll likely tell you they need three things:

  1. Ability to leverage Java / whatever frameworks to make them highly efficient.
  2. A good testing / CI / automation process.
  3. Not too much business, front-end, or ops noise.
  4. Don't tell me how to do my job.

AEM is built around experiences (FE) but powered through rich and deep integrations (BE) that keep businesses productive. How does a business consume CRM data? It's not through the front-end.

This is where we have to think about MVC at a macro level. FE might be asking for data in a concise JSON feed that can be consumed by a hot new template language (V), but BE is talking OSGi, separation of concerns, and scale-ability (C). Where the data is coming from is anyone's guess. If you live in the AEM world, you're hoping the JCR is building models the way you want (M). So whose domain is the model? I'd argue it's FE.

As an FE developer (FED), I build the dialogs that POST against SlingPostServlet and store my data exactly how I want. Do I want 45 properties outlining how this component should behave in responsive views or should I break them out as children? What does that mean for the back-end?

We're getting closer to a scenario where all parties can agree. FEDs just want a JSON end-point and a template language they're use to (JSX, HBS, whatever). BED's want to write productive Java classes (POJOs, Servlets, OSGi services). We just seem to be missing that final link. HTL is so incredibly powerful when rendered server-side, but doesn't satisfy the need of in-page updating. How do I sort a list without a query param and a page refresh? Angular? Handlebars? React? All of these solutions have felt like after thoughts or had down-sides when coupled with HTL.

At some point, the power of the back-end needs to be coupled with the front-end. Exposing the entire JCR tree to the front-end or writing servlets for every single view is not the answer.


front-endback-endJCR

Let's start with a few basic assumptions, a good error page is:

  1. Branded
  2. Helpful
  3. Localized
  4. Fast

Out of the box, AEM uses Sling's default error page. Most projects, however, will overlay sling/servlet/errorhandler/404 to create a branded error experience. This can cause performance problems with Dispatcher. Let's look at a typical go-live example.


  1. A user requests /us/en/sup.html
  2. Dispatcher doesn't have it in cache, so it must...
  3. Ask Publish if it has the resource
  4. Publish returns our branded 404.
  5. Dispatcher will not cache any error documents.
  6. Apache sends the user the branded 404 at the original request path.

This creates several problems. The big one? The request lifecycle above is for every request. This means we're constantly talking to Publish when a document 404s. What happens if a marketer sends out a bad URL? You'll DDoS yourself.

By combining a few clever configs, we can achieve our four original tenets for a good error page. Let's look at the first request:


  1. Our user requests a page.
  2. Dispatcher does not have it in cache. It must check with Publish.
  3. Publish doesn't have the page in question.
  4. Publish serves up the default Sling error page (it's... not attractive).
  5. Dispatcher identifies the error, doesn't cache, but let's Apache handle the request with the ErrorDocument directive.
  6. Dispatcher makes another Publish request based on the ErrorDocument path.
  7. Publish responds with a 200 and our branded 404 error page.
  8. Dispatcher caches the new 404 page.
  9. Apache delivers our 404 content at the original location (with the correct 404 status code).

This approach may seem complex, but let's take a look at every subsequent request:


In every following request, Publish can be hyper-efficient responding to 404s with the default Sling error page, but dispatcher will continue to keep the branded and localized version in cache for fast delivery. No more rendering of components for every error.

So, how can we achieve this? Let's start with the AEM layer.

AEM

This is the easy part. Your 404 page can be a simple content page built with your components. You build this page as you would any other. Want search? Add a search component. Want a sitemap, add your sitemap component. Best of all, this can follow your typical localization process to propagate to all other locales and languages.

Dispatcher

With Dispatcher, we need to change our http.conf to let Dispatcher pass the error to Apache. Take a look at line 24:

This let's us pass only our status code to Apache and tells it to deal with the error.

Important Note: I would strongly recommend only specifying the error codes you have HTML content for. There may be scenarios where you deliver a 500 and a JSON string to be handled client-side (handlebars, angular, etc.).

Apache

Our final piece. As of Apache 2.4.12, you can use 'variables' with an ErrorDocument. Here's how we can deliver our localized error pages:

By using an if statement and routing our locale, we can insert the correct ErrorDocument for a given locale.

Additional Reading

dispatcherapacheerrordocument