Managing Component Code Properly with the AEM Authoring Toolkit

Stepan Miakchilo
Exadel Marketing Technology Practice
8 min readJul 14, 2020

In our previous article, we introduced the AEM Authoring Toolkit — an open-source project maintained by a team from Exadel and hosted at GitHub. We took on the challenge of streamlining the development of AEM components for the benefit of site designers, authors, and users.
Why did we take that on? As AEM folks, we need to manage lots of textual markup. But still, we like coding the way it used to be — with data structures and relations, and close integration with the OOP. Being developers, we are strategically lazy whenever possible. We like to adapt and reuse stuff, instead of reinventing and copy-pasting all the time. At heart, we are creators. We adore neat and witty approaches, with all available flexibility and maximum benefit put together precisely with minimum written script.

This is the vantage point from which we revisited AEM authoring in the TouchUI environment.
Four essential questions guided our journey:

  • How can we manage the components’ attributes taking into account the Granite requirements?
  • How can we compose dialogs for multiple components reusing as much code as possible?
  • How can we organize the user interface in a neat structure and not let the structure crush you when you want to add more features?
  • How can we align the dialog with the in-place editing setup?

Setting Up Components: The Easy Way…and The Guru Way

Suppose you have a Sling model for your component with just two authorable fields — title and description. This is what you would have for a complete dialog rendering:

The plot is pretty simple: you have a @ValueMapValue (or whatever applicable Sling annotation) for each field, and the two Toolkit annotations — @DialogField and @TextField / @TextArea. The former is generic as it manifests requisites common for nearly all web widgets, such as the label, the tooltip, the ability to be enabled/disabled or required for input. The latter points to the particular widget to render.
There are somewhere between few and no obligatory requisites for these annotations, so you can save yourself a bit of typing. But this only means there are reasonable defaults under the hood. Surely you can redefine them in a type-safe manner (type hints are added to XML nodes while rendering).

Granite documents describe a great number of properties for each widget. Dig down to the widget node in CRX/DE, and you might (on occasion) find even more. Most of the time, we only add attributes we tested ourselves (and proved work) to the Toolkit API. Still, you may (and probably will) need others. Luckily, there are tools for that.
You may add@Property(name = "someName", value = "some value") right beneath your @TextField or any other widget annotation. This will render an additional attribute to the corresponding Granite node, or else overwrite an auto-generated one. You may also use the @Attribute structure as follows:

Thus, an extra granite:data node will be created under the widget’s node in cq:dialog so that the Granite frontend can use it.
Besides, there is the@CommonProperty annotation. Unlike the ones mentioned above, it is added to a class, not to a field. Where @Property relies on an attribute's name, the @CommonProperty rests upon an XPath - a comprehensive node path having a particular attribute, or even a group of nodes all matching the same criteria. Therefore, where @Property alters a single value, a flock of @CommonPropertys perform as a kind of "Swiss army knife", which can massively change the entire component - all or any of its XML nodes.
The lineage of dialogs
You may need another component, much like the MyComponent but with, for example, an additional field. The OOP solution is to extend the more populated component from the basic component. When it comes down to authoring interfaces...well, you know that JCR has its own way of inheritance, known as overlaying. But this, again, implies writing XML by hand.
Luckily the Toolkit is able to observe inheritance by its own means. If a class marked with@Dialog extends another class with Toolkit-processable fields, these fields are also accounted for.
Consider the following extension:

The Toolkit analyzes fields from however many valid superclasses and arranges them in a single layout. And here’s the result:

Organizing Content in Tabs

The snippets above illustrate the most straightforward “fixed columns” layout for a dialog. But AEM has more to offer: Tabs!
Tabs have been the leading visual metaphor for organizing content since…forever. In fact, even if you only have a few components, but they belong to different semantic domains, it makes sense to distribute them between a few tabs. The problem is, tabs are quite verbose to write down in markup.
So we came up with a Toolkit approach, actually two of them!
This is how you do tabs the most natural way:

Yes, most obviously a nested class represents a tab. This approach works. But let’s be honest, who really likes nested classes? At the very least, they complicate retrieving data in your HTL. Bet you can do without them! Revert to the previous code listing and change it as follows:

Okay, it also works as expected:

But wait! Can we have our nice Hobbit back? Turning to the extension class again…

See this? Tab definitions and widget-to-tab assignments are also “inherited”. Just do not forget to add layout = DialogLayout.TABS to the @Dialog annotation. Mind that widgets defined in a superclass go before the widgets from the current class. Want to change this order? Take care to set the ranking property of each @DialogField. If rankings are set, their order is preserved across the whole inheritance graph.

The Taming of the RTE

Have a look at Bilbo’s song verses. This is a piece of text, and most of the time such texts come with pretty formatting. AEM boasts its marvelous RichTextEditor component to address that with dozens of options. That is why in our practice nearly two of every three components have an authorable RTE field or even a couple of them.
The problem is…RichTextEditor is an absolute beast to configure.
It has — well — dozens of options, many with their own peculiar markup. It has toolbars (both for plain text and tables) and popovers. It comprises as many as three different views: for a modal dialog, an expanded dialog, and a fullscreen mode — each potentially having a specific feature set. And then it, with all its vastness and glamor, maps to the in-place editing facility. And accordingly, it needs to be rendered in some fourth iteration.
We are not even going to expose here our testing RTE setup. It takes up 20 precious kilobytes of XML code on the hard drive.
Instead, we will show you the Toolkit’s counterpart. This one will work in every environment.

Did you expect this to be shorter still? Or even longer? Well, this 40-line snippet is not small (and when it comes to the features, there are many that aren’t listed here). But at least this is controllable by means of your IDE, it’s syntax-checked, linted as it must be, and ultimately, it’s guaranteed to produce the same XML markup as many times as the Granite framework requires.
And that’s how you get this rendered, regardless of whether it’s in a window or fullscreen.

There’s just one extra thing to take care of — the in-place editing mode. Authors benefit from the touch-enabled experience which in the long run means doing stuff without dialogs. That’s why we’ve got that dark floating panel on the screen. Want to get it under control with the Toolkit? Assign "editable-title" and "editable-description" classes to the appropriate blocks in your component's HTL markup and then add the following underneath your @Dialog annotation:

Now you’ve got that pencil icon to the left of the wrench, and in-place editing works as anticipated…unless you expected more! Note that the RichTextEditor toolbar is just a basic one. It would be nice to have it looking and behaving the same way the dialog modal, wouldn’t it?

The Granite framework implies you would propagate another copy of the RTE setup to the cq:editConfig node. Well you may expect to get around with copying only Java code...but this is forty more lines of code!
You will be surprised to know you actually need just one extra line.

Code Extension Made Easy
Yes, you got this right — apart from adequately processing Java classes that extend each other, the Toolkit has its own peculiar notion of extending.
An annotated field, or an in-place editing entity A, can@Extend field B in the sense that A inherits widget settings and properties assigned to B while at the same time preserving the ability to alter or override them with settings of its own. No copy-pasting needed, even for annotations!
This principle is the same across the entire code base, not just for@InplaceEditingConfig. Any dialog field can @Extend another field from whatever class you want if it is reachable from the current. The only significant limitation: their "basic" widget annotation must be the same (scarcely would it make sense to @Extend, say, a path picker with a text area).
In this same way a field can @Extend its sibling from the very same class. Two RTE's in one dialog made easy! One RTE may, for instance, have a bunch of toolbar buttons, the other - a bunch-plus-one.

Surely you’ve got the idea. We can even suggest that in a large-scale project with lots of components, when many of them share the same signature, or a very close signature, you can benefit from creating a “referential widget storage”-a single class with many fields marked with, say, @RichTextEditor-s, @Select-s, @RadioGroup-s, @DatePicker-s and other heavy and lengthy annotations. Put it somewhere, then let your component classes refer to that library and @Extend particular fields from it.
That's all for this entry! Next time we'll discuss how to conditionally and interactively manipulate widgets in your dialogs and populate them with instant data.
See you real soon!

Originally published at https://exadel.com on July 14, 2020.

--

--