HTML components should be as simple and as powerful as functions.
greet(subject("Guten Tag"), ", ", object("Welt"), "!")
A tag is an invocation of a web component and the fragment between the start and end tag is its argument.
<greet>
<subject>Guten Tag</subject>,
<object>Welt</object>!
</greet>
The result is a component tree that governs a fragment of a document. However, components must also be reactive, adapting their output to changes to their input. So, like a cell in a spreadsheet, as its value gets incrementally re-evaluated whenenver one of its dependent variables changes, the fragment of the document it governs adapts incrementally to changes to the bound data.
Furthermore, an HTML module is a component declaration. Like a function declaration, it describes how it will receive its arguments and how to assemble the resulting component from other, constituent components. Like a module, it describes the tags it depends upon to assemble its own body.
In this example, list.html
declares a list component.
The component accepts the entire argument fragment as a single component
constructor, as indicated by accepts="[body]"
.
As directed by accepts="[body]"
, Guten Tag's calling convention arranges for
the fragment between <list>
and </list>
to become a component and binds it
to the <argument>
tag in the scope of list.html
.
The list can instantiate that component in any number of locations any number
of times.
<!-- list.html -->
<html>
<head>
<link rel="tag" href="gutentag/repeat.html">
<meta accepts="[body]" as="argument">
<meta exports="items:iteration" as="item">
</head>
<body>
<ul><repeat id="items">
<li id="item" type="a"><argument id="argument"></argument></li>
</repeat></ul>
</body>
</html>
Every instance of that component has its own lexical scope. Each scope is a name space mapping component identifiers to child components and may have a parent scope and an argument scope. Each tag exists in a scope and some tags, like the repeat tag, may introduce child scopes. If a tag instantiates its argument, the arguments are instantiated in their own lexical scope.
<!-- main.html -->
<html>
<head>
<link rel="tag" href="./list.html">
<link rel="tag" href="gutentags/text.html">
</head>
<body>
<list id="list">
<text id="item"></text>
</list>
</body>
</html>
In this example, creating a list component passes the text component as an
argument.
Even though that argument is instantiated in the list.html
module, it is
evaluated in the scope of the main.html
module, the module in which it is
declared: its lexical scope.
The gutentag/repeat.html
module is one of those modules that can create a
nested scope for its argument components.
Each of these scopes get introduced to list.html
as an iteration component
with the identifier items:iteration
, which can be addressed to bind its
value
or index
.
Since the list.html
component uses the <repeat>
tag around an <argument>
tag, it effectively inherits this ability to introduce scopes.
By exporting items:iteration
as item
, the <list>
tag in main.html
introduces a scope with a list:item
component in the scope of the text item,
with which you can again bind the value
or index
.
With some additional JavaScript (or a binding declaration not addressed by this module) to bind the value of the list to an array, and to bind that array to the inner component's repetition, this component would drive an actual document with a lettered list, incrementally updating the document in response to values added and removed from its model.
Guten Tag also supports several other calling conventions, allowing you to pass multiple argument constructors, raw html, raw text, and attributes. Components can also introduce multiple nested scopes for multi-dimensional data.
A guten tag is an HTML or XML file that defines a template for instantiating a
combination of JavaScript components and DOM elements.
Guten tags export a component constructor and can import other component
constructors, binding them to tag names in the scope of a document.
The markup translates to JavaScript, on-the-fly in the browser during
development, or in Node.js as a build step.
In this examle, we import the <repeat>
and <text>
tags from guten tag
modules.
<head>
<link rel="extends" href="./list">
<link rel="tag" href="gutentag/repeat.html">
<link rel="tag" href="gutentag/text.html">
</head>
Guten tags have a lexical scope for component and element identifiers, and can introduce components into caller scopes under the identifier of the caller. In this example, there is a repetition with an id of "items" that introduces "items:iteration" in the iteration scopes from the body of the repetition.
<ul><repeat id="items">
<li id="item" type="a"><text id="text"></text></li>
</repeat></ul>
Guten tag only provides the <text>
, <html>
, <repeat>
, <reveal>
, and
<choose>
tags and the system for loading tags.
Bring your own bindings, shims, data, animation, or virtual document if you need
them.
The "Guten Tag, Welt!" application is about 15K and 3K after uglify and gzip.
A tag is defined in HTML or XML and can import tags from other HTML modules.
This list.html
tag produces a list at whatever point in a document you
incorporate it.
An instance of a tag is a component.
<!doctype html>
<html>
<head>
<link rel="extends" href="./list">
<link rel="tag" href="gutentag/repeat.html">
<link rel="tag" href="gutentag/text.html">
</head>
<body>
<ul><repeat id="items">
<li id="item" type="a"><text id="text"></text></li>
</repeat></ul>
</body>
</html>
A JavaScript module, list.js
, connects the components of the list.
The HTML module exports a constructor for the module and calls into the
hookup
method of the underlying JavaScript implementation, if it exists.
'use strict';
module.exports = List;
function List() {}
List.prototype.hookup = function hookup(id, child, scope) {
if (id === "items:iteration") {
scope.text.value = child.value;
} else if (id === "this") {
this.items = scope.items;
}
};
Trivial tags can live without an underlying JavaScript implementation.
Tags modules compile to JavaScript that export a component constructor..
The constructor accepts a body
and a caller
scope.
"use strict";
module.exports = Component;
function Component(body, caller) {
this.scope = caller.root.nestComponents();
body.appendChild(document.createTextNode("Guten Tag, Welt!\n"));
}
The body
is a special kind of node in a virtual document.
It represents a point in the actual document that the given tag will control.
Bodies can be added and removed from the virtual document, and all of their
content will be (synchronously) added or removed from the actual document.
However, bodies do not introduce a container element, like a <div>
.
This is critical for the Gutenttag structural tags, <repeat>
, <reveal>
, and
<choose>
, since you may or may not need a container element around them or
inside them, and you may want one or more of these inline.
<table>
<tr>
<th><!-- corner --></th>
<th id="groupOneHeader" colspan=""></th>
<th id="groupTwoHeader" colspan=""></th>
</tr>
<tr>
<th><text id="rowHeader"></text></th>
<repeat id="groupOne">
<td><text id="cell"></text></td>
</repeat>
<repeat id="groupTwo">
<td><text id="cell"></text></td>
</repeat>
</tr>
</table>
In other cases, having a wrapper element would interfere with CSS selectors, particularly for the flex model, or would interfere with semantic markup.
<repeat id="stanzas"><p class="stanza">
<repeat id="lines">
<text id="line"></text>
<reveal id="medial"><br></reveal>
</repeat>
</p></repeat>
Sometimes it is useful to compose text without container elements at all.
<sp><text id="hello">, <text id="person">!</sp>
The caller
object is a scope container that inherits properties along its
prototype chain up to the root scope.
This makes the root scope object an ideal container for dependency injection.
The scope.root
refers to that root scope from any descendant scope.
Each scope also has a direct reference to its scope.parent
.
In a component, scope.this
will always refer to the component instantiated by
containing tag document.
So, in foo.html
, scope.this
is the containing instance of the Foo
component.
The scope.components
object maps component identifiers in that scope to their
corresponding component instance.
Every subcomponent has a scope, but many scopes share the same
scope.components
.
The body of an HTML tag is the root of a lexical scope and introduces an empty
scope.components
object to which each child component adds itslef.
In this example, the scope.components
object will have hello
and person
components.
Note that Gutentags trim implied white space between tags and the <sp>
special
tag notes explicit template text.
<!doctype html>
<html>
<head>
<link rel="tag" href="gutentag/text.html">
</head>
<body>
<text id="hello"></text><sp>, </sp>
<text id="person">!
</body>
</html>
The Gutentag building blocks, <repeat>
, <reveal>
, and <choose>
create
child scopes that introduce a new scope.components
object that inherits
prototypically from the containing scope's components,
scope.parent.components
.
In this example, the "hello" and "person" components are each within a
"greetings:iteration" component and have access to scope.components.header
,
but from the perspective of the "header", it is in a scope by itself.
<!doctype html>
<html>
<head>
<link rel="tag" href="gutentag/text.html">
<link rel="tag" href="gutentag/repeat.html">
</head>
<body>
<h1><text id="header"></h1>
<repeat id="greetings">
<text id="hello"></text><sp>, </sp>
<text id="person">!
</repeat>
</body>
</html>
Each scope may also have a scope.caller
property, referring to the lexical
scope of the tag that instantiated this component, and a scope.argument
referring to a template for the content of the instantiating tag.
Instantiating a tag from within a tag also passes its inner content as a
template in the form requested by that tag through its <meta accepts>
header.
For example, text.html
has <meta accepts="[text]">
, repeat.html
has <meta accepts="[body]">
, and choose.html
has <meta accepts="[entries]">
.
Each of these packs the content into caller.argument
in the fashion expected
by the component.
Every Gutentag application starts with an npm package.
You will need a package.json
.
Use npm init
to create one.
You will also need copies of the module system and Gutentag installed locally.
Tags require a module system that can load these HTML modules.
The System loader supports to-JavaScript translator modules.
During development, System supports loading modules installed by npm without a
build step, provided that the packages are compatible (no support for
directories with an implicit index.js
) and that the modules have not been
deduped (with npm dedupe
).
$ npm init
$ npm install --save system
$ npm install --save koerper
$ npm install --save gutentag
To enable Mr to load Gutentag HTML or XML files, add a "extensions" annotation
to package.json
.
{
"extensions": {
"html": "gutentag/extension",
"xml": "gutentag/extension"
}
}
A Gutentag application starts with a boilerplate index.html
.
This HTML is suitable for debugging locally with your favorite web server.
Refreshing the page reloads all modules without incurring a build step.
You have the option of using this page as a loading screen if your application
takes a significant amount of time to load.
It also would host all of your metadata and assets like icons and CSS themes.
<!doctype html>
<html>
<head>
</head>
<body>
<script src="node_modules/system/boot.js" data-import="./index"></script>
</body>
</html>
The HTML calls into the Mr bootstrapping script which in turn loads the entry
module, index.js
.
This boilerplate module just creates a virtual document, a root scope, and the
root component.
var Document = require("koerper");
var Scope = require("gutentag/scope");
var App = require("app.html");
var scope = new Scope();
var document = new Document(window.document.body);
var app = new App(document.documentElement, scope);
To bundle up your modules for production, System also provides a tool called
Bundle, that operates like Browserify and generates a bundle.
Bundle translates all of the HTML modules into JavaScript and bundles a very
minimal module system.
You can add a build step to your package.json and run it with npm run build
.
{
"scripts": {
"build": "bundle index.js | uglifyjs > bundle.js"
}
}
For my debut Gutentag application, there is a build script
that rolls up the bundled HTML and CSS directly to a gh-pages
branch, suitable
for pushing to Github.
The gutentags project provides the following building block tags, all of which
have very simple implementations that you can copy and modify for your needs.
They all have a mutable value
property.
text.html
controls inline texthtml.html
controls an inline block of HTMLrepeat.html
repeats its argumentreveal.html
shows or hides its argumentswitch.html
shows one of its argument tags
A text tag controls a text node based on its value
property.
The default text, if its value property is null
or undefined
, is the
innerText of the argument.
<!doctype html>
<html>
<head>
<link rel="tag" href="gutentag/text.html">
</head>
<body>
<text id="description">Beschreibung bevorstehende.</text>
</body>
</html>
text.value = "Guten Tag, Welt!";
Take a peek at essays/text.
An HTML tag controls the HTML that appears at its position based on its value
property.
Like a text tag, the HTML tag does not introduce a wrapper element.
Note that html
is a special tag in the HTML5 vocabulary, so this tag has to be
linked by an alternate tag name or used in XML.
<!doctype html>
<html>
<head>
<link rel="tag" href="gutentag/html.html" as="x-html">
</head>
<body>
<x-html id="description"></x-html>
</body>
</html>
html.value = "<b>Bold <i>and</b> italic</b></i>";
Take a peek at essays/html.
Repeats its content based on the values in the given value
array.
<!doctype html>
<html>
<head>
<link rel="extends" href="./list">
<link rel="tag" href="gutentag/repeat.html">
<link rel="tag" href="gutentag/text.html">
</head>
<body>
<ul><repeat id="items">
<li id="item" type="a"><text id="text"></text></li>
</repeat></ul>
</body>
</html>
The repetition creates a scope for each of its iterations.
In that scope, the iteration object is accessible by a name constructed from the
id of the iteration, plus ":iteration".
The iteration object has an index
and a value
property.
'use strict';
module.exports = List;
function List() {}
List.prototype.hookup = function hookup(id, component, scope) {
var components = scope.components;
if (id === "items:iteration") {
components.text.value = component.value;
}
};
The repetition creates new iterations on demand and reacts to changes to the given values array.
Take a peek at essays/repeat.
Reveals its content based on whether value
is truthy.
<!doctype html>
<html>
<head>
<link rel="extends" href="./blink">
<link rel="tag" href="gutentag/reveal.html">
<meta accepts="[body]" as="argument">
</head>
<body>
<reveal id="content"><argument></argument></reveal>
</body>
</html>
'use strict';
module.exports = Blink;
function Blink() {}
Blink.prototype.hookup = function hookup(id, component) {
if (id === "content") {
setInterval(function () {
component.value = !component.value;
}, 1000);
}
}
A <reveal id="content">
tag instantiates its inner content in a
content:revelation
scope each time it reveals that content.
Take a peek at essay/reveal.
Reveals one of its options, as expressed by named child tags, based on its
value
.
Constructs the children on demand.
<!doctype html>
<html>
<head>
<link rel="extends" href="./essay">
<link rel="tag" href="../../choose.html">
<link rel="tag" href="../../repeat.html">
<link rel="tag" href="../../text.html">
</head>
<body>
<repeat id="buttons">
<button id="button">
<text id="buttonLabel">—</text>
</button>
</repeat>
<choose id="options">
<a>Police</a>
<b>Officer</b>
<c>Wolves</c>
<d>Old Witch</d>
<e>Going to Sleep</e>
</choose>
</body>
</html>
choose.value = "e";
A <choose id="options">
tag instantiates one of its choices in a fresh scope
each time its value changes.
The name of that scope comes from the identifier of the component and the given
value, so the iteration would be called "options:e" in this case.
Take a peek at essays/choose.
The gutentags project provides only the minimum building blocks to get your project started, and establishes a convention for packaging your own tags.
If your package defines a single tag like autocomplete.html
, name your package
autocomplete.html
and define your main module as index.html
.
{
"name": "autocomplete.html",
"description": "An autocomplete guten tag",
"version": "1.0.0",
"main": "./index.html"
}
Invoking a tag in another tag may use the content between the start and end tag in various ways to pass an argument into the called tag. Tags must express how they receive their argument.
-
<meta accepts="[body]">
receive the entire argument as a component. The argument is instantiable in HTML tag definitions as the<argument>
tag.Use
caller.argument.component
, which is a component constructor. -
<meta accepts="[entries]">
receive each of the child nodes as a named argument component. The component constructor will receive an object with named properties for each component.Use
caller.argument.children
, which is an object mapping the name of the child tag to a component constructor. -
<meta accepts="[text]">
receives the entire argument as a string from itsinnerText
.Use
caller.argument.innerText
to access the caller template's inner text. -
<meta accepts="[html]">
receives the entire argument as a string from itsinnerHTML
.Use
caller.argument.innerHTML
to access the caller templates inner HTML.
For example, this tag parenthesizes its argument.
<!doctype html>
<html>
<head>
<meta accepts="[body]">
</head>
<body>(<argument></argument>)</body>
</html>
The caller.argument
object will also have a tagName
.
In a future version, it may also support attributes and a language for matching
other DOM patterns.
The building block components, <repeat>
, <reveal>
, and <choose>
all create
a child scope for the components instantiated by their inner template.
This has interesting implications for arguments instantiated within that scope.
Consider a <list>
tag that contains a repetition and accepts an argument
component for each row.
<!doctype html>
<html>
<head>
<link rel="extends" href="./list">
<link rel="tag" href="gutentag/repeat.html">
<meta accepts="[body]" as="row">
<meta exports="rows:iteration" as="row">
</head>
<body>
<ul><repeat id="rows">
<li><row></row></li>
</repeat></ul>
</body>
</html>
Another component instantiates the list with a text component in each row.
<!doctype html>
<html>
<head>
<link rel="extends" href="./essay">
<link rel="tag" href="gutentag/text.html">
<link rel="tag" href="./list.html">
</head>
<body>
<list id="items">
<text id="item"></text>
</list>
</body>
</html>
In the list component, each row is known as "rows:iteration". However, the repetition also cuts through the transitive caller scopes creating a new scope for instantiated arguments. In this case, the list component creates an "items:row" iteration based on the name of the caller ("items") and name exported by the list ("rows:iteration" gets exported as "row").
Thus, from within this essay, we observe the instantiation of "items:row" and can see the value of the iteration.
module.exports = Essay;
function Essay() {}
Essay.prototype.hookup = function hookup(id, child, scope) {
var components = scope.components;
if (id === "items:row") {
components.item.value = child.value;
} else if (id === "this") {
components.items.value = ["Guten Tag, Welt!", "Auf Widerseh'n, Welt!"];
}
};
Components support self recursion. This is useful for creating trees.
The <this></this>
tag stands for this component.
<!doctype html>
<html>
<head>
<link rel="extends" href="./tree">
<link rel="tag" href="../../text.html">
<link rel="tag" href="../../repeat.html">
<meta accepts="[body]">
</head>
<body>
<argument></argument>
<ul>
<repeat id="children">
<li><this id="child"><argument></argument></this></li>
</repeat>
</ul>
</body>
</html>
Unlike normal HTML, by default, white space is treated as insignificant.
All text nodes are trimmed and thrown away if they only contain spaces.
However, there is a built in <sp>
tag that explicitly marks parts of the
document where white space is significant.
In these regions, sequences of space are preserved.
In the following example, the string "Guten Tag, Welt!" is repeated for every
value of the greetings iteration.
The <sp>
tag ensures that these greetings are delimited by space.
<!doctype html>
<html>
<head>
<link rel="tag" href="../../repeat.html">
</head>
<body>
<repeat id="greetings"><sp>Guten Tag, Welt! </sp></repeat>
</body>
</html>
Gutentag supports XML for cases where the HTML5 parser gets in your way.
For example, the HTML5 parser does not allow a <repeat>
tag to exist within
a <table>
. XML does.
<html>
<head>
<link rel="extends" href="./grid"/>
<link rel="tag" href="gutentag/repeat.html"/>
<link rel="tag" href="gutentag/text.html"/>
</head>
<body>
<table>
<thead>
<tr id="columnRow">
<th></th>
<repeat id="columns">
<th><text id="ch"/></th>
</repeat>
</tr>
</thead>
<tbody>
<repeat id="rows">
<th><text id="rh"/></th>
<repeat id="cells">
<td><text id="cd"/></td>
</repeat>
</repeat>
</tbody>
</table>
</body>
</html>
Every node of the virtual document has an actualNode
and proxies common DOM
methods and properties including innerHTML
, innerText
, getAttribute
,
setAttribute
, hasAttribute
, and removeAttribute
, but does not emulate
anything fancy.
The virtual document, with its support for body nodes, is provided by the Koerper module.
Copyright (c) 2015 by Kristopher Michael Kowal and contributors. MIT License.