LogoLogo
HomeDiscourseBlogDiscord
  • Introduction
  • Introduction
    • Quick start
    • Aurelia for new developers
    • Hello world
      • Creating your first app
      • Your first component - part 1: the view model
      • Your first component - part 2: the view
      • Running our app
      • Next steps
  • Templates
    • Template Syntax
      • Attribute binding
      • Event binding
      • Text interpolation
      • Template promises
      • Template references
      • Template variables
      • Globals
    • Custom attributes
    • Value converters (pipes)
    • Binding behaviors
    • Form Inputs
    • CSS classes and styling
    • Conditional Rendering
    • List Rendering
    • Lambda Expressions
    • Local templates (inline templates)
    • SVG
  • Components
    • Component basics
    • Component lifecycles
    • Bindable properties
    • Styling components
    • Slotted content
    • Scope and context
    • CustomElement API
    • Template compilation
      • processContent
      • Extending templating syntax
      • Modifying template parsing with AttributePattern
      • Extending binding language
      • Using the template compiler
      • Attribute mapping
  • Getting to know Aurelia
    • Routing
      • @aurelia/router
        • Getting Started
        • Creating Routes
        • Routing Lifecycle
        • Viewports
        • Navigating
        • Route hooks
        • Router animation
        • Route Events
        • Router Tutorial
        • Router Recipes
      • @aurelia/router-lite
        • Getting started
        • Router configuration
        • Configuring routes
        • Viewports
        • Navigating
        • Lifecycle hooks
        • Router hooks
        • Router events
        • Navigation model
        • Current route
        • Transition plan
    • App configuration and startup
    • Enhance
    • Template controllers
    • Understanding synchronous binding
    • Dynamic composition
    • Portalling elements
    • Observation
      • Observing property changes with @observable
      • Effect observation
      • HTML observation
      • Using observerLocator
    • Watching data
    • Dependency injection (DI)
    • App Tasks
    • Task Queue
    • Event Aggregator
  • Developer Guides
    • Animation
    • Testing
      • Overview
      • Testing attributes
      • Testing components
      • Testing value converters
      • Working with the fluent API
      • Stubs, mocks & spies
    • Logging
    • Building plugins
    • Web Components
    • UI virtualization
    • Errors
      • Kernel Errors
      • Template Compiler Errors
      • Dialog Errors
      • Runtime HTML Errors
    • Bundlers
    • Recipes
      • Apollo GraphQL integration
      • Auth0 integration
      • Containerizing Aurelia apps with Docker
      • Cordova/Phonegap integration
      • CSS-in-JS with Emotion
      • DOM style injection
      • Firebase integration
      • Markdown integration
      • Multi root
      • Progress Web Apps (PWA's)
      • Securing an app
      • SignalR integration
      • Strongly-typed templates
      • TailwindCSS integration
      • WebSockets Integration
      • Web Workers Integration
    • Playground
      • Binding & Templating
      • Custom Attributes
        • Binding to Element Size
      • Integration
        • Microsoft FAST
        • Ionic
    • Migrating to Aurelia 2
      • For plugin authors
      • Side-by-side comparison
    • Cheat Sheet
  • Aurelia Packages
    • Validation
      • Validation Tutorial
      • Plugin Configuration
      • Defining & Customizing Rules
      • Architecture
      • Tagging Rules
      • Model Based Validation
      • Validation Controller
      • Validate Binding Behavior
      • Displaying Errors
      • I18n Internationalization
      • Migration Guide & Breaking Changes
    • i18n Internationalization
    • Fetch Client
      • Overview
      • Setup and Configuration
      • Response types
      • Working with forms
      • Intercepting responses & requests
      • Advanced
    • Event Aggregator
    • State
    • Store
      • Configuration and Setup
      • Middleware
    • Dialog
  • Tutorials
    • Building a ChatGPT inspired app
    • Building a realtime cryptocurrency price tracker
    • Building a todo application
    • Building a weather application
    • Building a widget-based dashboard
    • React inside Aurelia
    • Svelte inside Aurelia
    • Synthetic view
    • Vue inside Aurelia
  • Community Contribution
    • Joining the community
    • Code of conduct
    • Contributor guide
    • Building and testing aurelia
    • Writing documentation
    • Translating documentation
Powered by GitBook
On this page

Was this helpful?

Export as PDF
  1. Components
  2. Template compilation

processContent

Learn how to manipulate the DOM from the usage-side of a custom element using the processContent hook.

There are scenarios where we would like to transform the template provided by the usage-side. The 'processContent' hook lets us define a pre-compilation hook to make that transformation.

The signature of the hook function is as follows.

// pseudo-code; `typeof TCustomElement` doesn't work in Generics form.
<TCustomElement>(this: typeof TCustomElement, node: INode, platform: IPlatform) => boolean | void;

There are two important things to note here.

First is the node argument. It is the DOM tree on the usage-side for the custom element. For example, if there is a custom element named my-element, on which a 'processContent' hook is defined, and it is used somewhere as shown in the following markup, then when the hook is invoked, the node argument will provide the DOM tree that represents the following markup.

<my-element>
 <foo></foo>
 <bar></bar>
</my-element>

Then inside the hook this DOM tree can be transformed/mutated into a different DOM tree. The mutation can be addition/removal of attributes or element nodes.

Second is the return type boolean | void. Returning from this function is optional. Only an explicit false return value results in skipping the compilation (and thereby enhancing) of the child nodes in the DOM tree. The implication of skipping the compilation of the child nodes is that Aurelia will not touch those DOM fragments and will be kept as it is. In other words, if the mutated node contains custom elements, custom attributes, or template controllers, those will not be hydrated.

The platform argument is just the helper to have platform-agnostic operations as it abstracts the platform. Lastly the this argument signifies that the hook function always gets bound to the custom element class function for which the hook is defined.

The most straight forward way to define the hook is to use the processContent property while defining the custom-element.

import { customElement, INode, IPlatform } from '@aurelia/runtime-html';

// Use a standalone function
function processContent(node: INode, platform: IPlatform) { }
@customElement({ name: 'my-element', processContent })
export class MyElement { }

// ... or use a static method named 'processContent' (convention)
@customElement({ name: 'my-element' })
export class MyElement {
  static processContent(node: INode, platform: IPlatform) { }
}

Apart from this, there is also the @processContent decorator which can used class-level or method-level.

import { customElement, INode, IPlatform, processContent } from '@aurelia/runtime-html';

// ...or a standalone method
function processContent(this: typeof MyElement, node: INode, platform: IPlatform) { }
@processContent(processContent)
export class MyElement {
}

// ...or the method-level decorator
export class MyElement {
  @processContent()
  static processContent(node: INode, platform: IPlatform) { }
}

That's the API. Now let us say consider an example. Let us say that we want to create a custom elements that behaves as a tabs control. That is this custom element shows different sets of information grouped under a set of headers, and when the header is clicked the associated content is shown. To this end, we can conceptualize the markup for this custom element as follows.

<!--tabs.html-->
<div class="header">
  <au-slot name="header"></au-slot>
</div>
<div class="content">
  <au-slot name="content"></au-slot>
</div>

The markup has 2 slots for the header and content projection. While using the tabs custom element we want to have the following markup.

<!--app.html-->
<tabs>
  <tab header="Tab one">
    <span>content for first tab.</span>
  </tab>
  <tab header="Tab two">
    <span>content for second tab.</span>
  </tab>
  <tab header="Tab three">
    <span>content for third tab.</span>
  </tab>
</tabs>

Now note that there is no custom element named tab. The idea is to keep the usage-markup as much dev-friendly as possible, so that it is easy to maintain, and the semantics are quite clear. Also it is easy to refactor as now we know which parts belong together. To support this usage-syntax we will use the 'processContent' hook to rearrange the DOM tree, so that the nodes are correctly projected at the end. A prototype implementation is shown below.

// tabs.ts
import { INode, IPlatform, processContent } from '@aurelia/runtime-html';

class Tabs {

  @processContent()
  public static processTabs(node: INode, p: IPlatform): boolean {
    const el = node as Element;

    // At first we prepare two templates that will provide the projections to the `header` and `content` slot respectively.
    const headerTemplate = p.document.createElement('template');
    headerTemplate.setAttribute('au-slot', 'header');
    const contentTemplate = p.document.createElement('template');
    contentTemplate.setAttribute('au-slot', 'content');

    // Query the `<tab>` elements present in the `node`.
    const tabs = toArray(el.querySelectorAll('tab'));
    for (let i = 0; i < tabs.length; i++) {
      const tab = tabs[i];

      // Add header.
      const header = p.document.createElement('button');
      // Add a class binding to mark the active tab.
      header.setAttribute('class.bind', `$host.activeTabId=='${i}'?'active':''`);
      // Add a click delegate to activate a tab.
      header.setAttribute('click.delegate', `$host.showTab('${i}')`);
      header.appendChild(p.document.createTextNode(tab.getAttribute('header')));
      headerTemplate.content.appendChild(header);

      // Add content.
      const content = p.document.createElement('div');
      // Show the content if the tab is activated.
      content.setAttribute('if.bind', `$host.activeTabId=='${i}'`);
      content.append(...toArray(tab.childNodes));
      contentTemplate.content.appendChild(content);

      el.removeChild(tab);
    }
    // Set the first tab as the initial active tab.
    el.setAttribute('active-tab-id', '0');

    el.append(headerTemplate, contentTemplate);
  }

  @bindable public activeTabId: string;
  public showTab(tabId: string) {
    this.activeTabId = tabId;
  }
}

Example transformation function for default [au-slot]

processContent(node: INode, p: IPlatform) {
  const projection = p.document.createElement('template');
  projection.setAttribute('au-slot', '');
  const content = projection.content;
  for (const child of toArray(node.childNodes)) {
    if (!(child as Element).hasAttribute('au-slot')) {
      content.append(child);
    }
  }
  if (content.childElementCount > 0) {
    node.appendChild(projection);
  }
}
PreviousTemplate compilationNextExtending templating syntax

Last updated 2 months ago

Was this helpful?

If you are unfamiliar with the au-slot then visit the . 'processContent' can be very potent with au-slot.

Note the use of $host scope reference that is used when generating markup that will be projected into the slot. Host is required to access the activeTabId property and the showTab function of the Tabs custom element that is hosting the projected markup. More details available at .

If you have used , you might have noticed that in order to provide a projection the usage of [au-slot] attribute is mandatory, even if the projections are targeted to the default au-slot. With the help of the 'processContent' hook we can workaround this minor inconvenience. The following is a sample transformation function that loops over the direct children under node and demotes the nodes without any [au-slot] attribute to a synthetic template[au-slot] node.

Slotted content
documentation
au-slot