@jdlanglois/els
v1.4.0
Published
Ultra-lightweight stream-based DOM library with keyed lists and routing
Maintainers
Readme
@jdlanglois/els
A lightweight, stream-based DOM library that combines the simplicity of RE:DOM's el utility with the reactive power of Mithril Streams.
- ⚡️ Direct DOM: No virtual DOM overhead.
- 🔄 Reactive: Attributes, styles, and text nodes bind directly to streams.
- 🧹 Auto-Cleanup: Subscriptions are tracked and automatically ended when elements are removed.
- 🎯 Simple API: Familiar
el(selector, props, ...children)pattern.
Quick Start
bun add @jdlanglois/els mithril1. Creating Elements
The core of the library is the el function. It uses a CSS-like selector to create real DOM nodes.
import { el, mount } from '@jdlanglois/els';
// Create a simple card
const card = el('div.card#main',
{ title: 'Hover me' },
el('h1', 'Hello World'),
el('p', 'This is a standard DOM element.')
);
mount(document.body, card);2. Adding Reactivity
Instead of manual DOM updates, use Streams. When a stream's value changes, any part of the DOM bound to it updates automatically.
import { el, stream } from '@jdlanglois/els';
const count = stream(0);
const counter = el('div',
el('p', 'Count: ', count), // Text updates automatically
el('button', {
onclick: () => count(count() + 1),
style: { color: count.map(c => c > 10 ? 'red' : 'black') } // Styles update too!
}, 'Increment')
);3. Derived State with computed
Use computed to create values that depend on other streams. It automatically tracks dependencies—no manual dependency arrays needed.
import { stream, computed } from '@jdlanglois/els';
const first = stream('John');
const last = stream('Doe');
// Automatically reacts when either stream changes
const full = computed(() => `${first()} ${last()}`);
const greeting = el('h1', 'Hello, ', full);4. Components & Cleanup
Components are just functions that return elements. @jdlanglois/els handles memory management by tracking subscriptions and cleaning them up when an element is removed.
import { el, remove } from '@jdlanglois/els';
const UserProfile = (name: string) => {
return el('div.profile',
el('h2', name),
{ onremove: () => console.log('Cleaning up...') }
);
};
const profile = UserProfile('Alice');
// ... later ...
remove(profile); // Recursively stops all child streams and triggers onremoveAdvanced Features
Keyed Lists
For large collections, use list() for efficient reconciliation. It surgically moves DOM nodes instead of recreating them.
import { el, list, stream } from '@jdlanglois/els';
const users = stream([{ id: 1, name: 'Alice' }]);
const view = el('ul',
list(users, u => u.id, (user, index) =>
el('li', user.map(u => u.name))
)
);Routing
The built-in router supports both Hash and History API modes, with automatic link interception.
import { createRouter, tags } from '@jdlanglois/els';
const { h1, div } = tags;
const router = createRouter({ useHash: false });
const app = div(
router.view({
'/': () => h1('Home'),
'/users/:id': (params) => h1(`User: ${params.id}`),
'*': () => h1('404')
})
);Styling (CSS-in-JS)
Write scoped CSS using tagged template literals.
import { css } from '@jdlanglois/els';
const cardStyle = css`
padding: 20px;
&:hover { transform: scale(1.05); }
& span { color: blue; }
`;
const globalStyles = css.global`
body { margin: 0; background: #eee; }
`;
const myCard = el('div', { class: cardStyle }, 'Hover me!');
---
## Alternative Syntax Options
While the builder-pattern (`el`) is the fastest and smallest, you can also use these optional syntaxes for a more declarative feel.
### 1. HTML Tagged Templates (`@jdlanglois/els/html`)
For a `lit-html` like experience without needing a transpiler.
```typescript
import { html } from '@jdlanglois/els/html';
import { stream } from '@jdlanglois/els';
const count = stream(0);
const view = html`
<div class="counter">
<p>Count: ${count}</p>
<button @click="${() => count(count() + 1)}">Increment</button>
</div>
`;2. JSX / TSX (@jdlanglois/els/jsx)
For the most familiar declarative experience. Requires a transpiler (Vite, Bun, etc.).
Configuration (tsconfig.json):
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}
}Usage:
/** @jsx h */
import { h } from '@jdlanglois/els/jsx';
import { stream } from '@jdlanglois/els';
const App = () => {
const count = stream(0);
return (
<div>
<p>Count: {count}</p>
<button onclick={() => count(count() + 1)}>Increment</button>
</div>
);
};Production Optimization (Vite)
To achieve zero runtime overhead for styles, use the Vite plugin. It extracts css template literals into a static CSS file during build.
// vite.config.ts
import elPlugin from '@jdlanglois/els/vite';
export default {
plugins: [elPlugin()]
};Technical Appendix
Subscription Management
Subscriptions created via streams are stored using Symbol.for('el-subscriptions') on the DOM node. The remove(el) utility recursively traverses the tree to call .end(true) on every tracked subscription, ensuring no memory leaks.
Attribute vs Property
el smartly decides how to apply props:
- If the key exists as a property (e.g.,
className,onclick,value), it sets it directly. - If it's a
styleorclassobject, it handles merging/toggling. - Otherwise, it falls back to
setAttribute.
