Documentation / Directives

Directives

Some interactions belong in the browser. HyperChi’s hc-* attributes handle small pieces of local state alongside server-rendered HTML.

Start with a scope

Load the bundled hyperchi-directives.js runtime. Define state with hc-data, handle an event with hc-on, and render a value with hc-text.

<script src="/static/js/hyperchi-directives.js"></script>

<div hc-data="{ count: 0 }">
  <button hc-on:click="count++">Add one</button>
  <output hc-text="count">0</output>
</div>

<script>HyperChi.start();</script>
0

hc-data takes any JavaScript expression that returns an object. That object becomes a reactive scope for the element and everything inside it: a change to count updates every directive that reads it.

Name a reusable component

Register a factory before starting HyperChi, then reference it from your HTML. A method named init runs once when the scope is created, with this set to the scope.

HyperChi.registerComponent('counter', () => ({
  count: 0,
  increment() { this.count++ },
  init() { this.count = Number(sessionStorage.getItem('count')) || 0 },
}));
HyperChi.start();
<div hc-data="counter()">
  <button hc-on:click="increment()">Add one</button>
  <output hc-text="count">0</output>
</div>

Anything you register is also visible in every expression on the page, so a helper such as registerComponent('money', n => '$' + n.toFixed(2)) can be called as money(price).

Every directive

The runtime implements these fifteen attributes and nothing else.

Directive What it does
hc-data="expr" Creates a reactive scope from the object the expression returns.
hc-init="stmt" Runs a statement once when the element is processed.
hc-show="expr" Shows or hides the element with display; it stays in the DOM.
hc-if="expr" Removes the element from the DOM when false and restores it when true.
hc-for="item in items" Repeats the element per item; item, i in items also gives the index.
hc-text="expr" Sets textContent to the value.
hc-html="expr" Sets innerHTML to the value. See the security notes below.
hc-bind:attr="expr" Binds an attribute to the value. Shorthand :attr.
hc-on:event="stmt" Listens for an event. Shorthand @event.
hc-model="key" Two-way binding between a form control and a scope key.
hc-ref="name" Stores the element in $refs.name.
hc-effect="stmt" Runs a statement now and again whenever what it reads changes.
`hc-transition="enter:a leave:b"`
hc-persist="key:storageKey" Syncs a scope key with localStorage.
hc-cloak Removed once the element is processed, so CSS can hide it until then.

Class and style bindings take richer values. hc-bind:class accepts a string, an array, or an object of { name: condition }, and hc-bind:style accepts an object of camelCase properties. A binding to false or nothing removes the attribute, and true sets it empty, which suits disabled.

<div hc-data="{ open: false, items: ['Go', 'htmx'], name: '' }">
  <button hc-on:click.prevent="open = !open" hc-bind:aria-expanded="String(open)">Menu</button>
  <ul hc-show="open" hc-cloak>
    <li hc-for="item, i in items" hc-text="i + 1 + '. ' + item"></li>
  </ul>
  <input hc-model="name" placeholder="Your name">
  <button hc-bind:disabled="!name" hc-bind:class="{ ready: name }">Greet</button>
  <p hc-if="name" hc-text="'Hello, ' + name + '.'"></p>
</div>

Add [hc-cloak] { display: none; } to your stylesheet so cloaked elements stay hidden until the runtime reaches them.

Events and modifiers

hc-on:click, hc-on:input, and every other DOM event work, as do custom and htmx events: hc-on:htmx:after:swap="refresh($event)". A statement can read the event as $event. Chain modifiers after the event name with dots: .prevent calls preventDefault, .stop stops propagation, .self fires only when the element itself was the target, and .once removes the listener after the first call.

Scopes nest

A child hc-data sees its own values and every ancestor's. Reading a name looks from the nearest scope outward, and assigning to a name that an ancestor already owns changes the ancestor's value. That lets a small component update page-level state, such as a theme flag set on <html>. hc-for gives each repeated element its own scope for the item variable.

hc-persist="darkMode:theme" keeps darkMode in localStorage under theme, reading it back when the scope is created. Without the second part, the key is _hc_darkMode.

They survive htmx swaps

The runtime does not bind to the elements that exist at load. It watches the document for new nodes, and also runs after every htmx swap, so a fragment that arrives from c.Fragment or over a WebSocket is processed the moment it lands. Elements that are removed have their listeners and effects torn down. That is why the rule in HyperChi apps is hc-on:click and never an inline onclick: an inline handler has no scope to read, and a Content-Security-Policy without 'unsafe-inline' blocks it outright.

Keep component state in a registered hc-data function, so a fragment can say hc-data="tray()" without shipping a script. After you change the DOM some other way, HyperChi.refresh(element) processes it again. HyperChi.start({ root }) limits the runtime to one subtree.

Security notes

Use it with htmx

Pair the two by letting the server own data and the browser own presentation. A fragment returns HTML with hc-* attributes, htmx swaps it in, and the runtime wires it. For live regions, see realtime, and for the server side of fragments, templates.

The documentation topic filter, the search shortcut, the copy buttons, and the theme switch on this page all use these directives.