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:


#7

Is there a way to inject my custom elements in processContent? For example, in my Tabs view I have something like below:

<template class.bind="styles.tabs">
    <require from="my-package/elements/MyControl"></require>
    <slot></slot>
</template>

And in my processContent, I am doing something like below:

@processContent(Tabs.processTabs)
export class Tabs {
  public static processTabs(_compiler, _resources, node: HTMLElement, instruction: BehaviorInstruction): boolean {
    ...
    const tabHeader = document.createElement("my-control");
    tabHeader.setAttribute("value", header || ""); // header is a string
    // append tabHeader to node or to one of its children ...
    return true;
  }
}

However, the my-controls are not processed further; i.e. the elements are present in the DOM, but without further inner contents. It seems that custom elements can be injected using _resources, however, I have not found any such example so far. Is there a way to resolve this?

It might be possible that if I use <require from="my-package/elements/MyControl"></require> it the view where I use <tabs/> element, it will work correctly, but then my custom element is no longer in full control of itself.


#8

So actually what happens is Aurelia parses the content first, so when you add new elements it doesn’t know about them. It has already passed the point where it would be looking for new custom elements.


#9

Only things help here is to <require> the resources in client code or register those as global resources.
The later is not a problem in my case, as the Tabs control resides a plugin project, where the global resources can be configured. Though this option breaches the principle of keeping the stuffs together that belongs together, it is undoubtedly better than the previous option.

Here are couple of questions though, to make my understanding of this better.

  1. Let the Tabs view be as follows.

    <template class.bind="styles.tabs">
       <require from="my-package/elements/MyControl"></require>
       <slot></slot>
       <my-control id="123" value="This works"></my-control>
    </template>
    

    Then my-control#123 is processed correctly. Does it mean that processContent is called before processing the <require>s in the template? And once the processContent is over, the nodes added to slot are not processed further?

  2. I have tried to add view resources in processContent as follows:

    const myControlResource = new HtmlBehaviorResource();
    await myControlResource .load(Container.instance, MyControl).then((resource) => {
      console.log("loaded resource", resource);
    });
    _resources.registerElement("my-control", myControlResource);
    

    However, this didn’t work as well. Is there any way I can register the required elements correctly (so Aurelia can process the custom elements further) during this phase?


#10

Furthermore, is there a reason for supporting only boolean return value from processContent?If a Promise<boolean> return value from that function is supported, can that mitigate the problem of loading the additional resources asynchronously?

Sorry for bombarding with question. This is a the first time, in last 2/3 years, Aurelia is forcing me to go for such architecture/design that can be frowned upon :slight_smile:


#11

However, this didn’t work as well. Is there any way I can register the required elements correctly (so Aurelia can process the custom elements further) during this phase?

Yes, but I never do this so I dunno off the top of my head. @bigopon might know.

Furthermore, is there a reason for supporting only boolean return value from processContent ?If a Promise<boolean> return value from that function is supported, can that mitigate the problem of loading the additional resources asynchronously?

Yes, the process content hook was mostly added to prevent Aurelia from processing content, not to manipulate how it processes content so that you could handle your own DOM processing, particularly for jQuery style controls. The idea of manipulating content was more of an afterthought, I think. Since that time we’ve learned that there are better ways to approach 3rd party controls, but process content is the best way to handle manipulating content.


#12

@Sayan751 From your example code, it should just work. I don’t know exactly what issue you ran into, but I can help if you could share a reproduction, based on this template https://codesandbox.io/s/wnr6zxv6vl


#13

That is promising to hear. I will try to reproduce it in the sandbox.


#14

Or you can use normal one that represents how you do your app, instead of aurelia-script based one https://codesandbox.io/s/y41qjr36j . Sorry I forgot to give this option.


#15

Finally, I got time to reproduce the issue in sandbox.

Without loading additional resources in processContent: https://codesandbox.io/s/r4p7503wro

Attempt to load additional resources in processContent: https://codesandbox.io/s/w25n2mqqk5

The former works iff the respective require is uncommented in app.html. The problem with the second is without async processContent it shows DI error, which I believe is due to the failed attempt to “enhance” the custom element used in processContent. On the other hand, with async function, it simply does not wait the function to be completed. Thus, the actual “processing” part of the function is skipped before Aurelia processes the element.

Hope these help.


#16

Thanks.

For both examples, when you append new content to the node being processed, it’s appended to parent/ or consumer template, not the child/custom-element-template. Thus, having <require from='./MyText'/> didn’t work as you would expect it to be because the parent doesn’t have any knowledge about it.

For dynamically load MyText dependencies, you don’t have to do this, it’s already been loaded by the time the compilation happens, as it’s ensured to load everything before compilation. Example here https://codesandbox.io/s/0xlx9x2vy0

I think it’s the scope of MyText that wasn’t clear that caused the confusion.


#17

Thank you for the explanation.

I am trying to sum up to make my understanding better. By the time the processContent is called, Aurelia has already compiled the view of that custom element and thus, knows about the view resources required by the element. Therefore it is enough to register the additional custom elements “correctly” (getting the resource from the already compiled stuff, namely instruction.type.viewFactory.resources) in processContent. Is this correct?

Also as you indicated, a few private properties of Aurelia internals are being touched in the process. In that case, maybe it is better to make those properties part of public API, or create public API to expose those in a more developer/consumer friendly manner.

I think processContent is one of the most cool features of Aurelia, with next to no documentation. It deserves a section on its own merit, with bit more detailed explanation along with the different use cases for this.


#18

I am trying to sum up to make my understanding better. By the time the processContent is called, Aurelia has already compiled the view of that custom element and thus, knows about the view resources required by the element. Therefore it is enough to register the additional custom elements “correctly” (getting the resource from the already compiled stuff, namely instruction.type.viewFactory.resources ) in processContent . Is this correct?

That is correct, as you already noticed.

Also as you indicated, a few private properties of Aurelia internals are being touched in the process. In that case, maybe it is better to make those properties part of public API, or create public API to expose those in a more developer/consumer friendly manner.

As you stated, this is the first time in years of using Aurelia, you ever need it. That kind of justifies it’s current state. For the suggestion, there is no harm making them public, though I’d prefer to let @EisenbergEffect give some thoughts on APIs.

I think processContent is one of the most cool features of Aurelia, with next to no documentation. It deserves a section on its own merit, with bit more detailed explanation along with the different use cases for this.

Wish some community members could help the team more here :smile:


#19

I would be happy to help. Need a bit of help on “where to start” :slight_smile:


#20

You can start here http://github.com/aurelia/documentation. But let’s also ping @EisenbergEffect for more input :smile: