Better know a framework #18: what processContent is and what it is used for


#1

Abbreviation:

  • CE: custom element
  • CA: custom attribute

Terminologies:

  • Consumer template:
    A consumer template of a CE/CA is a template where a CE/CA is used). For example, the following is a consumer template of CE name-tag:
    <!-- app.html -->
    <template>
      First Name: <input value.bind="firstName" />
      Last Name: <input value.bind="lastName" />
    
      <name-tag name="${firstName} ${lastName}"></name-tag>
    </template>
    

How it looks like

Sometimes, you may come across a tutorial or a code block of Aurelia that looks like this

@processContent(...)
export class Example {
}

Notice decorator usage @processContent, it defines some metadata for the class (CE/CA) of how to process the content in consumer template. So what is / are the content?

  • For CE: Content is the children elements / nodes of the CE element in consumer template. For example, suppose we have CE named panel, in the following template:
    <template>
      <panel>
        <header></header>
        <div class="panel-body"></div>
      </panel>
    </template>
    
    content will be all child nodes of <panel> element
  • For CA: Content is the children elements / nodes of the element that the CA resides. For example, suppose we have CA named selectable, in the following template:
    <template>
      <div id="selectable-div" selectable>
        <div repeat.for="item of selection">${item}</div>
      </div>
    </template>
    
    conent will be all child nodes of <div/> with id selectable-div

What is it for

@processContent is used for ensuring the content of a target element to a more suitable template form.
For example, a <ux-select/> CE should only accept 2 CE in its content: <ux-option/> and <ux-optgroup/>. However, the user of <ux-select/> may accidentally supplied a <div/>, which will cause the widget to function incorrectly or even break:

<template>
  <ux-select>
    <ux-option>...</ux-option>
    <div>oops gonna break</div>
  </ux-select>
</template>

With @processContent, author of <ux-select/> can define a function that helps remove all invalid child nodes before progressing further, to either issue a warning so either developers can fix the template, or prevent the widget from breaking. processContent function takes 4 parameters:

  1. compiler: the view compiler instance being used for compilation
  2. resources: the view resources instance being used for resources lookup during compilation
  3. node: the element where the content of it needs to be processed
  4. instruction: the behavior instruction instance that is associated with either CE/CA in the consumer template

Back to our example above with the troublesome <div/>, we can use @processContent to remove it:

import { ViewCompiler, ViewResources, BehaviorInstruction, processContent } from 'aurelia-framework';

@processContent((
    compiler: ViewCompiler,
    resources: ViewResources,
    node: Element,
    instruction: BehaviorInstruction
  ): boolean => {
    // remove every child element that is not either `<ux-option/>` or `<ux-optgroup/>`
    // ... real code to remove ... ex. node.removeChild(node.lastElementChild)
    return true;
  }
)
export class UxSelect {

}

Notice the return true at the end of the function, by returning true, we are signaling the compiler to continue the compilation process for the content, as it was paused when it hit a CE or an element with CA on it. Some examples of advanced usage:

  • combining @processContent with return false to extract the content and compile into a factory, marked with an ID to use later. Everyone can go pretty clever & creative with this.
  • combining @processContent with replace-part & <template/> to target replaceable template in CE

Live example


If you have an example of @processContent you would like to share to help folks better understand it, please leave a link and I’ll incorporate it in live example section


#2

In layman’s terms, I like to think of @processContent as an interceptor. You can do some stuff to the template before the ViewCompiler starts working on it (with the option of telling the ViewCompiler not to touch it at all).

The ViewCompiler analyzes the attributes, element names and raw text in the template to decide where it needs to put custom attributes, custom elements or interpolations.

@processContent has 2 use cases that I myself find interesting and occasionally useful:

  1. Pass it some reusable function with some app-wide common replacements in it, for example adding the & t bindingBehavior to all interpolations (if you’re using i18n), or a boolean valueConverter to all input type="checkbox" value.bind expressions. With a TreeWalkeryou can easily make your views more maintainable by moving common bloat to the function.
  2. Implement your own mini templating syntax without having to do any complex compiling yourself. Perhaps you prefer a certain subset of the angular, or knockout data-* or vue syntax, or you just want to easily migrate something. With @processContent you can make the replacements without having to implement the compiler

#3

I’ve used @processContent extensively to do the heavy lifting for a widely used component library. An excellent example is a tabs component. What you might want to write is something like:

<tabs>
  <tab>
    <tab-header>
      Tab 1
    </tab-header>
    <tab-body>
      This is tab 1
    </tab-body>
  </tab>
</tabs>

But what you need rendered to look and behave correctly is something like this:

<div class="tab-headers">
  <span class="tab-header">Tab 1</span>
</div>
<div class="tabs">
  <div class="tab">
    This is Tab 1
  </div>
</div>

This is impossible to do with custom elements alone, as it requires a full rearrangement of components. This is where @processContent(_, _, element: HTMLElement, _) comes in. You can read, add, remove, and replace all elements in the custom element, and return true to let Aurelia pick up processing when you’re done.

There is an advanced approach that involves creating views and view slots, processing them in the created() callback, etc. I’ve found that this is a bit too complicated both to write and to read, so I’ve skipped this approach almost every time.

In vNext I believe there is already a proposal to make @processContent less of a transformation on the class and more of an transformation on the instance, which will allow not only manipulating the DOM elements but also setting variables on the custom element instance according to the incoming element.


#4

Nice, I really like the proposal for where it sounds like this is going for vnext. Do you have a link to the vnext Github issue for this (if there is one)? I’d be interested in watching this as it shapes up.


#5

There is no specific github issue for this atm, as its shaped is still being discussed. The reason is it should be in some form that can be used for both AOT and JIT scenario, while not losing too much power. of @processContent flexible API. If you have any idea, it’d be nice if you open a issue for that.


#6

Thanks @bigopon will keep an eye out for upcoming updates :+1: