mancha
mancha
is a simple HTML templating and reactivity library for simple people. It works on the
browser or the server. It can be used as a command-line tool, or imported as a Javascript module.
Here's a small sample of the things that you can do with mancha
:
<script src="//unpkg.com/mancha" target="main" css="utils" init></script>
<main class="p-4" :data="{count: 0, name: 'Stranger'}">
<template is="counter">
<div>
<slot></slot>
<button :on:click="count = count + 1">Counter: {{ count }}</button>
</div>
</template>
<counter class="my-2">Click me:</counter>
<p>Enter your name: <input type="text" :bind="name" /></p>
<p>Hello, <span class="underline">{{ name }}</span>!</p>
<footer class="text-xs">
<include src="html/partial/footer.tpl.html"></include>
</footer>
</main>
Why another front-end Javascript library?
There are plenty of other front-end Javascript libraries, many of them of great quality, including:
None of them have all the key features that make mancha
unique:
Feature | mancha | Svelte | React.js | Vue.js | petite-vue | Alpine.js |
---|
Simple to learn | ✔️ | ❌ | ❌ | ❌ | ✔️ | ✔️ |
< 15kb compressed | ✔️ | ❌ | ❌ | ❌ | ✔️ | ❌ |
Custom web components | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | ❌ |
Client-side rendering | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ✔️ |
Server-side rendering | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | ❌ |
mancha
is great for:
- prototyping, just plop a script tag in your HTML and off you go
- testing, individual components can be rendered and tested outside the browser
- progressive enhancement, from simple templating and basic reactivity to a full-blown app
A core benefit of using mancha
is that it allows you to compartmentalize the complexity of
front-end development. Whether you decide to break up your app into reusable partial sections via
<include>
or create custom web components, you can write HTML as if your mother was watching.
mancha
implements its own reactivity engine, so the bundled browser module contains no external
dependencies.
Preprocessing
As part of the rendering lifecycle, mancha
first preprocesses the HTML. The two main stages of
preprocessing consist of:
-
Resolution of <include>
tags
<button>Click Me</button>
<div>
<include src="button.tpl.html"></include>
</div>
<div>
<button>Click Me</button>
</div>
-
Registration and resolution of all custom web components
<template is="my-red-button">
<button style="background-color: red;">
<slot></slot>
</button>
</template>
<my-red-button :on:click="console.log('clicked')">
Click Me
</my-red-button>
Rendering
Once the HTML has been preprocessed, it is rendered by traversing every node in the DOM and applying
a series of plugins. Each plugin is only applied if specific conditions are met such as the HTML
element tag or attributes match a specific criteria. Here's the list of attributes handled:
:data
provides scoped variables to all subnodes, evaluated using jexpr
<div :data="{ name: 'Stranger' }"></div>
:for
clones the node and repeats it
<div :for="item in ['a', 'b', 'c']">{{ item }}</div>
:text
sets the textContent
value of a node
<div :data="{foo: 'bar'}" :text="foo"></div>
:html
sets the innerHTML
value of a node
<div :html="<span>Hello World</span>"></div>
:show
toggles $elem.style.display
to none
<div :data="{foo: false}" :show="foo"></div>
:bind
binds (two-way) a variable to the value
or checked
property of the element.
<div :data="{ name: 'Stranger' }">
<input type="text" :bind="name" />
</div>
:on:{event}
adds an event listener for event
to the node
<button :on:click="console.log('clicked')"></button>
:{attribute}
sets the corresponding property for attribute
in the node
<a :href="buildUrl()"></a>
{{ value }}
replaces value
in text nodes
<button :data="{label: 'Click Me'}">{{ label }}</button>
Evaluation
To avoid violation of Content Security Policy (CSP) that forbids the use of eval()
, Mancha
evaluates all expressions using jexpr
. This means that only simple expressions are
allowed, and spaces must be used to separate different expressions tokens. For example:
<body :data="{ pos: 1 }">
<p :text="'you are number ' + pos + ' in the queue'"></p>
</body>
<body :data="{ pos: 1, finished: false }">
<p :show="pos >= 1 && !finished">you are number {{ pos }} in the queue</p>
</body>
<body :data="{ pos: 1 }">
<p :text="pos % 2 == 0 ? 'even' : 'odd'"></p>
</body>
<body :data="{ pos : 1 }">
<p :text="buildQueueMessage()"></p>
<script>
const { $ } = Mancha;
$.buildQueueMessage = function () {
return "you are number " + this.pos + " in the queue";
};
</script>
</body>
<body :data="{ pos: 1 }">
<p :text="'you are number ' + pos + ' in the queue'"></p>
<button :on:click="pos = pos + 1">Click to get there faster</button>
</body>
<body :data="{ pos: 1 }">
<p :text="'you are number '+pos+' in the queue'"></p>
</body>
<button :on:click="console.log('yes'); answer = 'no'"></button>
<body :data="{ foo: () => 'yes' }">
<p :text="foo()"></p>
</body>
<body :data="{ pos: 1 }">
<p :text="'you are number ' + pos + ' in the queue'"></p>
<button :on:click="pos++">Click to get there faster</button>
</body>
Scoping
Contents of the :data
attribute are only available to subnodes in the HTML tree. This is better
illustrated with an example:
<body :data="{ name: 'stranger' }">
<h1>Hello, {{ name }}</h1>
<span>{{ message }}</span>
<p :data="{ name: 'danger', message: 'secret' }">
How are you, {{ name }}? The secret message is: "{{ message }}".
</p>
</body>
By default, the target root element is the body
tag. So, any variables defined in the body's
:data
attribute are available to the main renderer.
In the example above, the variable message
is only available to the <p>
tag and all elements
under that tag, if any. Since the variables are not accessible via the global object, you'll need
to retrieve the renderer from the element's properties:
const { $ } = Mancha;
await $.mount(document.body);
$.name = "world";
$.message = "bandit";
const subrenderer = document.querySelector("p").renderer;
subrenderer.$.message = "banana";
Styling
Some basic styling rules are built into the library and can be optionally used. The styling
component was designed to be used in the browser, and it's enabled by adding a css
attribute
to the <script>
tag that loads mancha
. The supported rulesets are:
basic
: inspired by these rules, the full CSS can be found
here.utils
: utility classes inspired by tailwindcss, the resulting CSS is
a drop-in replacement for a subset of the classes provided by tailwindcss
with the main
exception of the color palette which is borrowed from
material design.
Usage
Client Side Rendering (CSR)
To use mancha
on the client (browser), use the mancha
bundled file available via unpkg
.
<body :data="{ name: 'John' }">
<span>Hello, {{ name }}!</span>
</body>
<script src="//unpkg.com/mancha" target="body" css="basic+utils" init></script>
Script tag attributes:
init
: whether to automatically render upon script loadtarget
: document elements separated by +
to render e.g. "body" or "head+body" (defaults to
"body")css
: inject predefined CSS rulesets into the <head>
element, see the
styling section for more details.
For a more complete example, see examples/browser.
Compile Time Server Side Rendering (SSR)
To use mancha
on the server at compile time, you can use the npx mancha
command. For example,
if this is your project structure:
src/
├─ components/
| ├─ main.tpl.html
| ├─ footer.tpl.html
├─ index.html
├─ vars.json
You can run the following command to compile the site into a public
folder:
npx mancha --input="./src/index.html" --vars="$(cat vars.json)" --output="./public"
For a more complete example, see examples/compiled.
On Demand Server Side Rendering (SSR)
You can also use mancha
as part of your server's request handling. Assuming a similar folder
structure as described in the previous section, the following express
node server would render
the HTML code on demand for each incoming request:
import express from "express";
import { Renderer } from "mancha";
import vars from "./vars.json";
const app = express();
app.get("/", async (req, res) => {
const name = req.query.name || "Stranger";
const renderer = new Renderer({ name, ...vars });
const fragment = await renderer.preprocessLocal("src/index.html");
const html = renderer.serializeHTML(await renderer.renderNode(fragment));
res.set("Content-Type", "text/html");
res.send(html);
});
app.listen(process.env.PORT || 8080);
For a more complete example, see examples/express.
Web Worker Runtime Server Side Rendering (SSR)
For servers hosted as worker runtimes, such as Cloudflare Workers
, you will need to import a
stripped down version of mancha
that does not have the ability to read local files.
import { Renderer } from "mancha/dist/worker";
import htmlIndex from "./index.html";
import vars from "./vars.json";
self.addEventListener("fetch", async (event) => {
const renderer = new Renderer({ ...vars });
const fragment = await renderer.preprocessString(htmlIndex);
const html = renderer.serializeHTML(await renderer.renderNode(fragment));
event.respondWith(new Response(content, { headers: { "Content-Type": "text/html" } }));
});
To meet the size requirements of popular worker runtimes, the worker version of mancha
uses
htmlparser2
instead of jsdom
for the underlying HTML and DOM manipulation. This keeps the
footprint of mancha
and its dependencies under 100kb.
For a more complete example, see examples/wrangler.
Dependencies
The browser bundle contains a single external dependency, jexpr
. The unbundled version
can use htmlparser2
, which is compatible with web workers, or jsdom
.