sprae

9.1.1 • Public • Published

∴ spræ tests size npm

DOM tree microhydration

Sprae is a compact & ergonomic progressive enhancement framework.
It provides :-attributes for inline markup logic with signals reactivity.
Perfect for small-scale websites, static pages, landings, 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>

:[prop]="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[event]="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 by event.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>
-->

Signals

Sprae uses signals for reactivity (see sprae/signal.js). It can be switched to any alternative preact-flavored signals for compatibility or performance:

import sprae, { signal, computed, effect, batch, untracked } from 'sprae';
import * as signals from '@preact/signals-core';

sprae.use(signals);

sprae(el, { name: signal('Kitty') });
Provider Size Feature
ulive 350b Minimal implementation, basic performance, good for small states
@webreflection/signal 531b Class-based, better performance, good for small-medium states
usignal 850b Class-based with optimizations, good for medium states
@preact/signals-core 1.47kb Best performance, good for any states
signal-polyfill 2.5kb Standard signals, slowest performance. Use via adapter.

Evaluator

Expressions use new Function as default evaluator, which is fast & compact way, but violates "unsafe-eval" CSP. To make eval stricter & safer, as well as sandbox expressions, an alternative evaluator can be configured, eg. justin:

import sprae from 'sprae'
import justin from 'subscript/justin'

sprae.use({compile: justin}) // set up justin as default compiler

Justin is minimal JS subset. It avoids "unsafe-eval" CSP and provides sandboxing.

Operators:

++ -- ! - + ** * / % && || ??
= < <= > >= == != === !==
<< >> & ^ | ~ ?: . ?. [] ()=>{} in

Primitives:

[] {} "" ''
1 2.34 -5e6 0x7a
true false null undefined NaN

Directives

Custom directives can be added as:

import sprae, { directive } from 'sprae/core.js'

// define custom directive
directive.id = (el, evaluate, state) => {
  return () => el.id = evaluate(state)  // return update function
}

Custom build

Sprae can be tailored to project needs via sprae/core for performance, size or compatibility purposes:

// sprae.custom.js
import sprae, { directive } from 'sprae/core.js'
import * as signals from '@preact/signals'
import compile from 'subscript'
import diff from 'udomdiff

// include directives
import 'sprae/directive/if.js'
import 'sprae/directive/text.js'

// configure signals
sprae.use(signals)

// configure compiler
sprae.use({ compile })

// configure dom differ
sprae.use({ swap: (parent, from, to, before) => udomdiff(parent, from, to, node=>node, before) })

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.

Examples

🕉

Package Sidebar

Install

npm i sprae

Weekly Downloads

21

Version

9.1.1

License

MIT

Unpacked Size

58 kB

Total Files

21

Last publish

Collaborators

  • dfcreative
  • dy