Hologram v0.10: Events and middleware for Elixir running in the browser

bartblast1 pts0 comments

Hologram v0.10: Events, Middleware, and More - Hologram

Hologram v0.10: Events, Middleware, and More

v0.9 was about realtime. v0.10 goes the other way: it deepens the foundation, so more of your app runs in the browser as pure Elixir. Mostly that means events. The client-side event system grew up this release, gaining keyboard, scroll, resize, click-outside, and scroll-edge events, bindings on the window and document, and a set of modifiers for shaping the event stream, controlling propagation, and overriding the browser's default. It all lives in your templates, gets checked at compile time, and never makes you write JavaScript. There's a new server-side middleware layer too, plus two more pieces of Elixir that now behave the same in the browser as on the server: comprehensions and error handling.

That's a full game of Space Invaders, running as pure Elixir in your browser. No JavaScript to write, no canvas library, no game engine, just the event system driving an SVG, in a tiny bundle. Every keypress runs through the same $key_down and $key_up bindings and action/3 handlers you'd write for a form (keydown to move and shoot, keyup to stop), and the whole input layer is one tag:

window<br>$key_down.arrow_left="press_left"<br>$key_down.arrow_right="press_right"<br>$key_down.r="restart"<br>$key_down.space.prevent_default="shoot"<br>$key_up.arrow_left="release_left"<br>$key_up.arrow_right="release_right"<br>/>

Play it here, then read on for how it works.

A First-Class Event System

Before v0.10, Hologram covered the basics: clicks, form changes, focus, a few pointer and transition events. That list is a lot longer now, with global bindings and a set of modifiers on top. The new event types alone cover most of what an interactive UI reaches for:

Keyboard - $key_down and $key_up, with a rich event payload (key, code, modifier flags, repeat) and template-level key filters.

Scroll and resize - $scroll reports the scroll offset of an element or the page. $resize reports an element's box sizes, or fires on a window resize.

Click-outside - $click_outside fires when a click lands outside the bound element. Think dropdowns, popovers, and menus.

Scroll-edge (reach) - $reach_top, $reach_bottom, $reach_left, and $reach_right fire as a scroll container's edge comes into view. The basis for infinite scroll, load-more, and pull-to-refresh.

Key filters are where this gets good. You bind a handler to a specific key or combination right in the template, like $key_down.enter or $key_down.ctrl+k. Because Hologram compiles your templates, that filter is checked at compile time. Misspell a key as $key_down.entr and the build fails, pointing you at the closest valid name. In most frameworks the same typo is a silent no-op you only catch at runtime.

Window- and Document-Level Bindings

Some events don't belong to any rendered element: a global keyboard shortcut, a window resize, a page scroll. For those, the new and tags bind to the global window or document instead of an element. They render nothing and reuse the same $event syntax, key filters, and modifiers as any other binding. A listener stays live only while its tag is rendered, so putting one behind a conditional makes it listen only when that condition holds. The Space Invaders demo uses these to catch arrow keys across the whole page. A command-palette shortcut works the same way:

window $key_down.ctrl+k="open_palette" />

Shaping the Event Stream

Some events fire many times a second: typing in a search box, moving the pointer, scrolling a list. Two modifiers tame that stream, and both attach right on the event name (and get validated at compile time):

debounce(ms) - coalesces a burst into a single dispatch, ms after the events stop. Reach for it when you only care about the final state, like a search query after the user stops typing. Defaults to 250 ms.

throttle(ms) - caps dispatches to at most one per interval while events keep firing. Reach for it when you want steady updates during an interaction, like a live cursor readout or a drag preview. Defaults to 100 ms.

input $change.debounce(300)="search" />

There's also once, which fires a binding a single time and then stops, handy for a confirm button or lazy-loading the first time a container scrolls into view. The modifiers compose, too: $key_down.enter.debounce(300) debounces only the Enter key, and a throttled binding marked once fires its one throttled dispatch and then retires.

Controlling Propagation and the Native Default

DOM events bubble, and the browser has its own default behaviour for many of them. Three more modifiers let each binding say exactly what it wants:

stop_propagation - stops the event at the bound element, so ancestor bindings don't also fire (e.g. a delete button inside a clickable card).

prevent_default - prevents the browser's native default for that binding (e.g. Enter-to-send in a textarea, without inserting a newline).

allow_default - the mirror image: lets the native default through where...

events key_down event scroll browser window

Related Articles