Micro DOM aspects toolkit.
▶ Run
Spect is minimalistic DOM toolkit, providing aspects, reactivity and observables with 3 essential functions − $, h and v, for better compact UI code and efficient manipulations.
💎 Separation of cross-cutting concerns with CSS-like aspects.
🌳 Native first − healthy semantic HTML tree, vanilla js friendly.
📲 Unblocked, facilitated progressive enhancement.
🐤 No entry barrier − already familiar functions.
💫 0 tooling, 0 boilerplate code, 0 environment setup needed.
Low-profile − doesn't impose itself, can be used as side-utility; separate modules.
⛳ Good performance / size result.
<script type="module">
import { $, h, v } from 'https://unpkg.com/spect?module'
</script>Available from CDN: unpkg, pika, jsdelivr.
import { $, h, v } from 'spect'$( container? , selector , handler? )
Assign an aspect handler function to a selector within the container (by default document). Handler is called for each element matching the selector, returned function acts as disconnect callback. Returns live collection of matched elements.
import $ from 'spect/$'
// assign an aspect to all .foo elements
let foos = $('.foo', el => {
console.log('active')
return () => console.log('inactive')
})
// append .foo element
let foo = document.createElement('div')
foo.className = 'foo'
document.body.append(foo)
// ... "active"
foo.remove()
// ... "inactive"
// destroy selector observer
foos[Symbol.dispose]()el = h`...content`
HTML renderer with HTM syntax and reactive values support: Promise, Async Iterable, any Observable, RXjs, any observ*.
import {h, v} from 'spect'
// create reactive value
const text = v('foo')
// create live node
const a = h`<a>${ text }</a>`
a // <a>foo</a>
// updating value updates node
text.value = 'bar'
a // <a>bar</a>
// HTM syntax is fully supported
const frag = h`<x ...${{x: 1}}>1</x><y>2</y>`
// mount content on another element
h`<${a}>${ frag }</a>`
a // <a><x x="1">1</x><y>2</y></a>
// dispose values
a[Symbol.dispose]()Can also be used as JSX/hyperscript:
/* jsx h */
const a2 = <a>{ rxSubject } or { asyncIterable } or { promise }</a>ref = v( init? )
Takes an init value and returns a reactive mutable ref object with a single .value property that points to the inner value. ref implements Observable/AsyncIterable, allowing subscription to changes (essentially vue3/ref with Observable).
import v from 'spect/v'
// create value ref
let count = v(0)
count.value // 0
// subscribe to value changes
count.subscribe(value => {
console.log(value)
return () => console.log('teardown', value)
})
count.value = 1
// ... "1"
count.value = 2
// ... "teardown 1"
// "2"
// create mapped value ref
let double = count.map(value => value * 2)
double.value // 4
count.value = 3
double.value // 6
// combined value
let sum = v(count.value + double.value)
count.subscribe(v => sum.value = v + double.value)
double.subscribe(v => sum.value = count.value + v)
// async iterable
for await (const value of sum) console.log(value)
// dispose reference, disconnect listeners
sum[Symbol.dispose]()<div class="user">Loading...</div>
<script type="module">
import { $, h, v } from 'spect'
$('.user', async el => {
// create user state
const user = v({ name: 'guest' })
// render element content, map user state
h`<${el}>Hello, ${ user.map(u => u.name) }!</>`
// load data & set user
user.value = (await fetch('/user')).json()
})
</script><time id="timer"></time>
<script type="module">
import { $, v, h } from 'spect'
$('#timer', timer => {
const count = v(0), id = setInterval(() => count.value++, 1000)
h`<${timer}>${ count }</>`
return () => clearInterval(id)
})
</script><output id="count">0</output>
<button id="inc">+</button><button id="dec">-</button>
<script type="module">
import { $, h, v } from 'spect'
const count = v(0)
$('#count', el => count.subscribe(c => el.value = c))
$('#inc', el => el.onclick = e => count.value++)
$('#dec', el => el.onclick = e => count.value--)
</script><form class="todo">
<label for="add-todo">
<span>Add Todo</span>
<input name="text" required>
</label>
<button type="submit">Add</button>
<ul class="todo-list"><ul>
</form>
<script type="module">
import { $, h, v } from 'spect'
const todos = v([])
$('.todo-list', el => h`<${el}>${
todos.map(items =>
items.map(item => h`<li>${ item.text }</li>`)
)
}</>`)
$('.todo-form', form => form.addEventListener('submit', e => {
e.preventDefault()
if (!form.checkValidity()) return
todos.value = [...todos.value, { text: form.text.value }]
form.reset()
}))
</script><form></form>
<script type="module">
import { $, h, v } from 'spect'
const isValidEmail = s => /.+@.+\..+/i.test(s)
$('form', form => {
const valid = v(false)
h`<${form}>
<label for="email">Please enter an email address:</label>
<input#email onchange=${ e => valid.value = isValidEmail(e.target.value) }/>
The address is ${ v(valid, b => b ? "valid" : "invalid") }
</>`
})
</script><script>
import {v,h} from 'spect'
const showPrompt = v(false), proceed = v(false)
document.body.appendChild(h`<dialog open=${showPrompt}>
Proceed?
<menu>
<button onclick=${e => (showPrompt.value = false, proceed.value = false)}>Cancel</button>
<button onclick=${e => (showPrompt.value = false, proceed.value = true)}>Confirm</button>
</menu>
</>`)
</script>Sources of inspiration / analysis:
- $: fast-on-load, selector-set, insertionQuery, selector-observer, reuse, aspect-oriended-programming, pure-js libraries and others.
- h: lit-html, htm, htl, hyperscript, incremental-dom, snabbdom, nanomorph, uhtml and others.
- v: vue3/ref, knockout/observable, mobx/observable, rxjs, observable, react hooks, observable proposal, observ, mutant, iron, icaro, introspected, augmentor and others.
Spect has long story of research, at v13.0 it had repository reset. See changelog.
- element-props − unified access to element props with observable support. Comes handy for organizing components.
- strui − collection of UI streams, such as router, storage etc. Comes handy for building complex reactive web-apps (spect, rxjs etc).
MIT
ॐ