atril

Experimental JS rendering library

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

Component

Synonymous with custom element. Combines a view model (a data layer) with a view (an HTML template used for every such element).

The viewmodel class describes how to create a viewmodel object (VM for short) for each of these elements. The VM's data and methods are available in the view. It acts as the view's local scope — a feature missing from the native DOM API.

A custom element is registered under a new tag name, and activated during bootstrapping. Afterwards, atril manages the element, automatically updating the view whenever the data changes.

Basics

Example custom element:

// Viewmodel.

import {Component} from 'atril';

@Component({
  tagName: 'hello-world'
})
class ViewModel {
  name = 'world';
  static viewUrl = 'hello-world/hello-world.html';
}
<!-- Template. -->

<!-- Updates automatically -->
<h1>Hello, {{name}}!</h1>

<!-- Two-way databinding -->
<input twoway.value="name">

<!-- One-way databinding with manual feedback -->
<input bind.value="name" on.input="name = this.value">

<!-- One-way databinding with no feedback;
     on.input is needed to detect user activity -->
<input bind.value="name" on.input>
<!-- Usage in HTML -->

<hello-world></hello-world>
var Component = require('atril').Component;

Component({
  tagName: 'hello-world'
})(ViewModel);

function ViewModel() {
  this.name = 'world';
}

ViewModel.viewUrl = 'hello-world/hello-world.html';
<!-- Updates automatically -->
<h1>Hello, {{name}}!</h1>

<!-- Two-way databinding -->
<input twoway.value="name">

<!-- One-way databinding with manual feedback -->
<input bind.value="name" on.input="name = this.value">

<!-- One-way databinding with no feedback;
     on.input is needed to detect user activity -->
<input bind.value="name" on.input>

View

Each custom element has an optional view, an HTML template. Given a component class X, the view can be provided in the following ways:

X.view => string

@Component({tagName: 'my-element'})
class X {
  static view = `
    <p>Hello {{name}}!</p>
    <input twoway.value="name">
  `;
}

This works well when importing views through the SystemJS text plugin:

import view from './my-element.html!text';

@Component({tagName: 'my-element'})
class X {
  static view = view;
}

X.viewUrl => string

Primary method of view loading. The library automatically loads the view by the given URL, then activates the component once the view is available.

@Component({tagName: 'my-element'})
class X {
  static viewUrl = 'my-element/my-element.html';
}

Loaded views are synchronously available through the viewCache utility exposed by the library. It can also load a hitherto unavailable view, or set a view by the given URL without loading it over the network. For production, views should be converted to JS and precached in the viewCache. If you're building with gulp, use gulp-html-to-js to do this for you.

The build system for this documentation site includes view preprocessing; you're welcome to use it as an example. ([1])

X.view => Promise => string

You can return a promise that resolves to a view. Example using fetch:

let viewPromise = null;

@Component({tagName: 'my-element'})
class X {
  static get view() {
    return viewPromise || fetch('/my-secret-view-url')
      .then(response => viewPromise = response.text());
  }
}

This lets you load views asynchronously through custom means.

Contextual Dependencies

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

The component viewmodel has just one dependency: element.

import {Component, assign} from 'atril';

@Component({tagName: 'my-element'})
class VM {
  @assign element: Element;
  constructor() {
    console.log(this.element);
  }
}

You can also use a different property name:

import {Component, assign} from 'atril';

@Component({tagName: 'my-element'})
class VM {
  @assign('element') elem: Element;
  constructor() {
    console.log(this.elem);
  }
}
var Component = require('atril').Component;

Component({tagName: 'my-element'})(function() {
  function VM() {
    console.log(this.elem);
  }

  VM.assign = {elem: 'element'};

  return VM;
}());

Lifecycle

A component'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. Example usage:

class VM {
  @assign element: Element;

  onPhase() {
    let anchor = this.element.querySelector('a');
    if (this.isActive(anchor, location.pathname)) {
      anchor.classList.add('active');
    } else {
      anchor.classList.remove('active');
    }
  }
}

When a root of the component hierarchy is removed from the real DOM, this is called for each descendant component and the root itself. You can use this as a chance to free memory or perform other cleanup tasks. Example:

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

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