An open API service indexing awesome lists of open source software.

https://github.com/dy/sprae

∴ DOM tree microhydration
https://github.com/dy/sprae

alpinejs hydration petite-vue preact-signals progressive-enhancement reactive signals template template-parts

Last synced: about 2 months ago
JSON representation

∴ DOM tree microhydration

Awesome Lists containing this project

README

        

# ∴ spræ [![tests](https://github.com/dy/sprae/actions/workflows/node.js.yml/badge.svg)](https://github.com/dy/sprae/actions/workflows/node.js.yml) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/sprae)](https://bundlephobia.com/package/sprae) [![npm](https://img.shields.io/npm/v/sprae?color=orange)](https://www.npmjs.com/package/sprae)

> DOM tree microhydration

_Sprae_ is open & minimalistic progressive enhancement framework with _preact-signals_ reactivity.

Perfect for small websites, static pages, prototypes, lightweight UI or nextjs / SSR (see [JSX](#jsx)).

A light and fast alternative to _alpine_, _petite-vue_, _lucia_ etc (see [why](#justification)).

## Usage

```html


Hello there.

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'

```

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

### UMD

`sprae.umd` enables sprae via CDN, CJS, AMD etc.

```html

window.sprae; // global standalone

```

### Autoinit

`sprae.auto` autoinits sprae on document body.

```html

```

## Directives

#### `:if="condition"`, `:else`

Control flow of elements.

```html
foo
bar
baz

foo bar baz
```

#### `:each="item, index? in items"`

Multiply element.

```html




  • ```

    #### `:text="value"`

    Set text content of an element.

    ```html
    Welcome, Guest.

    Welcome, .
    ```

    #### `:class="value"`

    Set class value.

    ```html


    ```

    #### `:style="value"`

    Set style value.

    ```html


    ```

    #### `:value="value"`

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

    ```html

    ```

    #### `:="value"`, `:="values"`

    Set any attribute(s).

    ```html

    ```

    #### `:with="values"`

    Define values for a subtree.

    ```html

    ```

    #### `:fx="code"`

    Run effect, not changing any attribute.

    ```html


    ```

    #### `:ref="name"`, `:ref="el => (...)"`

    Expose element in state with `name` or get reference to element.

    ```html



  • ```

    #### `:on="handler"`, `:on..on="handler"`

    Attach event(s) listener with optional modifiers.

    ```html

    Not too often
    ```

    ##### Modifiers:

    * `.once`, `.passive`, `.capture` – listener [options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options).
    * `.prevent`, `.stop` (`.immediate`) – prevent default or stop (immediate) propagation.
    * `.window`, `.document`, `.parent`, `.outside`, `.self` – specify event target.
    * `.throttle-`, `.debounce-` – defer function call with one of the methods.
    * `.` – filtered by [`event.key`](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values):
    * `.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](https://unicode.org/reports/tr18/#General_Category_Property)
    * `.char` – any non-space character
    * `.ctrl-, .alt-, .meta-, .shift-` – 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.

    ```html

    ```

    #### `:aria="values"`

    Set `aria-*` attributes. Boolean values are stringified.

    ```html

    ```

    ## Signals

    Sprae uses _preact-flavored signals_ for reactivity and can take _signal_ values as inputs.

    Signals can be switched to an alternative preact/compatible implementation:

    ```js
    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';
    ```

    Provider | Size | Feature
    :---|:---|:---
    [`ulive`](https://ghub.io/ulive) | 350b | Minimal implementation, basic performance, good for small states.
    [`@webreflection/signal`](https://ghub.io/@webreflection/signal) | 531b | Class-based, better performance, good for small-medium states.
    [`usignal`](https://ghub.io/usignal) | 850b | Class-based with optimizations, good for medium states.
    [`@preact/signals-core`](https://ghub.io/@preact/signals-core) | 1.47kb | Best performance, good for any states, industry standard.
    [`signal-polyfill`](https://ghub.io/signal-polyfill) | 2.5kb | Proposal signals. Use via [adapter](https://gist.github.com/dy/bbac687464ccf5322ab0e2fd0680dc4d).

    ## 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_:

    ```js
    import sprae from 'sprae'
    import justin from 'subscript/justin'

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

    [_Justin_](https://github.com/dy/subscript#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`:

    ```js
    // sprae.custom.js
    import sprae, { dir, parse } 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"
    dir('id', (el, state, expr) => {
    // ...init
    return value => el.id = value // update
    })

    sprae.use({
    // configure signals
    ...signals,

    // configure compiler
    compile,

    // custom prefix, default is `:`
    prefix: 'js-'
    })
    ```

    ## JSX

    Sprae works with JSX via custom prefix.

    Case: Next.js server components can't do dynamic UI – active nav, tabs, sliders etc. Converting to client components breaks data fetching and adds overhead. Sprae can offload UI logic to keep server components intact.

    ```jsx
    // app/page.jsx - server component
    export default function Page() {
    return <>

    Home
    About

    ...
    >
    }
    ```

    ```jsx
    // layout.jsx
    import Script from 'next/script'

    export default function Layout({ children }) {
    return <>
    {children}

    >
    }
    ```

    ## Hints

    * To prevent [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content) add `[\:each],[\:if],[\:else] {visibility: hidden}`.
    * Attributes order matters, eg. `

  • ` is not the same as `
  • `.
    * Invalid self-closing tags like `` will cause error. Valid self-closing tags are: `li`, `p`, `dt`, `dd`, `option`, `tr`, `td`, `th`, `input`, `img`, `br`.
    * 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 current element use `:ref`.
    * `event` is not used, `:on*` attributes expect a function with event argument `:onevt="event => handle()"`, see [#46](https://github.com/dy/sprae/issues/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.
    * `:ref` comes after `:if` for mount/unmount events `
    `.

    ## Justification

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

    * [Template-parts](https://github.com/dy/template-parts) is stuck with native HTML quirks ([parsing table](https://github.com/github/template-parts/issues/24), [SVG attributes](https://github.com/github/template-parts/issues/25), [liquid syntax](https://shopify.github.io/liquid/tags/template/#raw) conflict etc).
    * [Alpine](https://github.com/alpinejs/alpine) / [petite-vue](https://github.com/vuejs/petite-vue) / [lucia](https://github.com/aidenybai/lucia) escape native HTML quirks, but have excessive API (`:`, `x-`, `{}`, `@`, `$`), tend to [self-encapsulate](https://github.com/alpinejs/alpine/discussions/3223) and not care about size/performance.

    _Sprae_ holds open & minimalistic philosophy:

    * Minimal syntax `:`.
    * _Signals_ for reactivity.
    * Pluggable directives, configurable internals.
    * Small, safe & performant.
    * Bits of organic sugar.
    * Aims at making developers happy 🫰

    > Perfection is not when there is nothing to add, but when there is nothing to take away.

    ## Examples

    * ToDo MVC: [demo](https://dy.github.io/sprae/examples/todomvc), [code](https://github.com/dy/sprae/blob/main/examples/todomvc.html)
    * JS Framework Benchmark: [demo](https://dy.github.io/sprae/examples/js-framework-benchmark), [code](https://github.com/dy/sprae/blob/main/examples/js-framework-benchmark.html)
    * Wavearea: [demo](https://dy.github.io/wavearea?src=//cdn.freesound.org/previews/586/586281_2332564-lq.mp3), [code](https://github.com/dy/wavearea)
    * Carousel: [demo](https://rwdevelopment.github.io/sprae_js_carousel/), [code](https://github.com/RWDevelopment/sprae_js_carousel)
    * Tabs: [demo](https://rwdevelopment.github.io/sprae_js_tabs/), [code](https://github.com/RWDevelopment/sprae_js_tabs?tab=readme-ov-file)
    * Prostogreen [demo](https://web-being.org/prostogreen/), [code](https://github.com/web-being/prostogreen/)

    🕉