shablon 0.0.1-rc.1 → 0.0.1-rc.3
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/README.md +40 -14
- package/package.json +2 -2
- package/src/router.js +10 -6
- package/src/state.js +89 -11
- package/src/template.js +21 -13
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Shablon - No-build JavaScript frontend framework
|
|
|
10
10
|
|
|
11
11
|
Shablon has very small learning curve (**4 main exported functions**) and it is suitable for building Single-page applications (SPA):
|
|
12
12
|
|
|
13
|
-
- State: `store(obj)` and `watch(
|
|
13
|
+
- State: `store(obj)` and `watch(trackedFunc, optUntrackedFunc)`
|
|
14
14
|
- Template: `t.[tag](attrs, ...children)`
|
|
15
15
|
- Router: `router(routes, options)`
|
|
16
16
|
|
|
@@ -112,10 +112,8 @@ You can find the bundle file at [`dist/shablon.iife.js`](https://github.com/gani
|
|
|
112
112
|
|
|
113
113
|
#### ES module (browsers and npm)
|
|
114
114
|
|
|
115
|
-
Alternatively, you can load the package as ES module by using the [`dist/shablon.es.js`](https://github.com/ganigeorgiev/shablon/blob/master/dist/shablon.es.js) file
|
|
116
|
-
|
|
117
|
-
<!-- Alternatively, you can load the package as ES module either by using the [`dist/shablon.es.js`](https://github.com/ganigeorgiev/shablon/blob/master/dist/shablon.es.js) file or
|
|
118
|
-
importing it from [npm](https://www.npmjs.com/package/shablon). -->
|
|
115
|
+
Alternatively, you can load the package as ES module either by using the [`dist/shablon.es.js`](https://github.com/ganigeorgiev/shablon/blob/master/dist/shablon.es.js) file or
|
|
116
|
+
importing it from [npm](https://www.npmjs.com/package/shablon).
|
|
119
117
|
|
|
120
118
|
- browsers:
|
|
121
119
|
```html
|
|
@@ -127,7 +125,7 @@ importing it from [npm](https://www.npmjs.com/package/shablon). -->
|
|
|
127
125
|
...
|
|
128
126
|
</script>
|
|
129
127
|
```
|
|
130
|
-
|
|
128
|
+
|
|
131
129
|
- npm (`npm -i shablon`):
|
|
132
130
|
```js
|
|
133
131
|
import { t, store, watch, router } from "shablon"
|
|
@@ -135,7 +133,7 @@ importing it from [npm](https://www.npmjs.com/package/shablon). -->
|
|
|
135
133
|
const data = store({ count: 0 })
|
|
136
134
|
...
|
|
137
135
|
```
|
|
138
|
-
|
|
136
|
+
|
|
139
137
|
## API
|
|
140
138
|
<details>
|
|
141
139
|
<summary><strong id="api.store">store(obj)</strong></summary>
|
|
@@ -168,12 +166,16 @@ data.activity = "rest"
|
|
|
168
166
|
|
|
169
167
|
|
|
170
168
|
<details>
|
|
171
|
-
<summary><strong id="api.watch">watch(
|
|
169
|
+
<summary><strong id="api.watch">watch(trackedFunc, optUntrackedFunc)</strong></summary>
|
|
172
170
|
|
|
173
|
-
Watch registers a callback function that fires
|
|
174
|
-
every time any of its `store` reactive
|
|
171
|
+
Watch registers a callback function that fires on initialization and
|
|
172
|
+
every time any of its evaluated `store` reactive properties change.
|
|
175
173
|
|
|
176
|
-
It returns a "watcher" object that could be used to `unwatch` the registered listener.
|
|
174
|
+
It returns a "watcher" object that could be used to `unwatch()` the registered listener.
|
|
175
|
+
|
|
176
|
+
_Optionally also accepts a second callback function that is excluded from the evaluated
|
|
177
|
+
store props tracking and instead is invoked only when `trackedFunc` is called
|
|
178
|
+
(could be used as a "track-only" watch pattern)._
|
|
177
179
|
|
|
178
180
|
For example:
|
|
179
181
|
|
|
@@ -189,7 +191,31 @@ w.unwatch()
|
|
|
189
191
|
data.count++ // doesn't trigger watch update
|
|
190
192
|
```
|
|
191
193
|
|
|
192
|
-
|
|
194
|
+
"Track-only" pattern example:
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
const data = store({
|
|
198
|
+
a: 0,
|
|
199
|
+
b: 0,
|
|
200
|
+
c: 0,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// watch only "a" and "b" props
|
|
204
|
+
watch(() => [
|
|
205
|
+
data.a,
|
|
206
|
+
data.b,
|
|
207
|
+
], () => {
|
|
208
|
+
console.log(data.a)
|
|
209
|
+
console.log(data.b)
|
|
210
|
+
console.log(data.c)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
data.a++ // trigger watch update
|
|
214
|
+
data.b++ // trigger watch update
|
|
215
|
+
data.c++ // doesn't trigger watch update
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Note that for reactive getters, initially the watch trackCallback will be invoked twice because we register a second internal watcher to cache the getter value.
|
|
193
219
|
|
|
194
220
|
</details>
|
|
195
221
|
|
|
@@ -220,8 +246,8 @@ When a reactive function is set as attribute value or child, it is invoked only
|
|
|
220
246
|
|
|
221
247
|
Each constructed tag has 3 additional optional lifecycle attributes:
|
|
222
248
|
|
|
223
|
-
- `onmount: func` - optional callback called when the element is inserted in the DOM
|
|
224
|
-
- `onunmount: func` - optional callback called when the element is removed from the DOM
|
|
249
|
+
- `onmount: func(el)` - optional callback called when the element is inserted in the DOM
|
|
250
|
+
- `onunmount: func(el)` - optional callback called when the element is removed from the DOM
|
|
225
251
|
- `rid: any` - "replacement id" is an identifier based on which we can decide whether to reuse the element or not during rerendering (e.g. on list change); the value could be anything comparable with `==`
|
|
226
252
|
|
|
227
253
|
</details>
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.0.1-rc.
|
|
2
|
+
"version": "0.0.1-rc.3",
|
|
3
3
|
"name": "shablon",
|
|
4
4
|
"description": "No-build JavaScript framework for Single-page applications",
|
|
5
5
|
"author": "Gani Georgiev",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"jsdom": "^26.0.0",
|
|
26
26
|
"prettier": "^3.6.2",
|
|
27
|
-
"rolldown": "^1.0.0-beta.
|
|
27
|
+
"rolldown": "^1.0.0-beta.50"
|
|
28
28
|
},
|
|
29
29
|
"prettier": {
|
|
30
30
|
"tabWidth": 4,
|
package/src/router.js
CHANGED
|
@@ -43,12 +43,16 @@ export function router(routes, options = { fallbackPath: "#/", transition: true
|
|
|
43
43
|
let prevDestroy;
|
|
44
44
|
|
|
45
45
|
let onHashChange = () => {
|
|
46
|
-
let path = window.location.hash
|
|
46
|
+
let path = window.location.hash;
|
|
47
47
|
|
|
48
|
-
let route =
|
|
49
|
-
findActiveRoute(defs, path) || findActiveRoute(defs, options.fallbackPath);
|
|
48
|
+
let route = findActiveRoute(defs, path);
|
|
50
49
|
if (!route) {
|
|
51
|
-
|
|
50
|
+
if (options.fallbackPath != path) {
|
|
51
|
+
window.location.hash = options.fallbackPath;
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.warn("missing route:", path);
|
|
52
56
|
return;
|
|
53
57
|
}
|
|
54
58
|
|
|
@@ -57,7 +61,7 @@ export function router(routes, options = { fallbackPath: "#/", transition: true
|
|
|
57
61
|
await prevDestroy?.();
|
|
58
62
|
prevDestroy = await route.handler(route);
|
|
59
63
|
} catch (err) {
|
|
60
|
-
console.warn("
|
|
64
|
+
console.warn("route navigation failed:", err);
|
|
61
65
|
}
|
|
62
66
|
};
|
|
63
67
|
|
|
@@ -116,7 +120,7 @@ function prepareRoutes(routes) {
|
|
|
116
120
|
parts[i].endsWith("}")
|
|
117
121
|
) {
|
|
118
122
|
// param
|
|
119
|
-
parts[i] = "(?<" + parts[i].substring(1, parts[i].length - 1) + "
|
|
123
|
+
parts[i] = "(?<" + parts[i].substring(1, parts[i].length - 1) + ">[^\\/#?]+)";
|
|
120
124
|
} else {
|
|
121
125
|
// regular path segment
|
|
122
126
|
parts[i] = RegExp.escape(parts[i]);
|
package/src/state.js
CHANGED
|
@@ -7,6 +7,7 @@ let cleanTimeoutId;
|
|
|
7
7
|
|
|
8
8
|
let idSym = Symbol();
|
|
9
9
|
let parentSym = Symbol();
|
|
10
|
+
let pathsResetedSym = Symbol();
|
|
10
11
|
let childrenSym = Symbol();
|
|
11
12
|
let pathsSubsSym = Symbol();
|
|
12
13
|
let unwatchedSym = Symbol();
|
|
@@ -14,11 +15,15 @@ let onRemoveSym = Symbol();
|
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Watch registers a callback function that fires on initialization and
|
|
17
|
-
* every time any of its `store` reactive
|
|
18
|
+
* every time any of its evaluated `store` reactive properties change.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
+
* It returns a "watcher" object that could be used to `unwatch()` the registered listener.
|
|
20
21
|
*
|
|
21
|
-
*
|
|
22
|
+
* Optionally also accepts a second callback function that is excluded from the evaluated
|
|
23
|
+
* store props tracking and instead is invoked only when `trackedFunc` is called
|
|
24
|
+
* (could be used as a "track-only" watch pattern).
|
|
25
|
+
*
|
|
26
|
+
* Simple example:
|
|
22
27
|
*
|
|
23
28
|
* ```js
|
|
24
29
|
* const data = store({ count: 0 })
|
|
@@ -32,19 +37,47 @@ let onRemoveSym = Symbol();
|
|
|
32
37
|
* data.count++ // doesn't trigger watch update
|
|
33
38
|
* ```
|
|
34
39
|
*
|
|
35
|
-
*
|
|
40
|
+
* "Track-only" example:
|
|
41
|
+
*
|
|
42
|
+
* ```js
|
|
43
|
+
* const data = store({
|
|
44
|
+
* a: 0,
|
|
45
|
+
* b: 0,
|
|
46
|
+
* c: 0,
|
|
47
|
+
* })
|
|
48
|
+
*
|
|
49
|
+
* // watch only "a" and "b" props
|
|
50
|
+
* watch(() => [
|
|
51
|
+
* data.a,
|
|
52
|
+
* data.b,
|
|
53
|
+
* ], () => {
|
|
54
|
+
* console.log(data.a)
|
|
55
|
+
* console.log(data.b)
|
|
56
|
+
* console.log(data.c)
|
|
57
|
+
* })
|
|
58
|
+
*
|
|
59
|
+
* data.a++ // trigger watch update
|
|
60
|
+
* data.b++ // trigger watch update
|
|
61
|
+
* data.c++ // doesn't trigger watch update
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @param {Function} trackedFunc
|
|
65
|
+
* @param {Function} [optUntrackedFunc]
|
|
36
66
|
* @return {{unwatch:Function, last:any, run:Function}}
|
|
37
67
|
*/
|
|
38
|
-
export function watch(
|
|
68
|
+
export function watch(trackedFunc, optUntrackedFunc) {
|
|
39
69
|
let watcher = {
|
|
40
|
-
[idSym]: "" + Math.random(),
|
|
41
|
-
}
|
|
70
|
+
[idSym]: "_" + Math.random(),
|
|
71
|
+
}
|
|
42
72
|
|
|
43
73
|
allWatchers.set(watcher[idSym], watcher);
|
|
44
74
|
|
|
45
75
|
watcher.run = () => {
|
|
76
|
+
let oldActiveWatcher;
|
|
77
|
+
|
|
46
78
|
// nested watcher -> register previous watcher as parent
|
|
47
79
|
if (activeWatcher) {
|
|
80
|
+
oldActiveWatcher = activeWatcher
|
|
48
81
|
watcher[parentSym] = activeWatcher[idSym];
|
|
49
82
|
|
|
50
83
|
// store immediate children references for quicker cleanup
|
|
@@ -53,8 +86,14 @@ export function watch(callback) {
|
|
|
53
86
|
}
|
|
54
87
|
|
|
55
88
|
activeWatcher = watcher;
|
|
56
|
-
|
|
57
|
-
|
|
89
|
+
activeWatcher[pathsResetedSym] = false;
|
|
90
|
+
watcher.last = trackedFunc();
|
|
91
|
+
|
|
92
|
+
activeWatcher = null;
|
|
93
|
+
optUntrackedFunc?.();
|
|
94
|
+
|
|
95
|
+
// restore original ref (if any)
|
|
96
|
+
activeWatcher = oldActiveWatcher;
|
|
58
97
|
};
|
|
59
98
|
|
|
60
99
|
watcher.unwatch = function () {
|
|
@@ -126,7 +165,7 @@ function removeWatcher(id) {
|
|
|
126
165
|
* Getters are also supported out of the box and they are invoked every
|
|
127
166
|
* time when any of their dependencies change.
|
|
128
167
|
* If a getter is used in a reactive function, its resulting value is cached,
|
|
129
|
-
* aka. if the final value hasn't changed it will not trigger an
|
|
168
|
+
* aka. if the final value hasn't changed it will not trigger an unnecessary reactive update.
|
|
130
169
|
*
|
|
131
170
|
* Multiple changes from one or many stores are also automatically batched in a microtask.
|
|
132
171
|
*
|
|
@@ -148,6 +187,10 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
148
187
|
|
|
149
188
|
let handler = {
|
|
150
189
|
get(obj, prop, target) {
|
|
190
|
+
if (prop === "__raw") {
|
|
191
|
+
return obj;
|
|
192
|
+
}
|
|
193
|
+
|
|
151
194
|
// getter?
|
|
152
195
|
let getterProp;
|
|
153
196
|
if (descriptors[prop]?.get) {
|
|
@@ -184,6 +227,42 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
184
227
|
let currentPath = getPath(obj, prop);
|
|
185
228
|
let activeWatcherId = activeWatcher[idSym];
|
|
186
229
|
|
|
230
|
+
activeWatcher[pathsSubsSym] = activeWatcher[pathsSubsSym] || new Set();
|
|
231
|
+
|
|
232
|
+
// If this is a rerun of the watcher function, resets any previous
|
|
233
|
+
// tracking paths because after this new run some of the old
|
|
234
|
+
// dependencies may no longer be reachable/evaluatable.
|
|
235
|
+
//
|
|
236
|
+
// For example, in the below code:
|
|
237
|
+
//
|
|
238
|
+
// ```js
|
|
239
|
+
// const data = store({ a: 0, b: 0, c: 0 })
|
|
240
|
+
//
|
|
241
|
+
// watch(() => {
|
|
242
|
+
// if (data.a > 0) {
|
|
243
|
+
// data.b
|
|
244
|
+
// } else {
|
|
245
|
+
// data.c
|
|
246
|
+
// }
|
|
247
|
+
// })
|
|
248
|
+
// ```
|
|
249
|
+
//
|
|
250
|
+
// initially ONLY "a" and "c" should be trackable because "b"
|
|
251
|
+
// is not reachable (aka. its getter is never invoked).
|
|
252
|
+
//
|
|
253
|
+
// If we increment `a++`, then in the new run ONLY "a" and "b" should be trackable
|
|
254
|
+
// because this time "c" is not reachable (aka. its getter is never invoked)
|
|
255
|
+
// and its previous tracking should be removed for this watcher.
|
|
256
|
+
//
|
|
257
|
+
// Note: The below code works because it reuses the same "subs" reference as in pathWatcherIds
|
|
258
|
+
// and this is intentional to avoid unnecessary iterations.
|
|
259
|
+
if (!activeWatcher[pathsResetedSym]) {
|
|
260
|
+
activeWatcher[pathsSubsSym].forEach((subs) => {
|
|
261
|
+
subs.delete(activeWatcherId)
|
|
262
|
+
})
|
|
263
|
+
activeWatcher[pathsResetedSym] = true;
|
|
264
|
+
}
|
|
265
|
+
|
|
187
266
|
let subs = pathWatcherIds.get(currentPath);
|
|
188
267
|
if (!subs) {
|
|
189
268
|
subs = new Set();
|
|
@@ -191,7 +270,6 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
191
270
|
}
|
|
192
271
|
subs.add(activeWatcherId);
|
|
193
272
|
|
|
194
|
-
activeWatcher[pathsSubsSym] = activeWatcher[pathsSubsSym] || new Set();
|
|
195
273
|
activeWatcher[pathsSubsSym].add(subs);
|
|
196
274
|
|
|
197
275
|
// register a child watcher to update the custom getter prop replacement
|
package/src/template.js
CHANGED
|
@@ -61,7 +61,7 @@ function initMutationObserver() {
|
|
|
61
61
|
function recursiveObserveCall(method, nodes) {
|
|
62
62
|
for (let n of nodes) {
|
|
63
63
|
if (n[method]) {
|
|
64
|
-
n[method]();
|
|
64
|
+
n[method](n);
|
|
65
65
|
}
|
|
66
66
|
if (n.childNodes) {
|
|
67
67
|
recursiveObserveCall(method, n.childNodes);
|
|
@@ -116,9 +116,9 @@ function tag(tagName, attrs = {}, ...children) {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
if (useSetAttr) {
|
|
119
|
-
el.setAttribute(attr, val());
|
|
119
|
+
el.setAttribute(attr, val(el));
|
|
120
120
|
} else {
|
|
121
|
-
el[attr] = val();
|
|
121
|
+
el[attr] = val(el);
|
|
122
122
|
}
|
|
123
123
|
});
|
|
124
124
|
}
|
|
@@ -140,7 +140,7 @@ function tag(tagName, attrs = {}, ...children) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
customMount?.();
|
|
143
|
+
customMount?.(el);
|
|
144
144
|
};
|
|
145
145
|
|
|
146
146
|
let customUnmount = el.onunmount;
|
|
@@ -165,7 +165,7 @@ function tag(tagName, attrs = {}, ...children) {
|
|
|
165
165
|
}
|
|
166
166
|
el[cleanupFuncsSym] = null;
|
|
167
167
|
|
|
168
|
-
customUnmount?.();
|
|
168
|
+
customUnmount?.(el);
|
|
169
169
|
};
|
|
170
170
|
|
|
171
171
|
setChildren(el, children);
|
|
@@ -177,14 +177,17 @@ function setChildren(el, children) {
|
|
|
177
177
|
children = toArray(children);
|
|
178
178
|
|
|
179
179
|
for (let childOrFunc of children) {
|
|
180
|
-
if (
|
|
181
|
-
// nested array
|
|
182
|
-
setChildren(el, childOrFunc);
|
|
183
|
-
} else if (typeof childOrFunc == "function") {
|
|
180
|
+
if (typeof childOrFunc == "function") {
|
|
184
181
|
initChildrenFuncWatcher(el, childOrFunc);
|
|
185
182
|
} else {
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
let normalized = normalizeNode(childOrFunc);
|
|
184
|
+
if (Array.isArray(normalized)) {
|
|
185
|
+
// nested array
|
|
186
|
+
setChildren(el, normalized);
|
|
187
|
+
} else if (normalized) {
|
|
188
|
+
// plain elem
|
|
189
|
+
el.appendChild(normalized);
|
|
190
|
+
}
|
|
188
191
|
}
|
|
189
192
|
}
|
|
190
193
|
}
|
|
@@ -211,7 +214,7 @@ function initChildrenFuncWatcher(el, childrenFunc) {
|
|
|
211
214
|
return;
|
|
212
215
|
}
|
|
213
216
|
|
|
214
|
-
let newChildren = toArray(childrenFunc());
|
|
217
|
+
let newChildren = toArray(childrenFunc(el));
|
|
215
218
|
let totalNewLength = newChildren.length;
|
|
216
219
|
let newKeysMap = new Map();
|
|
217
220
|
|
|
@@ -395,11 +398,16 @@ function toArray(val) {
|
|
|
395
398
|
|
|
396
399
|
function normalizeNode(child) {
|
|
397
400
|
// wrap as TextNode so that it can be "tracked" and used with appendChild or other similar methods
|
|
398
|
-
if (typeof child == "string" || typeof child == "number") {
|
|
401
|
+
if (typeof child == "string" || typeof child == "number" || typeof child == "boolean") {
|
|
399
402
|
let childNode = document.createTextNode(child);
|
|
400
403
|
childNode.rid = child;
|
|
401
404
|
return childNode;
|
|
402
405
|
}
|
|
403
406
|
|
|
407
|
+
// in case child is DOM Proxy element/array loaded from a store object
|
|
408
|
+
if (typeof child?.__raw != "undefined") {
|
|
409
|
+
return child.__raw
|
|
410
|
+
}
|
|
411
|
+
|
|
404
412
|
return child;
|
|
405
413
|
}
|