Skip to content

Ash515/sprae

 
 

Repository files navigation

∴ spræ tests size npm

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.

Usage

Autoinit

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>

Manual init

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.

State

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'

Attributes

:if="condition", :else

Control flow of elements.

<span :if="foo">foo</span>
<span :else :if="bar">bar</span>
<span :else>baz</span>

:each="item, index in items"

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>

:text="value"

Set text content of an element. Default text can be used as fallback:

Welcome, <span :text="user.name">Guest</span>.

:class="value"

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>

:style="value"

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>

:value="value"

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>

:with="data"

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>

:<prop>="value?"

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>

:="props?"

Spread multiple attibures.

<input :="{ id: name, name, type:'text', value }" />

:ref="id"

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>

:render="ref"

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>

Events

@<event>="handle", @<foo>@<bar>.<baz>="handle"

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>
Event 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).

Sandbox

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 }).

FOUC

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>

Dispose

To destroy state and detach sprae handlers, call element[Symbol.dispose]().

Benchmark

See js-framework-benchmark.

Results table

Benchmark

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

Examples

Justification

  • 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.

Alternatives

🕉

About

DOM tree microhydration

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • JavaScript 98.2%
  • HTML 1.8%