sprae
sprae copied to clipboard
∴ DOM tree microhydration
∴ spræ

DOM tree microhydration
Sprae is a compact & ergonomic progressive enhancement framework.
It provides :
-attributes for inline markup logic with signals-based reactivity.
Perfect for small-scale websites, prototypes, or lightweight UI.
Usage
<div id="container" :if="user">
Hello <span :text="user.name">World</span>.
</div>
<script type="module">
import sprae, { signal } from 'sprae'
const name = signal('Kitty')
sprae(container, { user: { name } }) // init
name.value = 'Dolly' // update
</script>
Sprae evaluates :
-directives and evaporates them, attaching state to html.
Directives
:if="condition"
, :else
Control flow of elements.
<span :if="foo">foo</span>
<span :else :if="bar">bar</span>
<span :else>baz</span>
<!-- fragment -->
<template :if="foo">foo <span>bar</span> baz</template>
:each="item, index in items"
Multiply element. Item is identified either by item.id
, item.key
or item
itself.
<ul><li :each="item in items" :text="item"/></ul>
<!-- cases -->
<li :each="item, idx in list" />
<li :each="val, key in obj" />
<li :each="idx in number" />
<!-- by condition -->
<li :if="items" :each="item in items" :text="item" />
<li :else>Empty list</li>
<!-- fragment -->
<template :each="item in items">
<dt :text="item.term"/>
<dd :text="item.definition"/>
</template>
<!-- prevent FOUC -->
<style>[:each] {visibility: hidden}</style>
:text="value"
Set text content of an element.
Welcome, <span :text="user.name">Guest</span>.
<!-- fragment -->
Welcome, <template :text="user.name" />.
:class="value"
Set class value, extends existing class
.
<!-- string with interpolation -->
<div :class="'foo $<bar>'"></div>
<!-- array/object a-la clsx -->
<div :class="[foo && 'foo', {bar: bar}]"></div>
:style="value"
Set style value, extends existing style
.
<!-- string with interpolation -->
<div :style="'foo: $<bar>'"></div>
<!-- object -->
<div :style="{foo: 'bar'}"></div>
<!-- CSS variable -->
<div :style="{'--baz': qux}"></div>
:value="value"
Set value of an input, textarea or select. Takes handle of checked
and selected
attributes.
<input :value="value" />
<textarea :value="value" />
<!-- selects right option -->
<select :value="selected">
<option :each="i in 5" :value="i" :text="i"></option>
</select>
:*="value"
, :="values"
Set any attribute(s).
<label :for="name" :text="name" />
<!-- multiple attributes -->
<input :id:name="name" />
<!-- spread attributes -->
<input :="{ id: name, name, type: 'text', value }" />
:scope="data"
Define or extend data scope for a subtree.
<x :scope="{ foo: signal('bar') }">
<!-- extends parent scope -->
<y :scope="{ baz: 'qux' }" :text="foo + baz"></y>
</x>
:ref="name"
Expose element to current scope with name
.
<textarea :ref="text" placeholder="Enter text..."></textarea>
<!-- iterable items -->
<li :each="item in items" :ref="item">
<input :onfocus..onblur=="e => (item.classList.add('editing'), e => item.classList.remove('editing'))"/>
</li>
:fx="code"
Run effect, not changing any attribute.
Optional cleanup is called in-between effect calls or on disposal.
<div :fx="a.value ? foo() : bar()" />
<!-- cleanup function -->
<div :fx="id = setInterval(tick, interval), () => clearInterval(tick)" />
:on*="handler"
Attach event(s) listener with possible modifiers.
<input type="checkbox" :onchange="e => isChecked = e.target.value">
<!-- multiple events -->
<input :value="text" :oninput:onchange="e => text = e.target.value">
<!-- events sequence -->
<button :onfocus..onblur="e => ( handleFocus(), e => handleBlur())">
<!-- event modifiers -->
<button :onclick.throttle-500="handler">Not too often</button>
Modifiers:
-
.once
,.passive
,.capture
– listener options. -
.prevent
,.stop
– prevent default or stop propagation. -
.window
,.document
,.outside
,.self
– specify event target. -
.throttle-<ms>
,.debounce-<ms>
– defer function call with one of the methods. -
.ctrl
,.shift
,.alt
,.meta
,.arrow
,.enter
,.escape
,.tab
,.space
,.backspace
,.delete
,.digit
,.letter
,.character
– filter byevent.key
. -
.ctrl-<key>, .alt-<key>, .meta-<key>, .shift-<key>
– key combinations, eg..ctrl-alt-delete
or.meta-x
. -
.*
– any other modifier has no effect, but allows binding multiple handlers to same event (like jQuery event classes).
:html="element"
🔌
Include as
import 'sprae/directive/html'
.
Set html content of an element or instantiate a template.
Hello, <span :html="userElement">Guest</span>.
<!-- fragment -->
Hello, <template :html="user.name">Guest</template>.
<!-- instantiate template -->
<template :ref="tpl"><span :text="foo"></span></template>
<div :html="tpl" :scope="{foo:'bar'}">...inserted here...</div>
:data="values"
🔌
Include as
import 'sprae/directive/data'
.
Set data-*
attributes. CamelCase is converted to dash-case.
<input :data="{foo: 1, barBaz: true}" />
<!-- <input data-foo="1" data-bar-baz /> -->
:aria="values"
🔌
Include as
import 'sprae/directive/aria'
.
Set aria-*
attributes. Boolean values are stringified.
<input role="combobox" :aria="{
controls: 'joketypes',
autocomplete: 'list',
expanded: false,
activeOption: 'item1',
activedescendant: ''
}" />
<!--
<input role="combobox" aria-controls="joketypes" aria-autocomplete="list" aria-expanded="false" aria-active-option="item1" aria-activedescendant>
-->
Expressions
Expressions use justin, a minimal JS subset. It avoids "unsafe-eval" CSP and provides sandboxing. Also it's fast.
Operators:
++ -- ! - + ** * / % && || ??
= < <= > >= == != === !==
<< >> & ^ | ~ ?: . ?. [] ()=>{} in
Primitives:
[] {} "" ''
1 2.34 -5e6 0x7a
true false null undefined NaN
Signals
Sprae uses minimal signals based on ulive
. It can be switched to @preact/signals-core
, @webreflection/signal
, usignal
, which are better for complex states:
import sprae, { signal, computed, effect, batch, untracked } from 'sprae';
import * as signals from '@preact/signals-core';
sprae.use(signals);
sprae(el, { name: signal('Kitty') });
Customization
Sprae build can be tailored to project needs via sprae/core
and sprae/directive/*
:
import sprae, { directive, compile } from 'sprae/core.js'
// include directives
import 'sprae/directive/if.js';
import 'sprae/directive/text.js';
// define custom directive
directive.id = (el, expr, state) => {
const evaluate = compile(state, 'id') // expression string -> evaluator
return () => el.id = evaluate(state) // return update function
}
v9 changes
- No autoinit → use manual init via
import sprae from 'sprae'; sprae(document.body, state)
. - No default globals (
console
,setTimeout
etc) - pass to state if required. -
:class="`abc ${def}`"
→:class="'abc $<def>'"
(justin) -
:with={x:'x'}
->:scope={x:'x'}
- No reactive store → use signals for reactive values.
-
:render="tpl"
→:html="tpl"
-
@click="event.target"
→:onclick="event => event.target"
- Async props / events are not supported, pass async functions via state.
- Directives order matters, eg.
<a :if :each :scope />
!==<a :scope :each :if />
- Only one directive per
<template>
, eg.<template :each />
, not<template :if :each/>
Justification
Template-parts / templize is progressive, but is stuck with native HTML quirks (parsing table, SVG attributes, liquid syntax conflict etc). Alpine / petite-vue / lucia escape native HTML quirks, but have excessive API (:
, x-
, {}
, @
, $
) and tend to self-encapsulate.
Sprae holds open & minimalistic philosophy, combining :
-directives with signals.