Quicklinks
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>
I'm included into the DOM conditionally.
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.
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.
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:
## Header * list item * list itemBy 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.
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:
element
— the virtual template
element;attribute
— the associated
Attr
object on the
template element;hint
— the part in the attribute name after the dot;expression
— the expression automatically compiled from the attribute value;scope
— the abstract data context in which to execute the expression (null
if the mold is not inside a custom element's view).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;
}());
A mold's life begins with a constructor
call. In addition, it can define two
lifecycle methods: onPhase
and onDestroy
.
onPhase
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.
onDestroy
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();
}
}