sprae 4.0.0 → 5.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,7 +1,7 @@
1
1
  {
2
2
  "name": "sprae",
3
3
  "description": "DOM microhydration.",
4
- "version": "4.0.0",
4
+ "version": "5.0.0",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
7
7
  "type": "module",
package/r&d.md CHANGED
@@ -518,15 +518,57 @@
518
518
  ? Some 'or' character `:onclick--onkeydown`
519
519
  ? We can redirect to main event, that's it for now
520
520
 
521
- ## [ ] Insert content by reusing the node/template,
521
+ ## [x] Insert content by reusing the node/template -> use `:render="ref" :with="data"`
522
522
 
523
523
  * Makes easy use of repeatable fragments, instead of web-components
524
524
  + sort-of "detached" for-each
525
525
  + reinforces :ref
526
526
  ? does it replace element or puts a content?
527
+ * Refs:
528
+ * https://github.com/justinfagnani/html-include-element - src=
529
+ * https://www.npmjs.com/package/imported-template - content=path-to-file
530
+ * https://github.com/SirPepe/html-import src="content.selector"
531
+ * https://github.com/Juicy/juicy-html - html="rawhtml"
532
+ * https://www.npmjs.com/package/html-import-wc - src=path
533
+ * https://github.com/sashafirsov/slotted-element - src=path-to-htmlor-json
534
+ * https://github.com/webcomponents/html-imports - href=filepath
535
+ * https://github.com/giuseppeg/xm - import src=filepath
536
+ * https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-include-html#link-tag-html-only
537
+ * https://github.com/maherbo/include-HTML-document-fragment - link href=path
538
+ * https://github.com/github/include-fragment-element - src=path-to-html
527
539
 
528
540
  1. :use="ref-id"
529
- 2. :
541
+ ```
542
+ <template :ref="abc" id="abc"><span :text="abc"></span></template>
543
+
544
+ <div :use="abc" :with="{abc:'def'}"></div>
545
+ <div :use="'#abc'" :with="{abc:'def'}"></div>
546
+ ```
547
+
548
+ - `<use>` from SVG replaces element, but we need inserting content
549
+
550
+ 2. :content="#template-id"
551
+
552
+ - conflicts with direct inline content
553
+
554
+ 3. :include="#template-id"
555
+
556
+ - conflicts with path to file
557
+
558
+ 4. :tpl="#template-id"
559
+
560
+ - some confusion of meaning
561
+
562
+ 5. :render="#template-id"
563
+
564
+ + compatible with liquid 5.0
565
+ + makes sense as :render=a :with=b
566
+
567
+ * Let's think consequently.
568
+ 1. `<template>` element has direct purpose for that
569
+ 2. We provide content fallback for unloaded elements in case of `:text` as `<x :text="abc">fallback</x>`
570
+ 3. There's too many ways to implement fetching - ideally we leave that concern out and focus only on including content
571
+ 4. The approach is almost ready declarative custom element. `<template>` is standard part of it - adds to 1.
530
572
 
531
573
  ## [x] Remove non-essential directives -> yep, less API friction
532
574
  * :aria - can be defined via plain attributes
package/readme.md CHANGED
@@ -12,9 +12,7 @@ To autoinit on document, include [`sprae.auto.js`](./sprae.auto.js):
12
12
  <!-- <script src="https://cdn.jsdelivr.net/npm/sprae/sprae.auto.js" defer></script> -->
13
13
  <script src="./path/to/sprae.auto.js" defer></script>
14
14
 
15
- <div :scope="{foo:'bar'}">
16
- <span :text="foo"></span>
17
- </div>
15
+ <a :each="id in ['a','b','c']" :href="`#${id}`"></a>
18
16
  ```
19
17
 
20
18
  To use as module, import [`sprae.js`](./sprae.js):
@@ -50,23 +48,6 @@ Sprae evaluates attributes starting with `:`:
50
48
 
51
49
  ## Attributes
52
50
 
53
- #### `:scope="data"`
54
-
55
- Define or extend data scope for a subtree.
56
-
57
- ```html
58
- <!-- Inline data -->
59
- <x :scope="{ foo: 'bar' }" :text="foo"></x>
60
-
61
- <!-- External data -->
62
- <y :scope="data"></y>
63
-
64
- <!-- Extend scope -->
65
- <x :scope="{ foo: 'bar' }">
66
- <y :scope="{ baz: 'qux' }" :text="foo + baz"></y>
67
- </x>
68
- ```
69
-
70
51
  #### `:if="condition"`, `:else`
71
52
 
72
53
  Control flow of elements.
@@ -148,7 +129,7 @@ Set value of an input, textarea or select. Takes handle of `checked` and `select
148
129
  </select>
149
130
  ```
150
131
 
151
- #### `:<prop>="value?"`, `:="props?"`
132
+ #### `:<prop>="value?"`
152
133
 
153
134
  Set any attribute value or run effect.
154
135
 
@@ -159,16 +140,21 @@ Set any attribute value or run effect.
159
140
  <!-- Multiple properties -->
160
141
  <input :id:name="name" />
161
142
 
162
- <!-- Bulk properties -->
163
- <input :="{ id: name, name, type:'text', value }" />
143
+ <!-- Effect (triggers any time bar changes) -->
144
+ <div :fx="void bar()" ></div>
145
+ ```
146
+
147
+ #### `:="props?"`
164
148
 
165
- <!-- Effects (trigger any time foo or bar changes) -->
166
- <div :="if (foo) bar()" :fx="void bar()" ></div>
149
+ Spread multiple attibures.
150
+
151
+ ```html
152
+ <input :="{ id: name, name, type:'text', value }" />
167
153
  ```
168
154
 
169
- #### `:on<event>="handler"`, `:on="events"`, `:<in>..<out>="handler"`
155
+ #### `:on<event>="handler"`, `:on<in>..on<out>="handler"`
170
156
 
171
- Add event listeners.
157
+ Add event listener or events chain.
172
158
 
173
159
  ```html
174
160
  <!-- Single event -->
@@ -189,9 +175,6 @@ Add event listeners.
189
175
 
190
176
  <!-- Event modifiers -->
191
177
  <button :onclick.throttle-500="handler">Not too often</button>
192
-
193
- <!-- Bulk/custom events -->
194
- <button :on="{ click: handler, touch: handler, special: handler }">Submit</button>
195
178
  ```
196
179
 
197
180
  ##### Event modifiers
@@ -205,6 +188,33 @@ Add event listeners.
205
188
  * `.ctrl-<key>, .alt-<key>, .meta-<key>, .shift-<key>` – key combinations, eg. `.ctrl-alt-delete` or `.meta-x`.
206
189
  * `.*` – any other modifier has no effect, but allows binding multiple handlers to same event (like jQuery event classes).
207
190
 
191
+ #### `:with="data"`
192
+
193
+ Define or extend data scope for a subtree.
194
+
195
+ ```html
196
+ <!-- Inline data -->
197
+ <x :with="{ foo: 'bar' }" :text="foo"></x>
198
+
199
+ <!-- External data -->
200
+ <y :with="data"></y>
201
+
202
+ <!-- Extend scope -->
203
+ <x :with="{ foo: 'bar' }">
204
+ <y :with="{ baz: 'qux' }" :text="foo + baz"></y>
205
+ </x>
206
+ ```
207
+
208
+ #### `:render="ref"`
209
+
210
+ Include template as element content.
211
+
212
+ ```html
213
+ <template :ref="foo"><span :text="foo"></span></template>
214
+
215
+ <div :render="foo" :with="{foo:'bar'}">unknown</div>
216
+ <div :render="foo" :with="{foo:'baz'}">unknown</div>
217
+ ```
208
218
 
209
219
  #### `:ref="id"`
210
220
 
@@ -233,8 +243,14 @@ Expose element to current data scope with the `id`:
233
243
  ## Sandbox
234
244
 
235
245
  Expressions are sandboxed, ie. have no access to global or window (since sprae can be run in server environment).
236
- Default sandbox provides: _Array_, _Object_, _Number_, _String_, _Boolean_, _Date_, _console_.
237
- Sandbox can be extended as `Object.assign(sprae.globals, { BigInt, window, document })` etc.
246
+
247
+ ```html
248
+ <div :x="window.x"></div>
249
+ <!-- window is undefined -->
250
+ ```
251
+
252
+ Default sandbox provides: _Array_, _Object_, _Number_, _String_, _Boolean_, _Date_, _console_.<br/>
253
+ Sandbox can be extended as `Object.assign(sprae.globals, { BigInt, window, document })`.
238
254
 
239
255
  ## Examples
240
256
 
package/src/directives.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import sprae from './core.js'
3
3
  import swap from './domdiff.js'
4
4
  import { state as createState} from './state.js'
5
- import { WeakishMap } from './weakish-map.js'
5
+ import { queueMicrotask, WeakishMap } from './util.js'
6
6
 
7
7
  // reserved directives - order matters!
8
8
  // primary initialized first by selector, secondary initialized by iterating attributes
@@ -10,8 +10,8 @@ export const primary = {}, secondary = {}
10
10
 
11
11
 
12
12
  // :if is interchangeable with :each depending on order, :if :each or :each :if have different meanings
13
- // as for :if :scope - :if must init first, since it is lazy, to avoid initializing component ahead of time by :scope
14
- // we consider :scope={x} :if={x} case insignificant
13
+ // as for :if :with - :if must init first, since it is lazy, to avoid initializing component ahead of time by :with
14
+ // we consider :with={x} :if={x} case insignificant
15
15
  primary['if'] = (el, expr) => {
16
16
  let holder = document.createTextNode(''),
17
17
  clauses = [parseExpr(el, expr, ':if')],
@@ -44,9 +44,9 @@ primary['if'] = (el, expr) => {
44
44
  }
45
45
  }
46
46
 
47
- // :scope must come before :each, but :if has primary importance
48
- primary['scope'] = (el, expr, rootState) => {
49
- let evaluate = parseExpr(el, expr, 'scope')
47
+ // :with must come before :each, but :if has primary importance
48
+ primary['with'] = (el, expr, rootState) => {
49
+ let evaluate = parseExpr(el, expr, ':with')
50
50
  const localState = evaluate(rootState)
51
51
  let state = createState(localState, rootState)
52
52
  // console.log(123, state.foo, state.bar)
@@ -58,7 +58,7 @@ const _each = Symbol(':each')
58
58
  // :each must init before :ref, :id or any others, since it defines scope
59
59
  primary['each'] = (tpl, expr) => {
60
60
  let each = parseForExpression(expr);
61
- if (!each) return exprError(new Error, tpl, expr);
61
+ if (!each) return exprError(new Error, tpl, expr, ':each');
62
62
 
63
63
  // FIXME: make sure no memory leak here
64
64
  // we need :if to be able to replace holder instead of tpl for :if :each case
@@ -68,7 +68,7 @@ primary['each'] = (tpl, expr) => {
68
68
  const evaluate = parseExpr(tpl, each[2], ':each');
69
69
 
70
70
  const keyExpr = tpl.getAttribute(':key');
71
- const itemKey = keyExpr ? parseExpr(null, keyExpr) : null;
71
+ const itemKey = keyExpr ? parseExpr(null, keyExpr, ':each') : null;
72
72
  tpl.removeAttribute(':key')
73
73
 
74
74
  const refExpr = tpl.getAttribute(':ref');
@@ -142,6 +142,16 @@ function parseForExpression(expression) {
142
142
  return [item, '', items]
143
143
  }
144
144
 
145
+ secondary['render'] = (el, expr, state) => {
146
+ let evaluate = parseExpr(el, expr, ':render'),
147
+ tpl = evaluate(state)
148
+
149
+ if (!tpl) exprError(new Error('Template not found'), el, expr, ':render');
150
+
151
+ let content = tpl.content.cloneNode(true);
152
+ el.replaceChildren(content)
153
+ sprae(el, state)
154
+ }
145
155
 
146
156
  secondary['ref'] = (el, expr, state) => {
147
157
  // FIXME: wait for complex ref use-case
@@ -426,9 +436,9 @@ function parseExpr(el, expression, dir) {
426
436
  }
427
437
  }
428
438
 
429
- function exprError(error, element, expression, dir) {
439
+ function exprError(error, element, expression, directive) {
430
440
  Object.assign( error, { element, expression } )
431
- console.warn(`∴ ${error.message}\n\n${dir}=${ expression ? `"${expression}"\n\n` : '' }`, element)
441
+ console.warn(`∴ ${error.message}\n\n${directive}=${ expression ? `"${expression}"\n\n` : '' }`, element)
432
442
  queueMicrotask(() => { throw error }, 0)
433
443
  }
434
444
 
package/src/state.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { queueMicrotask } from "./util.js"
2
+
1
3
  // currentFx stack of listeners
2
4
  let currentFx, batch = new Set, pendingUpdate
3
5
 
@@ -1,3 +1,8 @@
1
+
2
+ // reset stacktrace or plan for fastest next call
3
+ // https://twitter.com/_developit/status/1634033380940455936
4
+ export const queueMicrotask = Promise.prototype.then.bind(Promise.resolve());
5
+
1
6
  // based on https://github.com/WebReflection/not-so-weak/
2
7
  const refs = new WeakMap;
3
8
 
package/test/dom.js CHANGED
@@ -277,8 +277,8 @@ test('if: (#3) subsequent content is not abandoned', async () => {
277
277
  is(x.outerHTML, `<x><z>123</z></x>`)
278
278
  })
279
279
 
280
- test('if: + :scope doesnt prevent secondary effects from happening', async () => {
281
- let el = h`<div><x :if="x" :scope="{}" :text="x"></x></div>`
280
+ test('if: + :with doesnt prevent secondary effects from happening', async () => {
281
+ let el = h`<div><x :if="x" :with="{}" :text="x"></x></div>`
282
282
  let state = sprae(el, {x:''})
283
283
  is(el.innerHTML, ``)
284
284
  state.x = '123'
@@ -286,7 +286,7 @@ test('if: + :scope doesnt prevent secondary effects from happening', async () =>
286
286
  is(el.innerHTML, `<x>123</x>`)
287
287
 
288
288
  // NOTE: we ignore this case
289
- // let el2 = h`<div><x :if="x" :scope="{x:cond}" :text="x"></x></div>`
289
+ // let el2 = h`<div><x :if="x" :with="{x:cond}" :text="x"></x></div>`
290
290
  // let state2 = sprae(el, {cond:''})
291
291
  // is(el2.innerHTML, ``)
292
292
  // state2.cond = '123'
@@ -859,7 +859,7 @@ test('on: modifiers chain', async e => {
859
859
  })
860
860
 
861
861
  test('with: inline', async () => {
862
- let el = h`<x :scope="{foo:'bar'}"><y :text="foo + baz"></y></x>`
862
+ let el = h`<x :with="{foo:'bar'}"><y :text="foo + baz"></y></x>`
863
863
  let state = sprae(el, {baz: 'qux'})
864
864
  // FIXME: this doesn't inherit root scope baz property and instead uses hard-initialized one
865
865
  is(el.innerHTML, `<y>barqux</y>`)
@@ -868,7 +868,7 @@ test('with: inline', async () => {
868
868
  is(el.innerHTML, `<y>barquux</y>`)
869
869
  })
870
870
  test.skip('with: inline reactive', () => {
871
- let el = h`<x :scope="{foo:'bar'}"><y :text="foo + baz"></y></x>`
871
+ let el = h`<x :with="{foo:'bar'}"><y :text="foo + baz"></y></x>`
872
872
  let baz = signal('qux')
873
873
  sprae(el, {baz})
874
874
  // FIXME: this doesn't inherit root scope baz property and instead uses hard-initialized one
@@ -877,7 +877,7 @@ test.skip('with: inline reactive', () => {
877
877
  is(el.innerHTML, `<y>barquux</y>`)
878
878
  })
879
879
  test('with: data', async () => {
880
- let el = h`<x :scope="x"><y :text="foo"></y></x>`
880
+ let el = h`<x :with="x"><y :text="foo"></y></x>`
881
881
  let state = sprae(el, {x: {foo:'bar'}})
882
882
  is(el.innerHTML, `<y>bar</y>`)
883
883
  console.log('update', state.x)
@@ -889,7 +889,7 @@ test('with: data', async () => {
889
889
  test('with: transparency', async () => {
890
890
  // NOTE: y:text initializes through directive, not through parent
891
891
  // therefore by default :text uses parent's state, not defined by element itself
892
- let el = h`<x :scope="{foo:'foo'}"><y :scope="b" :text="foo+bar"></y></x>`
892
+ let el = h`<x :with="{foo:'foo'}"><y :with="b" :text="foo+bar"></y></x>`
893
893
  let params = sprae(el, {b:{bar:'bar'}})
894
894
  is(el.innerHTML, `<y>foobar</y>`)
895
895
  params.b.bar = 'baz'
@@ -897,7 +897,7 @@ test('with: transparency', async () => {
897
897
  is(el.innerHTML, `<y>foobaz</y>`)
898
898
  })
899
899
  test.skip('with: reactive transparency', () => {
900
- let el = h`<x :scope="{foo:1}"><y :scope="b.c" :text="foo+bar"></y></x>`
900
+ let el = h`<x :with="{foo:1}"><y :with="b.c" :text="foo+bar"></y></x>`
901
901
  const bar = signal('2')
902
902
  sprae(el, {b:{c:{bar}}})
903
903
  is(el.innerHTML, `<y>12</y>`)
@@ -905,7 +905,7 @@ test.skip('with: reactive transparency', () => {
905
905
  is(el.innerHTML, `<y>13</y>`)
906
906
  })
907
907
  test('with: writes to state', async () => {
908
- let a = h`<x :scope="{a:1}"><y :on="{x(){a++}}" :text="a"></y></x>`
908
+ let a = h`<x :with="{a:1}"><y :on="{x(){a++}}" :text="a"></y></x>`
909
909
  sprae(a)
910
910
  is(a.innerHTML, `<y>1</y>`)
911
911
  a.firstChild.dispatchEvent(new window.Event('x'))
@@ -916,6 +916,21 @@ test('with: writes to state', async () => {
916
916
  is(a.innerHTML, `<y>3</y>`)
917
917
  })
918
918
 
919
+ test(':render by ref', async () => {
920
+ let a = h`<template :ref="abc"><div :text="123"></div></template><x :render="abc">456</x>`
921
+ sprae(a)
922
+ is(a.outerHTML, `<template><div :text="123"></div></template><x><div>123</div></x>`)
923
+ })
924
+
925
+ test(':render :with', async () => {
926
+ let a = h`<template :ref="abc"><div :text="text"></div></template><x :render="abc" />`
927
+ let state = sprae(a, {text:'abc'})
928
+ is(a.outerHTML, `<template><div :text="text"></div></template><x><div>abc</div></x>`)
929
+ state.text = 'def'
930
+ await tick()
931
+ is(a.outerHTML, `<template><div :text="text"></div></template><x><div>def</div></x>`)
932
+ })
933
+
919
934
  test('ref: base', async () => {
920
935
  let a = h`<a :ref="a" :init="log.push(a), null" :text="b"></a>`
921
936
  let state = sprae(a, {log:[], b:1})
@@ -959,7 +974,7 @@ test(':: scope refers to current element', async () => {
959
974
 
960
975
  test(':: scope directives must come first', async () => {
961
976
  // NOTE: we init attributes in order of definition
962
- let a = h`<x :text="y" :scope="{y:1}" :ref="x"></x>`
977
+ let a = h`<x :text="y" :with="{y:1}" :ref="x"></x>`
963
978
  sprae(a, {})
964
979
  is(a.outerHTML, `<x>1</x>`)
965
980
  })
package/test/index.html CHANGED
@@ -3,7 +3,7 @@
3
3
  <title>Test</title>
4
4
 
5
5
  <script defer src="../sprae.auto.js"></script>
6
- <div :scope="{data:1}">
6
+ <div :with="{data:1}">
7
7
  <span :text="data"></span>
8
8
  </div>
9
9
 
package/todo.md CHANGED
@@ -8,7 +8,7 @@
8
8
  * [x] combinations: :else :if
9
9
  * [x] :each :if, :if :each
10
10
  * [x] :each :each
11
- * [x] :scope must be able to write state value as well
11
+ * [x] :with must be able to write state value as well
12
12
  * [x] docs: give example to each directive
13
13
  * [x] initialize per-element: <x :each><y :if></y><x> - tree-dependent (:each comes first).
14
14
  * [x] generalize common attributes :prop="xyz"
@@ -22,7 +22,7 @@
22
22
  * [x] bulk events :ona:onb
23
23
  * [x] multiprop setter :a:b="c"
24
24
  * [x] make `this` in expression an element
25
- * ~~[x] replace :ref with :scope="this as x"~~
25
+ * ~~[x] replace :ref with :with="this as x"~~
26
26
  * [x] :ref creates instance in current state, not creates a new state
27
27
  * [x] to avoid extending signal-struct, we must collect state data before, and call updates after for extended state
28
28
  * [x] optimization: replace element-props with direct (better) setters
@@ -31,11 +31,11 @@
31
31
  * ~~[x] report usignal problem~~ author is not really interested
32
32
  * [x] `this` doesn't refer to element/scope in event handlers
33
33
  * [x] :text="" empty values shouldn't throw
34
- * [x] implement :scope
34
+ * [x] implement :with
35
35
  * [x] update :value without losing focus / position
36
36
  * ~~[x] run tiredown if element got removed from condition or loop (free memory)~~ no need just make sure no refs to elements stored
37
37
  * [x] `sprae(el, newState)` can update element's state directly (as batch!?) -> must be tested against repeats in directives
38
- * [x] :if :ref, :if :scope -> context setters must come first always
38
+ * [x] :if :ref, :if :with -> context setters must come first always
39
39
  * [x] :style="{'--x':value}"
40
40
  * [x] :onkeydown.ctrl-alt-D
41
41
  * [ ] frameworks benchmark