sprae 0.0.0 → 0.1.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/package.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "sprae",
3
3
  "description": "Reactive directives with expressions for DOM microtemplating.",
4
- "version": "0.0.0",
4
+ "version": "0.1.0",
5
5
  "main": "sprae.js",
6
6
  "dependencies": {
7
- "sube": "^2.2.1",
8
7
  "element-props": "^2.0.4",
9
8
  "htm": "^3.1.1",
10
- "swapdom": "^1.1.1",
11
- "preact": "^10.11.2"
9
+ "preact": "^10.11.2",
10
+ "sube": "^2.2.1",
11
+ "swapdom": "^1.1.1"
12
12
  },
13
13
  "devDependencies": {
14
14
  "@preact/signals": "^1.1.2",
15
+ "es-module-shims": "^1.6.2",
15
16
  "hyperf": "^1.4.0",
16
17
  "tst": "^7.1.1",
17
18
  "value-ref": "^2.1.0",
@@ -22,7 +23,7 @@
22
23
  },
23
24
  "repository": {
24
25
  "type": "git",
25
- "url": "git+https://github.com/dy/sporae.git"
26
+ "url": "git+https://github.com/dy/sprae.git"
26
27
  },
27
28
  "keywords": [
28
29
  "preact",
package/readme.md CHANGED
@@ -1,27 +1,13 @@
1
1
  # ∴ spræ
2
2
 
3
- Reactive directives for DOM microtemplating: moisturize tree without trasplanting to JSX.<br/>
3
+ > Reactive directives for DOM microtemplating: moisturize tree without trasplanting to JSX.
4
+
4
5
  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
 
6
7
 
7
8
  ## Usage
8
9
 
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.
10
+ Sprae enables directives as attributes starting with `:`.
25
11
 
26
12
  ```html
27
13
  <div id="element" :if="user">
@@ -44,66 +30,62 @@ The more direct case is initializing sprae via JS.
44
30
  * `update` can be used for bulk-updating multiple props.
45
31
  * `data` is the initial state to render the template. It can include reactive values, see [reactivity](#reactivity).
46
32
 
33
+ <details>
34
+ <summary><h3>Autoinit</h3></summary>
47
35
 
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:
36
+ Sprae can be used without build step or JS, autoinitializing HTML:
68
37
 
69
38
  ```html
70
- <ul>
71
- <li :each="item, index in items" :id="'item-' + item.id" :data="{value:item.value}" :text="item.label"></li>
72
- </ul>
39
+ <script src="./sprae.js" defer init></script>
73
40
 
74
- <li :each="{{ item, index in array }}">
75
- <li :each="{{ key, value, index in object }}">
76
- <li :each="{{ value in object }}">
41
+ <div :scope="{ count: 0 }">
42
+ <span :text="count">
43
+ <button :on="{ click: e => count++ }">inc</button>
44
+ </div>
77
45
  ```
78
46
 
79
- ### Conditions
47
+ * `:scope` marks regions on the tree that should be controlled by sprae.
48
+ * `init` attribute tells sprae to automatically initialize all elements that have `:scope`.
80
49
 
81
- To optionally display an element, there are `if`, `else-if`, `else` directives.
50
+ </details>
82
51
 
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
52
 
90
- ### Adding directives
53
+ ## Directives
54
+
55
+ * [x] `:if="condition"`, `:else-if="condition"`, `:else` - controls flow of elements.
56
+ * [x] `:each="item, i? in list"`,`:each="i in number"` - create multiple instances of element by mapping list or number.
57
+ * [x] `:text="value"` - set text content of an element.
58
+ * [ ] `:value="value"` – bind value to input or textarea.
59
+ * [ ]`:id`, `:name`, `:for`, `:type`, `:hidden`, `:disabled`, `:href`, `:src` – common attributes setters.
60
+ * [ ] `:class="[ foo, 'bar' ]"` – set element class from an array, object or a string.
61
+ * [ ] `:style="{ top:1, position:'absolute' }"` – set element style from a string or an object.
62
+ * [ ] `:prop="{ alt:'foo', title:'bar' }"` – set any attribute / property.
63
+ * [ ] `:on="{ click:e=>{}, touch:e=>{} }"` – add event listeners.
64
+ * [ ] `:data="{ foo:1, bar:2 }"` – set [data-*](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*) attributes.
65
+ * [ ] `:aria="{ role:'progressbar' }"` – set [aria-role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) attributes.
66
+ * [ ] `:item="{ id: 1 }"` – set [item*](https://developer.mozilla.org/en-US/docs/Web/HTML/Microdata) microdata attribute.
67
+ * [ ] `:scope="data"` – autoinit subtree.
68
+
69
+ <details>
70
+ <summary><h3>Adding directives</h3></summary>
91
71
 
92
72
  Directives can be added by registering them via `directive(name, initializer)`:
93
73
 
94
74
  ```js
95
- import init, { directive } from 'sprae'
75
+ import init, { directive } from 'sprae';
96
76
 
97
- directive(':html', (el, expr) => {
77
+ directive(':html', (el) => {
98
78
  // ...initialize here
99
- const evaluate = parseExpression(expr)
79
+ const evaluate = parseExpression(el.getAttribute(':html'));
100
80
  return (state) => {
101
81
  // ...update here
102
- el.innerHTML = evaluate(state)
82
+ el.innerHTML = evaluate(state);
103
83
  }
104
- })
84
+ });
105
85
  ```
106
86
 
87
+ </details>
88
+
107
89
 
108
90
  ## Reactivity
109
91
 
@@ -128,8 +110,8 @@ Update happens when any value changes:
128
110
 
129
111
  // <div id="done">...</div>
130
112
 
131
- const loading = signal(true),
132
- result = signal(false);
113
+ const loading = signal(true), result = signal(false);
114
+
133
115
  spores(done, { loading, result })
134
116
 
135
117
  // <div id="done">loading</div>
@@ -144,11 +126,10 @@ Update happens when any value changes:
144
126
  Note: observers don't require disposal, since they're connected in weak fashion. Once element is disposed, observables are disconnected.
145
127
 
146
128
 
147
-
148
129
  ## Justification
149
130
 
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.
131
+ * [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](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). Also ergonomics of `attr="{{}}"` is inferior to `:attr=""` since it creates flash of uninitialized values.
132
+ * [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. Also they tend to [self-encapsulate](https://github.com/alpinejs/alpine/discussions/3223), making interop hard.
152
133
  * [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
134
 
154
135
  _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.
package/src/core.js CHANGED
@@ -1,3 +1,5 @@
1
+ import sube, { observable } from 'sube';
2
+
1
3
  // autoinit
2
4
  const s = document.currentScript
3
5
  if (s && s.hasAttribute('init')) {
@@ -5,46 +7,104 @@ if (s && s.hasAttribute('init')) {
5
7
  }
6
8
 
7
9
  // sprae element: apply directives
8
- export default function sprae(el, scope) {
9
- scope ||= {}
10
+ export default function sprae(el, initScope) {
11
+ initScope ||= {};
10
12
 
11
- let params, updates=[]
13
+ let updates=[], // all spray directive updators
14
+ ready=false;
12
15
 
13
16
  // prepare directives
14
- for (let dir in directives) updates[dir] = directives[dir](el, scope)
17
+ for (let dir in directives) updates[dir] = directives[dir](el);
15
18
 
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
- }
19
+ const update = (values) => { for (let dir in updates) updates[dir].forEach(update => update(values)); };
20
+
21
+ // hook up observables (deeply, to include item.text etc)
22
+ // that's least evil compared to dlv/dset or proxies
23
+ // returns dynamic values snapshot
24
+ const rsube = (scope) => {
25
+ let values = {}
26
+ for (let k in scope) {
27
+ let v = scope[k];
28
+ if (observable(v = scope[k])) registry.register(v, sube(v, v => (values[k] = v, ready && update(values))));
29
+ // FIXME: add []
30
+ else if (v?.constructor === Object) values[k] = rsube(v);
31
+ else values[k] = v;
32
+ }
33
+ return values;
34
+ };
35
+ const values = rsube(initScope);
36
+ update(values);
37
+ ready = true;
20
38
 
21
39
  // return update via destructuring of result to allow batch-update
22
- scope[Symbol.iterator] = function*(){ yield params; yield update; }
40
+ values[Symbol.iterator] = function*(){ yield proxy; yield (diff) => update(Object.assign(values, diff)); };
41
+
42
+ const proxy = new Proxy(values, {
43
+ set: (s, k, v) => (values[k]=v, update(values), 1),
44
+ deleteProperty: (s, k) => (values[k]=undefined, update(values), 1)
45
+ });
23
46
 
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
- })
47
+ return proxy
28
48
  }
29
49
 
30
50
  // dict of directives
31
- const directives = {}, store = new WeakMap
51
+ const directives = {}, store = new WeakSet
32
52
 
33
53
  // register a directive
34
- export const directive = (name, initializer) => (
35
- directives[name] = (container) => {
36
- const els = container.querySelectorAll(`[${name}]`)
37
- const updates = []
54
+ export const directive = (name, initializer) => {
55
+ const attr = `\\:${name}`, sel = `[${attr}]`
56
+
57
+ return directives[name] = (container) => {
58
+ const els = [...container.querySelectorAll(sel)];
59
+ if (container.matches(sel)) els.unshift(container);
60
+
61
+ const updates = [];
38
62
 
39
63
  // 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
- }
64
+ for (let el of els) {
65
+ if (!el.classList.contains(`∴${name}`))
66
+ el.classList.add(`∴${name}`), updates.push(initializer(el));
46
67
  }
47
68
 
48
69
  return updates
49
70
  }
50
- )
71
+ }
72
+
73
+ const registry = new FinalizationRegistry(unsub => unsub?.call?.())
74
+
75
+
76
+ let evaluatorMemo = {}
77
+
78
+ // borrowed from alpine: https://github.com/alpinejs/alpine/blob/main/packages/alpinejs/src/evaluator.js#L61
79
+ // it seems to be more robust than subscript
80
+ export function parseExpr(expression, el) {
81
+ if (evaluatorMemo[expression]) return evaluatorMemo[expression]
82
+
83
+ // Some expressions that are useful in Alpine are not valid as the right side of an expression.
84
+ // Here we'll detect if the expression isn't valid for an assignement and wrap it in a self-
85
+ // calling function so that we don't throw an error AND a "return" statement can b e used.
86
+ let rightSideSafeExpression = 0
87
+ // Support expressions starting with "if" statements like: "if (...) doSomething()"
88
+ || /^[\n\s]*if.*\(.*\)/.test(expression)
89
+ // Support expressions starting with "let/const" like: "let foo = 'bar'"
90
+ || /^(let|const)\s/.test(expression)
91
+ ? `(() => { ${expression} })()`
92
+ : expression
93
+
94
+ const safeFunction = () => {
95
+ try {
96
+ return new Function(['scope'], `let result; with (scope) { result = ${rightSideSafeExpression} }; return result;`)
97
+ } catch ( e ) {
98
+ return exprError(e, expression, el)
99
+ }
100
+ }
101
+
102
+ return evaluatorMemo[expression] = safeFunction()
103
+ }
104
+
105
+ export function exprError(error, expression, el) {
106
+ Object.assign( error, { el, expression } )
107
+ console.warn(`∴ Expression Error: ${error.message}\n\n${ expression ? 'Expression: \"' + expression + '\"\n\n' : '' }`, el)
108
+ setTimeout(() => { throw error }, 0)
109
+ return Promise.resolve()
110
+ }
@@ -0,0 +1,56 @@
1
+ import sprae, { directive, exprError, parseExpr } from '../core.js'
2
+
3
+ directive('each', (el) => {
4
+ let expr = el.getAttribute(':each');
5
+ el.removeAttribute(':each');
6
+
7
+ let each = parseForExpression(expr);
8
+ if (!each) return exprError(new Error, expr, el);
9
+
10
+ const getItems = parseExpr(each.items);
11
+
12
+ const holder = new Text
13
+ el.replaceWith(holder)
14
+
15
+ // FIXME: make sure no memory leak here
16
+ // FIXME: there can DOM swapper be used instead
17
+ let els = [];
18
+ return state => {
19
+ els.forEach(el => el.remove()); els = [];
20
+ let items = getItems(state);
21
+ if (typeof items === 'number') items = Array.from({length: items}, (item, i)=>i+1)
22
+ items?.forEach((item,i) => {
23
+ const scope = {...state};
24
+ scope[each.item] = item;
25
+ if (each.index) scope[each.index] = i;
26
+ let itemEl = el.cloneNode(true);
27
+ els.push(itemEl);
28
+ holder.before(itemEl);
29
+ sprae(itemEl, scope);
30
+ });
31
+ }
32
+ })
33
+
34
+ // This was taken AlpineJS, former VueJS 2.* core. Thanks Alpine & Vue!
35
+ function parseForExpression(expression) {
36
+ let forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
37
+ let stripParensRE = /^\s*\(|\)\s*$/g
38
+ let forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
39
+ let inMatch = expression.match(forAliasRE)
40
+
41
+ if (!inMatch) return
42
+
43
+ let res = {}
44
+ res.items = inMatch[2].trim()
45
+ let item = inMatch[1].replace(stripParensRE, '').trim()
46
+ let iteratorMatch = item.match(forIteratorRE)
47
+
48
+ if (iteratorMatch) {
49
+ res.item = item.replace(forIteratorRE, '').trim()
50
+ res.index = iteratorMatch[1].trim()
51
+ } else {
52
+ res.item = item
53
+ }
54
+
55
+ return res
56
+ }
@@ -0,0 +1,14 @@
1
+ import { directive, parseExpr } from '../core.js'
2
+
3
+ // hidden attribute directive example
4
+ directive('hidden', (el) => {
5
+ let expr = el.getAttribute(':hidden');
6
+ el.removeAttribute(':hidden');
7
+ let evaluate = parseExpr(expr);
8
+ return (state) => {
9
+ let value = evaluate(state);
10
+ if (value || value === '') el.setAttribute('hidden', '')
11
+ else el.removeAttribute('hidden')
12
+ }
13
+ })
14
+
@@ -0,0 +1,34 @@
1
+ import { directive, parseExpr } from '../core.js'
2
+
3
+ directive('if', (el) => {
4
+ let expr = el.getAttribute(':if');
5
+ el.removeAttribute(':if');
6
+
7
+ let cur = el, els = [el], clauses = [parseExpr(expr)], holder = new Text
8
+
9
+ // collect clauses
10
+ while (cur = el.nextElementSibling) {
11
+ if (expr = cur.getAttribute(':else-if')) {
12
+ cur.removeAttribute(':else-if');
13
+ cur.classList.add('∴else-if')
14
+ cur.remove();
15
+ els.push(cur); clauses.push(parseExpr(expr));
16
+ continue
17
+ }
18
+ if (cur.hasAttribute(':else')) {
19
+ cur.removeAttribute(':else');
20
+ cur.classList.add('∴else')
21
+ cur.remove();
22
+ els.push(cur); clauses.push(() => 1);
23
+ }
24
+ break;
25
+ }
26
+
27
+ cur = els[0]
28
+
29
+ return state => {
30
+ let idx = clauses.findIndex(match => match(state));
31
+ if (idx >= 0) cur.replaceWith(cur = els[idx]);
32
+ else cur.replaceWith(cur = holder);
33
+ }
34
+ })
@@ -0,0 +1,5 @@
1
+ // order defines precedence
2
+ import './each.js'
3
+ import './hidden.js'
4
+ import './text.js'
5
+ import './if.js' // if must go last, after other directives are initialized, since it removes :else, :else-if from tree
@@ -0,0 +1,13 @@
1
+ import { directive, parseExpr } from '../core.js'
2
+
3
+ // hidden attribute directive example
4
+ directive('text', (el) => {
5
+ let expr = el.getAttribute(':text');
6
+ el.removeAttribute(':text');
7
+ let evaluate = parseExpr(expr);
8
+ return (state) => {
9
+ let value = evaluate(state);
10
+ el.textContent = value == null ? '' : value;
11
+ }
12
+ })
13
+
package/src/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { directives } from './dirs.js';
2
- export { default } from './core.js';
1
+ export { default, directive } from './core.js';
2
+ import './directives/index.js';
@@ -0,0 +1,24 @@
1
+ <!doctype html>
2
+ <meta charset=utf-8>
3
+ <title>Test</title>
4
+
5
+ <script async src="../node_modules/es-module-shims/dist/es-module-shims.js"></script>
6
+ <script type="importmap">
7
+ {
8
+ "imports": {
9
+ "sprae" : "../src/index.js",
10
+ "tst" : "../node_modules/tst/tst.js",
11
+ "wait-please" : "../node_modules/wait-please/index.js",
12
+ "value-ref" : "../node_modules/value-ref/value-ref.js",
13
+ "hyperf" : "../node_modules/hyperf/hyperf.js",
14
+ "sube" : "../node_modules/sube/sube.js",
15
+ "preact": "../node_modules/preact/dist/preact.mjs",
16
+ "preact/hooks": "../node_modules/preact/hooks/dist/hooks.mjs",
17
+ "@preact/signals": "../node_modules/@preact/signals/dist/signals.mjs",
18
+ "@preact/signals-core": "../node_modules/@preact/signals-core/dist/signals-core.mjs"
19
+ }
20
+ }
21
+ </script>
22
+
23
+
24
+ <script src="./test.js" type="module"></script>
package/test/test.js ADDED
@@ -0,0 +1,183 @@
1
+ import v from 'value-ref'
2
+ import { signal } from '@preact/signals'
3
+ import test, {is, any, throws} from 'tst'
4
+ import {tick, time} from 'wait-please'
5
+ import sprae from 'sprae'
6
+ import h from 'hyperf'
7
+
8
+ test('hidden: core', async () => {
9
+ let el = h`<div :hidden="hidden"></div>`
10
+ let params = sprae(el, {hidden:true})
11
+ is(el.outerHTML, `<div class="∴hidden" hidden=""></div>`)
12
+ params.hidden = false
13
+ is(el.outerHTML, `<div class="∴hidden"></div>`)
14
+ })
15
+
16
+ test('hidden: reactive', async () => {
17
+ const hidden = signal(true)
18
+ let el = h`<div :hidden="hidden"></div>`
19
+ sprae(el, {hidden})
20
+ is(el.outerHTML, `<div class="∴hidden" hidden=""></div>`)
21
+ hidden.value = false
22
+ is(el.outerHTML, `<div class="∴hidden"></div>`)
23
+ })
24
+
25
+ test('text: core', async () => {
26
+ let el = h`<div :text="text"></div>`
27
+ let params = sprae(el, {text:'abc'})
28
+ is(el.outerHTML, `<div class="∴text">abc</div>`)
29
+ params.text = null
30
+ is(el.outerHTML, `<div class="∴text"></div>`)
31
+ })
32
+
33
+ test('conditions: base', async () => {
34
+ let el = h`<p>
35
+ <span :if="a==1">a</span>
36
+ <span :else-if="a==2">b</span>
37
+ <span :else >c</span>
38
+ </p>`
39
+
40
+ const params = sprae(el, { a: 1 })
41
+
42
+ is(el.innerHTML, '<span class="∴if">a</span>')
43
+ params.a = 2
44
+ is(el.innerHTML, '<span class="∴else-if">b</span>')
45
+ params.a = 3
46
+ is(el.innerHTML, '<span class="∴else">c</span>')
47
+
48
+ delete params.a
49
+ })
50
+
51
+ test('conditions: short with insertions', async () => {
52
+ let el = h`<p>
53
+ <span :if="a==1" :text="'1:'+a"></span>
54
+ <span :else-if="a==2" :text="'2:'+a"></span>
55
+ <span :else :text="a"></span>
56
+ </p>`
57
+
58
+ const params = sprae(el, { a: 1 })
59
+
60
+ is(el.innerHTML, '<span class="∴text ∴if">1:1</span>')
61
+ params.a = 2
62
+ is(el.innerHTML, '<span class="∴text ∴else-if">2:2</span>')
63
+ params.a = 3
64
+ is(el.innerHTML, '<span class="∴text ∴else">3</span>')
65
+ params.a = 4
66
+ is(el.innerHTML, '<span class="∴text ∴else">4</span>')
67
+
68
+ params.a = 1
69
+ is(el.innerHTML, '<span class="∴text ∴if">1:1</span>')
70
+ params.a = 4
71
+ is(el.innerHTML, '<span class="∴text ∴else">4</span>')
72
+
73
+ delete params.a
74
+ })
75
+
76
+ test('conditions: reactive values', async () => {
77
+ let el = h`<p>
78
+ <span :if="a==1" :text="'1:'+a"></span>
79
+ <span :else-if="a==2" :text="'2:'+a"></span>
80
+ <span :else :text="a"></span>
81
+ </p>`
82
+
83
+ const a = signal(1)
84
+ sprae(el, { a })
85
+
86
+ is(el.innerHTML, '<span class="∴text ∴if">1:1</span>')
87
+ a.value = 2
88
+ is(el.innerHTML, '<span class="∴text ∴else-if">2:2</span>')
89
+ a.value = 3
90
+ is(el.innerHTML, '<span class="∴text ∴else">3</span>')
91
+ a.value = 4
92
+ is(el.innerHTML, '<span class="∴text ∴else">4</span>')
93
+
94
+ a.value = 1
95
+ is(el.innerHTML, '<span class="∴text ∴if">1:1</span>')
96
+ a.value = 4
97
+ is(el.innerHTML, '<span class="∴text ∴else">4</span>')
98
+ })
99
+
100
+
101
+ test('each: base', async () => {
102
+ // FIXME: in some conspicuous reason jsdom fails to update text nodes somehow
103
+ let el = h`<p>
104
+ <span :each="a in b" :text="a"></span>
105
+ </p>`
106
+
107
+ const params = sprae(el, { b: [] })
108
+
109
+ is(el.innerHTML, '')
110
+ params.b = [1,2]
111
+ is(el.innerHTML, '<span class="∴each ∴text">1</span><span class="∴each ∴text">2</span>')
112
+ params.b = []
113
+ is(el.innerHTML, '')
114
+ delete params.b
115
+ is(el.innerHTML, '')
116
+ })
117
+
118
+ test('each: reactive values', async () => {
119
+ let el = h`<p>
120
+ <span :each="a in b" :text="a"></span>
121
+ </p>`
122
+
123
+ const b = signal([])
124
+ const params = sprae(el, { b })
125
+
126
+ is(el.innerHTML, '')
127
+ b.value = [1,2]
128
+ is(el.innerHTML, '<span class="∴each ∴text">1</span><span class="∴each ∴text">2</span>')
129
+ b.value = []
130
+ is(el.innerHTML, '')
131
+ delete params.b
132
+ is(el.innerHTML, '')
133
+ })
134
+
135
+ test('each: loop with condition', async () => {
136
+ let el = h`<p>
137
+ <span :each="a in b" :text="a" :if="c"></span>
138
+ </p>`
139
+
140
+ const params = sprae(el, { b: [1,2], c:false })
141
+
142
+ is(el.innerHTML, '')
143
+ params.c = true
144
+ is(el.innerHTML, '<span class="∴each ∴text ∴if">1</span><span class="∴each ∴text ∴if">2</span>')
145
+ params.b = [1]
146
+ is(el.innerHTML, '<span class="∴each ∴text ∴if">1</span>')
147
+ delete params.b
148
+ is(el.innerHTML, '')
149
+ })
150
+
151
+ test('each: loop within condition', async () => {
152
+ let el = h`<p>
153
+ <x :if="a==1"><y :each="i in a" :text="i"></y></x>
154
+ <x :else-if="a==2"><y :each="i in a" :text="-i"></y></x>
155
+ </p>`
156
+
157
+ const params = sprae(el, { a: 1 })
158
+
159
+ is(el.innerHTML, '<x class="∴if"><y class="∴each ∴text">1</y></x>')
160
+ params.a = 2
161
+ is(el.innerHTML, '<x class="∴else-if"><y class="∴each ∴text">-1</y><y class="∴each ∴text">-2</y></x>')
162
+ params.a = 0
163
+ is(el.innerHTML, '')
164
+ })
165
+
166
+ test('each: condition within loop', async () => {
167
+ let el = h`<p>
168
+ <x :each="a in b">
169
+ <y :if="a==1" :text="'1:'+a"></y>
170
+ <y :else-if="a==2" :text="'2:'+a"></y>
171
+ <y :else :text="a"></y>
172
+ </x>
173
+ </p>`
174
+
175
+ const params = sprae(el, { b: [1,2,3] })
176
+
177
+ is(el.innerHTML, '<x class="∴each"><y class="∴text ∴if">1:1</y></x><x class="∴each"><y class="∴text ∴else-if">2:2</y></x><x class="∴each"><y class="∴text ∴else">3</y></x>')
178
+ params.b = [2]
179
+ is(el.innerHTML, '<x class="∴each"><y class="∴text ∴else-if">2:2</y></x>')
180
+ delete params.b
181
+ is(el.innerHTML, '')
182
+ })
183
+
package/src/dirs.js DELETED
@@ -1,43 +0,0 @@
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 DELETED
@@ -1,33 +0,0 @@
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/test.js DELETED
@@ -1,84 +0,0 @@
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')