dna-poster

DNA

Progressive Web Components.

Templates

Templates are the main part of a component definition because they are used to render the state as well as instantiate and update child elements. During a render cycle, DNA uses an in-place DOM diffing algorithm to check which nodes are to update, create or remove. In order to efficiently compare DOM nodes, templates cannot be plain HTML strings. Use the render method and the html helper to return the template for the element:

import { Component, customElements, html } from '@chialab/dna';

class HelloWorld extends Component {
    render() {
        return html`<h1>Hello world!</h1>`;
    }
}

customElements.define('hello-world', HelloWorld);
Using JSX

If you familiar with JSX, you can also use it since DNA templates are 100% compatible with JSX transpiled output:

import { Component, customElements, h } from '@chialab/dna';

class HelloWorld extends Component {
    render() {
        return <h1>Hello world!</h1>;
    }
}

customElements.define('hello-world', HelloWorld);

Please rember to configure the @babel/plugin-transform-react-jsx in roder to use the DNA's h and Fragment helpers:

{
    "plugins": [
        ["@babel/plugin-transform-react-jsx", {
            "pragma": "h",
            "pragmaFrag": "Fragment",
        }]
    ]
}

Interpolation rules

When interpolating an expression, the following rules (based on the type of the result and context) are applied:

Type Content Attribute
string Add/update as Text node Add as value
number Add/update as Text node Add as value
boolean / Add/remove attribute, reference as property
null / Remove attribute
undefined / Remove attribute
Node Add/update node .toString() as value, reference as property
array Add/update iterated content .toString() as value, reference as property
object .toString() as Text node .toString() as value, reference as property
function .toString() as Text node .toString() as value, reference as property
Content interpolation
html`<span>${this.firstName} ${this.lastName}</span>`
JSX
<span>{this.firstName} {this.lastName}</span>
Raw
h('span', null, this.firstName, ' ', this.lastName)
Attribute interpolation
html`<input name=${this.name} disabled=${this.disabled} required />`
JSX
<input name={this.name} disabled={this.disabled} required />
Raw
h('input', { name: this.name, disabled: this.disabled, required: true })
Loops

When using loops it is necessary to keep in mind the interpolation rules: in order to correctly render a table or a list of data, we need to interpolate an array of templates:

html`<ul>
    ${this.items.map((item, index) => html`<li>${index}. ${item}</li>`)}
</ul>`
JSX
<ul>
    {this.items.map((item, index) => <li>{index}. {item}</li>)}
</ul>
Raw
h('ul', null, this.items.map((item, index) => 
    h('li', null, index, '. ', item)
))
Conditionals

You can create conditional interpolations based on a boolean value using ternary operator or logical expression which results in a template or any other value:

html`
    ${this.avatar & html`<img src=${this.avatar} />`}
    <h1>${this.title || 'Untitled'}</h1>
    ${this.members.length ?
        html`${this.members.length} members` :
        'No members'
    }
`
JSX
<>
    {this.avatar & <img src={this.avatar} />}
    <h1>{this.title || 'Untitled'}</h1>
    {this.members.length ?
        `${this.members.length} members` :
        'No members'
    }
</>
Raw

Promises

The DNA's render algorithm has builtin support for Promises in template: it interpolates the result of a Promise as if you were using the await statement and provides the helper until for status handling:

import { until } from '@chialab/dna';

const json = fetch('/data.json')
    .then(() => response.json())
    .then((data) => data.map(({ name }) => html`<li>${name}</li>`));

html`
    ${until(json, 'Loading...')}
    ${json
        .then((data) => html`<ul>${data}</ul>`)
        .catch((error) => html`<div>Error: ${error}</div>`)
    }
`
JSX
import { until } from '@chialab/dna';
<>
    {until(json, 'Loading...')}
    {json
        .then((data) => <ul>{data}</ul>)
        .catch((error) => <div>Error: {error}</div>)}
</>
Raw
h(Fragment, null,
    until(json, 'Loading...'),
    json
        .then((data) => h('ul', null, data))
        .catch((error) => h('div', null, 'Error ', error)),
)
Observables

As well as Promises, DNA treats Observables like as first class references. You can interpolate [Observable]s' values or pipe a template:

import { timer, interval } from 'rxjs';
import { take } from 'rxjs/operators';

const clock$ = timer(Date.now());
const numbers$ = interval(1000).pipe(take(4));

html`
    Timer: ${timer$},
    Numbers: ${numbers$.pipe((val) => val % 2 ? html`<strong>${val}</strong>` : val)}
`
JSX
<>
    Timer: {timer$},
    Numbers: {numbers$.pipe((val) => val % 2 ? <strong>{val}</strong> : val)}
</>
Raw
h(Fragment, null,
    'Timer: ',
    timer$,
    ', Numbers',
    numbers$.pipe((val) => val % 2 ? h('strong', null, val) : val)
)

HTML content

By default, HTML strings will be interpolated as plain content. It means that a property content valorized as "<h1>Hello</h1>" will not create a H1 element, but it will print the code as is. In order to render dynamic html content, you need to pass the code to the html helper:

import { html } from '@chialab/dna';

const content = '&lt;h1>Hello&lt;/h1>';

-&lt;x-label>{content}&lt;/x-label>;
+&lt;x-label>{html(content)}&lt;/x-label>;

Functional components

Sometimes, you may want to break up templates in smaller parts without having to define new Custom Elements. In this cases, you can use functional components. Functional components have first class support in many frameworks like React and Vue, but they require hooks in order to update DOM changes. Since DNA's state is reflected to the DOM and a "current context" is missing, the implemention is slightly different and does not require extra abstraction.

function Row({ children, id }, state, update) {
    let selected = state.get('selcted') ?? false;
    let toggle = () => {
        state.set('selected', !selected);
        update();
    };

    return html`&lt;tr id=${id} class="${{ selected }}" onclick=${toggle}>${children}>${children}&lt;/tr>`;
}

html`&lt;table>
    &lt;tbody>
        ${items.map((item) => html`&lt;${Row} ...${item}>
            &lt;td>${item.id}&lt;/td>
            &lt;td>${item.label}&lt;/td>
        &lt;/>`)}
    &lt;/tbody>
&lt;/table>`
JSX
function Row({ children, id }, state, refresh) {
    let selected = state.get('selcted') ?? false;
    let toggle = () => {
        state.set('selected', !selected);
        update();
    };

    return &lt;tr id=${id} class={ selected } onclick={toggle}>{...children}&lt;/tr>;
}

&lt;table>
    &lt;tbody>
        {items.map((item) => &lt;Row ...item>
            &lt;td>{item.id}&lt;/td>
            &lt;td>{item.label}&lt;/td>
        &lt;/Row>}
    &lt;/tbody>
&lt;/table>
Raw
function Row({ children, id }, state, update) {
    let selected = state.get('selcted') ?? false;
    let toggle = () => {
        state.set('selected', !selected);
        update();
    };

    return h('tr', { id, selected, onclick: toggle }, ...children);
}

h('table', null,
    h('tbody', null
        items.map((item) => h(Row, ...item,
            h('td', null, item.id)
            h('td', null, item.label)
        ))
    ),
)

Nodes

DNA can handle Node instances as children and hyper nodes as well. When passed as children, the very same node is positioned "as is" to the right place in the template:

import { DOM, render } from '@chialab/dna';

let paragraph = DOM.createElement('p');
paragraph.textContent = 'Lorem Ipsum';

render(html`&lt;div>${paragraph}&lt;/div>`, document.body);

will render:

&lt;body>
    &lt;div>
        &lt;p>Lorem Ipsum&lt;/p>
    &lt;/div>
&lt;/body>

If you want to add some properties to the instance, you can pass it as hyper node. This is useful if you want to reference some nodes in your component:

import { DOM, Component, html, customElements } from '@chialab/dna';

class Form extends Component {
    static get listeners() {
        return {
            'change input'(this: Form) {
                console.log(this.input.value);
            },
        };
    }

    input = DOM.createElement('input');

    render() {
        return html`&lt;form>
            &lt;${this.input} name="firstName" placeholder="Alan" />
        &lt;/form>`;
    }
}

customElements.define('x-form', Form);

Slotted children

One of the best practice for Web Components is to use the Shadow DOM to render and stylize component's children. Shadow DOM is a good choice for encapsulating styles and handling components children but not for composability. During the render cycle, DNA is able to replicate some ShadowDOM features like slotted children and styles encapsulation.

For example, we may declare a custom <dialog is="x-dialog"> tag with some layout features:

import { Component, customElements, html, property } from '@chialab/dna';

class Dialog extends Component {
    static get observedAttributes() {
        return ['title', 'content'];
    }

    @property() title = '';
    @property() content = '';

    render() {
        return html`
            &lt;div class="layout-container">
                &lt;div class="layout-header">
                    &lt;h1>${this.title}&lt;/h1>
                &lt;/div>
                &lt;div class="layout-content">
                    ${this.content}
                &lt;/div>
            &lt;/div>
        `;
    }
}

customElements.define('x-dialog', Dialog, { extends: 'dialog' });

This example has two problems:

  • Content is passed as property, which is not good for semantic

  • Body is interpolated as string, so HTML code is rendered as plain text.

Shadow DOM solves those two issues, rendering "soft" children of an element into the <slot> tag:

class Dialog extends Component {
-    static get observedAttributes() {
-        return ['title', 'content'];
-    }

-    @property() title = '';
-    @property() content = '';

    render() {
        return html`
            &lt;div class="layout-container">
-                &lt;div class="layout-header">
-                    &lt;h1>${this.title}&lt;/h1>
-                &lt;/div>
                &lt;div class="layout-content">
-                   ${this.content}
+                   &lt;slot />
                &lt;/div>
            &lt;/div>
        `;
    }
}

Now, every "soft" child of the <dialog is="x-dialog"> element is rendered into the layout:

&lt;dialog is="x-dialog">
    &lt;h1>How to use DNA&lt;/h1>
    &lt;img src="https://placekitten.com/300/200" />
    &lt;p>Lorem ipsum dolor sit amet consectetur adipisicing &lt;em>elit&lt;/em>.&lt;/p>
&lt;/dialog>

results

&lt;dialog is="x-dialog">
    &lt;div class="layout-container">
        &lt;div class="layout-content">
            &lt;h1>How to use DNA&lt;/h1>
            &lt;img src="https://placekitten.com/300/200" />
            &lt;p>Lorem ipsum dolor sit amet consectetur adipisicing &lt;em>elit&lt;/em>.&lt;/p>
        &lt;/div>
    &lt;/div>
&lt;/dialog>

We can also define multiple <slot> using a name, and reference them in the "soft" DOM using the slot="name" attribute, in order to handle more complex templates. The "unnamed" <slot> will colleced any element which does not specify a slot.

class Dialog extends Component {
    render() {
        return html`
            &lt;div class="layout-container">
+               &lt;div class="layout-header">
+                   &lt;slot name="title" />
+               &lt;/div>
                &lt;div class="layout-content">
                    &lt;slot />
                &lt;/div>
            &lt;/div>
        `;
    }
}

Update the HTML sample adding <h1> to the title slot.

&lt;dialog is="x-dialog">
-    &lt;h1>How to use DNA&lt;/h1>
+    &lt;h1 slot="title">How to use DNA&lt;/h1>
    &lt;img src="https://placekitten.com/300/200" />
    &lt;p>Lorem ipsum dolor sit amet consectetur adipisicing &lt;em>elit&lt;/em>.&lt;/p>
&lt;/dialog>

Now the resulting DOM would be:

&lt;dialog is="x-dialog">
    &lt;div class="layout-container">
        &lt;div class="layout-header">
            &lt;h1>How to use DNA&lt;/h1>
        &lt;/div>
        &lt;div class="layout-content">
            &lt;img src="https://placekitten.com/300/200" />
            &lt;p>Lorem ipsum dolor sit amet consectetur adipisicing &lt;em>elit&lt;/em>.&lt;/p>
        &lt;/div>
    &lt;/div>
&lt;/dialog>

Keyed elements

DNA optimizes rendering re-using elements when possible, comparing the tag name for elements, content for text nodes and constructor for components. Sometimes, you may prefer re-create a node instead of reusing the previous one. In this cases, you can use the key attribute to define an unique slug for the component that will be used for comparisons.

html`
    &lt;select>
        ${this.items.map((item) => html`
            &lt;option value=${item}>${item}&lt;/option>
        `)}
        &lt;option key="last" value="other">Other&lt;/option>
    &lt;/select>
`

In this example, once the last <option> element has been created, it never changes its DOM reference, since previous <option> generations always re-create the element instead of re-using the keyed one.