Groovy Component Composition With Traits

AEM components often need to exhibit similar elements of behavior, as many concerns met by components are common across component types. Tagging or “Categorization” mechanisms are a particularly good example of this. There may be many component types in your AEM application which expose a “tag” or “category” input via a Tag Input Field. These components will all use the same mechanisms to resolve the author input into a true Tag instance. In order to avoid duplicating code across these components, you could create an abstract class containing the tag / category lookup logic, however, this leaves you with the question of what to do when you have a second common behavior. While a rich hierarchy of abstract classes could be created to represent these various common fields, you are only afforded a linear inheritance progression which does not allow you to mix and match behaviors in the context of a single component.

Groovy Traits, introduced in Groovy 2.3, give you the ability to perform true composition of state and behavior. Traits act like an interface in so much as a class may implement multiple traits and they act like an abstract class in that they can have their own state and behavior. The Diamond inheritance problem, commonly cited as a reason for not allowing multiple inheritance, is alleviated by convention and configuration. In the case of such conflicts, Groovy will prefer implementations provided by the last declared trait unless you explicitly tell it to do otherwise.  

Using Groovy Traits in conjunction with the CQ Component Plugin will allow you to achieve true component composition by building small, reusable, and potentially authorable behaviors in traits and composing them into final component instances. The Categorization behavior described above could be encapsulated in the following trait.  

trait Categorizable implements Component {
 
   @DialogField(fieldLabel = "Categorization")
   @TagInputField
   List<Tag> getCategorizations() {
       def tagManager = resource.resourceResolver.adaptTo(TagManager)
 
       getAsList("categorizations", String).collect {
               tagManager.resolve(it)
           }.findAll {
               it != null
           }
   }
 
   List<String> getCategorizationIds() {
       return categorizations.collect {
           it.name
       }
   }
}

In this and the following examples, I am making use of the AEM Library, but the concepts stand for whatever mechanism you chose to use to create your component’s backing classes. This defines both the behavior and the authorability of an implementing component. Since all of the traits are essentially aggregated into a final class as far as the bytecode is concerned, the CQ Component Plugin will pick up the annotated DialogFields in the same way it would had you defined the traits behavior in the implementing component class.  

With this trait defined you can create a component which implements the trait. 

@Component(value = "Trait Composed Component")
class TraitComposedComponent extends AbstractComponent implements Categorizable {
 
   @DialogField(fieldLabel = "Title", ranking = -1000D)
   public String getTitle() {
       get("title", "Title")
   }
 
   @DialogField(fieldLabel = "Content", ranking = -990D)
   @RichTextEditor
   public String getContent() { 
       get("content", "Content") 
   }
 
}

Building upon this further, consider the common need to present the name of the author of a particular page. This behavior could be encapsulated into a trait as well.  

trait Authorable implements Component {
 
   public String getAuthorName() {
 
       def author = getResource().getResourceResolver()
          .adaptTo(UserManager.class)
          .getAuthorizable(getCurrentPage().getLastModifiedBy())
 
       def authorProperties = getService(UserPropertiesService)
               .createUserPropertiesManager(resource.resourceResolver)
               .getUserProperties(author, "profile")
 
       authorProperties.displayName
        
   }
}

Once done, any number of components may expose this behavior simply by implementing the trait.  

@Component(value = "Trait Composed Component")
class TraitComposedComponent extends AbstractComponent implements Categorizable, Authorable {
 
   @DialogField(fieldLabel = "Title", ranking = -1000D)
   public String getTitle() {
       get("title", "Title")
   }
 
   @DialogField(fieldLabel = "Content", ranking = -990D)
   @RichTextEditor
   public String getContent() { 
       get("content", "Content") 
   }
 
}
 

Trait composed component image

Again, as far as mechanisms like the CQ Component Plugin and Sightly rendering are concerned, component backing classes composed in this way behave the same as those wherein the behavior was defined inline. As such, rendering the component using Sightly can be done without any additional complication. 

<section data-sly-use.traitcomposedcomponent="com.yourcompany.your.package.TraitComposedComponent" class="${traitComposedComponent.categorizationIds @ join=' '}">
   <h2>${traitComposedComponent.title}</h2>
   <h3>By: ${traitComposedComponent.authorName}</h3>
   <div>${traitComposedComponent.content @ context='html'}</div>
</section>