sprae 5.3.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/todomvc.html +14 -15
- package/package.json +2 -2
- package/r&d.md +50 -17
- package/readme.md +63 -66
- package/sprae.auto.js +25 -65
- package/sprae.auto.min.js +1 -1
- package/sprae.js +25 -65
- package/sprae.min.js +1 -1
- package/src/core.js +16 -11
- package/src/directives.js +23 -68
- package/test/{dom.js → attrs.js} +9 -325
- package/test/events.js +138 -0
- package/test/test.js +12 -1
- package/todo.md +2 -2
package/examples/todomvc.html
CHANGED
|
@@ -14,39 +14,38 @@
|
|
|
14
14
|
<section class="todoapp">
|
|
15
15
|
<header class="header">
|
|
16
16
|
<h1>todos</h1>
|
|
17
|
-
<input class="new-todo" placeholder="What needs to be done?" autofocus
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}">
|
|
17
|
+
<input class="new-todo" placeholder="What needs to be done?" autofocus @keypress.enter="
|
|
18
|
+
save(todos = [...todos, { text: this.value, done: false}])
|
|
19
|
+
this.value = ''
|
|
20
|
+
">
|
|
22
21
|
</header>
|
|
23
22
|
<section class="main">
|
|
24
|
-
<input id="toggle-all" class="toggle-all" type="checkbox"
|
|
23
|
+
<input id="toggle-all" class="toggle-all" type="checkbox" @click="
|
|
25
24
|
const all = todos.every(item => item.done)
|
|
26
25
|
save(todos = todos.map(item => (item.done = !all, item)))
|
|
27
|
-
|
|
26
|
+
">
|
|
28
27
|
<label for="toggle-all">Mark all as complete</label>
|
|
29
28
|
<ul class="todo-list">
|
|
30
29
|
<li :ref="li" :each="item in todos"
|
|
31
30
|
:class="{completed: item.done}"
|
|
32
31
|
:hidden="hash === '#/active' ? item.done : hash === '#/completed' ? !item.done : false"
|
|
33
|
-
|
|
32
|
+
@dblclick="
|
|
34
33
|
li.querySelector('.edit').focus()
|
|
35
34
|
li.classList.add('editing')
|
|
36
|
-
|
|
35
|
+
">
|
|
37
36
|
<div class="view">
|
|
38
37
|
<input class="toggle" type="checkbox"
|
|
39
38
|
:checked="item.done"
|
|
40
|
-
|
|
39
|
+
@change="item.done = !item.done, save(todos)"
|
|
41
40
|
/>
|
|
42
41
|
<label :text="item.text"></label>
|
|
43
|
-
<button class="destroy"
|
|
42
|
+
<button class="destroy" @click="save(todos = todos.filter(i => i.text !== item.text))"></button>
|
|
44
43
|
</div>
|
|
45
44
|
<input class=edit
|
|
46
45
|
:value="item.text"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
@input="item.text = this.value; save(todos)"
|
|
47
|
+
@blur="li.classList.remove('editing');"
|
|
48
|
+
@keypress.enter="this.blur()"
|
|
50
49
|
/>
|
|
51
50
|
</li>
|
|
52
51
|
</ul>
|
|
@@ -62,7 +61,7 @@
|
|
|
62
61
|
</ul>
|
|
63
62
|
<button class="clear-completed"
|
|
64
63
|
:hidden="todos.every(item => !item.done)"
|
|
65
|
-
|
|
64
|
+
@click="save(todos = todos.filter(item => !item.done ? true : false))">
|
|
66
65
|
Clear completed
|
|
67
66
|
</button>
|
|
68
67
|
</footer>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprae",
|
|
3
3
|
"description": "DOM microhydration.",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "6.1.0",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"module": "src/index.js",
|
|
7
7
|
"type": "module",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"@preact/signals": "^1.1.3",
|
|
13
13
|
"es-module-shims": "^1.6.2",
|
|
14
14
|
"esbuild": "^0.15.14",
|
|
15
|
-
"hyperf": "^1.
|
|
15
|
+
"hyperf": "^1.6.2",
|
|
16
16
|
"jsdom": "^21.1.0",
|
|
17
17
|
"signal-struct": "^1.10.0",
|
|
18
18
|
"terser": "^5.15.1",
|
package/r&d.md
CHANGED
|
@@ -381,12 +381,57 @@
|
|
|
381
381
|
+ less problem detecting `const/let` in code
|
|
382
382
|
- conflicts with regular attrs logic: the code is immediately invoked and can assign a function.
|
|
383
383
|
- `:oninput="e => (handleCaret(e), updateTimecodes())"`
|
|
384
|
+
~ `:oninput="handleCaret(event), updateTimecodes()"`
|
|
384
385
|
- `:onbeforeinput="handleBeforeInput"`
|
|
386
|
+
~ `:onbeforeinput="handleBeforeInput(event)"`
|
|
385
387
|
- `:ondragenter..ondragleave:ondragenter..ondrop="e=>(this.classList.add('w-dragover'),e=>this.classList.remove('w-dragover'))"`
|
|
388
|
+
~ `:ondragenter..ondragleave:ondragenter..ondrop="this.classList.add('w-dragover'), e=>this.classList.remove('w-dragover')"`
|
|
386
389
|
- `:ondrop="e=>console.log(e.dataTransfer.types)||e.preventDefault()"`
|
|
390
|
+
~ `:ondrop.prevent="console.log(event.dataTransfer.types)"`
|
|
387
391
|
- `:onfocus="e => (e.relatedTarget ? e.relatedTarget.focus() : e.target.blur())"`
|
|
392
|
+
~ `:onfocus="event.relatedTarget ? event.relatedTarget.focus() : event.target.blur()"`
|
|
388
393
|
- `:onpopstate.window="e => goto(e.state)"`
|
|
394
|
+
~ `:onpopstate.window="goto(event.state)"`
|
|
389
395
|
- `:onclick.toggle="play"`
|
|
396
|
+
~ `:onclick.toggle="play()"`
|
|
397
|
+
|
|
398
|
+
## [x] Should we introduce `@click` for short-notation events? -> let's keep `:onx` for raw events, `@x` for normal events
|
|
399
|
+
+ gives shorter code for majority of cases
|
|
400
|
+
+ can be non-conflicting
|
|
401
|
+
+ compatible with all frameworks (vue, alpine, lucia, lit)
|
|
402
|
+
+ gives better meaning to modifiers - moves them outside of `:` attribute
|
|
403
|
+
- multiple events `@input@change="code"` is not nice
|
|
404
|
+
~ that's fine and even meaningful
|
|
405
|
+
- chain of events `@focus..@blur="return (e)=>{}"` creates confusing `return` outside of body, as well as inconsistent chain pattern
|
|
406
|
+
? remove that pattern
|
|
407
|
+
+ it's still unsatisfactory: `@mousedown.document..@mouseup.document="e=> (isMouseDown = true, e=> isMouseDown = false)"` works, but what if we want to add `@touchstart..@touchend`, or
|
|
408
|
+
. `@click..@click="play" @keydown.alt-space..@keydown.alt-space="play"`, so in other words we need cross-reaction `@click_or_altspace..@click_or_altspace`, not just one single chain.
|
|
409
|
+
+ actually here `@click@keydown.space..@click@keydown.space` is possible, unlike `:ona..onb` case, the question is how to provide sequence in attribute
|
|
410
|
+
+ the thing is that local state, introduced by initiator events, is not useful by itself, detached from scope.
|
|
411
|
+
. the state is better reflected in data scope, rather than by initiator event.
|
|
412
|
+
? what about temporaries like `@a..@b="id=setTimeout(),()=>clearTimeout(id)"` or `@a.toggle="stop=play(),stop"`
|
|
413
|
+
. use `:with={id:null} @a="id=setTimeout()" @b="clearTimeout(id)"`
|
|
414
|
+
- introduces illicit `event` variable ~ although compatible with standard, still obscure
|
|
415
|
+
- `@` prefix is unchangeable ~ can be removed, not set, but still on the verge.
|
|
416
|
+
- `@click.toggle="code"` has same problem as `@a..@b` - how can we make code separation in attribute?
|
|
417
|
+
+ remove toggle
|
|
418
|
+
+ overall less code
|
|
419
|
+
|
|
420
|
+
## [x] Multiple chain events resolution -> redirect to main event for now
|
|
421
|
+
* Consider
|
|
422
|
+
```
|
|
423
|
+
:onclick..onclick="play"
|
|
424
|
+
:onkeydown.document.alt-space..onkeydown.document.alt-space="play"
|
|
425
|
+
```
|
|
426
|
+
* When started by click and ended by alt-space, it doesn't clear the onclick
|
|
427
|
+
* We actually want here `:onclick:onkeydown.document.alt-space..onclick:onkeydown.document.alt-space`.
|
|
428
|
+
- this makes inconsistency of `..onclick` - colon is missing
|
|
429
|
+
- also it makes precedence of `:` and `..` unclear - what comes before what after.
|
|
430
|
+
|
|
431
|
+
? Can we use `:onclick.toggle="play"`?
|
|
432
|
+
- it doesn't help with switch-over
|
|
433
|
+
? Some 'or' character `:onclick--onkeydown`
|
|
434
|
+
? We can redirect to main event, that's it for now
|
|
390
435
|
|
|
391
436
|
## [x] Should getters convert to computed? -> yes, that's relatively cheap and useful
|
|
392
437
|
|
|
@@ -513,22 +558,6 @@
|
|
|
513
558
|
5. `:x="this.x=value"`
|
|
514
559
|
+ yepyepyep
|
|
515
560
|
|
|
516
|
-
## [x] Multiple chain events resolution -> redirect to main event for now
|
|
517
|
-
* Consider
|
|
518
|
-
```
|
|
519
|
-
:onclick..onclick="play"
|
|
520
|
-
:onkeydown.document.alt-space..onkeydown.document.alt-space="play"
|
|
521
|
-
```
|
|
522
|
-
* When started by click and ended by alt-space, it doesn't clear the onclick
|
|
523
|
-
* We actually want here `:onclick:onkeydown.document.alt-space..onclick:onkeydown.document.alt-space`.
|
|
524
|
-
- this makes inconsistency of `..onclick` - colon is missing
|
|
525
|
-
- also it makes precedence of `:` and `..` unclear - what comes before what after.
|
|
526
|
-
|
|
527
|
-
? Can we use `:onclick.toggle="play"`?
|
|
528
|
-
- it doesn't help with switch-over
|
|
529
|
-
? Some 'or' character `:onclick--onkeydown`
|
|
530
|
-
? We can redirect to main event, that's it for now
|
|
531
|
-
|
|
532
561
|
## [x] Insert content by reusing the node/template -> use `:render="ref" :with="data"`
|
|
533
562
|
|
|
534
563
|
* Makes easy use of repeatable fragments, instead of web-components
|
|
@@ -584,4 +613,8 @@
|
|
|
584
613
|
## [x] Remove non-essential directives -> yep, less API friction
|
|
585
614
|
* :aria - can be defined via plain attributes
|
|
586
615
|
* :data - confusable with :scope, doesn't provide much value, can be used as `:data-x=""` etc
|
|
587
|
-
* :={} - what's the meaning? Can be replaced with multiple attributes, no? No: no easy way to spread attributes.
|
|
616
|
+
* :={} - what's the meaning? Can be replaced with multiple attributes, no? No: no easy way to spread attributes.
|
|
617
|
+
|
|
618
|
+
## [x] let/const in expressions: allow or prohibit -> let's prohibit, force user to wrap into a function himself
|
|
619
|
+
|
|
620
|
+
- allowing forces wrapping internally, which creates return statement confustion
|
package/readme.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> DOM microhydration with `:` attributes.
|
|
4
4
|
|
|
5
|
-
_Sprae_ is tiny progressive enhancement framework, a minimal essential alternative to [alpine](https://github.com/alpinejs/alpine)/[lucia](https://github.com/aidenybai/lucia), [petite-vue](https://github.com/vuejs/petite-vue) or [template-parts](https://github.com/github/template-parts) with improved ergonomics[*](#justification). It enables simple markup logic without external scripts. Perfect for small websites, prototypes or UI logic.
|
|
5
|
+
_Sprae_ is tiny progressive enhancement framework, a minimal essential alternative to [alpine](https://github.com/alpinejs/alpine)/[lucia](https://github.com/aidenybai/lucia), [petite-vue](https://github.com/vuejs/petite-vue) or [template-parts](https://github.com/github/template-parts) with improved ergonomics[*](#justification--alternatives). It enables simple markup logic without external scripts. Perfect for small websites, prototypes or UI logic.
|
|
6
6
|
|
|
7
7
|
## Use
|
|
8
8
|
|
|
@@ -116,63 +116,6 @@ Set value of an input, textarea or select. Takes handle of `checked` and `select
|
|
|
116
116
|
</select>
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
#### `:<prop>="value?"`
|
|
120
|
-
|
|
121
|
-
Set any attribute value or run an effect.
|
|
122
|
-
|
|
123
|
-
```html
|
|
124
|
-
<!-- Single property -->
|
|
125
|
-
<label :for="name" :text="name" />
|
|
126
|
-
|
|
127
|
-
<!-- Multiple properties -->
|
|
128
|
-
<input :id:name="name" />
|
|
129
|
-
|
|
130
|
-
<!-- Effect (triggers any time bar changes) -->
|
|
131
|
-
<div :fx="void bar()" ></div>
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
#### `:="props?"`
|
|
135
|
-
|
|
136
|
-
Spread multiple attibures.
|
|
137
|
-
|
|
138
|
-
```html
|
|
139
|
-
<input :="{ id: name, name, type:'text', value }" />
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
#### `:on<event>="handler"`, `:on<in>..on<out>="handler"`
|
|
143
|
-
|
|
144
|
-
Add event listener or events chain.
|
|
145
|
-
|
|
146
|
-
```html
|
|
147
|
-
<!-- Single event -->
|
|
148
|
-
<input type="checkbox" :onchange="e => isChecked=e.target.value">
|
|
149
|
-
|
|
150
|
-
<!-- Multiple events -->
|
|
151
|
-
<input :value="text" :oninput:onchange="e => text=e.target.value">
|
|
152
|
-
|
|
153
|
-
<!-- Sequence of events -->
|
|
154
|
-
<button :onfocus..onblur="e => {
|
|
155
|
-
// onfocus
|
|
156
|
-
return e => {
|
|
157
|
-
// onblur
|
|
158
|
-
}
|
|
159
|
-
}">
|
|
160
|
-
|
|
161
|
-
<!-- Event modifiers -->
|
|
162
|
-
<button :onclick.throttle-500="handler">Not too often</button>
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
##### Event modifiers
|
|
166
|
-
|
|
167
|
-
* `.once`, `.passive`, `.capture` – listener [options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options).
|
|
168
|
-
* `.prevent`, `.stop` – prevent default or stop propagation.
|
|
169
|
-
* `.window`, `.document`, `.outside`, `.self` – specify event target.
|
|
170
|
-
* `.throttle-<ms>`, `.debounce-<ms>` – defer function call with one of the methods.
|
|
171
|
-
* `.toggle` – run function and its result in turn.
|
|
172
|
-
* `.ctrl`, `.shift`, `.alt`, `.meta`, `.arrow`, `.enter`, `.escape`, `.tab`, `.space`, `.backspace`, `.delete`, `.digit`, `.letter`, `.character` – filter by [`event.key`](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values).
|
|
173
|
-
* `.ctrl-<key>, .alt-<key>, .meta-<key>, .shift-<key>` – key combinations, eg. `.ctrl-alt-delete` or `.meta-x`.
|
|
174
|
-
* `.*` – any other modifier has no effect, but allows binding multiple handlers to same event (like jQuery event classes).
|
|
175
|
-
|
|
176
119
|
#### `:with="data"`
|
|
177
120
|
|
|
178
121
|
Define or extend data scope for a subtree.
|
|
@@ -197,8 +140,8 @@ Include template as element content.
|
|
|
197
140
|
```html
|
|
198
141
|
<template :ref="foo"><span :text="foo"></span></template>
|
|
199
142
|
|
|
200
|
-
<div :render="foo" :with="{foo:'bar'}"
|
|
201
|
-
<div :render="foo" :with="{foo:'baz'}"
|
|
143
|
+
<div :render="foo" :with="{foo:'bar'}">...inserted here...</div>
|
|
144
|
+
<div :render="foo" :with="{foo:'baz'}">...inserted here...</div>
|
|
202
145
|
```
|
|
203
146
|
|
|
204
147
|
#### `:ref="id"`
|
|
@@ -213,18 +156,73 @@ Expose element to current data scope with the `id`:
|
|
|
213
156
|
<!-- iterable items -->
|
|
214
157
|
<ul>
|
|
215
158
|
<li :each="item in items" :ref="item">
|
|
216
|
-
<input
|
|
159
|
+
<input @focus="item.classList.add('editing')" @blur="item.classList.remove('editing')"/>
|
|
217
160
|
</li>
|
|
218
161
|
</ul>
|
|
219
162
|
```
|
|
220
163
|
|
|
164
|
+
#### `:="props?"`
|
|
165
|
+
|
|
166
|
+
Spread multiple attibures.
|
|
167
|
+
|
|
168
|
+
```html
|
|
169
|
+
<input :="{ id: name, name, type:'text', value }" />
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### `:<prop>="value?"`
|
|
173
|
+
|
|
174
|
+
Set any attribute value or run an effect.
|
|
175
|
+
|
|
176
|
+
```html
|
|
177
|
+
<!-- Single property -->
|
|
178
|
+
<label :for="name" :text="name" />
|
|
179
|
+
|
|
180
|
+
<!-- Multiple properties -->
|
|
181
|
+
<input :id:name="name" />
|
|
182
|
+
|
|
183
|
+
<!-- Effect - returns undefined, triggers any time bar changes -->
|
|
184
|
+
<div :fx="void bar()" ></div>
|
|
185
|
+
|
|
186
|
+
<!-- Raw event listener (see events) -->
|
|
187
|
+
<div :onclick="e=>e.preventDefault()"></div>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
## Events
|
|
192
|
+
|
|
193
|
+
#### `@<event>="handle"`, `@<foo>@<bar>.<baz>="handle"`
|
|
194
|
+
|
|
195
|
+
Attach event(s) listener with possible modifiers. `event` variable holds current event.
|
|
196
|
+
|
|
197
|
+
```html
|
|
198
|
+
<!-- Single event -->
|
|
199
|
+
<input type="checkbox" @change="isChecked = event.target.value">
|
|
200
|
+
|
|
201
|
+
<!-- Multiple events -->
|
|
202
|
+
<input :value="text" @input@change="text = event.target.value">
|
|
203
|
+
|
|
204
|
+
<!-- Event modifiers -->
|
|
205
|
+
<button @click.throttle-500="handler(event)">Not too often</button>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
##### Event modifiers
|
|
209
|
+
|
|
210
|
+
* `.once`, `.passive`, `.capture` – listener [options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options).
|
|
211
|
+
* `.prevent`, `.stop` – prevent default or stop propagation.
|
|
212
|
+
* `.window`, `.document`, `.outside`, `.self` – specify event target.
|
|
213
|
+
* `.throttle-<ms>`, `.debounce-<ms>` – defer function call with one of the methods.
|
|
214
|
+
* `.ctrl`, `.shift`, `.alt`, `.meta`, `.arrow`, `.enter`, `.escape`, `.tab`, `.space`, `.backspace`, `.delete`, `.digit`, `.letter`, `.character` – filter by [`event.key`](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values).
|
|
215
|
+
* `.ctrl-<key>, .alt-<key>, .meta-<key>, .shift-<key>` – key combinations, eg. `.ctrl-alt-delete` or `.meta-x`.
|
|
216
|
+
* `.*` – any other modifier has no effect, but allows binding multiple handlers to same event (like jQuery event classes).
|
|
217
|
+
|
|
218
|
+
|
|
221
219
|
## Sandbox
|
|
222
220
|
|
|
223
221
|
Expressions are sandboxed, ie. have no access to global or window (since sprae can be run in server environment).
|
|
224
222
|
|
|
225
223
|
```html
|
|
226
|
-
<div :x="
|
|
227
|
-
<!--
|
|
224
|
+
<div :x="global.x"></div>
|
|
225
|
+
<!-- global is undefined -->
|
|
228
226
|
```
|
|
229
227
|
|
|
230
228
|
Default sandbox provides: _window_, _document_, _console_, _history_, _location_, _Array_, _Object_, _Number_, _String_, _Boolean_, _Date_, _Set_, _Map_.<br/>
|
|
@@ -247,8 +245,8 @@ To avoid _flash of unstyled content_, you can hide sprae attribute or add a cust
|
|
|
247
245
|
|
|
248
246
|
## Justification & alternatives
|
|
249
247
|
|
|
250
|
-
* [Template-parts](https://github.com/dy/template-parts) / [templize](https://github.com/dy/templize) is progressive, but is stuck with native HTML quirks ([parsing table](https://github.com/github/template-parts/issues/24), [SVG attributes](https://github.com/github/template-parts/issues/25), [liquid syntax](https://shopify.github.io/liquid/tags/template/#raw) conflict etc). Also ergonomics of `attr="{{}}"` is inferior to `:attr=""` since it creates flash of uninitialized values.
|
|
251
|
-
* [Alpine](https://github.com/alpinejs/alpine) / [vue](https://github.com/vuejs/petite-vue) / [lit](https://github.com/lit/lit/tree/main/packages/lit-html) / [lucia](https://github.com/aidenybai/lucia)
|
|
248
|
+
* [Template-parts](https://github.com/dy/template-parts) / [templize](https://github.com/dy/templize) is progressive, but is stuck with native HTML quirks ([parsing table](https://github.com/github/template-parts/issues/24), [SVG attributes](https://github.com/github/template-parts/issues/25), [liquid syntax](https://shopify.github.io/liquid/tags/template/#raw) conflict etc). Also ergonomics of `attr="{{}}"` is inferior to `:attr=""` since it creates flash of uninitialized values. Also it's just nice to keep `{{}}` generic, regardless of markup, and attributes as part of markup.
|
|
249
|
+
* [Alpine](https://github.com/alpinejs/alpine) / [vue](https://github.com/vuejs/petite-vue) / [lit](https://github.com/lit/lit/tree/main/packages/lit-html) / [lucia](https://github.com/aidenybai/lucia) escape native HTML quirks, but the syntax space (`:attr`, `v-*`,`x-*`, `l-*` `@evt`, `{{}}`) is too broad, as well as functionality. Perfection is when there's nothing to take away, not add (c). Also they tend to [self-encapsulate](https://github.com/alpinejs/alpine/discussions/3223) making interop hard, invent own tooling or complex reactivity.
|
|
252
250
|
* React/[preact](https://ghub.io/preact) does the job wiring up JS to HTML, but with an extreme of migrating HTML to JSX and enforcing SPA, which is not organic for HTML. Also it doesn't support reactive fields (needs render call).
|
|
253
251
|
|
|
254
252
|
_Sprae_ takes idea of _templize directives_/_alpine_/_vue_ attrs and builds upon <del>[_@preact/signals_](https://ghub.io/@preact/signals)</del> simple reactive state.
|
|
@@ -260,7 +258,6 @@ _Sprae_ takes idea of _templize directives_/_alpine_/_vue_ attrs and builds upon
|
|
|
260
258
|
* It reserves minimal syntax space as `:` convention (keeping tree neatly decorated, not scattered).
|
|
261
259
|
* Expressions are naturally reactive and incur minimal updates.
|
|
262
260
|
* Elements / data API is open and enable easy interop.
|
|
263
|
-
* It's just nice separation: attributes belong to client and template fields to server.
|
|
264
261
|
|
|
265
262
|
It is reminiscent of [XSLT](https://www.w3schools.com/xml/xsl_intro.asp), considered a [buried treasure](https://github.com/bahrus/be-restated) by web-connoisseurs.
|
|
266
263
|
|
package/sprae.auto.js
CHANGED
|
@@ -376,19 +376,6 @@
|
|
|
376
376
|
} : (value) => el.value = value;
|
|
377
377
|
return (state2) => update(evaluate(state2));
|
|
378
378
|
};
|
|
379
|
-
secondary["on"] = (el, expr) => {
|
|
380
|
-
let evaluate = parseExpr(el, expr, ":on");
|
|
381
|
-
return (state2) => {
|
|
382
|
-
let listeners = evaluate(state2);
|
|
383
|
-
let offs = [];
|
|
384
|
-
for (let evt in listeners)
|
|
385
|
-
offs.push(on(el, evt, listeners[evt]));
|
|
386
|
-
return () => {
|
|
387
|
-
for (let off of offs)
|
|
388
|
-
off();
|
|
389
|
-
};
|
|
390
|
-
};
|
|
391
|
-
};
|
|
392
379
|
var directives_default = (el, expr, state2, name) => {
|
|
393
380
|
let evt = name.startsWith("on") && name.slice(2);
|
|
394
381
|
let evaluate = parseExpr(el, expr, ":" + name);
|
|
@@ -402,43 +389,20 @@
|
|
|
402
389
|
};
|
|
403
390
|
return (state3) => attr(el, name, evaluate(state3));
|
|
404
391
|
};
|
|
405
|
-
var on = (
|
|
406
|
-
if (!
|
|
392
|
+
var on = (el, e, fn) => {
|
|
393
|
+
if (!fn)
|
|
407
394
|
return;
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
let off;
|
|
420
|
-
let curListener = (e) => {
|
|
421
|
-
if (cur)
|
|
422
|
-
off();
|
|
423
|
-
let nextFn = fn.call(target, e);
|
|
424
|
-
if (typeof nextFn !== "function")
|
|
425
|
-
nextFn = () => {
|
|
426
|
-
};
|
|
427
|
-
if (cur + 1 < ctxs.length)
|
|
428
|
-
onFn(nextFn, !cur ? 1 : cur + 1);
|
|
429
|
-
};
|
|
430
|
-
return off = addListenerWithMods(curListener, ctxs[cur]);
|
|
431
|
-
};
|
|
432
|
-
let rootOff = onFn(origFn);
|
|
433
|
-
return () => rootOff();
|
|
434
|
-
function addListenerWithMods(fn, { evt: evt2, target: target2, test, defer, stop, prevent, ...opts }) {
|
|
435
|
-
if (defer)
|
|
436
|
-
fn = defer(fn);
|
|
437
|
-
let cb = (e) => test(e) && (stop && e.stopPropagation(), prevent && e.preventDefault(), fn.call(target2, e));
|
|
438
|
-
target2.addEventListener(evt2, cb, opts);
|
|
439
|
-
return () => target2.removeEventListener(evt2, cb, opts);
|
|
440
|
-
}
|
|
441
|
-
;
|
|
395
|
+
const ctx = { evt: "", target: el, test: () => true };
|
|
396
|
+
ctx.evt = (e.startsWith("on") ? e.slice(2) : e).replace(
|
|
397
|
+
/\.(\w+)?-?([-\w]+)?/g,
|
|
398
|
+
(match, mod, param = "") => (ctx.test = mods[mod]?.(ctx, ...param.split("-")) || ctx.test, "")
|
|
399
|
+
);
|
|
400
|
+
const { evt, target, test, defer, stop, prevent, ...opts } = ctx;
|
|
401
|
+
if (defer)
|
|
402
|
+
fn = defer(fn);
|
|
403
|
+
const cb = (e2) => test(e2) && (stop && e2.stopPropagation(), prevent && e2.preventDefault(), fn.call(target, e2));
|
|
404
|
+
target.addEventListener(evt, cb, opts);
|
|
405
|
+
return () => target.removeEventListener(evt, cb, opts);
|
|
442
406
|
};
|
|
443
407
|
var mods = {
|
|
444
408
|
prevent(ctx) {
|
|
@@ -462,9 +426,6 @@
|
|
|
462
426
|
document(ctx) {
|
|
463
427
|
ctx.target = document;
|
|
464
428
|
},
|
|
465
|
-
toggle(ctx) {
|
|
466
|
-
ctx.defer = (fn, out) => (e) => out ? (out.call?.(ctx.target, e), out = null) : out = fn();
|
|
467
|
-
},
|
|
468
429
|
throttle(ctx, limit) {
|
|
469
430
|
ctx.defer = (fn) => throttle(fn, limit ? Number(limit) || 0 : 108);
|
|
470
431
|
},
|
|
@@ -549,9 +510,8 @@
|
|
|
549
510
|
function parseExpr(el, expression, dir) {
|
|
550
511
|
let evaluate = evaluatorMemo[expression];
|
|
551
512
|
if (!evaluate) {
|
|
552
|
-
let rightSideSafeExpression = /^[\n\s]*if.*\(.*\)/.test(expression) || /\b(let|const)\s/.test(expression) && !dir.startsWith(":on") ? `(() => {${expression}})()` : expression;
|
|
553
513
|
try {
|
|
554
|
-
evaluate = evaluatorMemo[expression] = new Function(`__scope`, `with (__scope) { return ${
|
|
514
|
+
evaluate = evaluatorMemo[expression] = new Function(`__scope`, `with (__scope) { return ${expression.trim()} };`);
|
|
555
515
|
} catch (e) {
|
|
556
516
|
return exprError(e, el, expression, dir);
|
|
557
517
|
}
|
|
@@ -606,18 +566,18 @@ ${directive}=${expression ? `"${expression}"
|
|
|
606
566
|
}
|
|
607
567
|
if (el.attributes) {
|
|
608
568
|
for (let i = 0; i < el.attributes.length; ) {
|
|
609
|
-
let attr2 = el.attributes[i];
|
|
610
|
-
if (
|
|
569
|
+
let attr2 = el.attributes[i], prefix = attr2.name[0];
|
|
570
|
+
if (prefix === ":" || prefix === "@") {
|
|
571
|
+
el.removeAttribute(attr2.name);
|
|
572
|
+
let expr = prefix === "@" ? `event=>{${attr2.value}}` : attr2.value, names = attr2.name.slice(1).split(prefix);
|
|
573
|
+
for (let name of names) {
|
|
574
|
+
if (prefix === "@")
|
|
575
|
+
name = `on` + name;
|
|
576
|
+
let dir = secondary[name] || directives_default;
|
|
577
|
+
updates.push(dir(el, expr, state2, name));
|
|
578
|
+
}
|
|
579
|
+
} else
|
|
611
580
|
i++;
|
|
612
|
-
continue;
|
|
613
|
-
}
|
|
614
|
-
el.removeAttribute(attr2.name);
|
|
615
|
-
let expr = attr2.value;
|
|
616
|
-
let attrNames = attr2.name.slice(1).split(":");
|
|
617
|
-
for (let attrName of attrNames) {
|
|
618
|
-
let dir = secondary[attrName] || directives_default;
|
|
619
|
-
updates.push(dir(el, expr, state2, attrName));
|
|
620
|
-
}
|
|
621
581
|
}
|
|
622
582
|
}
|
|
623
583
|
for (let i = 0, child; child = el.children[i]; i++) {
|
package/sprae.auto.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(()=>{var e,t,r=Promise.prototype.then.bind(Promise.resolve()),n=new WeakMap,l=class extends Map{#e=new FinalizationRegistry((e=>super.delete(e)));get size(){return[...this].length}constructor(e=[]){super();for(const[t,r]of e)this.set(t,r)}get(e){return super.get(e)?.deref()}set(e,t){let r=super.get(e);return r&&this.#e.unregister(r),r=(e=>n.get(e)||(e=>{const t=new WeakRef(e);return n.set(e,t),t})(e))(t),this.#e.register(t,e,r),super.set(e,r)}},s=new Set,i=new WeakMap,o=new WeakMap,a=new WeakMap,u=Symbol("parent"),c={Array:Array,Object:Object,Number:Number,String:String,Boolean:Boolean,Date:Date,console:console,window:window,document:document,history:history,location:location},f={has:()=>!0,get(t,r){if("symbol"==typeof r)return t[r];if(!(r in t))return t[u]?.[r];if(r in Object.prototype||Array.isArray(t)&&r in Array.prototype&&"length"!==r)return t[r];let n=t[r];if(e){let n=i.get(t);n||i.set(t,n={}),n[r]?n[r].includes(e)||n[r].push(e):n[r]=[e]}if(n&&n.constructor===Object||Array.isArray(n)){let e=o.get(n);return e||o.set(n,e=new Proxy(n,f)),e}return n},set(e,t,r){if(!(t in e)&&e[u]&&t in e[u])return e[u][t]=r;if(!Array.isArray(e)&&Object.is(e[t],r))return!0;e[t]=r;let n=i.get(e)?.[t];if(n)for(let e of n)s.add(e);return h(),!0},deleteProperty:(e,t)=>(e[t]=void 0,delete e[t],!0)},p=(e,t)=>{if(o.has(e))return o.get(e);if(a.has(e))return e;let r=new Proxy(e,f);o.set(e,r),a.set(r,e);let n=Object.getOwnPropertyDescriptors(e);for(let t in n){let l=n[t];l.get&&l.get&&(l.get=l.get.bind(r),Object.defineProperty(e,t,l))}return e[u]=t?p(t):c,r},y=t=>{const r=()=>{let n=e;e=r,t(),e=n};return r(),r},h=()=>{t||(t=!0,r((()=>{for(let e of s)e.call();s.clear(),t=!1})))},
|
|
1
|
+
(()=>{var e,t,r=Promise.prototype.then.bind(Promise.resolve()),n=new WeakMap,l=class extends Map{#e=new FinalizationRegistry((e=>super.delete(e)));get size(){return[...this].length}constructor(e=[]){super();for(const[t,r]of e)this.set(t,r)}get(e){return super.get(e)?.deref()}set(e,t){let r=super.get(e);return r&&this.#e.unregister(r),r=(e=>n.get(e)||(e=>{const t=new WeakRef(e);return n.set(e,t),t})(e))(t),this.#e.register(t,e,r),super.set(e,r)}},s=new Set,i=new WeakMap,o=new WeakMap,a=new WeakMap,u=Symbol("parent"),c={Array:Array,Object:Object,Number:Number,String:String,Boolean:Boolean,Date:Date,console:console,window:window,document:document,history:history,location:location},f={has:()=>!0,get(t,r){if("symbol"==typeof r)return t[r];if(!(r in t))return t[u]?.[r];if(r in Object.prototype||Array.isArray(t)&&r in Array.prototype&&"length"!==r)return t[r];let n=t[r];if(e){let n=i.get(t);n||i.set(t,n={}),n[r]?n[r].includes(e)||n[r].push(e):n[r]=[e]}if(n&&n.constructor===Object||Array.isArray(n)){let e=o.get(n);return e||o.set(n,e=new Proxy(n,f)),e}return n},set(e,t,r){if(!(t in e)&&e[u]&&t in e[u])return e[u][t]=r;if(!Array.isArray(e)&&Object.is(e[t],r))return!0;e[t]=r;let n=i.get(e)?.[t];if(n)for(let e of n)s.add(e);return h(),!0},deleteProperty:(e,t)=>(e[t]=void 0,delete e[t],!0)},p=(e,t)=>{if(o.has(e))return o.get(e);if(a.has(e))return e;let r=new Proxy(e,f);o.set(e,r),a.set(r,e);let n=Object.getOwnPropertyDescriptors(e);for(let t in n){let l=n[t];l.get&&l.get&&(l.get=l.get.bind(r),Object.defineProperty(e,t,l))}return e[u]=t?p(t):c,r},y=t=>{const r=()=>{let n=e;e=r,t(),e=n};return r(),r},h=()=>{t||(t=!0,r((()=>{for(let e of s)e.call();s.clear(),t=!1})))},d={},g={};d.if=(e,t)=>{let r=document.createTextNode(""),n=[j(e,t,":if")],l=[e],s=e;for(;(s=e.nextElementSibling)&&s.hasAttribute(":else");)s.removeAttribute(":else"),(t=s.getAttribute(":if"))?(s.removeAttribute(":if"),s.remove(),l.push(s),n.push(j(e,t,":else :if"))):(s.remove(),l.push(s),n.push((()=>1)));return e.replaceWith(s=r),e=>{let t=n.findIndex((t=>t(e)));l[t]!=s&&((s[m]||s).replaceWith(s=l[t]||r),O(s,e))}};var m=Symbol(":each");d.each=(e,t)=>{let r=function(e){let t=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,r=e.match(/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/);if(!r)return;let n=r[2].trim(),l=r[1].replace(/^\s*\(|\)\s*$/g,"").trim(),s=l.match(t);return s?[l.replace(t,"").trim(),s[1].trim(),n]:[l,"",n]}(t);if(!r)return E(new Error,e,t,":each");const n=e[m]=document.createTextNode("");e.replaceWith(n);const s=j(e,r[2],":each"),i=e.getAttribute(":key"),o=i?j(null,i,":each"):null;e.removeAttribute(":key");const a=e.getAttribute(":ref"),u=new l,c=new l;let f=[];return l=>{let i=s(l);i?"number"==typeof i?i=Array.from({length:i},((e,t)=>[t+1,t])):Array.isArray(i)?i=i.map(((e,t)=>[t+1,e])):"object"==typeof i?i=Object.entries(i):E(Error("Bad list value"),e,t,":each"):i=[];let y=[],h=[];for(let[t,n]of i){let s,i,f=o?.({[r[0]]:n,[r[1]]:t});null==f?s=e.cloneNode(!0):(s=c.get(f))||c.set(f,s=e.cloneNode(!0)),y.push(s),null!=f&&(i=u.get(f))?i[r[0]]=n:(i=p({[r[0]]:n,[a||""]:null,[r[1]]:t},l),null!=f&&u.set(f,i)),h.push(i)}!function(e,t,r,n){const l=new Map,s=new Map;let i,o;for(i=0;i<t.length;i++)l.set(t[i],i);for(i=0;i<r.length;i++)s.set(r[i],i);for(i=o=0;i!==t.length||o!==r.length;){var a=t[i],u=r[o];if(null===a)i++;else if(r.length<=o)e.removeChild(t[i]),i++;else if(t.length<=i)e.insertBefore(u,t[i]||n),o++;else if(a===u)i++,o++;else{var c=s.get(a),f=l.get(u);void 0===c?(e.removeChild(t[i]),i++):void 0===f?(e.insertBefore(u,t[i]||n),o++):(e.insertBefore(t[f],t[i]||n),t[f]=null,f>i+1&&i++,o++)}}}(n.parentNode,f,y,n),f=y;for(let e=0;e<y.length;e++)O(y[e],h[e])}},d.with=(e,t,r)=>{const n=j(e,t,":with")(r);O(e,p(n,r))},d.ref=(e,t,r)=>{r[t]=e},g.render=(e,t,r)=>{let n=j(e,t,":render")(r);n||E(new Error("Template not found"),e,t,":render");let l=n.content.cloneNode(!0);e.replaceChildren(l),O(e,r)},g.id=(e,t)=>{let r=j(e,t,":id");return t=>{return n=r(t),e.id=n||0===n?n:"";var n}},g.class=(e,t)=>{let r=j(e,t,":class"),n=e.getAttribute("class");return t=>{let l=r(t),s=[n];l&&("string"==typeof l?s.push(l):Array.isArray(l)?s.push(...l):s.push(...Object.entries(l).map((([e,t])=>t?e:"")))),(s=s.filter(Boolean).join(" "))?e.setAttribute("class",s):e.removeAttribute("class")}},g.style=(e,t)=>{let r=j(e,t,":style"),n=e.getAttribute("style")||"";return n.endsWith(";")||(n+="; "),t=>{let l=r(t);if("string"==typeof l)e.setAttribute("style",n+l);else{e.setAttribute("style",n);for(let t in l)e.style.setProperty(t,l[t])}}},g.text=(e,t)=>{let r=j(e,t,":text");return t=>{let n=r(t);e.textContent=null==n?"":n}},g[""]=(e,t)=>{let r=j(e,t,":");if(r)return t=>{let n=r(t);for(let t in n)x(e,t.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g,(e=>"-"+e.toLowerCase())),n[t])}},g.value=(e,t)=>{let r,n,l=j(e,t,":value"),s="text"===e.type||""===e.type?t=>e.setAttribute("value",e.value=null==t?"":t):"TEXTAREA"===e.tagName||"text"===e.type||""===e.type?t=>(r=e.selectionStart,n=e.selectionEnd,e.setAttribute("value",e.value=null==t?"":t),r&&e.setSelectionRange(r,n)):"checkbox"===e.type?t=>(e.value=t?"on":"",x(e,"checked",t)):"select-one"===e.type?t=>{for(let t in e.options)t.removeAttribute("selected");e.value=t,e.selectedOptions[0]?.setAttribute("selected","")}:t=>e.value=t;return e=>s(l(e))};var b=(e,t,r,n)=>{let l=n.startsWith("on")&&n.slice(2),s=j(e,t,":"+n);if(s)return l?t=>{let r=s(t)||(()=>{});return v(e,l,r)}:t=>x(e,n,s(t))},v=(e,t,r)=>{if(!r)return;const n={evt:"",target:e,test:()=>!0};n.evt=(t.startsWith("on")?t.slice(2):t).replace(/\.(\w+)?-?([-\w]+)?/g,((e,t,r="")=>(n.test=A[t]?.(n,...r.split("-"))||n.test,"")));const{evt:l,target:s,test:i,defer:o,stop:a,prevent:u,...c}=n;o&&(r=o(r));const f=e=>i(e)&&(a&&e.stopPropagation(),u&&e.preventDefault(),r.call(s,e));return s.addEventListener(l,f,c),()=>s.removeEventListener(l,f,c)},A={prevent(e){e.prevent=!0},stop(e){e.stop=!0},once(e){e.once=!0},passive(e){e.passive=!0},capture(e){e.capture=!0},window(e){e.target=window},document(e){e.target=document},throttle(e,t){e.defer=e=>k(e,t?Number(t)||0:108)},debounce(e,t){e.defer=e=>W(e,t?Number(t)||0:108)},outside:e=>t=>{let r=e.target;return!(r.contains(t.target)||!1===t.target.isConnected||r.offsetWidth<1&&r.offsetHeight<1)},self:e=>t=>t.target===e.target,ctrl:(e,...t)=>e=>w.ctrl(e)&&t.every((t=>w[t]?w[t](e):e.key===t)),shift:(e,...t)=>e=>w.shift(e)&&t.every((t=>w[t]?w[t](e):e.key===t)),alt:(e,...t)=>e=>w.alt(e)&&t.every((t=>w[t]?w[t](e):e.key===t)),meta:(e,...t)=>e=>w.meta(e)&&t.every((t=>w[t]?w[t](e):e.key===t)),arrow:e=>w.arrow,enter:e=>w.enter,escape:e=>w.escape,tab:e=>w.tab,space:e=>w.space,backspace:e=>w.backspace,delete:e=>w.delete,digit:e=>w.digit,letter:e=>w.letter,character:e=>w.character},w={ctrl:e=>e.ctrlKey||"Control"===e.key||"Ctrl"===e.key,shift:e=>e.shiftKey||"Shift"===e.key,alt:e=>e.altKey||"Alt"===e.key,meta:e=>e.metaKey||"Meta"===e.key||"Command"===e.key,arrow:e=>e.key.startsWith("Arrow"),enter:e=>"Enter"===e.key,escape:e=>e.key.startsWith("Esc"),tab:e=>"Tab"===e.key,space:e=>" "===e.key||"Space"===e.key||" "===e.key,backspace:e=>"Backspace"===e.key,delete:e=>"Delete"===e.key,digit:e=>/^\d$/.test(e.key),letter:e=>/^[a-zA-Z]$/.test(e.key),character:e=>/^\S$/.test(e.key)},k=(e,t)=>{let r,n,l=s=>{r=!0,setTimeout((()=>{if(r=!1,n)return n=!1,l(s),e(s)}),t)};return t=>r?n=!0:(l(t),e(t))},W=(e,t)=>{let r;return n=>{clearTimeout(r),r=setTimeout((()=>{r=null,e(n)}),t)}},x=(e,t,r)=>{null==r||!1===r?e.removeAttribute(t):e.setAttribute(t,!0===r?"":"number"==typeof r||"string"==typeof r?r:"")},S={};function j(e,t,r){let n=S[t];if(!n)try{n=S[t]=new Function("__scope",`with (__scope) { return ${t.trim()} };`)}catch(n){return E(n,e,t,r)}return l=>{let s;try{s=n.call(e,l)}catch(n){return E(n,e,t,r)}return s}}function E(e,t,n,l){Object.assign(e,{element:t,expression:n}),console.warn(`∴ ${e.message}\n\n${l}=${n?`"${n}"\n\n`:""}`,t),r((()=>{throw e}),0)}O.globals=c;var N=new WeakMap;function O(e,t){if(!e.children)return;if(N.has(e))return Object.assign(N.get(e),t);const r=p(t||{}),n=[],l=(e,t=e.parentNode)=>{for(let l in d){let s=":"+l;if(e.hasAttribute?.(s)){let i=e.getAttribute(s);if(e.removeAttribute(s),n.push(d[l](e,i,r,l)),N.has(e))return;if(e.parentNode!==t)return!1}}if(e.attributes)for(let t=0;t<e.attributes.length;){let l=e.attributes[t],s=l.name[0];if(":"===s||"@"===s){e.removeAttribute(l.name);let t="@"===s?`event=>{${l.value}}`:l.value,i=l.name.slice(1).split(s);for(let l of i){"@"===s&&(l="on"+l);let i=g[l]||b;n.push(i(e,t,r,l))}}else t++}for(let t,r=0;t=e.children[r];r++)!1===l(t,e)&&r--};l(e);for(let e of n)if(e){let t;y((()=>{"function"==typeof t&&t(),t=e(r)}))}return N.set(e,r),r}document.currentScript&&O(document.documentElement)})();
|