Overview
OOHTML offers a set of five features that make common UI development paradigms possible as native web platform features. These features may be used individually or together for some great UI-authoring capabilites. Here is an overview:
HTML Modules
HTML Modules is a templating feature that lets us write reusable HTML markup using the module, export and import paradigm. This feature establishes the standard <template>
element as the foundation of a module infrastructure for HTML and introduces new attributes, properties and events that together closes the loop.
A module is a regular <template>
element with a name
attribute - the module ID - and its contents are simply the exports.
<head>
<template name="module1">
<label for="age">How old are you?</div>
<input id="age" />
</template>
</head>
Exports may be more properly wrapped within an <export>
element of a designated name.
<head>
<template name="module1">
<export name="question">
<label for="age">How old are you?</label>
<input id="age" />
</export>
<div>This is another export</div>
</template>
</head>
Or they may be individually tagged to an export identifier using the exportgroup
attribute.
<head>
<template name="module1">
<label exportgroup="question" for="age">How old are you?</label>
<input exportgroup="question" name="age" />
<div>This is another export</div>
</template>
</head>
Either way, they are accessed the same way using the Modules API.
let module1 = document.templates.module1;
let questionExport = module1.exports.question;
let questionExportClone = questionExport.map(el => el.cloneNode(true));
Taking things further, template elements may reference remote content using the src
attribute.
<head>
<template name="module-remote" src="/bundle.html"></template>
</head>
The contents of the remote file automatically become the template's content on load.
Details are in the HTML Modules specification. Learn more about the convention, API, events, and the polyfill support.
HTML Imports
HTML Imports is a declarative way to use the exports of an HTML Module from anywhere in the main document.
Note that this is different from the HTML Imports that was spec'd with an early version of Web Components.
Here, an <import>
element in the <body>
area is used to place a corresponing export of an HTML module.
<body>
<import name="question" template="module1"></import>
</body>
Resolution takes place and the <import>
element is replaced by all of the imported contents.
<body>
<label for="age">How old are you?</label>
<input id="age" />
</body>
Also, multiple <import>
elements within a block can be scoped to just one module ID declaration.
<body>
<div template="module1">
<import name="question"></import>
<div>
<import name="export-2"></import>
</div>
</div>
</body>
On resolution, an <import>
element will stand by somewhere with a view to returning to its slot on any event that gets the slot empty. In fact, <import>
elements maintain a live relationship with the modules they point to and with the contents that go into their slot.
So, if we dynamically changed the module ID declaration above to point to another module, imports will be resolved again, this time, from the new module.
document.querySelector('div[template="module1"]').setAttribute('template', 'module2');
This opens up new simple ways to create very dynamic applications.
Details are in the HTML Imports specification. Learn more about the convention, dynamicity, Slot Inheritance, isomorphic rendering, and the polyfill support.
Namespaced HTML
Namespacing provides a way to let an element establish its own naming context for descendant elements. This makes it possible to keep IDs scoped to a context other than the document's global scope; thus the ability to write collision-free IDs across a page.
The following modular markup implements its IDs in namespaces:
<article id="continents" namespace>
<section id="europe" namespace>
<div id="about">About Europe</b></div>
<div id="countries">Countries in Europe</div>
</section>
<section id="asia" namespace>
<div id="about">About Asia</b></div>
<div id="countries">Countries in Asia</div>
</section>
</article>
The above gives us a conceptual model of repeating objects; each encapsulating its IDs:
continents
├⏤europe
│ ├⏤about
│ ├⏤countries
├⏤asia
├⏤about
├⏤countries
And beyond the point of giving us collision-free IDs, Namespaced HTML features an API that translates namespace models into real object trees:
let continents = document.namespace.continents;
let europe = continents.namespace.europe;
let asia = continents.namespace.asia;
let aboutAsia = continents.namespace.asia.namespace.about;
We get a document structure that translates to a bankable API for building great functionalities.
Much of our code in the examples below will now use the namespace
attribute in markup and the .namespace
property in JavaScript.
Details are in the Namespaced HTML specification. Learn more about the convention, Namespaced Selectors, API, observability, and the polyfill support.
The State API
The State API is a DOM API that lets us maintain application state at the document level and at individual element levels. It brings application state closer to the UI and makes it easy to keep the UI in sync with all the changes taking place.
This API exposes a document-level state object on a document.state
property, and an element-level state object on an element.state
property. Arbitrary values can be set and retrieved on state objects the same way we would with regular objects.
document.state.pageTitle = 'Hello World!';
console.log(document.state.pageTitle);
element.state.collapsed = true;
console.log(element.state.collapsed);
But state objects are unique in that they support observability. They are live objects that can be observed for changes using the Observer API.
Observer.observe(document.state, 'pageTitle', e => {
console.log('New Page Title: ' + e.value);
document.querySelector('title').innerHTML = e.value;
});
This lets us build very reactive applications natively.
Using an element's state API, here's how we could make a collapsible component.
<my-collapsible namespace>
<div id="control">Toggle Me</div>
<div id="content" style="height: 0px">
Some content
</div>
</my-collapsible>
customElements.define('my-collapsible', class extends HTMLElement {
constructor() {
super();
Observer.observe(this.state, 'collapsed', e => {
this.namespace.content.style.height = e.value ? '0px' : 'auto';
this.setAttribute('data-collapsed', e.value ? 'true' : 'false');
});
this.namespace.control.addEventListener('click', function() {
this.state.collapsed = !this.state.collapsed;
});
}
});
Other parts of the application are also able to access the state of this element.
let collapsible = document.querySelector('my-collapsible');
Observer.observe(collapsible.state, 'collapsed', e => {
console.log(e.value ? 'element collapsed' : 'element expanded');
});
Details are in the State API specification. Learn more about the API, deep observability, and the polyfill support.
Subscript
Subscript is a special language feature for JavaScript that lets us write reactive JavaScript code in plain JavaScript. Subscript UI is a <1KB extension of Subscript that further simplifies the concept of reactivity for UI development.
There are two approaches to using Subscript:
(A): SubscriptElement
SubscriptElement is an extension of SubscriptClass
- a base class Mixin for designating reactive class methods.
class MyClass extends SubscriptElement( HTMLElement ) {
static get subscriptMethods() {
return [ 'render' ];
}
render() {
}
}
An alert element:
customElements.define( 'my-alert', class Alert extends SubscriptElement( HTMLElement ) {
static get subscriptMethods() {
return [ 'render' ];
}
connectedCallback() {
this.render();
}
render() {
let messageElement = this.querySelector( '.message' );
messageElement.innerHTML = globalMessage;
}
} );
var globalMessage = 'This site uses cookies!';
<body>
<my-alert>
<div class="message"></div>
</my-alert>
</body>
(B): ScopedSubscript
ScopedSubscript is a <script>
-based flavour of Subscript that lets us write reactive <script>
elements right within an HTML document; each scoped to their host element instead of the global browser scope.
<div id="alert">
<script type="subscript">
console.log( this.id );
</script>
</div>
The <script>
above is scoped to the #alert
element - its host element; the this
variable is a reference to the script's host element.
This lets us have reactive UI logic jsut where they are needed, without involving any JavaScript classes or files.
An alert element:
var globalMessage = 'This site uses cookies!';
<body>
<div id="alert">
<div class="message"></div>
<script type="subscript">
let messageElement = this.querySelector( '.message' );
messageElement.innerHTML = globalMessage;
</script>
</div>
</body>
Details are in the Subscript specification. Learn more about the reactivity and observability concepts and the polyfill support.
Getting Started
You definitely want to visit the documentation for each of OOHTML's features and try everything out by pasting the code examples and running them right in your browser. Simply include the OOHTML polyfill on your page and get away with writing modular, reusable, reactive HTML without a tool!
We're putting together a collection of examples in the examples section.