sprae 0.1.0 → 1.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/package.json +10 -7
- package/plan.md +6 -0
- package/r&d.md +63 -3
- package/readme.md +70 -37
- package/sprae.js +317 -0
- package/sprae.min.js +1 -0
- package/src/core.js +54 -54
- package/src/directives/aria.js +10 -0
- package/src/directives/common.js +15 -0
- package/src/directives/data.js +10 -0
- package/src/directives/each.js +2 -5
- package/src/directives/if.js +1 -4
- package/src/directives/index.js +8 -2
- package/src/directives/on.js +12 -0
- package/src/directives/prop.js +10 -0
- package/src/directives/text.js +1 -4
- package/src/directives/value.js +26 -0
- package/src/directives/with.js +21 -0
- package/src/index.js +12 -2
- package/test/index.html +1 -1
- package/test/test.js +122 -2
- package/src/directives/hidden.js +0 -14
package/package.json
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprae",
|
|
3
3
|
"description": "Reactive directives with expressions for DOM microtemplating.",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"main": "sprae.js",
|
|
6
|
+
"type": "module",
|
|
6
7
|
"dependencies": {
|
|
7
|
-
"element-props": "^2.0
|
|
8
|
-
"
|
|
9
|
-
"preact": "^10.11.2",
|
|
10
|
-
"sube": "^2.2.1",
|
|
11
|
-
"swapdom": "^1.1.1"
|
|
8
|
+
"element-props": "^2.3.0",
|
|
9
|
+
"sube": "^2.2.1"
|
|
12
10
|
},
|
|
13
11
|
"devDependencies": {
|
|
14
12
|
"@preact/signals": "^1.1.2",
|
|
13
|
+
"browser-env": "^3.3.0",
|
|
15
14
|
"es-module-shims": "^1.6.2",
|
|
15
|
+
"esbuild": "^0.15.14",
|
|
16
16
|
"hyperf": "^1.4.0",
|
|
17
|
+
"terser": "^5.15.1",
|
|
17
18
|
"tst": "^7.1.1",
|
|
18
19
|
"value-ref": "^2.1.0",
|
|
19
20
|
"wait-please": "^3.1.0"
|
|
20
21
|
},
|
|
21
22
|
"scripts": {
|
|
22
|
-
"test": "node test"
|
|
23
|
+
"test": "node -r browser-env/register test/test.js",
|
|
24
|
+
"build": "esbuild --bundle ./src/index.js --outfile=sprae.js --format=esm",
|
|
25
|
+
"min": "terser sprae.js -o sprae.min.js --module -c passes=3 -m"
|
|
23
26
|
},
|
|
24
27
|
"repository": {
|
|
25
28
|
"type": "git",
|
package/plan.md
ADDED
package/r&d.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
## name
|
|
1
|
+
## [x] name -> sprae
|
|
2
2
|
|
|
3
3
|
* rasa
|
|
4
4
|
* dew
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
+ better assoc with hydration
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
## :attr, :data, :id, :class, :style, :on, :aria - do we enforce JS
|
|
37
|
+
## [x] :attr, :data, :id, :class, :style, :on, :aria - do we enforce JS syntax or support unscoped expression? -> Use JS convention, too many use-cases.
|
|
38
38
|
|
|
39
39
|
1. JS object
|
|
40
40
|
+ JS object can directly set value as `:props="obj"`
|
|
@@ -48,4 +48,64 @@
|
|
|
48
48
|
+ We anyways introduce custom-ish expression in `:each="item in items"`
|
|
49
49
|
- Vue introduces simple-ish parsing for that
|
|
50
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
|
|
51
|
+
- Custom expressions are confusing for style: `:style="a:1, b:2, c:3"` - very similar to direct style string
|
|
52
|
+
|
|
53
|
+
## [x] Attribute directive: `:={a:1}` vs `:attr={a:1}` vs `:prop={a:1}`
|
|
54
|
+
|
|
55
|
+
+ `:=obj` reminds pascal assignment operator, which is cool
|
|
56
|
+
+ `:={a:1,b:2}` is natural convention from vue/alpine as - all props in object are assigned as `:{attr}`
|
|
57
|
+
- We can use `:="{data}"` fro sprae autoinit, since scope has confusing name: `:scope={}`, `:sprae={}`, `:with={}`
|
|
58
|
+
-> let's use :prop= for now, since `:={}` can have multiple interpretations
|
|
59
|
+
|
|
60
|
+
## [x] Scopes mechanism: prototype inheritance chain vs multiple `with` wrappers
|
|
61
|
+
|
|
62
|
+
- prototype inheritance chain causes deps update difficulties
|
|
63
|
+
- prototype chain is messy-ish
|
|
64
|
+
- prototype chain is a bit more difficult to provide multiple parent scopes
|
|
65
|
+
- prototype state object is inheritance mess - can be super-hard to analyze
|
|
66
|
+
~ `with(a) with(b) with(c)` is the same as `with(a)` with prototype inheritance in terms of access.
|
|
67
|
+
- `with` chain allows runtime update of scopes, eg. child scope was updated to something new.
|
|
68
|
+
- `prototype` chain is fixed from the time of init.
|
|
69
|
+
- `prototype` chain hails to unidentified root scope and inherits from that. Maybe we should clarify scopes inhertiance and first implement reactive store (see next item).
|
|
70
|
+
|
|
71
|
+
? what if we avoid scope inheritance mechanism (what's the real use for it?) and instead just make reactive store object, so that :with directive subscribes to any input props, but "shadows" subtree?
|
|
72
|
+
? are there uses for inheritance
|
|
73
|
+
|
|
74
|
+
? Do we need scopes at all? Only for the purpose of autoinit?
|
|
75
|
+
- it seems scopes can introduce more confusion and mess in templates: indicating full paths is more beneficial
|
|
76
|
+
+ unless we introduce proper ":with="item.x as x""
|
|
77
|
+
+ prototype chain is a single object:
|
|
78
|
+
+ meaning updators receive one actual-for-element scope instance
|
|
79
|
+
+ that makes external API easier
|
|
80
|
+
+ that allows handling store via single reactive object
|
|
81
|
+
|
|
82
|
+
-> possibly we have to just subscribe via mechanism of signals-like deps, and :with just initializes subtree with extended object
|
|
83
|
+
|
|
84
|
+
## [ ] Should we inherit values from `init` in `sprae(el, init)`, instead of creating a snapshot of reactive values in `init`?
|
|
85
|
+
|
|
86
|
+
+ it allows passing any arbitrary scope to initialize from.
|
|
87
|
+
- it can make hard finding reactive sources...
|
|
88
|
+
+ it is sort-of neat pattern: object parent updates its particular state: it can also have observable method making object a store
|
|
89
|
+
-> can be delegated to a separate functionality - init just gets converted to reactive store
|
|
90
|
+
+ it sort-of makes `init` directly a scope (a parent of scope), which is more natural-ish rather than 2 independent entities
|
|
91
|
+
+ can pass both observables and direct state anywhere, eg. init child components from it
|
|
92
|
+
-> worthy of a separate library
|
|
93
|
+
|
|
94
|
+
## [ ] Per-directive initialize vs per-element initialize
|
|
95
|
+
|
|
96
|
+
+ Per-directive is very simple and trivial approach
|
|
97
|
+
- Per-directive doesn't read attributes order and init directives independently
|
|
98
|
+
~ Practically linear in-order init doesn't make much service either here
|
|
99
|
+
- Per-directive is a bit hard to deal with scopes
|
|
100
|
+
|
|
101
|
+
## [ ] avoid updating unchanged directives if values don't affect them
|
|
102
|
+
|
|
103
|
+
? what if we use preact/signals to subscribe only to required props?
|
|
104
|
+
-> parseExpr is going to need to be handled by core.js (not directives), and detect & subscribe to dependencies itself
|
|
105
|
+
-> so that directive updator gets invoked only when any of expr dependencies change
|
|
106
|
+
|
|
107
|
+
## [ ] Replace :else-if with :else :if
|
|
108
|
+
|
|
109
|
+
+ `:else :if=""` is meaningful expansion of both directives
|
|
110
|
+
+ `:else :if` is coming from JS
|
|
111
|
+
+ `:else :if` doesn't throw error in JSDOM tests
|
package/readme.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# ∴ spræ
|
|
1
|
+
# ∴ spræ [](https://bundlephobia.com/result?p=sprae)
|
|
2
2
|
|
|
3
|
-
> Reactive
|
|
3
|
+
> Reactive microdirectives for soft DOM hydration.
|
|
4
4
|
|
|
5
|
-
A lightweight essential alternative to [alpine](https://github.com/alpinejs/alpine), [petite-vue](https://github.com/vuejs/petite-vue)
|
|
5
|
+
A lightweight essential alternative to [alpine](https://github.com/alpinejs/alpine), [petite-vue](https://github.com/vuejs/petite-vue), [templize](https://github.com/dy/templize) or JSX with better ergonomics[*](#justification).
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
## Usage
|
|
@@ -15,68 +15,101 @@ Sprae enables directives as attributes starting with `:`.
|
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
17
|
<script type="module">
|
|
18
|
-
import
|
|
18
|
+
import sprae from 'sprae';
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
const
|
|
20
|
+
const init = { user: { displayName: 'Dmitry Ivanov' } }
|
|
21
|
+
const state = sprae(user, init);
|
|
22
22
|
|
|
23
|
-
state.user.displayName = 'dy'
|
|
24
|
-
update({user: {displayName: 'dy'}}) // alternatively
|
|
23
|
+
state.user.displayName = 'dy' // automatically updates DOM
|
|
25
24
|
</script>
|
|
26
25
|
```
|
|
27
26
|
|
|
28
|
-
* `
|
|
29
|
-
* `state` is proxy reflecting
|
|
30
|
-
* `
|
|
31
|
-
|
|
27
|
+
* `sprae` initializes directives within an `element` subtree with passed `init` data.
|
|
28
|
+
* `state` is proxy reflecting directives values, changing any of its props updates directives.
|
|
29
|
+
* `init` is the initial state to render the template. It can include reactive values, see [reactivity](#reactivity).
|
|
30
|
+
|
|
32
31
|
|
|
33
32
|
<details>
|
|
34
|
-
<summary><
|
|
33
|
+
<summary><strong>Bulk update</strong></summary>
|
|
34
|
+
|
|
35
|
+
To update multiple values at once, state can be expanded as:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
let [values, update] = state;
|
|
39
|
+
update({ user: { displayName: 'dy' } });
|
|
40
|
+
```
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
* `values` holds actual rendered state values. Changing it doesn't rerender DOM, unlike `state`.
|
|
43
|
+
* `update` useful for bulk-updating multiple values at once.
|
|
44
|
+
|
|
45
|
+
</details>
|
|
46
|
+
|
|
47
|
+
<!--
|
|
48
|
+
<details>
|
|
49
|
+
<summary><strong>Autoinit</strong></summary>
|
|
50
|
+
|
|
51
|
+
Sprae can be used without build step or JS, autoinitializing document:
|
|
37
52
|
|
|
38
53
|
```html
|
|
39
54
|
<script src="./sprae.js" defer init></script>
|
|
40
55
|
|
|
41
|
-
<div :
|
|
56
|
+
<div :with="{ count: 0 }">
|
|
42
57
|
<span :text="count">
|
|
43
58
|
<button :on="{ click: e => count++ }">inc</button>
|
|
44
59
|
</div>
|
|
45
60
|
```
|
|
46
61
|
|
|
47
|
-
* `:
|
|
48
|
-
* `init` attribute tells sprae to automatically initialize
|
|
62
|
+
* `:with` defines data for regions of the tree to autoinit sprae on.
|
|
63
|
+
* `init` attribute tells sprae to automatically initialize document.
|
|
49
64
|
|
|
50
65
|
</details>
|
|
51
|
-
|
|
66
|
+
-->
|
|
52
67
|
|
|
53
68
|
## Directives
|
|
54
69
|
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* [ ] `:
|
|
70
|
+
* `:if="condition"`, `:else` - controls flow of elements.
|
|
71
|
+
* `:each="item, i? in list|number"` - create multiple instances of element from list or 1..number range.
|
|
72
|
+
* `:text="value"` - set text content of an element.
|
|
73
|
+
* `:value="value"` – bind value to input or textarea (reflected in model).
|
|
74
|
+
* `:id`, `:name`, `:for`, `:type`, `:hidden`, `:disabled`, `:href`, `:src` – common attributes setters.
|
|
75
|
+
* `:class="[ foo, 'bar' ]"` – set element class from an array, object or a string.
|
|
76
|
+
* `:style="{ top:1, position:'absolute' }"` – set element style from a string or an object.
|
|
77
|
+
* `:prop="{ alt:'foo', title:'bar' }"` – set any other attribute / property.
|
|
78
|
+
* `:on="{ click:e=>{}, touch:e=>{} }"` – add event listeners.
|
|
79
|
+
* `:data="{ foo:1, bar:2 }"` – set [data-*](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*) attributes.
|
|
80
|
+
* `:aria="{ role:'progressbar' }"` – set [aria-role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) attributes.
|
|
81
|
+
* `:with="data"` – provide data alias for subtree fragment.
|
|
82
|
+
<!-- * [ ] `:item="{ id: 1 }"` – set [item*](https://developer.mozilla.org/en-US/docs/Web/HTML/Microdata) microdata attribute. -->
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
<details>
|
|
87
|
+
<summary><strong>Combining directives</strong></summary>
|
|
88
|
+
|
|
89
|
+
Directives can be naturally combined as:
|
|
90
|
+
|
|
91
|
+
```html
|
|
92
|
+
<span :if="foo">foo</span>
|
|
93
|
+
<span :else :if="bar">bar</span>
|
|
94
|
+
<span :else>baz</span>
|
|
95
|
+
|
|
96
|
+
<span :if="items" :each="item in items"></span>
|
|
97
|
+
<span :else>Empty list</span>
|
|
98
|
+
```
|
|
99
|
+
</details>
|
|
100
|
+
|
|
68
101
|
|
|
69
102
|
<details>
|
|
70
|
-
<summary><
|
|
103
|
+
<summary><strong>Adding directives</strong></summary>
|
|
71
104
|
|
|
72
105
|
Directives can be added by registering them via `directive(name, initializer)`:
|
|
73
106
|
|
|
74
107
|
```js
|
|
75
|
-
import
|
|
108
|
+
import { directive, parseExpr } from 'sprae';
|
|
76
109
|
|
|
77
|
-
directive(':html', (el) => {
|
|
110
|
+
directive(':html', (el, expr) => {
|
|
78
111
|
// ...initialize here
|
|
79
|
-
const evaluate =
|
|
112
|
+
const evaluate = parseExpr(expr);
|
|
80
113
|
return (state) => {
|
|
81
114
|
// ...update here
|
|
82
115
|
el.innerHTML = evaluate(state);
|
|
@@ -105,14 +138,14 @@ Update happens when any value changes:
|
|
|
105
138
|
<div id="done" :text="loading ? 'loading' : result">...</div>
|
|
106
139
|
|
|
107
140
|
<script>
|
|
108
|
-
import
|
|
141
|
+
import sprae from 'sprae';
|
|
109
142
|
import { signals } from '@preact/signals';
|
|
110
143
|
|
|
111
144
|
// <div id="done">...</div>
|
|
112
145
|
|
|
113
146
|
const loading = signal(true), result = signal(false);
|
|
114
147
|
|
|
115
|
-
|
|
148
|
+
sprae(done, { loading, result })
|
|
116
149
|
|
|
117
150
|
// <div id="done">loading</div>
|
|
118
151
|
|
package/sprae.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// node_modules/sube/sube.js
|
|
2
|
+
Symbol.observable ||= Symbol("observable");
|
|
3
|
+
var observable = (arg) => arg && !!(arg[Symbol.observable] || arg[Symbol.asyncIterator] || arg.call && arg.set || arg.subscribe || arg.then);
|
|
4
|
+
var registry = new FinalizationRegistry((unsub) => unsub.call?.());
|
|
5
|
+
var unsubr = (sub) => sub && (() => sub.unsubscribe?.());
|
|
6
|
+
var sube_default = (target, next, error, complete, stop, unsub) => target && (unsub = unsubr((target[Symbol.observable]?.() || target).subscribe?.(next, error, complete)) || target.set && target.call?.(stop, next) || (target.then?.((v) => (!stop && next(v), complete?.()), error) || (async (v) => {
|
|
7
|
+
try {
|
|
8
|
+
for await (v of target) {
|
|
9
|
+
if (stop)
|
|
10
|
+
return;
|
|
11
|
+
next(v);
|
|
12
|
+
}
|
|
13
|
+
complete?.();
|
|
14
|
+
} catch (err) {
|
|
15
|
+
error?.(err);
|
|
16
|
+
}
|
|
17
|
+
})()) && ((_) => stop = 1), registry.register(target, unsub), unsub);
|
|
18
|
+
|
|
19
|
+
// src/core.js
|
|
20
|
+
var curEl;
|
|
21
|
+
var curDir;
|
|
22
|
+
function sprae(el2, initScope) {
|
|
23
|
+
initScope ||= {};
|
|
24
|
+
let updates = [], ready = false;
|
|
25
|
+
const update = (values2) => {
|
|
26
|
+
updates.forEach((update2) => update2(values2));
|
|
27
|
+
};
|
|
28
|
+
const rsube = (scope) => {
|
|
29
|
+
let values2 = {};
|
|
30
|
+
for (let k in scope) {
|
|
31
|
+
let v = scope[k];
|
|
32
|
+
if (observable(v = scope[k]))
|
|
33
|
+
values2[k] = null, registry2.register(v, sube_default(v, (v2) => (values2[k] = v2, ready && update(values2))));
|
|
34
|
+
else if (v?.constructor === Object)
|
|
35
|
+
values2[k] = rsube(v);
|
|
36
|
+
else
|
|
37
|
+
values2[k] = v;
|
|
38
|
+
}
|
|
39
|
+
return values2;
|
|
40
|
+
};
|
|
41
|
+
const values = rsube(initScope);
|
|
42
|
+
ready = true;
|
|
43
|
+
for (let name in directives) {
|
|
44
|
+
const sel = `[${name.replace(":", "\\:")}]`, initDirective = directives[name];
|
|
45
|
+
const els = [...el2.querySelectorAll(sel)];
|
|
46
|
+
if (el2.matches?.(sel))
|
|
47
|
+
els.unshift(el2);
|
|
48
|
+
let update2;
|
|
49
|
+
for (let el3 of els)
|
|
50
|
+
if (update2 = initDirective(el3, values))
|
|
51
|
+
updates.push(update2);
|
|
52
|
+
}
|
|
53
|
+
;
|
|
54
|
+
update(values);
|
|
55
|
+
values[Symbol.iterator] = function* () {
|
|
56
|
+
yield proxy;
|
|
57
|
+
yield (diff) => update(Object.assign(values, diff));
|
|
58
|
+
};
|
|
59
|
+
const proxy = new Proxy(values, {
|
|
60
|
+
set: (s, k, v) => (values[k] = v, update(values), 1),
|
|
61
|
+
deleteProperty: (s, k) => (values[k] = void 0, update(values), 1)
|
|
62
|
+
});
|
|
63
|
+
return proxy;
|
|
64
|
+
}
|
|
65
|
+
var directives = {};
|
|
66
|
+
var directive = (name, initializer) => {
|
|
67
|
+
const className = name.replace(":", "\u2234");
|
|
68
|
+
return directives[name] = (el2, initValues) => {
|
|
69
|
+
if (el2.classList.contains(className))
|
|
70
|
+
return;
|
|
71
|
+
el2.classList.add(className);
|
|
72
|
+
let expr = el2.getAttribute(name);
|
|
73
|
+
el2.removeAttribute(name);
|
|
74
|
+
return initializer(el2, expr, initValues);
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
var registry2 = new FinalizationRegistry((unsub) => unsub?.call?.());
|
|
78
|
+
var evaluatorMemo = {};
|
|
79
|
+
function parseExpr(expression) {
|
|
80
|
+
if (evaluatorMemo[expression])
|
|
81
|
+
return evaluatorMemo[expression];
|
|
82
|
+
let rightSideSafeExpression = /^[\n\s]*if.*\(.*\)/.test(expression) || /^(let|const)\s/.test(expression) ? `(() => { ${expression} })()` : expression;
|
|
83
|
+
const safeFunction = () => {
|
|
84
|
+
try {
|
|
85
|
+
return new Function(["scope"], `let result; with (scope) { result = ${rightSideSafeExpression} }; return result;`);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return exprError(e, expression);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
return evaluatorMemo[expression] = safeFunction();
|
|
91
|
+
}
|
|
92
|
+
function exprError(error, expression) {
|
|
93
|
+
Object.assign(error, { expression });
|
|
94
|
+
console.warn(`\u2234 ${error.message}
|
|
95
|
+
|
|
96
|
+
${curDir}=${expression ? `"${expression}"
|
|
97
|
+
|
|
98
|
+
` : ""}`, curEl);
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
throw error;
|
|
101
|
+
}, 0);
|
|
102
|
+
return Promise.resolve();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/directives/with.js
|
|
106
|
+
directive(":with", (el2, expr, rootValues) => {
|
|
107
|
+
let evaluate = parseExpr(expr);
|
|
108
|
+
let subscope = Object.create(rootValues);
|
|
109
|
+
Object.assign(subscope, evaluate(rootValues));
|
|
110
|
+
let [subvalues, subupdate] = sprae(el2, subscope);
|
|
111
|
+
return (values) => {
|
|
112
|
+
let withValues = evaluate(values);
|
|
113
|
+
subupdate(withValues);
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// src/directives/each.js
|
|
118
|
+
directive(":each", (el2, expr) => {
|
|
119
|
+
let each = parseForExpression(expr);
|
|
120
|
+
if (!each)
|
|
121
|
+
return exprError(new Error(), expr);
|
|
122
|
+
const getItems = parseExpr(each.items);
|
|
123
|
+
const holder = new Text();
|
|
124
|
+
el2.replaceWith(holder);
|
|
125
|
+
let els = [];
|
|
126
|
+
return (state) => {
|
|
127
|
+
els.forEach((el3) => el3.remove());
|
|
128
|
+
els = [];
|
|
129
|
+
let items = getItems(state);
|
|
130
|
+
if (typeof items === "number")
|
|
131
|
+
items = Array.from({ length: items }, (item, i) => i + 1);
|
|
132
|
+
items?.forEach((item, i) => {
|
|
133
|
+
const scope = { ...state };
|
|
134
|
+
scope[each.item] = item;
|
|
135
|
+
if (each.index)
|
|
136
|
+
scope[each.index] = i;
|
|
137
|
+
let itemEl = el2.cloneNode(true);
|
|
138
|
+
els.push(itemEl);
|
|
139
|
+
holder.before(itemEl);
|
|
140
|
+
sprae(itemEl, scope);
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
function parseForExpression(expression) {
|
|
145
|
+
let forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/;
|
|
146
|
+
let stripParensRE = /^\s*\(|\)\s*$/g;
|
|
147
|
+
let forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/;
|
|
148
|
+
let inMatch = expression.match(forAliasRE);
|
|
149
|
+
if (!inMatch)
|
|
150
|
+
return;
|
|
151
|
+
let res = {};
|
|
152
|
+
res.items = inMatch[2].trim();
|
|
153
|
+
let item = inMatch[1].replace(stripParensRE, "").trim();
|
|
154
|
+
let iteratorMatch = item.match(forIteratorRE);
|
|
155
|
+
if (iteratorMatch) {
|
|
156
|
+
res.item = item.replace(forIteratorRE, "").trim();
|
|
157
|
+
res.index = iteratorMatch[1].trim();
|
|
158
|
+
} else {
|
|
159
|
+
res.item = item;
|
|
160
|
+
}
|
|
161
|
+
return res;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/directives/text.js
|
|
165
|
+
directive(":text", (el2, expr) => {
|
|
166
|
+
let evaluate = parseExpr(expr);
|
|
167
|
+
return (state) => {
|
|
168
|
+
let value = evaluate(state);
|
|
169
|
+
el2.textContent = value == null ? "" : value;
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// src/directives/if.js
|
|
174
|
+
directive(":if", (el2, expr) => {
|
|
175
|
+
let cur = el2, els = [el2], clauses = [parseExpr(expr)], holder = new Text();
|
|
176
|
+
while (cur = el2.nextElementSibling) {
|
|
177
|
+
if (expr = cur.getAttribute(":else-if")) {
|
|
178
|
+
cur.removeAttribute(":else-if");
|
|
179
|
+
cur.classList.add("\u2234else-if");
|
|
180
|
+
cur.remove();
|
|
181
|
+
els.push(cur);
|
|
182
|
+
clauses.push(parseExpr(expr));
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (cur.hasAttribute(":else")) {
|
|
186
|
+
cur.removeAttribute(":else");
|
|
187
|
+
cur.classList.add("\u2234else");
|
|
188
|
+
cur.remove();
|
|
189
|
+
els.push(cur);
|
|
190
|
+
clauses.push(() => 1);
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
cur = els[0];
|
|
195
|
+
return (state) => {
|
|
196
|
+
let idx = clauses.findIndex((match) => match(state));
|
|
197
|
+
if (idx >= 0)
|
|
198
|
+
cur.replaceWith(cur = els[idx]);
|
|
199
|
+
else
|
|
200
|
+
cur.replaceWith(cur = holder);
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// node_modules/element-props/element-props.js
|
|
205
|
+
var prop = (el2, k, v) => {
|
|
206
|
+
if (k.startsWith("on"))
|
|
207
|
+
k = k.toLowerCase();
|
|
208
|
+
if (el2[k] !== v) {
|
|
209
|
+
el2[k] = v;
|
|
210
|
+
}
|
|
211
|
+
if (v === false || v == null)
|
|
212
|
+
el2.removeAttribute(k);
|
|
213
|
+
else if (typeof v !== "function")
|
|
214
|
+
el2.setAttribute(
|
|
215
|
+
dashcase(k),
|
|
216
|
+
v === true ? "" : typeof v === "number" || typeof v === "string" ? v : k === "class" ? (Array.isArray(v) ? v : Object.entries(v).map(([k2, v2]) => v2 ? k2 : "")).filter(Boolean).join(" ") : k === "style" ? Object.entries(v).map(([k2, v2]) => `${k2}: ${v2}`).join(";") : ""
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
var input = (el2) => [
|
|
220
|
+
el2.type === "checkbox" ? () => el2.checked : () => el2.value,
|
|
221
|
+
el2.type === "text" || el2.type === "" ? (value) => el2.value = value == null ? "" : value : el2.type === "checkbox" ? (value) => (el2.value = value ? "on" : "", prop(el2, "checked", value)) : el2.type === "select-one" ? (value) => ([...el2.options].map((el3) => el3.removeAttribute("selected")), el2.value = value, el2.selectedOptions[0]?.setAttribute("selected", "")) : (value) => el2.value = value
|
|
222
|
+
];
|
|
223
|
+
var el = document.createElement("div");
|
|
224
|
+
var dashcase = (str) => {
|
|
225
|
+
el.dataset[str] = "";
|
|
226
|
+
let dashStr = el.attributes[0].name.slice(5);
|
|
227
|
+
delete el.dataset[str];
|
|
228
|
+
return dashStr;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// src/directives/common.js
|
|
232
|
+
common(`id`), common(`name`), common(`for`), common(`type`), common(`hidden`), common(`disabled`), common(`href`), common(`src`), common(`style`), common(`class`);
|
|
233
|
+
function common(name) {
|
|
234
|
+
directive(":" + name, (el2, expr) => {
|
|
235
|
+
let evaluate = parseExpr(expr);
|
|
236
|
+
return (state) => {
|
|
237
|
+
let value = evaluate(state);
|
|
238
|
+
prop(el2, name, value);
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/directives/prop.js
|
|
244
|
+
directive(":prop", (el2, expr) => {
|
|
245
|
+
let evaluate = parseExpr(expr);
|
|
246
|
+
return (state) => {
|
|
247
|
+
let value = evaluate(state);
|
|
248
|
+
for (let key in value)
|
|
249
|
+
prop(el2, key, value[key]);
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// src/directives/data.js
|
|
254
|
+
directive(":data", (el2, expr) => {
|
|
255
|
+
let evaluate = parseExpr(expr);
|
|
256
|
+
return (state) => {
|
|
257
|
+
let value = evaluate(state);
|
|
258
|
+
for (let key in value)
|
|
259
|
+
el2.dataset[key] = value[key];
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// src/directives/aria.js
|
|
264
|
+
directive(":aria", (el2, expr) => {
|
|
265
|
+
let evaluate = parseExpr(expr);
|
|
266
|
+
return (state) => {
|
|
267
|
+
let value = evaluate(state);
|
|
268
|
+
for (let key in value)
|
|
269
|
+
prop(el2, "aria" + key[0].toUpperCase() + key.slice(1), value[key]);
|
|
270
|
+
};
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// src/directives/value.js
|
|
274
|
+
directive(":value", (el2, expr) => {
|
|
275
|
+
let evaluateGet = parseExpr(expr);
|
|
276
|
+
let [get, set] = input(el2);
|
|
277
|
+
let evaluateSet = parseSetter(expr);
|
|
278
|
+
let curState, onchange = (e) => evaluateSet(curState, get(el2));
|
|
279
|
+
el2.addEventListener("input", onchange);
|
|
280
|
+
el2.addEventListener("change", onchange);
|
|
281
|
+
return (state) => {
|
|
282
|
+
let value = evaluateGet(curState = state);
|
|
283
|
+
prop(el2, "value", value);
|
|
284
|
+
set(value);
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
var memo = {};
|
|
288
|
+
function parseSetter(expr) {
|
|
289
|
+
if (memo[expr])
|
|
290
|
+
return memo[expr];
|
|
291
|
+
return memo[expr] = new Function(
|
|
292
|
+
["scope", "value"],
|
|
293
|
+
`with (scope) { ${expr} = value };`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/directives/on.js
|
|
298
|
+
directive(":on", (el2, expr) => {
|
|
299
|
+
let evaluate = parseExpr(expr);
|
|
300
|
+
let listeners = {};
|
|
301
|
+
return (state) => {
|
|
302
|
+
for (let evt in listeners)
|
|
303
|
+
el2.removeEventListener(evt, listeners[evt]);
|
|
304
|
+
listeners = evaluate(state);
|
|
305
|
+
for (let evt in listeners)
|
|
306
|
+
el2.addEventListener(evt, listeners[evt]);
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// src/index.js
|
|
311
|
+
var src_default = sprae;
|
|
312
|
+
export {
|
|
313
|
+
src_default as default,
|
|
314
|
+
directive,
|
|
315
|
+
exprError,
|
|
316
|
+
parseExpr
|
|
317
|
+
};
|
package/sprae.min.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Symbol.observable||=Symbol("observable");var e=new FinalizationRegistry((e=>e.call?.())),t=(t,r,n,l,s,i)=>{return t&&(o=(t[Symbol.observable]?.()||t).subscribe?.(r,n,l),i=o&&(()=>o.unsubscribe?.())||t.set&&t.call?.(s,r)||(t.then?.((e=>(!s&&r(e),l?.())),n)||(async e=>{try{for await(e of t){if(s)return;r(e)}l?.()}catch(e){n?.(e)}})())&&(e=>s=1),e.register(t,i),i);var o};function r(e,r){r||={};let l=[],i=!1;const o=e=>{l.forEach((t=>t(e)))},a=e=>{let r={};for(let l in e){let c=e[l];(n=c=e[l])&&(n[Symbol.observable]||n[Symbol.asyncIterator]||n.call&&n.set||n.subscribe||n.then)?(r[l]=null,s.register(c,t(c,(e=>(r[l]=e,i&&o(r)))))):r[l]=c?.constructor===Object?a(c):c}var n;return r},c=a(r);i=!0;for(let t in n){const r=`[${t.replace(":","\\:")}]`,s=n[t],i=[...e.querySelectorAll(r)];let o;e.matches?.(r)&&i.unshift(e);for(let e of i)(o=s(e,c))&&l.push(o)}o(c),c[Symbol.iterator]=function*(){yield u,yield e=>o(Object.assign(c,e))};const u=new Proxy(c,{set:(e,t,r)=>(c[t]=r,o(c),1),deleteProperty:(e,t)=>(c[t]=void 0,o(c),1)});return u}var n={},l=(e,t)=>{const r=e.replace(":","∴");return n[e]=(n,l)=>{if(n.classList.contains(r))return;n.classList.add(r);let s=n.getAttribute(e);return n.removeAttribute(e),t(n,s,l)}},s=new FinalizationRegistry((e=>e?.call?.())),i={};function o(e){if(i[e])return i[e];let t=/^[\n\s]*if.*\(.*\)/.test(e)||/^(let|const)\s/.test(e)?`(() => { ${e} })()`:e;return i[e]=(()=>{try{return new Function(["scope"],`let result; with (scope) { result = ${t} }; return result;`)}catch(t){return a(t,e)}})()}function a(e,t){return Object.assign(e,{expression:t}),console.warn(`∴ ${e.message}\n\nundefined=${t?`"${t}"\n\n`:""}`,void 0),setTimeout((()=>{throw e}),0),Promise.resolve()}l(":with",((e,t,n)=>{let l=o(t),s=Object.create(n);Object.assign(s,l(n));let[i,a]=r(e,s);return e=>{let t=l(e);a(t)}})),l(":each",((e,t)=>{let n=function(e){let t=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,r=e.match(/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/);if(!r)return;let n={};n.items=r[2].trim();let l=r[1].replace(/^\s*\(|\)\s*$/g,"").trim(),s=l.match(t);return s?(n.item=l.replace(t,"").trim(),n.index=s[1].trim()):n.item=l,n}(t);if(!n)return a(new Error,t);const l=o(n.items),s=new Text;e.replaceWith(s);let i=[];return t=>{i.forEach((e=>e.remove())),i=[];let o=l(t);"number"==typeof o&&(o=Array.from({length:o},((e,t)=>t+1))),o?.forEach(((l,o)=>{const a={...t};a[n.item]=l,n.index&&(a[n.index]=o);let c=e.cloneNode(!0);i.push(c),s.before(c),r(c,a)}))}})),l(":text",((e,t)=>{let r=o(t);return t=>{let n=r(t);e.textContent=null==n?"":n}})),l(":if",((e,t)=>{let r=e,n=[e],l=[o(t)],s=new Text;for(;r=e.nextElementSibling;){if(!(t=r.getAttribute(":else-if"))){r.hasAttribute(":else")&&(r.removeAttribute(":else"),r.classList.add("∴else"),r.remove(),n.push(r),l.push((()=>1)));break}r.removeAttribute(":else-if"),r.classList.add("∴else-if"),r.remove(),n.push(r),l.push(o(t))}return r=n[0],e=>{let t=l.findIndex((t=>t(e)));t>=0?r.replaceWith(r=n[t]):r.replaceWith(r=s)}}));var c=(e,t,r)=>{t.startsWith("on")&&(t=t.toLowerCase()),e[t]!==r&&(e[t]=r),!1===r||null==r?e.removeAttribute(t):"function"!=typeof r&&e.setAttribute(f(t),!0===r?"":"number"==typeof r||"string"==typeof r?r:"class"===t?(Array.isArray(r)?r:Object.entries(r).map((([e,t])=>t?e:""))).filter(Boolean).join(" "):"style"===t?Object.entries(r).map((([e,t])=>`${e}: ${t}`)).join(";"):"")},u=document.createElement("div"),f=e=>{u.dataset[e]="";let t=u.attributes[0].name.slice(5);return delete u.dataset[e],t};function b(e){l(":"+e,((t,r)=>{let n=o(r);return r=>{let l=n(r);c(t,e,l)}}))}b("id"),b("name"),b("for"),b("type"),b("hidden"),b("disabled"),b("href"),b("src"),b("style"),b("class"),l(":prop",((e,t)=>{let r=o(t);return t=>{let n=r(t);for(let t in n)c(e,t,n[t])}})),l(":data",((e,t)=>{let r=o(t);return t=>{let n=r(t);for(let t in n)e.dataset[t]=n[t]}})),l(":aria",((e,t)=>{let r=o(t);return t=>{let n=r(t);for(let t in n)c(e,"aria"+t[0].toUpperCase()+t.slice(1),n[t])}})),l(":value",((e,t)=>{let r,n=o(t),[l,s]=(e=>["checkbox"===e.type?()=>e.checked:()=>e.value,"text"===e.type||""===e.type?t=>e.value=null==t?"":t:"checkbox"===e.type?t=>(e.value=t?"on":"",c(e,"checked",t)):"select-one"===e.type?t=>([...e.options].map((e=>e.removeAttribute("selected"))),e.value=t,e.selectedOptions[0]?.setAttribute("selected","")):t=>e.value=t])(e),i=function(e){return d[e]?d[e]:d[e]=new Function(["scope","value"],`with (scope) { ${e} = value };`)}(t),a=t=>i(r,l(e));return e.addEventListener("input",a),e.addEventListener("change",a),t=>{let l=n(r=t);c(e,"value",l),s(l)}}));var d={};l(":on",((e,t)=>{let r=o(t),n={};return t=>{for(let t in n)e.removeEventListener(t,n[t]);n=r(t);for(let t in n)e.addEventListener(t,n[t])}}));var m=r;export{m as default,l as directive,a as exprError,o as parseExpr};
|
package/src/core.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import sube, { observable } from 'sube';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
const s = document.currentScript
|
|
5
|
-
if (s && s.hasAttribute('init')) {
|
|
6
|
-
sprae(document.documentElement)
|
|
7
|
-
}
|
|
8
|
-
|
|
3
|
+
let curEl, curDir;
|
|
9
4
|
// sprae element: apply directives
|
|
10
5
|
export default function sprae(el, initScope) {
|
|
11
6
|
initScope ||= {};
|
|
@@ -13,10 +8,7 @@ export default function sprae(el, initScope) {
|
|
|
13
8
|
let updates=[], // all spray directive updators
|
|
14
9
|
ready=false;
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
for (let dir in directives) updates[dir] = directives[dir](el);
|
|
18
|
-
|
|
19
|
-
const update = (values) => { for (let dir in updates) updates[dir].forEach(update => update(values)); };
|
|
11
|
+
const update = (values) => { updates.forEach(update => update(values)); };
|
|
20
12
|
|
|
21
13
|
// hook up observables (deeply, to include item.text etc)
|
|
22
14
|
// that's least evil compared to dlv/dset or proxies
|
|
@@ -25,7 +17,7 @@ export default function sprae(el, initScope) {
|
|
|
25
17
|
let values = {}
|
|
26
18
|
for (let k in scope) {
|
|
27
19
|
let v = scope[k];
|
|
28
|
-
if (observable(v = scope[k])) registry.register(v, sube(v, v => (values[k] = v, ready && update(values))));
|
|
20
|
+
if (observable(v = scope[k])) values[k] = null, registry.register(v, sube(v, v => (values[k] = v, ready && update(values))));
|
|
29
21
|
// FIXME: add []
|
|
30
22
|
else if (v?.constructor === Object) values[k] = rsube(v);
|
|
31
23
|
else values[k] = v;
|
|
@@ -33,9 +25,24 @@ export default function sprae(el, initScope) {
|
|
|
33
25
|
return values;
|
|
34
26
|
};
|
|
35
27
|
const values = rsube(initScope);
|
|
36
|
-
update(values);
|
|
37
28
|
ready = true;
|
|
38
29
|
|
|
30
|
+
// prepare directives - need to be after subscribing to values to get init state here
|
|
31
|
+
for (let name in directives) {
|
|
32
|
+
// updates[dir] = directives[dir](el)
|
|
33
|
+
const sel = `[${name.replace(':','\\:')}]`,
|
|
34
|
+
initDirective = directives[name]
|
|
35
|
+
|
|
36
|
+
// FIXME: possibly linear init of directives is better, who knows
|
|
37
|
+
const els = [...el.querySelectorAll(sel)];
|
|
38
|
+
if (el.matches?.(sel)) els.unshift(el);
|
|
39
|
+
|
|
40
|
+
let update
|
|
41
|
+
for (let el of els) if (update = initDirective(el, values)) updates.push(update);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
update(values);
|
|
45
|
+
|
|
39
46
|
// return update via destructuring of result to allow batch-update
|
|
40
47
|
values[Symbol.iterator] = function*(){ yield proxy; yield (diff) => update(Object.assign(values, diff)); };
|
|
41
48
|
|
|
@@ -48,63 +55,56 @@ export default function sprae(el, initScope) {
|
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
// dict of directives
|
|
51
|
-
const directives = {}
|
|
58
|
+
const directives = {}
|
|
52
59
|
|
|
53
60
|
// register a directive
|
|
54
61
|
export const directive = (name, initializer) => {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
for (let el of els) {
|
|
65
|
-
if (!el.classList.contains(`∴${name}`))
|
|
66
|
-
el.classList.add(`∴${name}`), updates.push(initializer(el));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return updates
|
|
62
|
+
const className = name.replace(':','∴')
|
|
63
|
+
|
|
64
|
+
// create initializer of a directive on an element
|
|
65
|
+
return directives[name] = (el, initValues) => {
|
|
66
|
+
if (el.classList.contains(className)) return
|
|
67
|
+
el.classList.add(className)
|
|
68
|
+
let expr = el.getAttribute(name)
|
|
69
|
+
el.removeAttribute(name)
|
|
70
|
+
return initializer(el, expr, initValues);
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
const registry = new FinalizationRegistry(unsub => unsub?.call?.())
|
|
74
75
|
|
|
75
|
-
|
|
76
76
|
let evaluatorMemo = {}
|
|
77
77
|
|
|
78
78
|
// borrowed from alpine: https://github.com/alpinejs/alpine/blob/main/packages/alpinejs/src/evaluator.js#L61
|
|
79
79
|
// it seems to be more robust than subscript
|
|
80
|
-
export function parseExpr(expression
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
80
|
+
export function parseExpr(expression) {
|
|
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)
|
|
100
99
|
}
|
|
100
|
+
}
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
return evaluatorMemo[expression] = safeFunction()
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
export function exprError(error, expression
|
|
106
|
-
Object.assign( error, {
|
|
107
|
-
console.warn(`∴
|
|
105
|
+
export function exprError(error, expression) {
|
|
106
|
+
Object.assign( error, { expression } )
|
|
107
|
+
console.warn(`∴ ${error.message}\n\n${curDir}=${ expression ? `"${expression}"\n\n` : '' }`, curEl)
|
|
108
108
|
setTimeout(() => { throw error }, 0)
|
|
109
109
|
return Promise.resolve()
|
|
110
|
-
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { directive, parseExpr } from '../core.js'
|
|
2
|
+
import { prop } from 'element-props'
|
|
3
|
+
|
|
4
|
+
directive(':aria', (el, expr) => {
|
|
5
|
+
let evaluate = parseExpr(expr);
|
|
6
|
+
return (state) => {
|
|
7
|
+
let value = evaluate(state);
|
|
8
|
+
for (let key in value) prop(el, 'aria'+key[0].toUpperCase()+key.slice(1), value[key]);
|
|
9
|
+
}
|
|
10
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// common directives just set/map value as is
|
|
2
|
+
import { directive, parseExpr } from '../core.js'
|
|
3
|
+
import {prop} from 'element-props'
|
|
4
|
+
|
|
5
|
+
common(`id`), common(`name`), common(`for`), common(`type`), common(`hidden`), common(`disabled`), common(`href`), common(`src`), common(`style`), common(`class`)
|
|
6
|
+
|
|
7
|
+
function common(name) {
|
|
8
|
+
directive(':'+name, (el,expr) => {
|
|
9
|
+
let evaluate = parseExpr(expr)
|
|
10
|
+
return state => {
|
|
11
|
+
let value = evaluate(state);
|
|
12
|
+
prop(el, name, value)
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { directive, parseExpr } from '../core.js'
|
|
2
|
+
import { prop } from 'element-props'
|
|
3
|
+
|
|
4
|
+
directive(':data', (el, expr) => {
|
|
5
|
+
let evaluate = parseExpr(expr);
|
|
6
|
+
return (state) => {
|
|
7
|
+
let value = evaluate(state);
|
|
8
|
+
for (let key in value) el.dataset[key] = value[key];
|
|
9
|
+
}
|
|
10
|
+
})
|
package/src/directives/each.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import sprae, { directive, exprError, parseExpr } from '../core.js'
|
|
2
2
|
|
|
3
|
-
directive('each', (el) => {
|
|
4
|
-
let expr = el.getAttribute(':each');
|
|
5
|
-
el.removeAttribute(':each');
|
|
6
|
-
|
|
3
|
+
directive(':each', (el, expr) => {
|
|
7
4
|
let each = parseForExpression(expr);
|
|
8
|
-
if (!each) return exprError(new Error, expr
|
|
5
|
+
if (!each) return exprError(new Error, expr);
|
|
9
6
|
|
|
10
7
|
const getItems = parseExpr(each.items);
|
|
11
8
|
|
package/src/directives/if.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { directive, parseExpr } from '../core.js'
|
|
2
2
|
|
|
3
|
-
directive('if', (el) => {
|
|
4
|
-
let expr = el.getAttribute(':if');
|
|
5
|
-
el.removeAttribute(':if');
|
|
6
|
-
|
|
3
|
+
directive(':if', (el, expr) => {
|
|
7
4
|
let cur = el, els = [el], clauses = [parseExpr(expr)], holder = new Text
|
|
8
5
|
|
|
9
6
|
// collect clauses
|
package/src/directives/index.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// order defines precedence
|
|
2
|
+
import './with.js'
|
|
2
3
|
import './each.js'
|
|
3
|
-
import './hidden.js'
|
|
4
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
|
|
5
|
+
import './if.js' // if must go last, after other directives are initialized, since it removes :else, :else-if from tree
|
|
6
|
+
import './common.js'
|
|
7
|
+
import './prop.js'
|
|
8
|
+
import './data.js'
|
|
9
|
+
import './aria.js'
|
|
10
|
+
import './value.js'
|
|
11
|
+
import './on.js'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { directive, parseExpr } from '../core.js'
|
|
2
|
+
|
|
3
|
+
directive(':on', (el, expr) => {
|
|
4
|
+
let evaluate = parseExpr(expr);
|
|
5
|
+
let listeners = {}
|
|
6
|
+
return (state) => {
|
|
7
|
+
for (let evt in listeners) el.removeEventListener(evt, listeners[evt]);
|
|
8
|
+
listeners = evaluate(state);
|
|
9
|
+
for (let evt in listeners) el.addEventListener(evt, listeners[evt]);
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { directive, parseExpr } from '../core.js'
|
|
2
|
+
import { prop } from 'element-props'
|
|
3
|
+
|
|
4
|
+
directive(':prop', (el, expr) => {
|
|
5
|
+
let evaluate = parseExpr(expr);
|
|
6
|
+
return (state) => {
|
|
7
|
+
let value = evaluate(state);
|
|
8
|
+
for (let key in value) prop(el, key, value[key]);
|
|
9
|
+
}
|
|
10
|
+
})
|
package/src/directives/text.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { directive, parseExpr } from '../core.js'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
directive('text', (el) => {
|
|
5
|
-
let expr = el.getAttribute(':text');
|
|
6
|
-
el.removeAttribute(':text');
|
|
3
|
+
directive(':text', (el, expr) => {
|
|
7
4
|
let evaluate = parseExpr(expr);
|
|
8
5
|
return (state) => {
|
|
9
6
|
let value = evaluate(state);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { input, prop } from "element-props";
|
|
2
|
+
import { directive, parseExpr } from '../core.js';
|
|
3
|
+
|
|
4
|
+
// connect expr to element value
|
|
5
|
+
directive(':value', (el, expr) => {
|
|
6
|
+
let evaluateGet = parseExpr(expr);
|
|
7
|
+
let [get, set] = input(el);
|
|
8
|
+
let evaluateSet = parseSetter(expr);
|
|
9
|
+
let curState, onchange = e => evaluateSet(curState, get(el));
|
|
10
|
+
el.addEventListener('input', onchange);
|
|
11
|
+
el.addEventListener('change', onchange);
|
|
12
|
+
return (state) => {
|
|
13
|
+
let value = evaluateGet(curState = state);
|
|
14
|
+
prop(el, 'value', value)
|
|
15
|
+
set(value);
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const memo = {}
|
|
20
|
+
function parseSetter(expr) {
|
|
21
|
+
if (memo[expr]) return memo[expr]
|
|
22
|
+
return memo[expr] = new Function(
|
|
23
|
+
['scope', 'value'],
|
|
24
|
+
`with (scope) { ${expr} = value };`
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import sprae, { directive, parseExpr } from '../core.js'
|
|
2
|
+
|
|
3
|
+
directive(':with', (el, expr, rootValues) => {
|
|
4
|
+
let evaluate = parseExpr(expr);
|
|
5
|
+
|
|
6
|
+
// it subsprays with shadowed values
|
|
7
|
+
// rootValues get updated by parent directives
|
|
8
|
+
// subscope doesn't contain reactive values
|
|
9
|
+
let subscope = Object.create(rootValues)
|
|
10
|
+
|
|
11
|
+
// FIXME: wonder if we better pass initial state rather than values snapshot, to let subtree subscribe to more complete set
|
|
12
|
+
// FIXME: likely initial set can be reactive itself then
|
|
13
|
+
Object.assign(subscope, evaluate(rootValues))
|
|
14
|
+
let [subvalues, subupdate] = sprae(el, subscope)
|
|
15
|
+
|
|
16
|
+
return (values) => {
|
|
17
|
+
let withValues = evaluate(values);
|
|
18
|
+
subupdate(withValues)
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
package/src/index.js
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
import './directives/index.js';
|
|
1
|
+
import sprae from './core.js';
|
|
2
|
+
import './directives/index.js';
|
|
3
|
+
|
|
4
|
+
export default sprae;
|
|
5
|
+
export * from './core.js';
|
|
6
|
+
|
|
7
|
+
// autoinit
|
|
8
|
+
// NOTE: abandoning for now, since requires a separate non-module JS entry, until use-case appears
|
|
9
|
+
// const s = document.currentScript
|
|
10
|
+
// if (s && s.hasAttribute('init')) {
|
|
11
|
+
// sprae(document.documentElement)
|
|
12
|
+
// }
|
package/test/index.html
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"value-ref" : "../node_modules/value-ref/value-ref.js",
|
|
13
13
|
"hyperf" : "../node_modules/hyperf/hyperf.js",
|
|
14
14
|
"sube" : "../node_modules/sube/sube.js",
|
|
15
|
+
"element-props" : "../node_modules/element-props/element-props.js",
|
|
15
16
|
"preact": "../node_modules/preact/dist/preact.mjs",
|
|
16
17
|
"preact/hooks": "../node_modules/preact/hooks/dist/hooks.mjs",
|
|
17
18
|
"@preact/signals": "../node_modules/@preact/signals/dist/signals.mjs",
|
|
@@ -20,5 +21,4 @@
|
|
|
20
21
|
}
|
|
21
22
|
</script>
|
|
22
23
|
|
|
23
|
-
|
|
24
24
|
<script src="./test.js" type="module"></script>
|
package/test/test.js
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
import v from 'value-ref'
|
|
2
1
|
import { signal } from '@preact/signals'
|
|
3
2
|
import test, {is, any, throws} from 'tst'
|
|
4
3
|
import {tick, time} from 'wait-please'
|
|
5
|
-
import sprae from '
|
|
4
|
+
import sprae from '../src/index.js'
|
|
6
5
|
import h from 'hyperf'
|
|
7
6
|
|
|
7
|
+
Object.defineProperty(DocumentFragment.prototype, 'outerHTML', {
|
|
8
|
+
get() {
|
|
9
|
+
let s = ''
|
|
10
|
+
this.childNodes.forEach(n => {
|
|
11
|
+
s += n.nodeType === 3 ? n.textContent : n.outerHTML != null ? n.outerHTML : ''
|
|
12
|
+
})
|
|
13
|
+
return s
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test.skip('autoinit', async () => {
|
|
18
|
+
is(window.x.innerHTML, '1')
|
|
19
|
+
})
|
|
20
|
+
|
|
8
21
|
test('hidden: core', async () => {
|
|
9
22
|
let el = h`<div :hidden="hidden"></div>`
|
|
10
23
|
let params = sprae(el, {hidden:true})
|
|
@@ -22,6 +35,65 @@ test('hidden: reactive', async () => {
|
|
|
22
35
|
is(el.outerHTML, `<div class="∴hidden"></div>`)
|
|
23
36
|
})
|
|
24
37
|
|
|
38
|
+
test('common: reactive', async () => {
|
|
39
|
+
let el = h`<label :for="name" :text="name" ></label><input :id="name" :name="name" :type="name" :disabled="!name"/><a :href="url"></a><img :src="url"/>`
|
|
40
|
+
let params = sprae(el, {name:'text', url:'//google.com'})
|
|
41
|
+
is(el.outerHTML, `<label class="∴text ∴for" for="text">text</label><input class="∴id ∴name ∴type ∴disabled" id="text" name="text" type="text"><a class="∴href" href="//google.com"></a><img class="∴src" src="//google.com">`)
|
|
42
|
+
params.name = 'email'
|
|
43
|
+
is(el.outerHTML, `<label class="∴text ∴for" for="email">email</label><input class="∴id ∴name ∴type ∴disabled" id="email" name="email" type="email"><a class="∴href" href="//google.com"></a><img class="∴src" src="//google.com">`)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('common: style', async () => {
|
|
47
|
+
let el = h`<x :style="style"></x>`
|
|
48
|
+
let params = sprae(el, {style: "top: 1px"})
|
|
49
|
+
is(el.outerHTML, `<x class="∴style" style="top: 1px"></x>`)
|
|
50
|
+
params.style = {top: '2px'}
|
|
51
|
+
is(el.outerHTML, `<x class="∴style" style="top: 2px"></x>`)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('common: class', async () => {
|
|
55
|
+
let el = h`<x :class="a"></x><y :class="[b, c]"></y><z :class="{b:true, c:d}"></z>`
|
|
56
|
+
const c = signal('z')
|
|
57
|
+
let params = sprae(el, {a:'x', b:'y', c, d:false});
|
|
58
|
+
is(el.outerHTML, `<x class="x"></x><y class="y z"></y><z class="b"></z>`);
|
|
59
|
+
params.d = true;
|
|
60
|
+
is(el.outerHTML, `<x class="x"></x><y class="y z"></y><z class="b c"></z>`);
|
|
61
|
+
c.value = 'w'
|
|
62
|
+
is(el.outerHTML, `<x class="x"></x><y class="y w"></y><z class="b c"></z>`);
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('props: base', async () => {
|
|
66
|
+
let el = h`<input :prop="{for:1, title:2, help:3, type:4, placeholder: 5}"/>`
|
|
67
|
+
let params = sprae(el)
|
|
68
|
+
is(el.outerHTML, `<input class="∴prop" for="1" title="2" help="3" type="4" placeholder="5">`)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('data: base', async () => {
|
|
72
|
+
let el = h`<input :data="{a:1, fooBar:2}"/>`
|
|
73
|
+
let params = sprae(el)
|
|
74
|
+
is(el.outerHTML, `<input class="∴data" data-a="1" data-foo-bar="2">`)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('aria: base', async () => {
|
|
78
|
+
let el = h`<input type="text" id="jokes" role="combobox" :aria="{controls:'joketypes', autocomplete:'list', expanded:false, activeOption:'item1', activedescendant:''}"/>`
|
|
79
|
+
sprae(el)
|
|
80
|
+
is(el.outerHTML, `<input type="text" id="jokes" role="combobox" class="∴aria" aria-controls="joketypes" aria-autocomplete="list" aria-expanded="false" aria-active-option="item1" aria-activedescendant="">`)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('input: direct', async () => {
|
|
84
|
+
let el = h`<input :value="a" />`
|
|
85
|
+
let state = sprae(el, {a:1})
|
|
86
|
+
is(el.value, '1')
|
|
87
|
+
is(el.outerHTML, `<input class="∴value" value="1">`)
|
|
88
|
+
state.a = 2
|
|
89
|
+
is(el.value, '2')
|
|
90
|
+
is(el.outerHTML, `<input class="∴value" value="2">`)
|
|
91
|
+
|
|
92
|
+
el.value = 3
|
|
93
|
+
el.dispatchEvent(new window.Event('change'))
|
|
94
|
+
is(state.a, '3')
|
|
95
|
+
})
|
|
96
|
+
|
|
25
97
|
test('text: core', async () => {
|
|
26
98
|
let el = h`<div :text="text"></div>`
|
|
27
99
|
let params = sprae(el, {text:'abc'})
|
|
@@ -181,3 +253,51 @@ test('each: condition within loop', async () => {
|
|
|
181
253
|
is(el.innerHTML, '')
|
|
182
254
|
})
|
|
183
255
|
|
|
256
|
+
test('on: base', () => {
|
|
257
|
+
let el = h`<div :on="{click(e){log.push('click')},x}"></div>`
|
|
258
|
+
let log = signal([])
|
|
259
|
+
let params = sprae(el, {x(){log.value.push('x')}, log})
|
|
260
|
+
|
|
261
|
+
is(el.outerHTML, `<div class="∴on"></div>`);
|
|
262
|
+
el.dispatchEvent(new window.Event('click'));
|
|
263
|
+
is(log.value, ['click'])
|
|
264
|
+
el.dispatchEvent(new window.Event('x'));
|
|
265
|
+
is(log.value, ['click','x'])
|
|
266
|
+
|
|
267
|
+
params.x = function(){log.value.push('xx')}
|
|
268
|
+
el.dispatchEvent(new window.Event('x'));
|
|
269
|
+
is(log.value, ['click','x','xx']);
|
|
270
|
+
|
|
271
|
+
delete params.x;
|
|
272
|
+
el.dispatchEvent(new window.Event('x'));
|
|
273
|
+
is(log.value, ['click','x','xx']);
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
test('with: inline', () => {
|
|
277
|
+
let el = h`<x :with="{foo:'bar', baz}"><y :text="foo + baz"></y></x>`
|
|
278
|
+
let state = sprae(el, {baz: 'qux'})
|
|
279
|
+
// FIXME: this doesn't inherit root scope baz property and instead uses hard-initialized one
|
|
280
|
+
is(el.innerHTML, `<y class="∴text">barqux</y>`)
|
|
281
|
+
state.baz = 'quux'
|
|
282
|
+
is(el.innerHTML, `<y class="∴text">barquux</y>`)
|
|
283
|
+
})
|
|
284
|
+
test('with: data', () => {
|
|
285
|
+
let el = h`<x :with="x"><y :text="foo"></y></x>`
|
|
286
|
+
let [state, update] = sprae(el, {x:{foo:'bar'}})
|
|
287
|
+
is(el.innerHTML, `<y class="∴text">bar</y>`)
|
|
288
|
+
update({x:{foo:'baz'}})
|
|
289
|
+
is(el.innerHTML, `<y class="∴text">baz</y>`)
|
|
290
|
+
})
|
|
291
|
+
test('with: inheritance', () => {
|
|
292
|
+
// NOTE: y:text initializes through directive, not through parent
|
|
293
|
+
// therefore by default :text uses parent's state, not defined by element itself
|
|
294
|
+
let el = h`<x :with="{foo:'foo'}"><y :with="b" :text="foo+bar"></y></x>`
|
|
295
|
+
sprae(el, {b:{bar:'bar'}})
|
|
296
|
+
is(el.innerHTML, `<y class="∴with ∴text">foobar</y>`)
|
|
297
|
+
})
|
|
298
|
+
test('with: reactive inheritance', () => {
|
|
299
|
+
let el = h`<x :with="{foo:1}"><y :with="b.c" :text="foo+bar"></y></x>`
|
|
300
|
+
const bar = signal('2')
|
|
301
|
+
sprae(el, {b:{c:{bar}}})
|
|
302
|
+
is(el.innerHTML, `<y class="∴with ∴text">12</y>`)
|
|
303
|
+
})
|
package/src/directives/hidden.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
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
|
-
|