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 CHANGED
@@ -1,25 +1,28 @@
1
1
  {
2
2
  "name": "sprae",
3
3
  "description": "Reactive directives with expressions for DOM microtemplating.",
4
- "version": "0.1.0",
4
+ "version": "1.0.0",
5
5
  "main": "sprae.js",
6
+ "type": "module",
6
7
  "dependencies": {
7
- "element-props": "^2.0.4",
8
- "htm": "^3.1.1",
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
@@ -0,0 +1,6 @@
1
+ * [ ] finish directives
2
+ * [ ] better list diffing
3
+ * [?] ordered directives init (:each + :if vs :if + :each) -> find out if really needed
4
+ * [x] ~~autoinit~~ -> too much maintenance burden
5
+ * [ ] node tests
6
+ * [ ] better deps updating
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 object or support unscoped expression? -> Use JS convention, too many use-cases.
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æ [![size](https://img.shields.io/bundlephobia/minzip/sprae?label=size)](https://bundlephobia.com/result?p=sprae)
2
2
 
3
- > Reactive directives for DOM microtemplating: moisturize tree without trasplanting to JSX.
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) and [templize](https://github.com/dy/templize) with better ergonomics[*](#justification).
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 init from 'sprae';
18
+ import sprae from 'sprae';
19
19
 
20
- const data = { user: { displayName: 'Dmitry Ivanov' } }
21
- const [state, update] = init(user, data);
20
+ const init = { user: { displayName: 'Dmitry Ivanov' } }
21
+ const state = sprae(user, init);
22
22
 
23
- state.user.displayName = 'dy' // update value
24
- update({user: {displayName: 'dy'}}) // alternatively
23
+ state.user.displayName = 'dy' // automatically updates DOM
25
24
  </script>
26
25
  ```
27
26
 
28
- * `init` initializes directives within an `element` subtree with passed `data`.
29
- * `state` is proxy reflecting used values, changing any of its props updates directives.
30
- * `update` can be used for bulk-updating multiple props.
31
- * `data` is the initial state to render the template. It can include reactive values, see [reactivity](#reactivity).
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><h3>Autoinit</h3></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
- Sprae can be used without build step or JS, autoinitializing HTML:
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 :scope="{ count: 0 }">
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
- * `: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`.
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
- * [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.
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><h3>Adding directives</h3></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 init, { directive } from 'sprae';
108
+ import { directive, parseExpr } from 'sprae';
76
109
 
77
- directive(':html', (el) => {
110
+ directive(':html', (el, expr) => {
78
111
  // ...initialize here
79
- const evaluate = parseExpression(el.getAttribute(':html'));
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 spores from 'sprae';
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
- spores(done, { loading, result })
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
- // autoinit
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
- // prepare directives
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 = {}, store = new WeakSet
58
+ const directives = {}
52
59
 
53
60
  // register a directive
54
61
  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 = [];
62
-
63
- // replace all shortcuts with inner templates
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, 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
- }
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
- return evaluatorMemo[expression] = safeFunction()
102
+ return evaluatorMemo[expression] = safeFunction()
103
103
  }
104
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)
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
+ })
@@ -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, el);
5
+ if (!each) return exprError(new Error, expr);
9
6
 
10
7
  const getItems = parseExpr(each.items);
11
8
 
@@ -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
@@ -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
+ })
@@ -1,9 +1,6 @@
1
1
  import { directive, parseExpr } from '../core.js'
2
2
 
3
- // hidden attribute directive example
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
- export { default, directive } from './core.js';
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 'sprae'
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
+ })
@@ -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
-