11.0.0 • Published 8 months ago

sprae v11.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
8 months ago

∴ spræ tests npm bundle size npm

DOM tree microhydration

Sprae is open & minimalistic progressive enhancement framework. Perfect for small-scale websites, static pages, landings, prototypes, or lightweight UI. A light alternative to alpine, petite-vue etc (see why).

Usage

<div id="container" :if="user">
  Hello <span :text="user.name">there</span>.
</div>

<script type="module">
  import sprae from './sprae.js' // https://unpkg.com/sprae/dist/sprae.min.js

  // init
  const container = document.querySelector('#container');
  const state = sprae(container, { user: { name: 'friend' } })

  // update
  state.user.name = 'love'
</script>

Sprae evaluates :-directives and evaporates them, returning reactive state for updates.

Standalone

UMD version enables sprae via CDN, as CJS, AMD etc.

<!-- `init` attribute autoinits sprae on document with initial state (optional) -->
<script src="https://cdn.jsdelivr.net/npm/sprae/dist/sprae.umd.js" init="{ user: 'buddy' }"></script>

<script>
  window.sprae(el); // global standalone
</script>

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.

<!-- :text order matters -->
<ul><li :each="item in items" :text="item"/></ul>

<!-- cases -->
<li :each="item, idx in array" />
<li :each="value, key in object" />
<li :each="count, idx in number" />

<!-- fragment -->
<template :each="item in items">
  <dt :text="item.term"/>
  <dd :text="item.definition"/>
</template>

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

<!-- appends class -->
<div class="foo" :class="bar"></div>

<!-- array/object, a-la clsx -->
<div :class="[foo && 'foo', {bar: bar}]"></div>

:style="value"

Set style value.

<!-- extends style -->
<div style="foo: bar" :style="'bar-baz: qux'">

<!-- object -->
<div :style="{barBaz: 'qux'}"></div>

<!-- CSS variable -->
<div :style="{'--bar-baz': qux}"></div>

:value="value"

Set value to/from an input, textarea or select (like alpinejs x-model).

<input :value="value" />
<textarea :value="value" />

<!-- selects right option & handles selected attr -->
<select :value="selected">
  <option :each="i in 5" :value="i" :text="i"></option>
</select>

<!-- handles checked attr -->
<input type="checkbox" :value="item.done" />

:<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 }" />

:with="values"

Define values for a subtree.

<x :with="{ foo: 'bar' }">
  <y :with="{ baz: 'qux' }" :text="foo + baz"></y>
</x>

:fx="code"

Run effect, not changing any attribute.

<div :fx="a.value ? foo() : bar()" />

<!-- cleanup function -->
<div :fx="id = setInterval(tick, 1000), () => clearInterval(id)" />

:ref="el => (...)"

Get reference to element (instead of this).

<!-- initialize el (oninit) -->
<textarea :ref="el => autoResize(el)" placeholder="Enter text..."></textarea>

<!-- expose element in (sub)state -->
<li :each="item in items" :with="{li:null}" :ref="el => li = el">
  <input :onfocus..onblur="e => (li.classList.add('editing'), e => li.classList.remove('editing'))"/>
</li>

:on<event>="handler", :on<in>..on<out>="handler"

Attach event(s) listener with optional modifiers.

<input type="checkbox" :onchange="e => isChecked = e.target.value">

<!-- multiple events -->
<input :value="text" :oninput:onchange="e => text = e.target.value">

<!-- sequence of events -->
<button :onfocus..onblur="e => ( handleFocus(), e => handleBlur())">

<!-- modifiers -->
<button :onclick.throttle-500="handler">Not too often</button>
Modifiers:
  • .once, .passive, .capture – listener options.
  • .prevent, .stop (.immediate) – prevent default or stop (immediate) propagation.
  • .window, .document, .parent, .outside, .self – specify event target.
  • .throttle-<ms>, .debounce-<ms> – defer function call with one of the methods.
  • .<key> – filtered by event.key:
    • .ctrl, .shift, .alt, .meta, .enter, .esc, .tab, .space – direct key
    • .delete – delete or backspace
    • .arrow – up, right, down or left arrow
    • .digit – 0-9
    • .letter – A-Z, a-z or any unicode letter
    • .char – any non-space character
    • .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).

:data="values"

Set data-* attributes. CamelCase is converted to dash-case.

<input :data="{foo: 1, barBaz: true}" />
<!-- <input data-foo="1" data-bar-baz /> -->

:aria="values"

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 preact-flavored signals for reactivity and can take signal values as inputs. Signals can be switched to any preact/compatible implementation:

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

// switch sprae signals to @preact/signals-core
sprae.use(signals);

// use signal as state value
const name = signal('Kitty')
sprae(el, { name });

// update state
name.value = 'Dolly';
ProviderSizeFeature
ulive350bMinimal implementation, basic performance, good for small states.
@webreflection/signal531bClass-based, better performance, good for small-medium states.
usignal850bClass-based with optimizations, good for medium states.
@preact/signals-core1.47kbBest performance, good for any states, industry standard.
signal-polyfill2.5kbProposal signals. 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 used, 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 that avoids "unsafe-eval" CSP and provides sandboxing.

Operators:

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

Primitives:

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

Custom Build

Sprae can be tailored to project needs via sprae/core:

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

// standard directives
import 'sprae/directive/default.js'
import 'sprae/directive/if.js'
import 'sprae/directive/text.js'

// custom directive :id="expression"
directive.id = (el, evaluate, state) => {
  return () => el.id = evaluate(state)
}

// configure signals
sprae.use(signals)

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

Hints

  • To prevent FOUC add <style>[:each],[:if],[:else] {visibility: hidden}</style>.
  • Attributes order matters, eg. <li :each="el in els" :text="el.name"></li> is not the same as <li :text="el.name" :each="el in els"></li>.
  • Mind invalid self-closing tags, eg. <a :text="item" /> will cause an error. Main valid self-closing tags are li, p, dt, dd, option, tr, td, th.
  • Properties prefixed with _ are untracked: let state = sprae(el, {_x:2}); state._x++; // no effect.
  • To destroy state and detach sprae handlers, call element[Symbol.dispose]().
  • State getters/setters work as computed effects, eg. sprae(el, { x:1, get x2(){ return this.x * 2} }).
  • this is not used, to get access to current element use <input :ref="el" :text="el.value"/>.
  • event is not used, :on* attributes expect a function with event object as first argument :onevt="event => handle()", see #46.
  • key is not used, :each uses direct list mapping instead of dom diffing.
  • await is not supported in attributes, it’s a strong indicator you need to put these methods into state.

Justification

Modern frontend stack is unhealthy, like non-organic processed food. There are alternatives, but:

Sprae holds open & minimalistic philosophy:

  • Slim : API, signals for reactivity.
  • Pluggable directives & configurable internals.
  • Small, safe & performant.
  • Bits of organic sugar.
  • Aims at making developers happy 🫰

Examples

11.0.0

8 months ago

10.11.2

8 months ago

10.11.0

9 months ago

10.12.2

8 months ago

10.12.3

8 months ago

10.12.0

8 months ago

10.12.1

8 months ago

10.13.0

8 months ago

10.10.6

10 months ago

10.4.1

1 year ago

10.8.0

12 months ago

10.8.1

12 months ago

10.8.2

12 months ago

10.4.0

1 year ago

10.7.0

1 year ago

10.3.0

1 year ago

10.6.0

1 year ago

10.6.1

1 year ago

10.6.2

1 year ago

10.6.3

1 year ago

10.2.0

1 year ago

10.5.0

1 year ago

10.1.4

1 year ago

10.1.5

1 year ago

10.9.0

11 months ago

10.9.1

11 months ago

10.9.2

11 months ago

10.9.3

11 months ago

10.1.0

1 year ago

10.1.1

1 year ago

10.1.2

1 year ago

10.1.3

1 year ago

10.9.4

11 months ago

10.10.4

11 months ago

10.10.5

11 months ago

10.10.2

11 months ago

10.10.3

11 months ago

10.10.0

11 months ago

10.10.1

11 months ago

10.0.0

1 year ago

10.0.1

1 year ago

9.1.1

1 year ago

9.1.0

1 year ago

9.0.1

1 year ago

9.0.0

1 year ago

8.1.0

1 year ago

8.1.2

1 year ago

8.1.1

1 year ago

8.1.3

1 year ago

8.0.3

1 year ago

8.0.2

1 year ago

7.0.0

2 years ago

8.0.1

2 years ago

8.0.0

2 years ago

6.1.2

2 years ago

6.1.1

2 years ago

5.3.0

2 years ago

5.1.0

2 years ago

6.1.0

2 years ago

2.11.0

2 years ago

2.11.1

2 years ago

2.8.1

2 years ago

3.0.1

2 years ago

3.0.0

2 years ago

2.13.0

2 years ago

2.11.2

2 years ago

4.0.0

2 years ago

5.0.4

2 years ago

5.0.3

2 years ago

5.2.0

2 years ago

5.0.2

2 years ago

5.0.1

2 years ago

5.0.0

2 years ago

6.0.0

2 years ago

2.10.1

2 years ago

2.12.0

2 years ago

2.10.2

2 years ago

2.10.0

2 years ago

3.1.0

2 years ago

2.8.3

2 years ago

2.8.2

2 years ago

2.8.4

2 years ago

2.14.1

2 years ago

2.12.3

2 years ago

2.14.2

2 years ago

2.12.4

2 years ago

2.12.1

2 years ago

2.14.0

2 years ago

2.12.2

2 years ago

2.8.0

2 years ago

2.7.1

2 years ago

2.7.0

2 years ago

2.6.1

2 years ago

2.6.0

2 years ago

2.5.5

2 years ago

2.5.4

2 years ago

2.5.3

2 years ago

2.5.2

2 years ago

2.5.1

2 years ago

2.5.0

2 years ago

2.4.0

2 years ago

2.3.2

2 years ago

2.3.1

2 years ago

2.3.0

2 years ago

2.2.4

2 years ago

2.2.3

2 years ago

2.2.2

2 years ago

2.2.1

2 years ago

2.2.0

3 years ago

2.1.2

3 years ago

2.1.1

3 years ago

2.1.0

3 years ago

2.0.0

3 years ago

1.0.0

3 years ago

0.1.0

3 years ago

0.0.0

3 years ago