Documentation

Getting Started

Install the kit and wire signals, actions, and directives together.

Install

Every package is published two ways: as an individual @alwatr/* package, or as a subpath of the single monolithic alwatr package. Pick whichever fits your dependency-management style — the code you import from is identical.

individual packages
bun add @alwatr/flux @alwatr/fsm
monolithic package
bun add alwatr
imports, either way
// individual packages
import {createStateSignal} from '@alwatr/flux';

// or the monolithic package, via subpath
import {createStateSignal} from 'alwatr/flux';

TypeScript configuration

Directives use the ES2024 accessor keyword with TC39 Stage 3 decorators — the modern, standardized decorator proposal, not the legacy experimental one.

Warning

Do not set experimentalDecorators: true in your tsconfig.json. Alwatr's decorators (@directive, @state, @query, @attribute) target the standard decorator spec and are incompatible with the legacy flag.

tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "esnext",
    "moduleResolution": "bundler",
    "strict": true
  }
}

A minimal counter

A single file is enough to see all three primitives — signal, action, directive — click into place. First declare your action's payload type via TypeScript declaration merging, so the whole action bus stays type-checked end to end:

counter.ts
import {
  createStateSignal,
  actionService,
  directive,
  Directive,
  bootstrapDirectives,
  ActionService,
} from '@alwatr/flux';

declare module '@alwatr/flux' {
  interface ActionRecord {
    ui_increment: void;
    ui_decrement: void;
  }
}

// State — the single source of truth.
const count = createStateSignal({name: 'count', initialValue: 0});

// Controller — the only thing allowed to write to the signal.
actionService.on('ui_increment', () => count.update((n) => n + 1));
actionService.on('ui_decrement', () => count.update((n) => n - 1));

// View — a directive subscribes to the signal and updates its own element.
@directive('counter_display')
class CounterDisplayDirective extends Directive {
  protected override init_(): void {
    this.subscribe_(count, (value) => {
      this.element_.textContent = String(value);
    });
  }
}

// Bootstrap once, at app start.
bootstrapDirectives();
actionService.setupDelegation(ActionService.DEFAULT_DELEGATED_EVENTS);

HTML markup

No client-side templating is involved — this markup can be static HTML, or emitted by a server-side renderer such as @alwatr/loom. The on-click attributes dispatch actions; the counter_display attribute activates the directive above.

index.html
<button on-click="ui_decrement">−</button>
<span counter_display>0</span>
<button on-click="ui_increment">+</button>

That's the whole cycle: a click dispatches ui_increment, the controller updates the count signal, and the directive's subscription writes the new value straight into the DOM — no re-render, no diffing, one surgical text update.

Bootstrap once

Call bootstrapDirectives() and actionService.setupDelegation(...) exactly once, at application start (a single app.ts entry point is the idiomatic place). Both are idempotent and safe to call before the DOM is fully parsed — bootstrapDirectives() defers to DOMContentLoaded automatically if needed.

Next steps

  • Read Architecture & UDF for the full mental model behind the cycle you just wired up.
  • Go deep on Signals — Computed, Effect, Channel, and the persistent storage-backed variants.
  • Learn every modifier and payload resolver in Actions & Events.