Tint
A natural template engine for the HTML DOM.
- takes valid
HTML
as input. - converts the result to any hyperscript function you want to use.
- layouts completely separate from javascript logic.
- works with all javascript frameworks that support hyperscript.
- you can use the template as a server-side rendered version of your app.
- you can use a regular HTML file as a single file component without ANY javascript build tools.
- hyperscript frameworks can be used by any team of designers or people with no javascript knowledge.
- the template syntax allow very simple and elegant templates without repeat yourself.
- see also Merlin, a tint based framework.
Showcase
The classic TODO app, with an initial server-rendered state.
<html>
<head>
<script type="module">
import compile from "https://cdn.jsdelivr.net/gh/marcodpt/tint/template.js"
const render = compile(document.getElementById("app"))
const state = {
todos: [
"read a book",
"plant a tree"
],
value: "",
AddTodo: () => {
state.todos.push(state.value)
state.value = ""
render(state)
},
NewValue: ev => {
state.value = ev.target.value
}
}
render(state)
</script>
</head>
<body>
<main id="app">
<h1>To do list</h1>
<input type="text" value:="value" oninput:="NewValue">
<ul>
<li each:="todos" text:></li>
</ul>
<button onclick:="AddTodo">New!</button>
</main>
</body>
</html>
Result:
<main id="app">
<h1>To do list</h1>
<input type="text" value="">
<ul>
<li>read a book</li>
<li>plant a tree</li>
</ul>
<button>New!</button>
</main>
It looks like a normal template engine, but internally compiles the template to:
({ todos, value, NewValue, AddTodo }) =>
h("main", {}, [
h("h1", {}, text("To do list")),
h("input", { type: "text", oninput: NewValue, value }),
h("ul", {},
todos.map((todo) => h("li", {}, text(todo)))
),
h("button", { onclick: AddTodo }, text("New!")),
])
where h
and text
can be any hyperscript function you want to use.
You can use it with these frameworks:
With your help, we can grow this list and improve the work done on already supported frameworks.
With a little trick, you can even render your application on the server side, without the complications of the build steps.
<html>
<head>
<script type="module">
import compile from "https://cdn.jsdelivr.net/gh/marcodpt/tint/template.js"
const app = document.getElementById("app")
const render = compile(app)
const state = {
todos: Array.from(app.querySelectorAll('li')).map(e => e.textContent),
value: "",
AddTodo: () => {
state.todos.push(state.value)
state.value = ""
render(state)
},
NewValue: ev => {
state.value = ev.target.value
}
}
render(state)
</script>
</head>
<body>
<main id="app">
<h1>To do list</h1>
<input type="text" value:="value" oninput:="NewValue">
<ul>
<li each:="todos" text:>read a book</li>
<li not:>plant a tree</li>
</ul>
<button onclick:="AddTodo">New!</button>
</main>
</body>
</html>
What have you achieved:
- The happiness of designers who can write in plain html.
- The happiness of customers, which has a very fast and interactive application, rendered on the server side, search engine friendly and at the same time as dynamic as necessary.
- The happiness of developers, who don't need complicated settings for the build steps, they can use normal html files to create single file components and they can use any hyperscript framework they want.
To celebrate the widespread happiness, how about taking a look at the documentation.
Docs
To generate the docs and create a server for tests.
mdbook serve
Deno support
Testing tint
in deno
deno test --allow-read tests/deno.js
Currently this is the only suported and tested version (deno_dom@v0.1.38)
import {DOMParser} from "https://deno.land/x/deno_dom@v0.1.38/deno-dom-wasm.ts";
const parser = new DOMParser()
const document = parser.parseFromString(
Deno.readTextFileSync('path/to/file.html'),
"text/html"
)
Philosophy
- Separation: Functions and data transformations belong in javascript, design and visual presentation to the html and css, and the data belongs to the database.
- Designers and people with no javascript knowledge should understand it and quickly become productive.
- Templates must be valid
XML
/HTML
that can be inserted into atemplate
tag or anywhere in the DOM. - It must not conflict with other template engines and frameworks.
- Each layout should be written only once, without repetition.
- Simplicity matters.
- Elegant syntax is important.
Contributing
Everything within this documentation is tested here. And it will always like this. Any changes to the documentation, any contributions MUST be present in the tests.
If the tests do not pass in your browser, if you find any bugs, please raise an issue.
Any changes must be within the philosophy of this project.
It's a very simple project. Any contribution is greatly appreciated.
Influences and thanks
This work is hugely influenced by these amazing template engines and frameworks:
A huge thank you to all the people who contributed to these projects.
Usage as a template engine
compile(element, template?, document?) -> render(scope)
Returns the view associated with element
.
Optionally, you can use a template
element to replace the element
's inner
HTML with your own contents.
- element: The
element
that will be the root of the result. If you don't pass atemplate
, it will be treated as a complete template. In case you provide atemplate
it will only be used as the root of the result, ignoring its content. - template: The optional
template
element you want to render inside . - document: Useful when
tint
is used indeno
, you must pass the parsed document, inside the browser just ignore it. - scope: The data passed to interpolate the
element
, any JSON object even with javascript functions is valid!
Hello world example!
<html>
<head>
<script type="module">
import compile from "https://cdn.jsdelivr.net/gh/marcodpt/tint/template.js"
const render = compile(document.getElementById("app"))
render({
message: "Hello World!"
})
</script>
</head>
<body>
<div id="app">
<h1 text:="message">Loading...</h1>
</div>
</body>
</html>
Result:
<div id="app">
<h1>Hello world!</h1>
</div>
Hello world with external template example!
<html>
<head>
<script type="module">
import compile from "https://cdn.jsdelivr.net/gh/marcodpt/tint/template.js"
const render = compile(
document.getElementById("app"),
document.getElementById("view")
)
render({
message: "Hello World!"
})
</script>
</head>
<body>
<div id="app">
<p>Loading...</p>
</div>
<template id="view">
<h1 text:="message"></h1>
</template>
</body>
</html>
Result:
<div id="app">
<h1>Hello world!</h1>
</div>
TODO app without any framework or virtual DOM
This is not the recommended way to do this. You'll update the DOM more than necessary, lose focus, and maybe other things. But here's a demo anyway.
<html>
<head>
<script type="module">
import compile from "https://cdn.jsdelivr.net/gh/marcodpt/tint/template.js"
const render = compile(document.getElementById("app"))
const state = {
todos: [],
value: "",
AddTodo: () => {
state.todos.push(state.value)
state.value = ""
render(state)
},
NewValue: ev => {
state.value = ev.target.value
}
}
render(state)
</script>
</head>
<body>
<main id="app">
<h1>To do list</h1>
<input type="text" value:="value" oninput:="NewValue">
<ul>
<li each:="todos" text:></li>
</ul>
<button onclick:="AddTodo">New!</button>
</main>
</body>
</html>
Usage with Hyperapp
We delete the view
property because tint
will automatically generate it
based on the node
that is passed to the app
.
We've also introduced an actions
object for your static methods and an
optional template
property when the node is rendered from a template.
Everything else is exactly equals on hyperapp.
Here is the thread that gave rise to this wrapper.
TODO app sample
<html>
<head>
<script type="module">
import app from "https://cdn.jsdelivr.net/gh/marcodpt/tint/hyperapp.js"
app({
actions: {
AddTodo: state => ({
...state,
value: "",
todos: state.todos.concat(state.value),
}),
NewValue: (state, event) => ({
...state,
value: event.target.value,
})
},
init: {
todos: [],
value: ""
},
node: document.getElementById("app")
})
</script>
</head>
<body>
<main id="app">
<h1>To do list</h1>
<input type="text" value:="value" oninput:="NewValue">
<ul>
<li each:="todos" text:></li>
</ul>
<button onclick:="AddTodo">New!</button>
</main>
</body>
</html>
Usage with Superfine
TODO app sample
<html>
<head>
<script type="module">
import superfine from "https://cdn.jsdelivr.net/gh/marcodpt/tint/superfine.js"
const state = {
todos: [],
value: "",
AddTodo: () => {
state.todos.push(state.value)
state.value = ""
setState(state)
},
NewValue: ev => {
state.value = ev.target.value
}
}
const setState = superfine(document.getElementById('app'))
setState(state)
</script>
</head>
<body>
<main id="app">
<h1>To do list</h1>
<input type="text" value:="value" oninput:="NewValue">
<ul>
<li each:="todos" text:></li>
</ul>
<button onclick:="AddTodo">New!</button>
</main>
</body>
</html>
Usage with Mithril.js
In this example, you must also import mithril into the page yourself before the wrapper.
The view
method is generated by the state
property.
All other components methods are available. Ex: oninit
, oncreate
, etc.
TODO app sample
<html>
<head>
<script src="https://unpkg.com/mithril/mithril.js"></script>
<script type="module">
import component from 'https://cdn.jsdelivr.net/gh/marcodpt/tint/mithril.js'
const state = {
todos: [],
value: "",
AddTodo: () => {
state.todos.push(state.value)
state.value = ""
},
NewValue: ev => {
state.value = ev.target.value
}
}
const todo = component(document.getElementById('view-todo'), {
oninit: () => {
console.log('component oninit')
},
state: state
})
m.mount(document.getElementById("app"), todo)
</script>
</head>
<body>
<main id="app">
<div id="view-todo">
<h1>To do list</h1>
<input type="text" value:="value" oninput:="NewValue">
<ul>
<li each:="todos" text:></li>
</ul>
<button onclick:="AddTodo">New!</button>
</div>
</main>
</body>
</html>
Usage with preact
Our preact
wrapper is not the best. But it serves to show an example
with a global state and no components. If you think you can improve this
wrapper, please submit a pull request.
TODO app sample
<html>
<head>
<script type="module">
import preact from "https://cdn.jsdelivr.net/gh/marcodpt/tint/preact.js"
const state = {
todos: [],
value: "",
AddTodo: () => {
state.todos.push(state.value)
state.value = ""
render()
},
NewValue: ev => {
state.value = ev.target.value
}
}
const render = preact(state, document.getElementById("app"))
</script>
</head>
<body>
<main id="app">
<div>
<h1>To do list</h1>
<input type="text" value:="value" oninput:="NewValue">
<ul>
<li each:="todos" text:></li>
</ul>
<button onclick:="AddTodo">New!</button>
</div>
</main>
</body>
</html>
Usage as a low-level library
To build all the wrappers for the TODO
application, we use this package as a
low-level library.
import tint from 'https://cdn.jsdelivr.net/gh/marcodpt/tint/index.js'
Here is the source code for each of the wrappers:
If you want to create a wrapper for a framework that is not on this list or
if you want to improve any of the wrappers we've created here, you'll need
use tint
as described in this section.
If you created something interesting, or a wrapper for another framework or improved any example. Submit a pull request or open an issue with your code.
tint(h, text) -> compile
Returns a compile
function based on your hyperscript DOM/vDOM choice.
- h(tagName, attributes, children): a required hyperscript function that create a DOM or virtual DOM element.
- text(str): a
hyperscript function that create
DOM or vDOM text nodes.
If no
text
function is passed, it will useh(str)
.
compile(element, template?, document?) -> render
Returns the view associated with element
.
Optionally, you can use a template
element to replace the element
's inner
HTML with your own contents.
- element: The
element
that will be the root of the result. If you don't pass atemplate
, it will be treated as a complete template. In case you provide atemplate
it will only be used as the root of the result, ignoring its content. - template: The optional
template
element you want to render inside . - document: Useful when
tint
is used indeno
, you must pass the parsed document, inside the browser just ignore it.
render(scope) -> DOM/vDOM element
Returns the DOM/vDOM generated by applying the scope
in the view
- scope: The data passed to interpolate the
element
, any JSON data even with javascript functions is valid!
Introduction
Tint is a DOM-based template engine and compiles the output to hyperscript functions.
While on the surface it may look like a normal (text-based) template engine, it has some important differences:
- you can use functions and will be bound to DOM element events.
- the syntax must always be valid HTML, in order to use the javascript DOM.
- although it is currently implemented in the browser and deno, as long as the DOM API is available, can be implemented in javascript runtimes such as node.
- there is no dot syntax, javascript expressions, filters, delimiters for text interpolation or any kind of logic other than the special attributes available, this is a decision made in order for the template to be as free of logic as possible.
- all complex data operations must be done in javascript before rendering the template.
Dynamic attributes (:attribute)
Simple eval
{
target: "#/page/1"
}
<a class="primary" href:="target">Go to page 1</a>
Result:
<a class="primary" href="#/page/1">Go to page 1</a>
Boolean attributes
{
isDisabled: true,
isChecked: false
}
<input type="checkbox" checked:="isChecked" disabled:="isDisabled">
Result:
<input type="checkbox" disabled="">
Extending attributes
{
isDisabled: false,
btn: "primary"
}
<button class="btn btn-" class:="btn" disabled disabled:="isDisabled">
Submit
</button>
Result:
<button class="btn btn-primary">
Submit
</button>
Function calls
{
action: (ev) => {
const btn = ev.target.closest('button')
btn.disabled = true
btn.textContent = 'Submited!'
}
}
<button class="btn btn-primary" onclick:="action">
Submit
</button>
Result:
<button class="btn btn-primary">
Submit
</button>
After clicking the button:
<button class="btn btn-primary" disabled="">
Submited!
</button>
text
Replace node text
{
content: "Hello John!"
}
<h1 text:="content">Hello World!</h1>
Result:
<h1>Hello John</h1>
Use template to interpolate text
{
name: "John"
}
<h1>
Hello <template text:="name"></template>, how are you?
</h1>
Result:
<h1>
Hello John, how are you?
</h1>
HTML strings will be escaped
{
raw: "var x = y > 4 && z / 3 == 2 ? 1 : 2"
}
<code text:="raw"></code>
Result:
<code>var x = y > 4 && z / 3 == 2 ? 1 : 2</code>
if/not
Remove node with a conditional test
{
john: false,
mary: true
}
<div>
John:
<i if:="john" class="fas fa-check"></i>
<i not:="john" class="fas fa-times"></i>
</div><div>
Mary:
<i if:="mary" class="fas fa-check"></i>
<i not:="mary" class="fas fa-times"></i>
</div>
Result:
<div>
John: <i class="fas fa-times"></i>
</div><div>
Mary: <i class="fas fa-check"></i>
</div>
Some critical js values
[
null,
0,
1,
-1,
"",
"0",
[],
{},
undefined
]
<div>
null: <template if:="0">true</template><template not:="0">false</template>
0: <template if:="1">true</template><template not:="1">false</template>
1: <template if:="2">true</template><template not:="2">false</template>
-1: <template if:="3">true</template><template not:="3">false</template>
"": <template if:="4">true</template><template not:="4">false</template>
"0": <template if:="5">true</template><template not:="5">false</template>
[]: <template if:="6">true</template><template not:="6">false</template>
{}: <template if:="7">true</template><template not:="7">false</template>
undefined: <template if:="8">true</template><template not:="8">false</template>
</div>
Result:
<div>
null: false
0: false
1: true
-1: true
"": false
"0": true
[]: true
{}: true
undefined: false
</div>
show/hide
Hide node with a conditional test
{
john: false,
mary: true
}
<div>
John:
<i show:="john" class="fas fa-check"></i>
<i hide:="john" class="fas fa-times"></i>
</div><div>
Mary:
<i show:="mary" class="fas fa-check" style="max-width:200px"></i>
<i hide:="mary" class="fas fa-times" style="max-width:200px"></i>
</div>
Result:
<div>
John:
<i style="display: none;" class="fas fa-check"></i>
<i class="fas fa-times"></i>
</div><div>
Mary:
<i class="fas fa-check" style="max-width:200px"></i>
<i style="display: none;max-width:200px" class="fas fa-times"></i>
</div>
switch/case
Choose a imediate children tag based on a criteria.
{
input: "text",
name: "bio",
title: "Bio"
}
<form switch:="input">
<label text:="title"></label>
<select
case="boolean"
name:="name"
>
<option value="0">No</option>
<option value="1">Yes</option>
</select>
<textarea
case="text"
name:="name"
rows="6"
></textarea>
<input
case="default"
type="text"
name:="name"
>
<button>Submit</button>
</form>
Result:
<form>
<label>Bio</label>
<textarea name="bio" rows="6"></textarea>
<button>Submit</button>
</form>
You can use template for case
{
color: "red"
}
<div switch:="color">
My favorite color is:
<template case="red">Red</template>
<template case="green">Green</template>
<template case="blue">Blue</template>
</div>
Result:
<div>
My favorite color is: Red
</div>
You can use template for switch
{
color: "green"
}
<template switch:="color">
My favorite color is:
<b case="red">Red</b>
<b case="green">Green</b>
<b case="blue">Blue</b>
</template>
Result:
My favorite color is: <b>Green</b>
You can use in both
{
color: "blue"
}
<template switch:="color">
My favorite color is:
<template case="red">Red</template>
<template case="green">Green</template>
<template case="blue">Blue</template>
</template>
Result:
My favorite color is: Blue
with
Change scope within tag.
{
name: "Mary",
friend: {
name: "John"
}
}
<div>
<p>My name is: <template text:="name"></template></p>
<p with:="friend">My name is: <template text:="name"></template></p>
<p>My name is: <template text:="name"></template></p>
</div>
Result:
<div>
<p>My name is: Mary</p>
<p>My name is: John</p>
<p>My name is: Mary</p>
</div>
Parent keys access.
{
greeting: "Hello",
name: "Mary",
friend: {
name: "John"
}
}
<div>
<p><b text:="greeting"></b><span text:="name"></span></p>
<p with:="friend"><b text:="greeting"></b><span text:="name"></span></p>
<p><b text:="greeting"></b><span text:="name"></span></p>
</div>
Result:
<div>
<p><b>Hello</b><span>Mary</span></p>
<p><b>Hello</b><span>John</span></p>
<p><b>Hello</b><span>Mary</span></p>
</div>
Simple types access.
[["Mary", "John"], "dog", 3.14]
<div with:="0">
<p text:="0"></p>
<template with:="1">
<p text:></p>
</template>
</div>
<div with:="1">
<p text:></p>
</div>
<div with:="2">
<template with:>
<p text:></p>
</template>
</div>
<div with:="3">
<p>This will not render</p>
</div>
Result:
<div>
<p>Mary</p>
<p>John</p>
</div>
<div>
<p>dog</p>
</div>
<div>
<p>3.14</p>
</div>
each
Simple array iteration.
["dog", "cat", "horse"]
<template each:>
<template text:></template>
</template>
Result:
dog
cat
horse
Complex array iteration.
{
links: ["Delete", "Edit"],
rows: [
{
id: 1,
name: "Mary",
css: "dark",
links: [
{
title: "Delete",
href: "#/delete/1"
}, {
title: "Edit",
href: "#/edit/1"
}
]
}, {
id: 2,
name: "John",
css: "light",
links: [
{
title: "Delete",
href: "#/delete/2"
}, {
title: "Edit",
href: "#/edit/2"
}
]
}
]
}
<table>
<thead>
<tr>
<th each:="links" text:></th>
<th>Id</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr each:="rows" class:="css">
<td each:="links">
<a href:="href" href:="id" text:="title"></a>
</td>
<td text:="id"></td>
<td text:="name"></td>
</tr>
</tbody>
</table>
Result:
<table>
<thead>
<tr>
<th>Delete</th><th>Edit</th>
<th>Id</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr class="dark">
<td>
<a href="#/delete/1">Delete</a>
</td><td>
<a href="#/edit/1">Edit</a>
</td>
<td>1</td>
<td>Mary</td>
</tr><tr class="light">
<td>
<a href="#/delete/2">Delete</a>
</td><td>
<a href="#/edit/2">Edit</a>
</td>
<td>2</td>
<td>John</td>
</tr>
</tbody>
</table>
Custom Tags
Observe that you can only use as custom tags templates that id contains -
.
As described here.
With the following tag in your HTML
template
<template id="my-button">
<button class="btn btn-" class:="btn" text:="text" click:="click">
<slot></slot>
</button>
</template>
Reuse your templates inside another template.
{
button: "primary"
}
<div>
<my-button btn:="button">
Action
</my-button>
</div>
Result:
<div>
<button class="btn btn-primary">
Action
</button>
</div>
Iterate with custom tags.
[
{button: "secondary", title: "Cancel"},
{button: "primary", title: "Submit"}
]
<div>
<my-button each: btn:="button" text:="title"></my-button>
</div>
Result:
<div>
<button class="btn btn-secondary">Cancel</button>
<button class="btn btn-primary">Submit</button>
</div>
Recursive tags.
[
{
title: "animals",
children: [
{
title: "dog"
}, {
title: "cat"
}
]
}, {
title: "countries",
children: [
{
title: "US",
children: [
{
title: "NY",
children: [
{title: "New York"}
]
}, {
title: "CA",
children: [
{title: "San Francisco"},
{title: "Los Angeles"}
]
}
]
}
]
}, {
title: "home"
}
]
<template id="my-list">
<ul if:="items">
<li each:="items">
<span text:="title"></span>
<my-list items:="children"></my-list>
</li>
</ul>
</template>
Result:
<ul>
<li>
<span>animals</span>
<ul>
<li>
<span>dog</span>
</li>
<li>
<span>cat</span>
</li>
</ul>
</li>
<li>
<span>countries</span>
<ul>
<li>
<span>US</span>
<ul>
<li>
<span>NY</span>
<ul>
<li>
<span>New York</span>
</li>
</ul>
</li>
<li>
<span>CA</span>
<ul>
<li>
<span>San Francisco</span>
</li>
<li>
<span>Los Angeles</span>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<span>home</span>
</li>
</ul>
bind
Spread attributes
{
class: "message",
style: "white-space:pre-wrap;",
text: "Hello John!"
}
<h1 bind:>Hello World!</h1>
Result:
<h1 class="message" style="white-space:pre-wrap;">Hello John!</h1>
Very useful with custom tags
With the following tag in your HTML
template
:
<template id="my-button">
<button class="btn btn-" class:="btn" text:="text" click:="click">
<slot></slot>
</button>
</template>
Render this:
[
{
btn: "secondary",
text: "Cancel",
click: (ev) => {
ev.target.textContent = 'canceled!';
}
},
{
btn: "primary",
text: "Submit",
click: (ev) => {
ev.target.textContent = 'submited!';
}
}
]
<my-button each: bind:></my-button>
Result:
<button class="btn btn-secondary">Cancel</button>
<button class="btn btn-primary">Submit</button>
After clicking on the two buttons:
<button class="btn btn-secondary">canceled!</button>
<button class="btn btn-primary">submited!</button>