sprae 0.0.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 +12 -8
- package/plan.md +6 -0
- package/r&d.md +63 -3
- package/readme.md +79 -65
- package/sprae.js +317 -0
- package/sprae.min.js +1 -0
- package/src/core.js +95 -35
- package/src/directives/aria.js +10 -0
- package/src/directives/common.js +15 -0
- package/src/directives/data.js +10 -0
- package/src/directives/each.js +53 -0
- package/src/directives/if.js +31 -0
- package/src/directives/index.js +11 -0
- package/src/directives/on.js +12 -0
- package/src/directives/prop.js +10 -0
- package/src/directives/text.js +10 -0
- package/src/directives/value.js +26 -0
- package/src/directives/with.js +21 -0
- package/src/index.js +12 -2
- package/test/index.html +24 -0
- package/test/test.js +303 -0
- package/src/dirs.js +0 -43
- package/src/eval.js +0 -33
- package/test.js +0 -84
package/package.json
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprae",
|
|
3
3
|
"description": "Reactive directives with expressions for DOM microtemplating.",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"main": "sprae.js",
|
|
6
|
+
"type": "module",
|
|
6
7
|
"dependencies": {
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"htm": "^3.1.1",
|
|
10
|
-
"swapdom": "^1.1.1",
|
|
11
|
-
"preact": "^10.11.2"
|
|
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",
|
|
14
|
+
"es-module-shims": "^1.6.2",
|
|
15
|
+
"esbuild": "^0.15.14",
|
|
15
16
|
"hyperf": "^1.4.0",
|
|
17
|
+
"terser": "^5.15.1",
|
|
16
18
|
"tst": "^7.1.1",
|
|
17
19
|
"value-ref": "^2.1.0",
|
|
18
20
|
"wait-please": "^3.1.0"
|
|
19
21
|
},
|
|
20
22
|
"scripts": {
|
|
21
|
-
"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"
|
|
22
26
|
},
|
|
23
27
|
"repository": {
|
|
24
28
|
"type": "git",
|
|
25
|
-
"url": "git+https://github.com/dy/
|
|
29
|
+
"url": "git+https://github.com/dy/sprae.git"
|
|
26
30
|
},
|
|
27
31
|
"keywords": [
|
|
28
32
|
"preact",
|
package/plan.md
ADDED
package/r&d.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
## name
|
|
1
|
+
## [x] name -> sprae
|
|
2
2
|
|
|
3
3
|
* rasa
|
|
4
4
|
* dew
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
+ better assoc with hydration
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
## :attr, :data, :id, :class, :style, :on, :aria - do we enforce JS
|
|
37
|
+
## [x] :attr, :data, :id, :class, :style, :on, :aria - do we enforce JS syntax or support unscoped expression? -> Use JS convention, too many use-cases.
|
|
38
38
|
|
|
39
39
|
1. JS object
|
|
40
40
|
+ JS object can directly set value as `:props="obj"`
|
|
@@ -48,4 +48,64 @@
|
|
|
48
48
|
+ We anyways introduce custom-ish expression in `:each="item in items"`
|
|
49
49
|
- Vue introduces simple-ish parsing for that
|
|
50
50
|
+ Custom expressions are shorter: `:attr="a:1, b:2, c:3"`
|
|
51
|
-
- Custom expressions are confusing for style: `:style="a:1, b:2, c:3"` - very similar to direct style string
|
|
51
|
+
- Custom expressions are confusing for style: `:style="a:1, b:2, c:3"` - very similar to direct style string
|
|
52
|
+
|
|
53
|
+
## [x] Attribute directive: `:={a:1}` vs `:attr={a:1}` vs `:prop={a:1}`
|
|
54
|
+
|
|
55
|
+
+ `:=obj` reminds pascal assignment operator, which is cool
|
|
56
|
+
+ `:={a:1,b:2}` is natural convention from vue/alpine as - all props in object are assigned as `:{attr}`
|
|
57
|
+
- We can use `:="{data}"` fro sprae autoinit, since scope has confusing name: `:scope={}`, `:sprae={}`, `:with={}`
|
|
58
|
+
-> let's use :prop= for now, since `:={}` can have multiple interpretations
|
|
59
|
+
|
|
60
|
+
## [x] Scopes mechanism: prototype inheritance chain vs multiple `with` wrappers
|
|
61
|
+
|
|
62
|
+
- prototype inheritance chain causes deps update difficulties
|
|
63
|
+
- prototype chain is messy-ish
|
|
64
|
+
- prototype chain is a bit more difficult to provide multiple parent scopes
|
|
65
|
+
- prototype state object is inheritance mess - can be super-hard to analyze
|
|
66
|
+
~ `with(a) with(b) with(c)` is the same as `with(a)` with prototype inheritance in terms of access.
|
|
67
|
+
- `with` chain allows runtime update of scopes, eg. child scope was updated to something new.
|
|
68
|
+
- `prototype` chain is fixed from the time of init.
|
|
69
|
+
- `prototype` chain hails to unidentified root scope and inherits from that. Maybe we should clarify scopes inhertiance and first implement reactive store (see next item).
|
|
70
|
+
|
|
71
|
+
? what if we avoid scope inheritance mechanism (what's the real use for it?) and instead just make reactive store object, so that :with directive subscribes to any input props, but "shadows" subtree?
|
|
72
|
+
? are there uses for inheritance
|
|
73
|
+
|
|
74
|
+
? Do we need scopes at all? Only for the purpose of autoinit?
|
|
75
|
+
- it seems scopes can introduce more confusion and mess in templates: indicating full paths is more beneficial
|
|
76
|
+
+ unless we introduce proper ":with="item.x as x""
|
|
77
|
+
+ prototype chain is a single object:
|
|
78
|
+
+ meaning updators receive one actual-for-element scope instance
|
|
79
|
+
+ that makes external API easier
|
|
80
|
+
+ that allows handling store via single reactive object
|
|
81
|
+
|
|
82
|
+
-> possibly we have to just subscribe via mechanism of signals-like deps, and :with just initializes subtree with extended object
|
|
83
|
+
|
|
84
|
+
## [ ] Should we inherit values from `init` in `sprae(el, init)`, instead of creating a snapshot of reactive values in `init`?
|
|
85
|
+
|
|
86
|
+
+ it allows passing any arbitrary scope to initialize from.
|
|
87
|
+
- it can make hard finding reactive sources...
|
|
88
|
+
+ it is sort-of neat pattern: object parent updates its particular state: it can also have observable method making object a store
|
|
89
|
+
-> can be delegated to a separate functionality - init just gets converted to reactive store
|
|
90
|
+
+ it sort-of makes `init` directly a scope (a parent of scope), which is more natural-ish rather than 2 independent entities
|
|
91
|
+
+ can pass both observables and direct state anywhere, eg. init child components from it
|
|
92
|
+
-> worthy of a separate library
|
|
93
|
+
|
|
94
|
+
## [ ] Per-directive initialize vs per-element initialize
|
|
95
|
+
|
|
96
|
+
+ Per-directive is very simple and trivial approach
|
|
97
|
+
- Per-directive doesn't read attributes order and init directives independently
|
|
98
|
+
~ Practically linear in-order init doesn't make much service either here
|
|
99
|
+
- Per-directive is a bit hard to deal with scopes
|
|
100
|
+
|
|
101
|
+
## [ ] avoid updating unchanged directives if values don't affect them
|
|
102
|
+
|
|
103
|
+
? what if we use preact/signals to subscribe only to required props?
|
|
104
|
+
-> parseExpr is going to need to be handled by core.js (not directives), and detect & subscribe to dependencies itself
|
|
105
|
+
-> so that directive updator gets invoked only when any of expr dependencies change
|
|
106
|
+
|
|
107
|
+
## [ ] Replace :else-if with :else :if
|
|
108
|
+
|
|
109
|
+
+ `:else :if=""` is meaningful expansion of both directives
|
|
110
|
+
+ `:else :if` is coming from JS
|
|
111
|
+
+ `:else :if` doesn't throw error in JSDOM tests
|
package/readme.md
CHANGED
|
@@ -1,27 +1,13 @@
|
|
|
1
|
-
# ∴ spræ
|
|
1
|
+
# ∴ spræ [](https://bundlephobia.com/result?p=sprae)
|
|
2
2
|
|
|
3
|
-
Reactive
|
|
4
|
-
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).
|
|
3
|
+
> Reactive microdirectives for soft DOM hydration.
|
|
5
4
|
|
|
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
|
-
## Usage
|
|
8
|
-
|
|
9
|
-
```html
|
|
10
|
-
<script src="./sprae.js" defer init></script>
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
<span :text="count">
|
|
14
|
-
<button :on="{ click: e => count++ }">inc</button>
|
|
15
|
-
</div>
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
* `:scope` marks regions on the tree that should be controlled by sprae.
|
|
19
|
-
* `init` attribute tells sprae to automatically initialize all elements that have `:scope`.
|
|
20
|
-
* any attribute starting with `:` is considered a sprae directive.
|
|
21
|
-
|
|
22
|
-
## Manual init
|
|
8
|
+
## Usage
|
|
23
9
|
|
|
24
|
-
|
|
10
|
+
Sprae enables directives as attributes starting with `:`.
|
|
25
11
|
|
|
26
12
|
```html
|
|
27
13
|
<div id="element" :if="user">
|
|
@@ -29,81 +15,110 @@ The more direct case is initializing sprae via JS.
|
|
|
29
15
|
</div>
|
|
30
16
|
|
|
31
17
|
<script type="module">
|
|
32
|
-
import
|
|
18
|
+
import sprae from 'sprae';
|
|
33
19
|
|
|
34
|
-
const
|
|
35
|
-
const
|
|
20
|
+
const init = { user: { displayName: 'Dmitry Ivanov' } }
|
|
21
|
+
const state = sprae(user, init);
|
|
36
22
|
|
|
37
|
-
state.user.displayName = 'dy'
|
|
38
|
-
update({user: {displayName: 'dy'}}) // alternatively
|
|
23
|
+
state.user.displayName = 'dy' // automatically updates DOM
|
|
39
24
|
</script>
|
|
40
25
|
```
|
|
41
26
|
|
|
42
|
-
* `
|
|
43
|
-
* `state` is proxy reflecting
|
|
44
|
-
* `
|
|
45
|
-
* `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).
|
|
46
30
|
|
|
47
31
|
|
|
32
|
+
<details>
|
|
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
|
+
```
|
|
41
|
+
|
|
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:
|
|
52
|
+
|
|
53
|
+
```html
|
|
54
|
+
<script src="./sprae.js" defer init></script>
|
|
55
|
+
|
|
56
|
+
<div :with="{ count: 0 }">
|
|
57
|
+
<span :text="count">
|
|
58
|
+
<button :on="{ click: e => count++ }">inc</button>
|
|
59
|
+
</div>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
* `:with` defines data for regions of the tree to autoinit sprae on.
|
|
63
|
+
* `init` attribute tells sprae to automatically initialize document.
|
|
64
|
+
|
|
65
|
+
</details>
|
|
66
|
+
-->
|
|
67
|
+
|
|
48
68
|
## Directives
|
|
49
69
|
|
|
50
|
-
* `:
|
|
51
|
-
* `:
|
|
52
|
-
* `:each="item, idx? in list"` - map list to instances of an element.
|
|
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.
|
|
53
72
|
* `:text="value"` - set text content of an element.
|
|
54
|
-
* `:value="value"` – bind value to input or textarea.
|
|
73
|
+
* `:value="value"` – bind value to input or textarea (reflected in model).
|
|
55
74
|
* `:id`, `:name`, `:for`, `:type`, `:hidden`, `:disabled`, `:href`, `:src` – common attributes setters.
|
|
56
75
|
* `:class="[ foo, 'bar' ]"` – set element class from an array, object or a string.
|
|
57
76
|
* `:style="{ top:1, position:'absolute' }"` – set element style from a string or an object.
|
|
58
|
-
* `:prop="{ alt:'foo', title:'bar' }"` – set any attribute / property.
|
|
77
|
+
* `:prop="{ alt:'foo', title:'bar' }"` – set any other attribute / property.
|
|
59
78
|
* `:on="{ click:e=>{}, touch:e=>{} }"` – add event listeners.
|
|
60
79
|
* `:data="{ foo:1, bar:2 }"` – set [data-*](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*) attributes.
|
|
61
80
|
* `:aria="{ role:'progressbar' }"` – set [aria-role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) attributes.
|
|
62
|
-
* `:
|
|
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. -->
|
|
63
83
|
|
|
64
|
-
<!--
|
|
65
|
-
### Loops
|
|
66
84
|
|
|
67
|
-
Iterating over set of items can be done with `:each` directive:
|
|
68
85
|
|
|
69
|
-
|
|
70
|
-
<
|
|
71
|
-
<li :each="item, index in items" :id="'item-' + item.id" :data="{value:item.value}" :text="item.label"></li>
|
|
72
|
-
</ul>
|
|
73
|
-
|
|
74
|
-
<li :each="{{ item, index in array }}">
|
|
75
|
-
<li :each="{{ key, value, index in object }}">
|
|
76
|
-
<li :each="{{ value in object }}">
|
|
77
|
-
```
|
|
86
|
+
<details>
|
|
87
|
+
<summary><strong>Combining directives</strong></summary>
|
|
78
88
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
To optionally display an element, there are `if`, `else-if`, `else` directives.
|
|
89
|
+
Directives can be naturally combined as:
|
|
82
90
|
|
|
83
91
|
```html
|
|
84
|
-
<span :if="
|
|
85
|
-
<span :else
|
|
86
|
-
<span :else>
|
|
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>
|
|
87
98
|
```
|
|
88
|
-
|
|
99
|
+
</details>
|
|
100
|
+
|
|
89
101
|
|
|
90
|
-
|
|
102
|
+
<details>
|
|
103
|
+
<summary><strong>Adding directives</strong></summary>
|
|
91
104
|
|
|
92
105
|
Directives can be added by registering them via `directive(name, initializer)`:
|
|
93
106
|
|
|
94
107
|
```js
|
|
95
|
-
import
|
|
108
|
+
import { directive, parseExpr } from 'sprae';
|
|
96
109
|
|
|
97
110
|
directive(':html', (el, expr) => {
|
|
98
111
|
// ...initialize here
|
|
99
|
-
const evaluate =
|
|
112
|
+
const evaluate = parseExpr(expr);
|
|
100
113
|
return (state) => {
|
|
101
114
|
// ...update here
|
|
102
|
-
el.innerHTML = evaluate(state)
|
|
115
|
+
el.innerHTML = evaluate(state);
|
|
103
116
|
}
|
|
104
|
-
})
|
|
117
|
+
});
|
|
105
118
|
```
|
|
106
119
|
|
|
120
|
+
</details>
|
|
121
|
+
|
|
107
122
|
|
|
108
123
|
## Reactivity
|
|
109
124
|
|
|
@@ -123,14 +138,14 @@ Update happens when any value changes:
|
|
|
123
138
|
<div id="done" :text="loading ? 'loading' : result">...</div>
|
|
124
139
|
|
|
125
140
|
<script>
|
|
126
|
-
import
|
|
141
|
+
import sprae from 'sprae';
|
|
127
142
|
import { signals } from '@preact/signals';
|
|
128
143
|
|
|
129
144
|
// <div id="done">...</div>
|
|
130
145
|
|
|
131
|
-
const loading = signal(true),
|
|
132
|
-
|
|
133
|
-
|
|
146
|
+
const loading = signal(true), result = signal(false);
|
|
147
|
+
|
|
148
|
+
sprae(done, { loading, result })
|
|
134
149
|
|
|
135
150
|
// <div id="done">loading</div>
|
|
136
151
|
|
|
@@ -144,11 +159,10 @@ Update happens when any value changes:
|
|
|
144
159
|
Note: observers don't require disposal, since they're connected in weak fashion. Once element is disposed, observables are disconnected.
|
|
145
160
|
|
|
146
161
|
|
|
147
|
-
|
|
148
162
|
## Justification
|
|
149
163
|
|
|
150
|
-
* [Template-parts](https://github.com/dy/template-parts) / [templize](https://github.com/dy/templize) is progressive, but is stuck with native HTML quirks (parsing table
|
|
151
|
-
* [Alpine](https://github.com/alpinejs/alpine) / [vue](https://github.com/vuejs/petite-vue) / [lit](https://github.com/lit/lit/tree/main/packages/lit-html) escapes native HTML quirks, but the syntax is a bit scattered: `:attr`, `v-*`,`x-*`, `@evt`, `{{}}` can be expressed with single convention. Besides, functionality is too broad and can be reduced to essence.
|
|
164
|
+
* [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.
|
|
165
|
+
* [Alpine](https://github.com/alpinejs/alpine) / [vue](https://github.com/vuejs/petite-vue) / [lit](https://github.com/lit/lit/tree/main/packages/lit-html) escapes native HTML quirks, but the syntax is a bit scattered: `:attr`, `v-*`,`x-*`, `@evt`, `{{}}` can be expressed with single convention. Besides, functionality is too broad and can be reduced to essence. Also they tend to [self-encapsulate](https://github.com/alpinejs/alpine/discussions/3223), making interop hard.
|
|
152
166
|
* [preact](https://ghub.io/preact) with HTML as JSX is a nice way to wire JS to templates, but it doesn't really support reactive fields (needs render call). Also migrating HTML to JS is an extreme with unwanted side-effects.
|
|
153
167
|
|
|
154
168
|
_Sprae_ takes elegant syntax convention of _alpine_ and method of _templize_ to connect any reactive values (like [@preact/signals](https://ghub.io/@preact/signals) or observables) to static HTML.
|
package/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};
|