@beforesemicolon/html v0.2.5-beta
HTML Templating System
Simple Reactive HTML Template System
⚠️ This is still in Beta and contains parts which are still under development and experimentation.
⚠️ Do NOT use in production!
The beforesemicolon html is a plug-and-play template system for those who need the bare minimal yet powerful
way to build user interface. Its small size and ready-to-go nature makes it perfect for quick prototypes,
UI components library, browser extensions, and side projects. But make no mistake, it has all the templating
features for a big project and serves as a perfect start to build any UI framework or library.
Motivation
Most UI libraries need too much setup and require build with a steep learning curve. If you find a good templating system its either not powerful enough or requires extra things to make it work by itself.
This templating system is standalone system. You don't need anything else to start rendering and reacting to changes.
It requires no build, its tiny, and the API is literally 2 main things to learn, and you are ready to go. It is pretty much HTML and Javascript so the learning curve is extremely small.
Example
Below is a simple todo app and as you can see, its pretty much HTML and Javascript.
import {html, state, repeat} from "@beforesemicolon/html";
interface TodoItem {
name: string;
description: string;
id: string;
}
const [todos, updateTodos] = state<Array<TodoItem>>([])
const createTodo = () => {
const name = window.prompt("Enter todo name");
const description = window.prompt("Enter todo description") ?? '';
if (name) {
updateTodos(prev => [...prev, {name, description, id: crypto.randomUUID()}])
}
}
const deleteTodo = id => {
updateTodos(prev => prev.filter(todo => todo.id !== id))
}
const TodoItem = ({name, description, id}: TodoItem) => html`
<div class="todo-item">
<h3>${name}</h3>
<p>${description}</p>
<button type="button" onclick="${() => deleteTodo(id)}">delete</button>
</div>
`;
const TodoApp = html`
<h2>Todo App</h2>
<button type="button" onclick="${createTodo}">add new</button>
<div class="todo-list">
${repeat(todos, TodoItem)}
</div>
`
TodoApp.render(document.body)More examples
This is a simple example of a button, but you can check:
Install
npm i @beforesemicolon/htmlUse directly in the Browser
This library requires no build or parsing. The CDN package is one digit killobyte in size, tiny!
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Grab the latest version -->
<script src="https://unpkg.com/@beforesemicolon/html/dist/client.js"></script>
<!-- Or a specific version -->
<script src="https://unpkg.com/@beforesemicolon/html@1.0.0/dist/client.js"></script>
</head>
<body></body>
</html>Documentation
Table of Content
- html API
- Template values
- Dynamic values
- Injecting HTML
- The
refAttribute - The
attrAttribute - Memoise elements
- Render Helpers
- element
- Component Patterns
html API
The main API you will be interacting with is the html tag function for
Javascript Template Literals.
All you need to do is specify the HTML string you would like to render.
import {html} from "@beforesemicolon/html"
const helloWorld = html`<h1>Hello World</h1>`;What you get back is an instance of HtmlTemplate which exposes the following properties
and methods:
render(method);update(method);onUpdate(method);replace(method);refs;renderTarget;nodes;
render
The render method simply takes and HTMLElement or a
ShadowRoot instance where you want the content to be placed. It does that by appending
to the element you provided.
import {html} from "@beforesemicolon/html"
const page = html`
<h1>Page Title</h1>
<p>Page description</p>
<button>Page CTA Action</button>
`;
page.render(document.body); // <- appends to the bodyThe render method will only render at a specific place once. Calling it multiple times with same values
will have no effect.
You may want to move all Nodes to a different place in the DOM
and for that you must use the force flag to do so.
// appends all already rendered nodes into the .wrapper element
page.render(document.querySelector('.wrapper'), true);update
The update method updates the rendered nodes based on changes made. It only updates the elements which values
associated with have changed. The DOM is not affected if the value remains the same and the update is
called repeatedly.
import {html} from "@beforesemicolon/html"
let title = "Page Title";
const page = html`
// use a lambda to mark the value as dynamic, meaning, it can change
<h1>${() => title}</h1>
<p>Page description</p>
<button>Page CTA Action</button>
`;
page.render(document.body);
title = "Page Title Changed";
page.update(); // make the page aware of the title changeSee Injected values and Dynamic values sections for more details on values you can inject in your templates.
onUpdate
The onUpdate is a method you can use to provide a function that must be called every time the update was called.
This is regardless of whether the DOM changed or not. It is a perfecrt place to react to anything that might have changed
in the DOM.
const [count, updateCount] = state<number>(0);
const counter = html`<span>${count}</span>`;
const unsubscribe = counter.onUpdate(() => {
// logic here
})
updateCount(23); // will trigger onUpdateIt returns a function you can call to unsubscribe and the function you provide will not get called with anything.
replace
The replace method works like the render method but instead of appending to the provided element
it replaces it. It takes any DOM element or a html template instance replacing only
if the element(s) is rendered somewhere in the document.
const btn = html`<button>Page CTA Action</button>`;
btn.replace(document.body.querySelector('.target'))One specific thing about this method is that it will not replace HTML, BODY, or HEAD elements.
Also, it will not replace ShadowRoot as well. Providing invalid replacing target will
result in an error.
Another cool things is that you can provide other HTMLTemplate instances, and it will replace it
as well.
const span = html`<span>sample</button>`;
span.render(document.body)
const btn = html`<button>Page CTA Action</button>`;
// will remove all nodes of the span template
// replacing them with all nodes from btn template
btn.replace(span)refs
The refs property is a readonly Object of elements keyed by the name of your choosing. See ref Attribute section.
renderTarget
renderTarget is a readonly property will contain the element you passed to the render method.
nodes
nodes is a readonly property will contain all the direct child node references specified in your html template.
These nodes list may change based on rendering conditions placed inside the template.
Template values
Because the template is just a Javascript Template String/Literal, you may place values anywhere valid in HTML you want and will work fine.
import {html} from "@beforesemicolon/html"
let title = "Page Title";
let description = "Page description";
const page = html`
<h1>${title}</h1>
<p>${description}</p>
<button>Page CTA Action</button>
`;You must place values inside valid places like between quotes for attributes or inside or outside a tag. For example, below is invalid
html`<input ${'disabled'} />`;
// will render <input />html`<input placeholder="${'Enter text'}" />`;
// will render <input placeholder="Enter text" />Any value will be treated as static values but if you pass a function, these are treated like getter functions. These are called Dynamic values and should be used whenever you know a certain value will change.
In general, keep the values that will not change as static to avoid not needed calculations.
Dynamic values
We call "dynamic value" anything added to the template via a function which are called to retrieve the values.
import {html} from "@beforesemicolon/html"
let title = "Page Title";
let description = "Page description";
const page = html`
<h1>${() => title}</h1>
<p>${() => description}</p>
<button>Page CTA Action</button>
`;The html template will call these functions to collect the values by default.
Whenever you make a change to the values, you may call the update method to reflect these changes on the DOM.
import {html} from "@beforesemicolon/html"
let title = "Page Title";
let description = "Page description";
const page = html`
<h1>${() => title}</h1>
<p>${() => description}</p>
<button>Page CTA Action</button>
`;
title = "Title changed";
page.update(); // will update the title in the DOMstate
The best way to work with dynamic values is to use the built-in state utility.
import {html, state} from "@beforesemicolon/html"
let [count, setCount] = state(0);
const page = html`
<p>${count}</p>
<button type="button"
onclick="${() => setCount(prev => prev + 1)}">+</button>
<button type="button"
onclick="${() => setCount(prev => prev - 1)}">-</button>
`;The state function takes two arguments:
value: the initial valuesubscriber(optional): a function that gets called for every value change
It returns an array with 3 functions:
getter: a function that returns the valuesettera function that set the value by taking a new one or a function that returns the new value.unsubscribera function that stops listening to value changes.
setCount(count => count + 1)
// or
setCount(count() + 1)Which one to use is up to you and makes no difference in how things get handled.
understanding state
The state is not a signal and does not work like React state. State is a simple pair of getter and setter
you can subscribe to and ONLY the template that uses it can respond to its change.
// this template IS subscribed to the state and will
// respond to any stat changes regardless of where it is been used
const countParagraph = html`<p>${count}</p>`;
// this template is NOT subscribed to the state
// it will not respond to changes of the state but
const page = html`
${countParagraph}
<button type="button"
onclick="${() => setCount(prev => prev + 1)}">+</button>
<button type="button"
onclick="${() => setCount(prev => prev - 1)}">-</button>
`;This is perfect because it guarantees that ONLY the part of the template using the state gets re-rendered making the template fast and efficient.
It is important to understand that just because you are using state getter in the template, doest NOT mean that the template is subscribed to its value. The state getter needs to be used DIRECTLY in the template or as a DIRECT ARGUMENT of a reactive helper.
All the 2 built-in helpers are reactive and can take state getter as argument but if you pass a function, for example, that uses the state getter, the template will not rect to its changes.
const [todos, updateTodos] = state([]);
// the repeat is provided with the state getter directly which is shared with template
// causing the template to react to its value changes
html`${repeat(todos, todo => html`<li>${todo.name}</li>`)}`; // will update on todos state change
// the repeat is provided a function that returns a getter value and does have acces to the state
// causing it to NOT react to state changes
html`${repeat(() => todos(), todo => html`<li>${todo.name}</li>`)}`; // will NOT update on todos state changeYou will need to use the state directly in the template or with reactive-helpers directly in order for the DOM to react to changes, otherwise you must call the update method for the template
Injecting HTML
Sometimes you just want to inject HTML or any type of text as is safely in the DOM.
If you want to just place text as is, simply injecting it in the template.
const someCode = '<p>will encode HTML characters safely into the DOM</p>';
const code = html`<pre><code>${someCode}</code></pre>`;If you want the text to be parsed, use the html as a function and pass it an array of html strings..
const someCode = '<p>will be treated as HTML DOM element</p>';
const code = html`<pre><code>${html[someCode]}</code></pre>`;The ref Attribute
The ref attribute is an attribute you can use to mark the elements you want to have access to.
import {html} from "@beforesemicolon/html"
const page = html`
<h1>Page Title</h1>
// set a ref attribute on any element
<p ref="desc">Page description</p>
<button>Page CTA Action</button>
`;
page.render(document.body) // must render to get the DOM referencesThe way you access these element references is by using the refs Object from the HTMLTemplate instance
returned by html.
const [pDescTag] = page.refs["desc"]; // return array of element referencesThe attr Attribute
The attr attribute is a powerful attribute that lets you dynamically set attributes based on dynamic values.
It can be used to set almost any attribute and can even target specific properties as you will learn about bellow.
booleans
Pattern: attr.NAME_OF_THE_ATTRIBUTE="BOOLEAN_RENDER_FLAG"
HTML has boolean attributes which are special attributes that do not necessarily need value. You can look at list of valid HTML attributes to see which ones are considered to be booleans.
They affect the element just by being set. Therefore, doing something like bellow will not have the effect you desire:
let disabled = false;
const btn = html`<button disabled="${disabled}">click me</button>`Because it results in:
<button disabled="false">click me</button>Which still makes the button disabled.
Instead, use the attr attribute.
let disabled = false;
const btn = html`<button attr.disabled="${disabled}">click me</button>`which will result in:
<button>click me</button>This library is aware of all valid HTML boolean attributes and will take care of them for you via the attr attribute.
class
Pattern: attr.class.NAME_OF_THE_CLASS="BOOLEAN_RENDER_FLAG" or attr.class="NAME_OF_THE_CLASS | BOOLEAN_RENDER_FLAG"
The examples bellow show 2 patterns on how to use the attr attribute to set class names.
It will only add the loading class if the loading variable value is true.
let loading = true;
const btn = html`<button attr.class.loading="${loading}">click me</button>`or
let loading = true;
const btn = html`<button attr.class="loading | ${loading}">click me</button>`style
Pattern: attr.style.STYLE_PROPERTY="STYLE_PROPERTY_VALUE | BOOLEAN_RENDER_FLAG" or attr.style="VALID_INLINE_CSS | BOOLEAN_RENDER_FLAG"
The examples bellow show 2 patterns on how to use the attr attribute to set inline style.
It will only the background color if the cta variable value is true
let cta = true;
const btn = html`<button attr.style.background-color="orange | ${cta}" >click me</button>`Or
let cta = true;
const btn = html`<button attr.style="background-color: orange | ${cta}" >click me</button>`data
Pattern: attr.data.NAME_OF_THE_DATA="DATA_VALUE, BOOLEAN_RENDER_FLAG"
The bellow example shows how to use attr to dynamically set a data attribute.
let label = true;
const btn = html`<button attr.data.aria-label="${label} | ${label.trim().length > 1}" >click me</button>`any other attributes
Pattern: attr.NAME_OF_THE_ATTRIBUTE="ATTRIBUTE_VALUE | BOOLEAN_RENDER_FLAG"
BOOLEAN_RENDER_FLAG
The boolean flag is optional when using attr attribute and defaults to true. In that case you can just
not use the attr attribute all together if the boolean will always be true or omitted.
let label = true;
html`<button attr.data.aria-label="${label}" >click me</button>`
// same as
html`<button data.aria-label="${label}" >click me</button>`The only time the boolean value can be omitted is with boolean attributes, even if they have values, like in the case of hidden which is a boolean attribute and have possible values.
Memoise elements
When you use dynamic values the value is capture with every update which works fine for primitive
values but not so much with new instance because html template with rely on data to create new DOM
which will result in new DOM elements created on every update.
In those cases you need to create static references and return them.
Allow me to elaborate:
Bellow is okay because the value returned is a primitive value (string).
let valid = true;
html`<span>${() => valid ? "valid" : "invalid"}</span>`;
setInterval(() => {
valid = false;
span.update()
}, 1000)The interval runs every second and html will try to update the element but will not touch the DOM because the value
is always the same. However, the following will trigger a DOM update on every update.
let valid = true;
html`<span>${() => valid ? html`valid` : html`invalid`}</span>`;
setInterval(() => {
valid = false;
span.update()
}, 1000)This will update the DOM every second even though visually the thing rendered will be an "invalid" text node.
This is because every time this function is called, it will generate a new instance of HTMLTemplate which
for html is a change.
The way to fix this is by creating static references
let valid = true;
const validText = html`valid`;
const invalidText = html`invalid`;
const span = html`<span>${() => valid ? validText : invalidText}</span>`;
span.render(document.body)
setInterval(() => {
valid = true;
span.update()
}, 1000)This will not waste DOM updates because no matter how many times the interval runs, html will always get the same instance
of HTMLTemplate because they are the value of the variables declared above.
In general always be aware of this whenever the values returned by these dynamic values are not primitive values.
All built-in helper already memoise data and that's why you should use them.
Render Helpers
A render helper is just a function you place in the template that handles some logic related to rendering some HTML. They can take anything you want and return anything you want.
There are two built-in render helpers:
- when : to conditionally render content based on some state or logic
- repeat : to repeat content on the DOM based on some number or list
All these are reactive helpers which means that can take a state getter and react to the changes.
You can also create custom helpers which can be static or reactive.
when
when(
flag: boolean | () => boolean,
ifTrue: any | () => any,
ifFalse?: any | () => any
)
The when helper is like a ternary, it takes a value and one or two things to render.
const [loading, setLoading] = state(false);
const btn = html`
<button>${when(loading,
html`<span>Loading...</span>`,
html`<span>Click Me</span>`
)}</button>
`;If you provide a state getter directly to the when helper it will react to its changes and re-run.
repeat
repeat<T>(countOrArray: number | Array<T> | () => number | Array<T>, renderCallback: () => any)
The repeat helper will handle any template needs to repeat elements on the DOM.
It can repeat elements based on given count and you can use callback to read the count value
and use it in the template.
import {html, repeat} from "@beforesemicolon/html";
const todos = html`
<ul>
${repeat(10, (n) => html`<li>item ${n}</li>`)}
</ul>
`;You may also provide an array of items to be rendered.
import {html, repeat} from "@beforesemicolon/html";
interface Todo {
name: string;
}
const [todos, updateTodos] = state<Todo>([])
html`<ul>
${repeat<Todo>(todos, (item) => html`<li>${item.name}</li>`)}
</ul>`;In case the list is empty, you may provide a third parameter to render things when the list is empty.
html`<ul>
${repeat<Todo>(todos, (item) => html`<li>${item.name}</li>`, () => html`<p>No Items</p>`)}
</ul>`;effect
The effect helper is the simplest helper and it is mean to make different parts of the template react to other
states. For example, below we have 2 states status and todos.
const [status, setStatus] = state('pending');
const [todos, setTodos] = state([]);
const renderTodo = todo => html`<div>${todo.name}</div>`;
const todosByStatus = () => todos().filter(todo => todo.status === status());
html`
<h2>Status: ${status}</h2>
${repeat(todosByStatus, renderTodo)}
`There are 2 problems here:
- the todos will only render once because the
repeathelper does not see thetodosstate which is hidden inside thetodosByStatusfunction. Will not re-render if the list changes. - the todos will not re-render if the
statusstate changes because only the place in the DOM connected to the state that changed will get re-render and in this case the todo list depends on the status state
Enters the effect helper which allows us to list states and then, as the last argument, the thing that needs to be updated in the DOM
if any of the states change.
html`
<h2>Status: ${status}</h2>
${effect(status, todos, html`${repeat(todosByStatus, renderTodo)}`)}
`Now, whenever the todo or status state changes, the DOM will get updated accordingly.
Custom Helper
A custom helper is simply a function that is placed in the template and aids with some rendering logic.
const draggable = (target) => {
const dragStart = (event) => {
event.dataTransfer.setData("text/plain", event.target.id);
}
return html`<div draggable ondragstart="${dragStart}">${target}</div>`
}
html`${draggable(html`<div>Drag Me</div>`)}`;Reactive helper
A reactive helper is just a function wrapped by helper util that might need to react to some state data.
import {helper, html, state} from "@beforesemicolon/html";
const ellipsis = helper((list, max, content) => {
const data = typeof list === "function" ? list() : list;
if(data.length > max) {
return html`${data.map(content)}...`;
}
return data.map(content);
});
const [names, setNames] = state([]);
html`${ellipsis(names, 5, name => html`<div>${name}</div>`)}`;Custom helpers can also return function to actually do the rendering and the outer function to cache data.
const ellipsis = helper((list, max, content) => {
// use the outer function to create static data and cache things
let renderList = [];
// return a new render function that handles the render logic
return () => {
const data = typeof list === "function" ? list() : list;
// grab previous generated item HTMLTemplate or create new one
renderList = data.slice(0, max).map((item, idx) => renderList[idx] || content(item))
if(renderList.length === max) {
return html`${renderList}...`;
}
return renderList;
}
});
const [names, setNames] = state([]);
html`${ellipsis(names, 5, name => html`<div>${name}</div>`)}`;It is recommended that you use the outer function to cache or do certain things only once and the inner function for things that need to happen on every state change.
A good example is this draggable helper which does all its thing in the outer function and returns a function that will handle the rendering, in this case, just returns it.
const draggable = (target) => {
const id = Math.floor(Math.random() * 1000000);
const onDragStart = e => {
e.dataTransfer.setData("text/plain", String(id));
e.target.style.opacity = '0.2'
}
const onDragEnd = e => {
e.target.style.opacity = '1'
}
const content = html`
<div draggable="true" class="draggable-item" id="${id}"
ondragstart="${onDragStart}"
ondragend="${onDragEnd}"
>${target}</div>`;
return () => content;
}What this means is that no matter how the state change, what will render will remain the same causing the DOM to never shift between changes.
A helper can also be used for attribute values.
const is = helper(<T>(state: () => T, val: unknown) => {
return () => state() === val
})
const [tab, setTab] = state('tab')
html`<li attr.class="active | ${is(tab, 'home')}">Home</li>`element
There is also a element utility function which is just a simpler way to create DOM elements. What is special
about it is that you can do one call and the entire element is put together. What is more interesting
in it is that it handle Web Component props the way it should be.
import {html, element, state} from "@beforesemicolon/html"
let [count, setCount] = state(0);
const incBtn = element('button', {
textContent: "+",
attributes: {
type: "button",
onclick: () => {
setCount(prev => prev + 1)
}
}
});
const page = html`
<p>${count}</p>
${incBtn}
`;It takes two arguments:
TagName: the name of the element you want to createoptionstextContent: pure texthtmlContent: html stringattributes: any key value pair for the element, including event listeners and web component props.ns: the namespaceURI which is useful if you are tyring to create SVG elements.
Component Patterns
This library is not a UI library but because it handles such a crucial feature of UI libraries, it can be used to create components of any type easily. For example:
- Web Component
- Functional Component
- Class Component
Web Component
Perhaps, the most powerful way to create components is to use Web Components API.
The html library handles to complex and tedious aspects of it all and the component is just a wrapper with magical hooks
to tap into.
Bellow is an example of a simple button which does not need to do all the tedious DOM manipulations.
class BFSButton extends HTMLElement {
static observedAttributes = ["disabled", "type"];
template: HTMLTemplate;
disabled = false;
type = "button";
constructor() {
super();
const shadow = this.attachShadow({mode: "open"});
const handleClick = (e) => {
e.stopPropagation();
e.preventDefault();
this.dispatchEvent(new CustomEvent("click"));
}
this.template = html`
<button
attr.disabled="${() => this.disabled}"
type="${() => this.type}"
onclick="${handleClick}"
>
<slot></slot>
</button>
`;
this.template.render(shadow);
}
attributeChangedCallback(name, oldVal, newVal) {
switch(name) {
case "disabled":
this.disabled = this.hasAttribute("disabled");
break;
case "type":
this.type = ["submit", "button"].includes(newVal) ? newVal : this.type;
break;
}
this.template.update();
}
}
customElements.define("bfs-button", BFSButton);You can now use the tag like so:
<bfs-button type="submit" onclick="console.log(event)">save</bfs-button>
<bfs-button disabled>saved</bfs-button>
<bfs-button >click me</bfs-button>Functional Component
Bellow is a simple example of how you can create functional components easily by putting all the logic
related to a html template in a single function and return the HTMLTemplate instance.
// button.html.ts
interface ButtonProps {
type?: "button" | "submit";
disabled?: boolean;
label?: string;
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
}
const Button = ({
type = "button",
disabled = false,
label = "",
onClick = () => {}
}: ButtonProps) => html`
<button type="${type}" attr.disabled="${disabled}" onclick="${onClick}">
${label}
</button>
`
// app.html.ts
const App = () => {
const [count, setCount] = state(0);
const plusBtn = Button({
label: "+",
onClick: () => {
setCount(prev => prev + 1);
}
})
const minusBtn = Button({
label: "-",
onClick: () => {
setCount(prev => prev - 1);
}
})
return html`
<h1>Count App</h1>
<p>${count}</p>
${plusBtn} ${minusBtn}
`;
}
App().render(document.body)This is in no way telling you how you SHOULD create functional components. This is just an example and you can approach this in whatever way works for you.
Class Component
Here an example of how you can create a components using class as well.
// button.html.ts
class Button {
constructor({
type = "button",
disabled = false,
label = "",
onClick = () => {}
}: ButtonProps) {
return html`
<button type="${type}" attr.disabled="${disabled}" onclick="${onClick}">
${label}
</button>
`;
}
}
// app.html.ts
class App {
app: HTMLTemplate;
count = 0;
plusBtn = new Button({
label: "+",
onClick: () => {
this.count += 1;
this.app.update();
}
})
minusBtn = new Button({
label: "-",
onClick: () => {
this.count -= 1;
this.app.update();
}
})
constructor(target: HTMLElement) {
this.app = html`
<h1>Cool App</h1>
<p>${() => this.count}</p>
${this.plusBtn} ${this.minusBtn}
`;
this.app.render(target);
}
}
new App(document.body)This is in no way telling you how you SHOULD create class components. This is just an example and you can approach this in whatever way works for you.
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago