shablon 0.0.1-rc.2 → 0.0.1-rc.4
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 -9
- package/package.json +2 -2
- package/src/router.js +10 -6
- package/src/state.js +108 -17
- 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
|
|
|
@@ -142,7 +142,7 @@ importing it from [npm](https://www.npmjs.com/package/shablon).
|
|
|
142
142
|
|
|
143
143
|
The keys of an `obj` must be "stringifiable" because they are used internally to construct a path to the reactive value.
|
|
144
144
|
|
|
145
|
-
The values can be any valid JS value, including nested arrays and objects (aka. it is recursively reactive).
|
|
145
|
+
The values can be any valid JS primitive value, including nested plain arrays and objects (aka. it is recursively reactive).
|
|
146
146
|
|
|
147
147
|
Getters are also supported and can be used as reactive computed properties.
|
|
148
148
|
The value of a reactive getter is "cached", meaning that even if one of the getter dependency changes, as long as the resulting value is the same there will be no unnecessary watch events fired.
|
|
@@ -162,16 +162,25 @@ data.age++
|
|
|
162
162
|
data.activity = "rest"
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
+
> Note that Object values like `Date`, `Set`, `Map`, `WeakRef`, `WeakSet` and `WeakMap` values are not wrapped in a nested `Proxy` and they will be resolved as they are to avoid access errors.
|
|
166
|
+
> For other custom object types thay you may want to access without a `Proxy` you can use the special `__raw` key, e.g. `data.myCustomType.__raw.someKey`.
|
|
167
|
+
|
|
165
168
|
</details>
|
|
166
169
|
|
|
167
170
|
|
|
168
171
|
<details>
|
|
169
|
-
<summary><strong id="api.watch">watch(
|
|
172
|
+
<summary><strong id="api.watch">watch(trackedFunc, optUntrackedFunc)</strong></summary>
|
|
173
|
+
|
|
174
|
+
Watch registers a callback function that fires on initialization and
|
|
175
|
+
every time any of its evaluated `store` reactive properties change.
|
|
176
|
+
|
|
177
|
+
Note that for reactive getters, initially the watch `trackedFunc` will be invoked twice because we register a second internal watcher to cache the getter value.
|
|
170
178
|
|
|
171
|
-
|
|
172
|
-
every time any of its `store` reactive dependencies change.
|
|
179
|
+
It returns a "watcher" object that could be used to `unwatch()` the registered listener.
|
|
173
180
|
|
|
174
|
-
|
|
181
|
+
_Optionally also accepts a second callback function that is excluded from the evaluated
|
|
182
|
+
store props tracking and instead is invoked only when `trackedFunc` is called
|
|
183
|
+
(could be used as a "track-only" watch pattern)._
|
|
175
184
|
|
|
176
185
|
For example:
|
|
177
186
|
|
|
@@ -187,7 +196,29 @@ w.unwatch()
|
|
|
187
196
|
data.count++ // doesn't trigger watch update
|
|
188
197
|
```
|
|
189
198
|
|
|
190
|
-
|
|
199
|
+
"Track-only" pattern example:
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
const data = store({
|
|
203
|
+
a: 0,
|
|
204
|
+
b: 0,
|
|
205
|
+
c: 0,
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// watch only "a" and "b" props
|
|
209
|
+
watch(() => [
|
|
210
|
+
data.a,
|
|
211
|
+
data.b,
|
|
212
|
+
], () => {
|
|
213
|
+
console.log(data.a)
|
|
214
|
+
console.log(data.b)
|
|
215
|
+
console.log(data.c)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
data.a++ // trigger watch update
|
|
219
|
+
data.b++ // trigger watch update
|
|
220
|
+
data.c++ // doesn't trigger watch update
|
|
221
|
+
```
|
|
191
222
|
|
|
192
223
|
</details>
|
|
193
224
|
|
|
@@ -218,8 +249,8 @@ When a reactive function is set as attribute value or child, it is invoked only
|
|
|
218
249
|
|
|
219
250
|
Each constructed tag has 3 additional optional lifecycle attributes:
|
|
220
251
|
|
|
221
|
-
- `onmount: func` - optional callback called when the element is inserted in the DOM
|
|
222
|
-
- `onunmount: func` - optional callback called when the element is removed from the DOM
|
|
252
|
+
- `onmount: func(el)` - optional callback called when the element is inserted in the DOM
|
|
253
|
+
- `onunmount: func(el)` - optional callback called when the element is removed from the DOM
|
|
223
254
|
- `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 `==`
|
|
224
255
|
|
|
225
256
|
</details>
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.0.1-rc.
|
|
2
|
+
"version": "0.0.1-rc.4",
|
|
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
70
|
[idSym]: "_" + Math.random(),
|
|
41
|
-
}
|
|
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) {
|
|
@@ -164,19 +207,21 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
164
207
|
prop = "@" + prop;
|
|
165
208
|
}
|
|
166
209
|
|
|
210
|
+
const propVal = obj[prop]
|
|
211
|
+
|
|
167
212
|
// directly return symbols and functions (pop, push, etc.)
|
|
168
|
-
if (typeof prop == "symbol" || typeof
|
|
169
|
-
return
|
|
213
|
+
if (typeof prop == "symbol" || typeof propVal == "function") {
|
|
214
|
+
return propVal;
|
|
170
215
|
}
|
|
171
216
|
|
|
172
217
|
// wrap child object or array as sub store
|
|
173
218
|
if (
|
|
174
|
-
typeof
|
|
175
|
-
|
|
176
|
-
!
|
|
219
|
+
propVal !== null && typeof propVal == "object" &&
|
|
220
|
+
!propVal[parentSym] &&
|
|
221
|
+
!isExcludedInstance(propVal)
|
|
177
222
|
) {
|
|
178
|
-
|
|
179
|
-
obj[prop] = createProxy(
|
|
223
|
+
propVal[parentSym] = [obj, prop];
|
|
224
|
+
obj[prop] = createProxy(propVal, pathWatcherIds);
|
|
180
225
|
}
|
|
181
226
|
|
|
182
227
|
// register watch subscriber (if any)
|
|
@@ -184,6 +229,42 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
184
229
|
let currentPath = getPath(obj, prop);
|
|
185
230
|
let activeWatcherId = activeWatcher[idSym];
|
|
186
231
|
|
|
232
|
+
activeWatcher[pathsSubsSym] = activeWatcher[pathsSubsSym] || new Set();
|
|
233
|
+
|
|
234
|
+
// If this is a rerun of the watcher function, resets any previous
|
|
235
|
+
// tracking paths because after this new run some of the old
|
|
236
|
+
// dependencies may no longer be reachable/evaluatable.
|
|
237
|
+
//
|
|
238
|
+
// For example, in the below code:
|
|
239
|
+
//
|
|
240
|
+
// ```js
|
|
241
|
+
// const data = store({ a: 0, b: 0, c: 0 })
|
|
242
|
+
//
|
|
243
|
+
// watch(() => {
|
|
244
|
+
// if (data.a > 0) {
|
|
245
|
+
// data.b
|
|
246
|
+
// } else {
|
|
247
|
+
// data.c
|
|
248
|
+
// }
|
|
249
|
+
// })
|
|
250
|
+
// ```
|
|
251
|
+
//
|
|
252
|
+
// initially ONLY "a" and "c" should be trackable because "b"
|
|
253
|
+
// is not reachable (aka. its getter is never invoked).
|
|
254
|
+
//
|
|
255
|
+
// If we increment `a++`, then in the new run ONLY "a" and "b" should be trackable
|
|
256
|
+
// because this time "c" is not reachable (aka. its getter is never invoked)
|
|
257
|
+
// and its previous tracking should be removed for this watcher.
|
|
258
|
+
//
|
|
259
|
+
// Note: The below code works because it reuses the same "subs" reference as in pathWatcherIds
|
|
260
|
+
// and this is intentional to avoid unnecessary iterations.
|
|
261
|
+
if (!activeWatcher[pathsResetedSym]) {
|
|
262
|
+
activeWatcher[pathsSubsSym].forEach((subs) => {
|
|
263
|
+
subs.delete(activeWatcherId)
|
|
264
|
+
})
|
|
265
|
+
activeWatcher[pathsResetedSym] = true;
|
|
266
|
+
}
|
|
267
|
+
|
|
187
268
|
let subs = pathWatcherIds.get(currentPath);
|
|
188
269
|
if (!subs) {
|
|
189
270
|
subs = new Set();
|
|
@@ -191,7 +272,6 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
191
272
|
}
|
|
192
273
|
subs.add(activeWatcherId);
|
|
193
274
|
|
|
194
|
-
activeWatcher[pathsSubsSym] = activeWatcher[pathsSubsSym] || new Set();
|
|
195
275
|
activeWatcher[pathsSubsSym].add(subs);
|
|
196
276
|
|
|
197
277
|
// register a child watcher to update the custom getter prop replacement
|
|
@@ -264,6 +344,17 @@ function getPath(obj, prop) {
|
|
|
264
344
|
return currentPath;
|
|
265
345
|
}
|
|
266
346
|
|
|
347
|
+
function isExcludedInstance(val) {
|
|
348
|
+
return (
|
|
349
|
+
(val instanceof Date) ||
|
|
350
|
+
(val instanceof Set) ||
|
|
351
|
+
(val instanceof Map) ||
|
|
352
|
+
(val instanceof WeakRef) ||
|
|
353
|
+
(val instanceof WeakMap) ||
|
|
354
|
+
(val instanceof WeakSet)
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
267
358
|
function callWatchers(obj, prop, pathWatcherIds) {
|
|
268
359
|
let currentPath = getPath(obj, prop);
|
|
269
360
|
|
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
|
}
|