sprae 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "sprae",
3
+ "description": "Reactive directives with expressions for DOM microtemplating.",
4
+ "version": "0.0.0",
5
+ "main": "sprae.js",
6
+ "dependencies": {
7
+ "sube": "^2.2.1",
8
+ "element-props": "^2.0.4",
9
+ "htm": "^3.1.1",
10
+ "swapdom": "^1.1.1",
11
+ "preact": "^10.11.2"
12
+ },
13
+ "devDependencies": {
14
+ "@preact/signals": "^1.1.2",
15
+ "hyperf": "^1.4.0",
16
+ "tst": "^7.1.1",
17
+ "value-ref": "^2.1.0",
18
+ "wait-please": "^3.1.0"
19
+ },
20
+ "scripts": {
21
+ "test": "node test"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/dy/sporae.git"
26
+ },
27
+ "keywords": [
28
+ "preact",
29
+ "preact-signals",
30
+ "signals",
31
+ "observable",
32
+ "reactive",
33
+ "petite-vue",
34
+ "vue",
35
+ "template-parts",
36
+ "templize",
37
+ "alpine",
38
+ "alpinejs",
39
+ "directives",
40
+ "templating"
41
+ ],
42
+ "author": "Dmitry Ivanov <df.creative@gmail.com>",
43
+ "license": "ISC",
44
+ "bugs": {
45
+ "url": "https://github.com/dy/sprae/issues"
46
+ },
47
+ "homepage": "https://github.com/dy/sprae#readme"
48
+ }
package/r&d.md ADDED
@@ -0,0 +1,51 @@
1
+ ## name
2
+
3
+ * rasa
4
+ * dew
5
+ * water
6
+ * humidify
7
+ * sprinkle
8
+ * va
9
+ * soma
10
+ * ley
11
+ * ros
12
+ * dewdrops
13
+ * mtn-dew
14
+ * pores
15
+ * drips
16
+ * straw
17
+ * spores
18
+ * spor
19
+ + spores
20
+ + russian
21
+ - spor
22
+ + sport
23
+ * spore
24
+ * sporae
25
+ + scottish twist
26
+ + sounds russian-ish too
27
+ - a bit unusual from API perspective...
28
+ + similar to algae
29
+ + dots associate with colon prefix
30
+ * spour
31
+ * sprae?
32
+ + reference to sporae and similar assoication
33
+ + simpler word
34
+ + better assoc with hydration
35
+
36
+
37
+ ## :attr, :data, :id, :class, :style, :on, :aria - do we enforce JS object or support unscoped expression? -> Use JS convention, too many use-cases.
38
+
39
+ 1. JS object
40
+ + JS object can directly set value as `:props="obj"`
41
+ - JS object is a bit verbose `:props="{a:1, b:2}"`, `:on="{click(e){}, touch(e){}}"`
42
+ + It's very explicitly JS, no confusion must be introduced
43
+ - It makes HTML look a bit more noisy
44
+ + It is more familiar
45
+ + JS syntax saves redundant questions and an item from docs
46
+
47
+ 2. Custom expressions
48
+ + We anyways introduce custom-ish expression in `:each="item in items"`
49
+ - Vue introduces simple-ish parsing for that
50
+ + Custom expressions are shorter: `:attr="a:1, b:2, c:3"`
51
+ - Custom expressions are confusing for style: `:style="a:1, b:2, c:3"` - very similar to direct style string
package/readme.md ADDED
@@ -0,0 +1,163 @@
1
+ # ∴ spræ
2
+
3
+ Reactive directives for DOM microtemplating: moisturize tree without trasplanting to JSX.<br/>
4
+ A lightweight essential alternative to [alpine](https://github.com/alpinejs/alpine), [petite-vue](https://github.com/vuejs/petite-vue) and [templize](https://github.com/dy/templize) with better ergonomics[*](#justification).
5
+
6
+
7
+ ## Usage
8
+
9
+ ```html
10
+ <script src="./sprae.js" defer init></script>
11
+
12
+ <div :scope="{ count: 0 }">
13
+ <span :text="count">
14
+ <button :on="{ click: e => count++ }">inc</button>
15
+ </div>
16
+ ```
17
+
18
+ * `:scope` marks regions on the tree that should be controlled by sprae.
19
+ * `init` attribute tells sprae to automatically initialize all elements that have `:scope`.
20
+ * any attribute starting with `:` is considered a sprae directive.
21
+
22
+ ## Manual init
23
+
24
+ The more direct case is initializing sprae via JS.
25
+
26
+ ```html
27
+ <div id="element" :if="user">
28
+ Logged in as <span :text="user.displayName">Guest.</span>
29
+ </div>
30
+
31
+ <script type="module">
32
+ import init from 'sprae';
33
+
34
+ const data = { user: { displayName: 'Dmitry Ivanov' } }
35
+ const [state, update] = init(user, data);
36
+
37
+ state.user.displayName = 'dy' // update value
38
+ update({user: {displayName: 'dy'}}) // alternatively
39
+ </script>
40
+ ```
41
+
42
+ * `init` initializes directives within an `element` subtree with passed `data`.
43
+ * `state` is proxy reflecting used values, changing any of its props updates directives.
44
+ * `update` can be used for bulk-updating multiple props.
45
+ * `data` is the initial state to render the template. It can include reactive values, see [reactivity](#reactivity).
46
+
47
+
48
+ ## Directives
49
+
50
+ * `:scope="data"` – autoinit subtree.
51
+ * `:if="condition"`, `:else-if="condition"`, `:else` - controls flow of elements.
52
+ * `:each="item, idx? in list"` - map list to instances of an element.
53
+ * `:text="value"` - set text content of an element.
54
+ * `:value="value"` – bind value to input or textarea.
55
+ * `:id`, `:name`, `:for`, `:type`, `:hidden`, `:disabled`, `:href`, `:src` – common attributes setters.
56
+ * `:class="[ foo, 'bar' ]"` – set element class from an array, object or a string.
57
+ * `:style="{ top:1, position:'absolute' }"` – set element style from a string or an object.
58
+ * `:prop="{ alt:'foo', title:'bar' }"` – set any attribute / property.
59
+ * `:on="{ click:e=>{}, touch:e=>{} }"` – add event listeners.
60
+ * `:data="{ foo:1, bar:2 }"` – set [data-*](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*) attributes.
61
+ * `:aria="{ role:'progressbar' }"` – set [aria-role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) attributes.
62
+ * `:item="{ id: 1 }"` – set [item*](https://developer.mozilla.org/en-US/docs/Web/HTML/Microdata) microdata attribute.
63
+
64
+ <!--
65
+ ### Loops
66
+
67
+ Iterating over set of items can be done with `:each` directive:
68
+
69
+ ```html
70
+ <ul>
71
+ <li :each="item, index in items" :id="'item-' + item.id" :data="{value:item.value}" :text="item.label"></li>
72
+ </ul>
73
+
74
+ <li :each="{{ item, index in array }}">
75
+ <li :each="{{ key, value, index in object }}">
76
+ <li :each="{{ value in object }}">
77
+ ```
78
+
79
+ ### Conditions
80
+
81
+ To optionally display an element, there are `if`, `else-if`, `else` directives.
82
+
83
+ ```html
84
+ <span :if="status == 0">Inactive</span>
85
+ <span :else-if="status == 1">Active</span>
86
+ <span :else>Finished</span>
87
+ ```
88
+ -->
89
+
90
+ ### Adding directives
91
+
92
+ Directives can be added by registering them via `directive(name, initializer)`:
93
+
94
+ ```js
95
+ import init, { directive } from 'sprae'
96
+
97
+ directive(':html', (el, expr) => {
98
+ // ...initialize here
99
+ const evaluate = parseExpression(expr)
100
+ return (state) => {
101
+ // ...update here
102
+ el.innerHTML = evaluate(state)
103
+ }
104
+ })
105
+ ```
106
+
107
+
108
+ ## Reactivity
109
+
110
+ Directive expressions are naturally reactive, ie. data may contain any async/reactive values, such as:
111
+
112
+ * _Promise_ / _Thenable_
113
+ * _Observable_ / _Subject_ / _Subscribable_
114
+ * _AsyncIterable_
115
+ * _observ-*_
116
+ * etc., see [sube](https://github.com/dy/sube/blob/main/README.md) for the full list.
117
+
118
+ This way, for example, _@preact/signals_ or _rxjs_ can be connected directly bypassing subscription or reading value.
119
+
120
+ Update happens when any value changes:
121
+
122
+ ```html
123
+ <div id="done" :text="loading ? 'loading' : result">...</div>
124
+
125
+ <script>
126
+ import spores from 'sprae';
127
+ import { signals } from '@preact/signals';
128
+
129
+ // <div id="done">...</div>
130
+
131
+ const loading = signal(true),
132
+ result = signal(false);
133
+ spores(done, { loading, result })
134
+
135
+ // <div id="done">loading</div>
136
+
137
+ setTimeout(() => (loading.value = true, result.value = 'done'), 1000)
138
+
139
+ // ... 1s after
140
+ // <div id="done">done</div>
141
+ </script>
142
+ ```
143
+
144
+ Note: observers don't require disposal, since they're connected in weak fashion. Once element is disposed, observables are disconnected.
145
+
146
+
147
+
148
+ ## Justification
149
+
150
+ * [Template-parts](https://github.com/dy/template-parts) / [templize](https://github.com/dy/templize) is progressive, but is stuck with native HTML quirks (parsing table case, svg attributes, conflict with liquid syntax etc). Besides ergonomics of `attr="{{}}"` is inferior to `:attr=""` since it creates flash of uninitialized values.
151
+ * [Alpine](https://github.com/alpinejs/alpine) / [vue](https://github.com/vuejs/petite-vue) / [lit](https://github.com/lit/lit/tree/main/packages/lit-html) escapes native HTML quirks, but the syntax is a bit scattered: `:attr`, `v-*`,`x-*`, `@evt`, `{{}}` can be expressed with single convention. Besides, functionality is too broad and can be reduced to essence.
152
+ * [preact](https://ghub.io/preact) with HTML as JSX is a nice way to wire JS to templates, but it doesn't really support reactive fields (needs render call). Also migrating HTML to JS is an extreme with unwanted side-effects.
153
+
154
+ _Sprae_ takes elegant syntax convention of _alpine_ and method of _templize_ to connect any reactive values (like [@preact/signals](https://ghub.io/@preact/signals) or observables) to static HTML.
155
+
156
+ * It doesn't break static html markup.
157
+ * It doesn't intrude native syntax space.
158
+ * It falls back to element content if uninitialized.
159
+ * It provides means for island hydration.
160
+ * It doesn'y introduce syntax scatter.
161
+ * It supports simple expressions with exposed reactive data types.
162
+
163
+ <p align="center"><a href="https://github.com/krsnzd/license/">🕉</a></p>
package/src/core.js ADDED
@@ -0,0 +1,50 @@
1
+ // autoinit
2
+ const s = document.currentScript
3
+ if (s && s.hasAttribute('init')) {
4
+ sprae(document.documentElement)
5
+ }
6
+
7
+ // sprae element: apply directives
8
+ export default function sprae(el, scope) {
9
+ scope ||= {}
10
+
11
+ let params, updates=[]
12
+
13
+ // prepare directives
14
+ for (let dir in directives) updates[dir] = directives[dir](el, scope)
15
+
16
+ const update = (diff=scope) => {
17
+ if (diff !== scope) Object.assign(scope, diff);
18
+ for (let dir in updates) updates[dir].forEach(update => update(scope))
19
+ }
20
+
21
+ // return update via destructuring of result to allow batch-update
22
+ scope[Symbol.iterator] = function*(){ yield params; yield update; }
23
+
24
+ return params = new Proxy(scope, {
25
+ set: (s, k, v) => (scope[k]=v, update(), 1),
26
+ deleteProperty: (s,k) => (delete scope[k], update(), 1)
27
+ })
28
+ }
29
+
30
+ // dict of directives
31
+ const directives = {}, store = new WeakMap
32
+
33
+ // register a directive
34
+ export const directive = (name, initializer) => (
35
+ directives[name] = (container) => {
36
+ const els = container.querySelectorAll(`[${name}]`)
37
+ const updates = []
38
+
39
+ // replace all shortcuts with inner templates
40
+ for (el of els) {
41
+ // FIXME: make sure no leak is introduced here
42
+ if (!store.has(el)) {
43
+ store.add(el)
44
+ updates.push(initializer(el, el.getAttribute(name)))
45
+ }
46
+ }
47
+
48
+ return updates
49
+ }
50
+ )
package/src/dirs.js ADDED
@@ -0,0 +1,43 @@
1
+ import { directive } from './core'
2
+ import { parseExpr } from './eval'
3
+
4
+ // hidden attribute directive example
5
+ directive(':hidden', (el, expr) => {
6
+ let evaluate = parseExpr(expr)
7
+ return (state) => {
8
+ let value = evaluate(state)
9
+ prop(el, 'hidden', value)
10
+ }
11
+ })
12
+
13
+
14
+ // directive(':if', (el, expr, stash) => {
15
+ // stasj
16
+ // let stash = {els: [el], clauses:[parse(expr)], holder:new Text}, current = el
17
+ // ifStash.set(el, stash)
18
+ // return state => {
19
+ // let idx = stash.clauses.findIndex(match => match(state))
20
+ // if (idx >= 0) current.replaceWith(current = els[idx])
21
+ // else current.replaceWith(current = holder)
22
+ // }
23
+ // })
24
+ // directive(':else-if', (el, expr) => {
25
+ // const ifNode = prev('text-with-if')
26
+ // const stash = ifStash.get(ifNode)
27
+ // stash.els.push(el)
28
+ // stash.els.
29
+ // return
30
+ // })
31
+ // directive(':else', (el, expr) => {
32
+
33
+ // })
34
+
35
+ // directive(':each', (instance, part) => {
36
+ // let evalLoop = part.eval
37
+ // part.eval = state => {
38
+ // let [itemId, items] = evalLoop(state), list=[]
39
+ // // FIXME: cache els instead of recreating. Causes difficulties tracking el children
40
+ // for (let item of items) list.push(new TemplateInstance(part.template, {...state,[itemId]:item}, processor))
41
+ // return list
42
+ // }
43
+ // })
package/src/eval.js ADDED
@@ -0,0 +1,33 @@
1
+ let evaluatorMemo = {}
2
+
3
+ // borrowed from alpine: https://github.com/alpinejs/alpine/blob/main/packages/alpinejs/src/evaluator.js#L61
4
+ // it seems to be more robust than subscript
5
+ export function parseExpr(expression, el) {
6
+ if (evaluatorMemo[expression]) return evaluatorMemo[expression]
7
+
8
+ let AsyncFunction = Object.getPrototypeOf(async function(){}).constructor
9
+
10
+ // Some expressions that are useful in Alpine are not valid as the right side of an expression.
11
+ // Here we'll detect if the expression isn't valid for an assignement and wrap it in a self-
12
+ // calling function so that we don't throw an error AND a "return" statement can b e used.
13
+ let rightSideSafeExpression = 0
14
+ // Support expressions starting with "if" statements like: "if (...) doSomething()"
15
+ || /^[\n\s]*if.*\(.*\)/.test(expression)
16
+ // Support expressions starting with "let/const" like: "let foo = 'bar'"
17
+ || /^(let|const)\s/.test(expression)
18
+ ? `(() => { ${expression} })()`
19
+ : expression
20
+
21
+ const safeAsyncFunction = () => {
22
+ try {
23
+ return new AsyncFunction(['scope'], `let result; with (scope) { result = ${rightSideSafeExpression} }; return result;`)
24
+ } catch ( error ) {
25
+ Object.assign( error, { el, expression } )
26
+ console.warn(`∴ Expression Error: ${error.message}\n\n${ expression ? 'Expression: \"' + expression + '\"\n\n' : '' }`, el)
27
+ setTimeout(() => { throw error }, 0)
28
+ return Promise.resolve()
29
+ }
30
+ }
31
+
32
+ return evaluatorMemo[expression] = safeAsyncFunction()
33
+ }
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { directives } from './dirs.js';
2
+ export { default } from './core.js';
package/test.js ADDED
@@ -0,0 +1,84 @@
1
+ import v from 'value-ref'
2
+ import { signal } from '@preact/signals'
3
+ import test, {is, throws} from 'tst'
4
+ import {tick, time} from 'wait-please'
5
+ import sprae from '../sprae.js'
6
+ import h from 'hyperf'
7
+
8
+
9
+ test.only('hidden', async () => {
10
+ let el = `<div :hidden="hidden"></div>`
11
+ let params = sprae
12
+ })
13
+
14
+ test('conditions: short', async () => {
15
+ let el = h`<p>
16
+ <span :if="a==1">a</span>
17
+ <span :else-if="a==2">b</span>
18
+ <span :else >c</span>
19
+ </p>`
20
+
21
+ const params = sprae(el, { a: 1 })
22
+
23
+ is(el.innerHTML, '<span>a</span>')
24
+ params.a = 2
25
+ is(el.innerHTML, '<span>b</span>')
26
+ params.a = 3
27
+ is(el.innerHTML, '<span>c</span>')
28
+
29
+ delete params.a
30
+ })
31
+
32
+ test('conditions: short with insertions', async () => {
33
+ let el = h`<p>
34
+ <span :if="a==1" :text="['1:',a]"></span>
35
+ <span :else-if="a==2" :text="['2:',a]"></span>
36
+ <span :else :text="a"></span>
37
+ </p>`
38
+
39
+ const params = templize(el, { a: 1 }, exprProcessor)
40
+
41
+ is(el.innerHTML, '<span>1:1</span>')
42
+ params.a = 2
43
+ is(el.innerHTML, '<span>2:2</span>')
44
+ params.a = 3
45
+ is(el.innerHTML, '<span>3</span>')
46
+ params.a = 4
47
+ is(el.innerHTML, '<span>4</span>')
48
+
49
+ params.a = 1
50
+ is(el.innerHTML, '<span>1:1</span>')
51
+ params.a = 4
52
+ is(el.innerHTML, '<span>4</span>')
53
+
54
+ delete params.a
55
+ })
56
+
57
+ test.todo('conditions: reactive values', async () => {
58
+
59
+ })
60
+
61
+
62
+ test('loops: short', async () => {
63
+ // FIXME: in some conspicuous reason jsdom fails to update text nodes somehow
64
+ let el = h`<p>
65
+ <span :each="item in items" :text="item"></span>
66
+ </p>`
67
+
68
+ const params = sprae(el, { items: [] })
69
+
70
+ is(el.innerHTML, '')
71
+ params.items = [1,2]
72
+ is(el.innerHTML, '<span>1</span><span>2</span>')
73
+ params.items = []
74
+ is(el.innerHTML, '')
75
+
76
+ delete params.items
77
+ })
78
+
79
+ test.todo('loops: reactive values', async () => {
80
+ })
81
+
82
+ test.todo('loops: condition within loop')
83
+
84
+ test.todo('loops: loop within condition')