atril

Experimental JS rendering library

Ideas from ReactJS, Polymer, Angular 2, Aurelia, made simple

Mold

Molds let you modulate the structure of the virtual DOM. They fill the role between custom elements that can only define the view, and custom attributes that can only modify existing DOM nodes.

A mold is a combination of a template tag with a custom attribute. It can be used as a template (enclosing some content). As a shortcut, you can also use it as an attribute on a normal element; in this case it's automatically expanded into a template.

In essense, a mold has "admin access" to the part of the virtual DOM enclosed by it. Example of a mold in practice:

<label>
  <input twoway.checked="checked" type="checkbox">
  <span>Toggle</span>
</label>

<!-- Conditional rendering -->
<template if.="checked">
  <p>I'm included into the DOM conditionally.</p>
</template>

The mold (in this case, the controller of the if. attribute) decides what to with the content caught inside the template tag (in this case, the <p>). It could ignore the content, clone and multiply it, or replace it with something else entirely.

In this case, the if. attribute simply includes the content when the condition is met, and removes it when not.

Basics

First, let's understand the HTML5 template element. When HMTL is parsed into the virtual DOM, the contents of each template tag are put into the special property content. atril shims this behaviour in non-supporting browsers when compiling the DOM.

The mold controller (the class decorated with @Mold) has access to its virtual template element. During its constructor call or in the lifecycle method onPhase it has a chance to take the mold's initial content, perform arbitrary transformations, and append the result to the template element itself. The mold's "output" is the resulting childNodes of the template element.

When a mold is constructed, and every time it's phased, the library checks the template's contents to see if they need to be recompiled. Any custom elements, custom attributes, or molds in the output are automatically activated. Then the contents are synced to the real DOM. The template element itself is not included into the DOM.

Let's implement a mold that compiles and outputs markdown. Using the demo project from Quickstart, create a file src/app/molds/to- markdown.ts with the following.

import {Mold, assign} from 'atril';
import marked from 'marked';

@Mold({
  attributeName: 'markdown-live'
})
class Ctrl {
  @assign element: HTMLTemplateElement;
  @assign expression: Function;
  @assign scope: any;

  buffer: HTMLElement;
  lastValue: string;

  constructor() {
    this.buffer = document.createElement('div');
    this.rewrite();
  }

  onPhase() {
    this.rewrite();
  }

  rewrite() {
    let value = this.expression(this.scope) || '' + '';
    if (value === this.lastValue) return;
    this.buffer.innerHTML = marked(value);

    // Remove existing content.
    while (this.element.hasChildNodes()) {
      this.element.removeChild(this.element.firstChild);
    }

    // Add new content.
    while (this.buffer.hasChildNodes()) {
      this.element.appendChild(this.buffer.removeChild(this.buffer.firstChild));
    }

    this.lastValue = value;
  }
}

Then use it in your view like so:

<textarea twoway.value="myContent"></textarea>
<template markdown-live.="myContent"></template>

Why go through all this fiddly DOM manipulation? Wouldn't it be easier to just keep one element in the DOM and replace its innerHTML with the compiled results?

For plain markdown, it would be. However, there's more to it:

Let's see what happens if our markdown contains atril markup.

The contents of the template tag were automatically compiled by the library and activated just like a normal part of the view.

Mutation

More often you want to use a mold to modify existing virtual markup. In the previous example, we used a separate input to generate the markdown. Now let's put it directly into the template.

Here's the implementation:

import {Mold, assign} from 'atril';
import marked from 'marked';

@Mold({
  attributeName: 'markdown'
})
class Ctrl {
  @assign element: HTMLTemplateElement;

  constructor() {
    let content = this.element.content;

    // Convert existing content into text.
    let buffer = document.createElement('div');
    while (content.hasChildNodes()) {
      buffer.appendChild(content.firstChild);
    }

    // Render into markdown.
    let result = marked(buffer.innerHTML);
    buffer.innerHTML = result;

    while (buffer.hasChildNodes()) {
      this.element.appendChild(buffer.removeChild(buffer.firstChild));
    }
  }
}

Use it in HTML like so:

<template markdown.>
## Header

* list item
* list item
</template>

The mold automatically converts its content into markdown, and here's the result:

Optimisation

By default, the library automatically recompiles the mold's output (the contents of the template tag). If you create new parts of the virtual DOM that use atril features like custom elements, they will work automatically.

However, this has a performance cost. Because the library doesn't know which parts of the virtual DOM you could have modified, it has to scan the entire subtree. If your mold reuses some parts of its virtual DOM, leaving them unchanged between phases, you can "hint" the library not to rescan them.

Excerpt from the if. implementation:

@Mold({attributeName: 'if'})
class If {
  /* ... */

  constructor() {
      /* ... */
      Meta.getOrAddMeta(child).isDomImmutable = true;
      /* ... */
  }
}

The Meta object is a metadata container associated with each node in the virtual DOM tree. The library adds them automatically when compiling nodes, but you can also add a meta to a newly created node.

If a node is marked as isDomImmutable in its metadata, the library will only compile it once, and skip on subsequent reflows. "Immutability" refers to the inner DOM structure of that virtual element, and doesn't prevent DOM updates like text interpolations.

By hinting which mold children won't change, you conserve a considerable amount of performance.

Contextual Dependencies

The library uses a variant of dependency injection — dependency assignment — to give you contextual dependencies for each mold controller. To get hold of them, use the @assign decorator (ES7/TypeScript) or the static assign property on the constructor function (ES5).

A mold has the following contextual dependencies:

Example:

import {Mold, assign} from 'atril';

@Mold({attributeName: 'my-mold'})
class Ctrl {
  @assign element: HTMLTemplateElement;
  @assign attribute: Attr;
  @assign hint: string;
  @assign expression: Function;
  @assign scope: any;

  constructor() {
    // <template my-mold.calc="2 + 2"></template>
    console.log(element);
    // my-mold.calc="2 + 2"
    console.log(attribute);
    // 'calc'
    console.log(hint);
    // function that returns 4
    console.log(expression);
    // outer viewmodel or null
    console.log(scope);
  }
}
<div my-mold.calc="2 + 2"></div>
var Mold = require('atril').Mold;

Mold({attributeName: 'my-mold'})(function() {
  function Ctrl() {}

  // Property names to the left, dependency tokens to the right.
  Ctrl.assign = {
    element: 'element',
    attribute: 'attribute',
    hint: 'hint',
    expression: 'expression',
    scope: 'scope'
  };

  return Ctrl;
}());

Lifecycle

A mold's life begins with a constructor call. In addition, it can define two lifecycle methods: onPhase and onDestroy.

This is called whenever the library reflows the tree of components and bindings in response to user activity. For an example, see the markdown-live.* implementation above.

When the root of this virtual DOM branch is irrevocably removed from the hierarchy, this method is invoked on all components, attributes, and molds. You can use this as a chance to free memory or perform other cleanup tasks. Example:

class Ctrl {
  constructor() {
    createWastefulResource();
  }

  onDestroy() {
    deleteWastefulResource();
  }
}
Overview Quickstart Component Attribute Mold Databinding Bootstrapping if for let on class ref demo