o1framework
v1.0.2
Published
simple reactive DOM micro-framework
Readme
o1
Reactive DOM micro-framework. No virtual DOM, no build step required, no JSX. Just HTML attributes and plain JavaScript.
Built with LiveScript + prelude-ls. Bundles to ~7 KB gzipped.
Quick start
<span o-mark="count">0</span>
<button onclick="data.count++">+</button>
<script src="dist/o1.min.js"></script>
<script>
const en = O1.default;
const data = en.init();
data.count = 0;
</script>Or via npm:
npm install o1import { default as en } from 'o1';
const data = en.init();
data.count = 0;How it works
Assign a property on the reactive data object — every DOM element bound via o-* attributes updates automatically. The reactivity engine uses Proxy to intercept mutations and an O(1) registry to find bound elements without querying the DOM.
Directives
o-mark — text binding
<span o-mark="user.name">placeholder</span>Sets textContent when the bound value changes. Objects are serialized to JSON.
o-model — two-way binding
<input type="text" o-model="name" />
<input type="checkbox" o-model="agreed" />
<select o-model="country">
<option value="pl">Poland</option>
<option value="us">USA</option>
</select>Supports text, number, checkbox, radio, select, and textarea. Updates flow in both directions: data to DOM and DOM to data.
o-if / o-ifnot — conditional rendering
<template o-if="loggedIn">
<p>Welcome back!</p>
</template>
<template o-ifnot="loading">
<p>Content loaded.</p>
</template>Wraps content in a <template> and inserts/removes children based on a truthy/falsy value.
o-show — toggle visibility
<span o-show="isVisible">Now you see me</span>Sets display: none when the value is falsy, clears the inline display when truthy. Lighter than o-if — the element stays in the DOM.
o-bind — dynamic attributes
<a o-bind="url:href">Link</a>
<button o-bind="isDisabled:disabled">Click</button>
<img o-bind="imageUrl:src" />Parametric directive — the part after : is the attribute name. Boolean handling: true sets the attribute with an empty value, false/null/undefined removes it.
o-on — declarative events
<button o-on="handleClick:click">Click me</button>data.handleClick = en.fn(() => { data.count++; });Parametric directive — the part after : is the event name. Use en.fn() to store event handlers in reactive data without them being treated as computed values.
Arrays
Use # as an index placeholder:
<ul>
<li o-mark="items.#">
<span o-mark="items.#.name"></span>
</li>
</ul>data.items = [{ name: 'Alice' }, { name: 'Bob' }];The framework clones the template element for each array item and rewrites # to the actual index. On mutation it only touches affected elements.
Array mutating methods work reactively — push, pop, shift, unshift, splice, sort, reverse all trigger DOM updates automatically:
data.items.push({ name: 'Charlie' });
data.items.splice(1, 1);
data.items.sort();
data.items.reverse();Custom directives
en.directive('color', ({ el, value }) => {
el.style.color = String(value);
});<span o-color="themeColor">Hello</span>Parametric directives accept an inline argument after a colon:
en.directive('style', ({ el, value, param }) => {
el.style.setProperty(param, String(value));
}, true);<div o-style="bg:background-color"></div>Computed values
data.firstName = 'Jan';
data.lastName = 'Kowalski';
data.fullName = en.computed(() => `${data.firstName} ${data.lastName}`);Dependencies are tracked automatically. Async functions are supported — stale results from earlier calls are discarded.
data.post = en.computed(async () => {
const res = await fetch(`/api/posts/${data.postId}`);
return res.json();
});Watchers
en.watch('count', value => console.log('count changed to', value));
// Remove a specific watcher
en.unwatch('count', myCallback);
// Remove all watchers for a key
en.unwatch('count');
// Remove all watchers
en.unwatch();Ancestor watchers fire too — watching user triggers when user.name changes.
Batching
en.batch(() => {
data.x = 1;
data.y = 2;
data.z = 3;
});
// Single DOM update cycle for all three changesMultiple instances
import { createInstance } from 'o1';
const en1 = createInstance();
const data1 = en1.init();
const en2 = createInstance();
en2.prefix('app'); // uses app-mark, app-model, etc.
const data2 = en2.init();Each instance has its own reactive context, watchers, directives, and registry.
Components
Define reusable Web Components via named templates:
<template name="user-card">
<style>
.card { border: 1px solid #ccc; padding: 1rem; }
</style>
<div class="card">
<slot name="name">Unknown</slot>
</div>
</template>
<user-card>
<span slot="name" o-mark="userName">Alice</span>
</user-card>Templates are auto-registered as custom elements with Shadow DOM. External templates can be loaded:
await en.load('components/card.html');API reference
| Method | Description |
|--------|-------------|
| en.init() | Initialize reactive root, scan DOM, return reactive data object |
| en.computed(fn) | Create a computed value with automatic dependency tracking |
| en.watch(key, fn) | Watch a reactive key for changes |
| en.unwatch([key], [fn]) | Remove watchers (specific, by key, or all) |
| en.directive(name, cb, isParametric?) | Register a custom directive |
| en.prefix(value?) | Set the attribute prefix (default: o-) |
| en.batch(fn) | Batch multiple updates into one DOM cycle |
| en.load(files) | Load external template files (returns Promise) |
| en.register(root?) | Register component templates from HTML string or DOM element |
| en.fn(f) | Wrap a function for use as event handler in reactive data |
| en.nextTick() | Returns a Promise that resolves after the current DOM update cycle |
| en.destroy() | Tear down instance: remove listeners, clear state |
Building
npm run build # compile LiveScript + bundle with esbuild
npm test # run vitest suiteProduces three bundles:
dist/index.mjs— ES modulesdist/index.cjs— CommonJSdist/o1.min.js— IIFE (exposeswindow.O1)
Examples
See the examples/ directory for working demos: counter, todo list, async computed, components, routing, model binding, and more.
License
MIT
