DOM tree hydration with reactivity.
Sprae is compact ergonomic progressive enhancement framework.
It provides reactive :
-attributes that enable simple markup logic without need for complex scripts.
Perfect for small-scale websites, prototypes or UI logic.
It is tiny, performant and open alternative to alpine, petite-vue or template-parts.
To autoinit document, include sprae.auto.js
:
<!-- <script src="https://cdn.jsdelivr.net/npm/sprae/dist/sprae.auto.js" defer></script> -->
<script defer src="./path/to/sprae.auto.js"></script>
<ul>
<li :each="item in ['apple', 'bananas', 'citrus']"">
<a :href="`#${item}`" :text="item" />
</li>
</ul>
To init manually as module, import sprae.js
:
<div id="container" :if="user">
Logged in as <span :text="user.name">Guest.</span>
</div>
<script type="module">
// import sprae from 'https://cdn.jsdelivr.net/npm/sprae/dist/sprae.js';
import sprae from './path/to/sprae.js';
const state = sprae(container, { user: { name: 'Dmitry Ivanov' } });
state.user.name = 'dy'; // updates DOM
</script>
Sprae evaluates :
-attributes and evaporates them.
Sprae creates reactive state that mirrors current DOM values.
It is based on @preact/signals and can take them as inputs.
import { signal } from 'sprae' // or '@preact/signals-core'
const version = signal('alpha')
// Sprae container with initial state values
const state = sprae(container, { foo: 'bar', version })
// Modify state property 'foo', triggering a DOM update
state.foo = 'baz'
// Update the version signal, which also triggers a DOM refresh
version.value = 'beta'
Control flow of elements.
<span :if="foo">foo</span>
<span :else :if="bar">bar</span>
<span :else>baz</span>
Multiply element.
<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" />
<!-- Loop by condition -->
<li :if="items" :each="item in items" :text="item" />
<li :else>Empty list</li>
Set text content of an element. Default text can be used as fallback:
Welcome, <span :text="user.name">Guest</span>.
Set class value from either a string, array or object.
<!-- set from string -->
<div :class="`foo ${bar}`"></div>
<!-- extends existing class as "foo bar" -->
<div class="foo" :class="`bar`"></div>
<!-- clsx: object / list -->
<div :class="[foo && 'foo', {bar: bar}]"></div>
Set style value from an object or a string. Extends existing style
attribute, if any.
<!-- from string -->
<div :style="`foo: ${bar}`"></div>
<!-- from object -->
<div :style="{foo: 'bar'}"></div>
<!-- set CSS variable -->
<div :style="{'--baz': qux}"></div>
Set value of an input, textarea or select. Takes handle of checked
and selected
attributes.
<!-- set from value -->
<input :value="value" />
<textarea :value="value" />
<!-- selects right option -->
<select :value="selected">
<option :each="i in 5" :value="i" :text="i"></option>
</select>
Define or extend data scope for a subtree.
<!-- Inline data -->
<x :with="{ foo: 'bar' }" :text="foo"></x>
<!-- External data -->
<y :with="data"></y>
<!-- Extend scope -->
<x :with="{ foo: 'bar' }">
<y :with="{ baz: 'qux' }" :text="foo + baz"></y>
</x>
Set any attribute value or run an effect.
<!-- Single property -->
<label :for="name" :text="name" />
<!-- Multiple properties -->
<input :id:name="name" />
<!-- Effect - returns undefined, triggers any time bar changes -->
<div :fx="void bar()" ></div>
<!-- Raw event listener (see events) -->
<div :onclick="e=>e.preventDefault()"></div>
Spread multiple attibures.
<input :="{ id: name, name, type:'text', value }" />
Expose element to current data scope with the id
:
<!-- single item -->
<textarea :ref="text" placeholder="Enter text..."></textarea>
<span :text="text.value"></span>
<!-- iterable items -->
<ul>
<li :each="item in items" :ref="item">
<input @focus="item.classList.add('editing')" @blur="item.classList.remove('editing')"/>
</li>
</ul>
Include template as element content.
<!-- assign template element to foo variable -->
<template :ref="foo"><span :text="foo"></span></template>
<!-- rended template as content -->
<div :render="foo" :with="{foo:'bar'}">...inserted here...</div>
<div :render="foo" :with="{foo:'baz'}">...inserted here...</div>
Attach event(s) listener with possible modifiers. event
variable holds current event. Allows async handlers.
<!-- Single event -->
<input type="checkbox" @change="isChecked = event.target.value">
<!-- Multiple events -->
<input :value="text" @input@change="text = event.target.value">
<!-- Event modifiers -->
<button @click.throttle-500="handler(event)">Not too often</button>
.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).
Expressions are sandboxed, ie. don't access global/window scope by default (since sprae can be run in server environment).
<div :x="scrollY"></div>
<!-- scrollY is undefined -->
Default sandbox provides most popular global objects: Array, Object, Number, String, Boolean, Date, console, window, document, history, navigator, location, screen, localStorage, sessionStorage, alert, prompt, confirm, fetch, performance, setTimeout, setInterval, requestAnimationFrame.
Sandbox can be extended as Object.assign(sprae.globals, { BigInt })
.
To avoid flash of unstyled content, you can hide sprae attribute or add a custom effect, eg. :hidden
- that will be removed once sprae is initialized:
<div :hidden></div>
<style>[:each],[:hidden] {visibility: hidden}</style>
To destroy state and detach sprae handlers, call element[Symbol.dispose]()
.
How to run
# prerequisite
npm ci
npm run install-server
npm start
# build
cd frameworks/non-keyed/sprae
npm ci
npm run build-prod
# bench
cd ../../..
cd webdriver-ts
npm ci
npm run compile
npm run bench keyed/sprae
# show results
cd ..
cd webdriver-ts-results
npm ci
cd ..
cd webdriver-ts
npm run results
- Template-parts / templize is progressive, but is stuck with native HTML quirks (parsing table, SVG attributes, liquid syntax conflict etc). Also ergonomics of
attr="{{}}"
is inferior to:attr=""
since it creates flash of uninitialized values. Also it's just nice to keep{{}}
generic, regardless of markup, and attributes as part of markup. - Alpine / vue / lit escape native HTML quirks, but the syntax space (
:attr
,v-*
,x-*
,l-*
@evt
,{{}}
) is too broad, as well as functionality. Perfection is when there's nothing to take away, not add (c). Also they tend to self-encapsulate making interop hard, invent own tooling or complex reactivity. - React / preact does the job wiring up JS to HTML, but with an extreme of migrating HTML to JSX and enforcing SPA, which is not organic for HTML. Also it doesn't support reactive fields (needs render call).
Sprae takes idea of templize / alpine / vue attributes and builds simple reactive state based on @preact/signals.
- It doesn't break or modify static html markup.
- It falls back to element content if uninitialized.
- It doesn't enforce SPA nor JSX.
- It enables island hydration.
- It reserves minimal syntax space as
:
convention (keeping tree neatly decorated, not scattered). - Expressions are naturally reactive and incur minimal updates.
- Elements / data API is open and enable easy interop.
It is reminiscent of XSLT, considered a buried treasure by web-connoisseurs.
- Alpine
Luciadeprecated- Petite-vue
- nuejs