Tint

A natural template engine for the HTML DOM.

Docs

  • 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 a template 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 a template, it will be treated as a complete template. In case you provide a template 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 in deno, 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 use h(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 a template, it will be treated as a complete template. In case you provide a template 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 in deno, 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 &gt; 4 &amp;&amp; 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>