npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

sprae

v11.0.1

Published

DOM microhydration.

Downloads

306

Readme

∴ 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 element -->
<textarea :ref="el => (/* onmount */, () => (/* onunmount */))" 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>

<!-- set innerHTML -->
<div :ref="el => el.innerHTML = '...'"></div>

: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 an alternative 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';

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, industry standard. signal-polyfill | 2.5kb | Proposal 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>.
  • Invalid self-closing tags like <a :text="item" /> will cause error. 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 => (...)" />.
  • event is not used, :on* attributes expect a function with event 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