spect

Aspect-oriented DOM framework

Usage no npm install needed!

<script type="module">
  import spect from 'https://cdn.skypack.dev/spect';
</script>

README

spect

Micro DOM aspects toolkit.

npm bundle size npm

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.

:gem: Separation of cross-cutting concerns with CSS-like aspects.

:deciduous_tree: Native first − healthy semantic HTML tree, vanilla js friendly.

:calling: Unblocked, facilitated progressive enhancement.

:baby_chick: No entry barrier − already familiar functions.

:dizzy: 0 tooling, 0 boilerplate code, 0 environment setup needed.

:shipit: Low-profile − doesn't impose itself, can be used as side-utility; separate modules.

:golf: Good performance / size result.

Installation

A. Directly as a module:

<script type="module">
import { $, h, v } from 'https://unpkg.com/spect?module'
</script>

Available from CDN: unpkg, pika, jsdelivr.

B. As a dependency from npm:

npm i spect

import { $, h, v } from 'spect'

API

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]()

spect/h

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>

spect/v

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]()

Examples

Hello World

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

Timer

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

Counter

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

Todo list

<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 validator

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

Prompt

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

See all examples.

R&D

Sources of inspiration / analysis:

Spect has long story of research, at v13.0 it had repository reset. See changelog.

Related

  • 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).

License

MIT