Skip to content

Repository files navigation

ae logo

ae

npm CI license

Tiny attribute-based behavior + reactivity library. HTML is the source of truth — you write real markup, ae attaches behavior to it. No virtual DOM, no hydration, no template syntax, no build step required.

3.9 KB min+gzip · zero dependencies · TypeScript · one data-ae attribute

Install

npm install @aeroapp/ae
yarn add @aeroapp/ae
pnpm add @aeroapp/ae
bun add @aeroapp/ae

Or straight from a CDN, no tooling at all — esm.sh, unpkg, and jsDelivr all serve it the moment you need it:

<script type="module">
  import { ae } from 'https://esm.sh/@aeroapp/ae';
  // or: 'https://unpkg.com/@aeroapp/ae'                  (minified build)
  // or: 'https://cdn.jsdelivr.net/npm/@aeroapp/ae/+esm'
</script>

Pin an exact version in production — https://esm.sh/@aeroapp/ae@X.Y.Z (see the npm badge above for the current release).

<button data-ae="save">Save</button>
<span data-ae="status"></span>
import { ae } from '@aeroapp/ae';

const count = ae.signal(0);

ae('status').render(el => el.textContent = `${count.value} items`);

ae('save')
  .press(() => count.value++)
  .hover(el => el.classList.add('hot'), el => el.classList.remove('hot'));

Handles are live: elements added to the DOM later — by you, by .list(), by anything — get bound automatically via one shared MutationObserver, and cleaned up completely when they leave. No manual unbinding, no leaks.

Feature tour

Signalsae.signal(v), ae.computed(fn), ae.effect(fn). Writes are batched per microtask; computeds are lazy and only notify when their value actually changes.

Rendering.render(fn) auto-tracks every signal read inside and re-runs on change. Sugar helpers .text() / .cls() / .attr() / .show() accept a plain value (applied once), a signal, or a function (both reactive).

Events.press(fn) is semantic activation: click everywhere, plus native-like Enter/Space only for elements the browser doesn't natively activate (keyboard-accessible div[tabindex], [role=button] for free, no double-fire on real buttons). Enter fires on keydown, Space on keyup — exactly like a native button — and keys inside nested form controls or editable text are never hijacked. .hover(enter, leave) and the .on(type, fn, opts) escape hatch round it out — all listeners per element, so non-bubbling events just work.

Keyed lists.list(items, render, key) stamps a native <template> per item with keyed reconciliation: reorders move nodes without remounting, unchanged items don't re-render, removed items are disposed.

<ul data-ae="todos">
  <template><li><b data-ae="title"></b></li></template>
</ul>
ae('todos').list(todos, (li, todo, i) => {
  ae.parts(li).title.textContent = `${i + 1}. ${todo.text}`;
}, todo => todo.id);

ae('remove').press(btn => {                 // which item was clicked?
  const todo = ae.itemOf(btn);              // ae knows — no key stamping
  todos.value = todos.value.filter(t => t.id !== todo.id);
});

Two-way forms.input(signal) wires by field type: strings for text/select, booleans for checkboxes, real numbers for number/range, a group value for radios, and string[] for <select multiple>. Writes are equality-guarded so echoes never move the caret.

Scoped rootsae(name, root) limits a handle to descendants of root: per-list behavior without global name collisions.

Shadow DOMae.observe(shadowRoot) extends the same liveness into a shadow tree: existing marked content mounts immediately, host removal cleans everything up, and the returned disposer undoes it all.

Animationae.transition(fn) runs your signal writes inside a browser View Transition: list enters, exits, and reorders animate in pure CSS, and elements with a view-transition-name morph — even across lists. Falls back to a plain call where unsupported.

Docs

Every member gets a signature, its parameters, its sharp edges, worked examples, and live demos:

page covers
Handles & lifecycle ae(name), ae(name, root), .mount, .scope, .els, .each, ae.parts, ae.itemOf, ae.observe
Signals & scheduling ae.signal, ae.computed, ae.effect, ae.isSignal, ae.settled, ae.transition, Reactive<T>
Rendering & events .render, .text, .cls, .attr, .show, .press, .hover, .on
Forms .input for text, checkbox, number/range, radio groups, and <select multiple>
Lists .list, the template, keys, reconciliation, nested lists, animation

Read it at https://nft.github.io/ae/reference.html, or locally with bun run servehttp://localhost:4242. Full prose spec and design notes: API.md. Compact API reference for AI agents: llms.txt.

Real app: examples/kanban.html — a kanban with drag & drop, dynamic columns, inline editing, undo, filtering, and localStorage persistence in ~170 lines of JS. One-pager with live demos: site/index.html (deployed to GitHub Pages).

Performance

Keyed-list stress numbers, measured end-to-end — signal write through reconciliation, mount pipeline, layout, and paint (headless Chrome, Apple M4 Max; median of 3 runs). Each row carries three live data-ae bindings, two of them event listeners.

operation time
create 1,000 rows 26 ms
create 10,000 rows 262 ms
append 1,000 to 10,000 69 ms
update every 10th of 11,000 117 ms
swap 2 rows of 11,000 75 ms
clear 11,000 rows 58 ms

Reproduce: bun run serveexamples/bench.html (add ?auto for the full suite).

Development

bun install
bun run build          # tsc → dist/*.js + declarations (entry: dist/ae.js)
bun run test           # jsdom smoke suite (build first)
bun run test:browser   # Playwright: Chromium, Firefox, WebKit
bun run serve          # demo at http://localhost:4242
bun run min            # dist/ae.min.js

Guarantees (the short version)

  • Batching — signal writes coalesce; each effect runs at most once per flush.
  • Absolute disposal — a disposed effect never runs again, even if already queued.
  • Net-state lifecycle — mount/cleanup reflect the net DOM change per task: moves don't remount, a→b→a renames are no-ops.
  • Fault isolation — one throwing effect/binding/cleanup never takes down the rest.
  • Runaway guard — a self-triggering effect trips a circuit breaker instead of hanging the tab.

License

MIT

About

Attribute-based reactivity for real HTML — one data-ae attribute wires signals, keyed lists, two-way forms, and View-Transition animation. 3 KB, zero deps, no VDOM, no build step.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages