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
- Host: GitHub
- URL: https://github.com/dy/sprae
- Owner: dy
- License: mit
- Created: 2022-11-13T01:19:23.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2024-05-01T02:02:52.000Z (about 1 year ago)
- Last Synced: 2024-05-02T00:03:55.073Z (about 1 year ago)
- Topics: alpinejs, hydration, petite-vue, preact-signals, progressive-enhancement, reactive, signals, template, template-parts
- Language: JavaScript
- Homepage: https://dy.github.io/sprae/
- Size: 1.82 MB
- Stars: 93
- Watchers: 5
- Forks: 2
- Open Issues: 1
-
Metadata Files:
- Readme: readme.md
- License: license
Awesome Lists containing this project
README
# ∴ spræ [](https://github.com/dy/sprae/actions/workflows/node.js.yml) [](https://bundlephobia.com/package/sprae) [](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
bazfoo 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. `
* 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/)