shablon 0.0.1-rc.4 → 0.0.1-rc.5
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 +4 -4
- package/package.json +1 -1
- package/src/router.js +3 -2
- package/src/state.js +165 -87
- package/src/template.js +11 -3
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Shablon - No-build JavaScript frontend framework
|
|
|
6
6
|
>
|
|
7
7
|
> **Don't use it yet - it hasn't been actually tested in real applications and it may change without notice!**
|
|
8
8
|
|
|
9
|
-
**Shablon** _("template" in Bulgarian)_ is a ~
|
|
9
|
+
**Shablon** _("template" in Bulgarian)_ is a ~6KB JS framework that comes with deeply reactive state management, plain JS extendable templates and hash-based router.
|
|
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
|
|
|
@@ -162,8 +162,8 @@ data.age++
|
|
|
162
162
|
data.activity = "rest"
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
-
> Note that
|
|
166
|
-
>
|
|
165
|
+
> Note that only plain objects and arrays are wrapped in a nested `Proxy`! `Date`, `Set`, `Map`, `WeakRef`, `WeakSet`, `WeakMap` or any custom object will be resolved as they are to avoid access errors.
|
|
166
|
+
> You can access the original object without the Proxy trap using the special `__raw` key, e.g. `data.someObj.__raw.someKey`.
|
|
167
167
|
|
|
168
168
|
</details>
|
|
169
169
|
|
|
@@ -209,7 +209,7 @@ const data = store({
|
|
|
209
209
|
watch(() => [
|
|
210
210
|
data.a,
|
|
211
211
|
data.b,
|
|
212
|
-
], () => {
|
|
212
|
+
], (_) => { // receive the return result of trackedFunc
|
|
213
213
|
console.log(data.a)
|
|
214
214
|
console.log(data.b)
|
|
215
215
|
console.log(data.c)
|
package/package.json
CHANGED
package/src/router.js
CHANGED
|
@@ -49,7 +49,7 @@ export function router(routes, options = { fallbackPath: "#/", transition: true
|
|
|
49
49
|
if (!route) {
|
|
50
50
|
if (options.fallbackPath != path) {
|
|
51
51
|
window.location.hash = options.fallbackPath;
|
|
52
|
-
return
|
|
52
|
+
return;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
console.warn("missing route:", path);
|
|
@@ -120,7 +120,8 @@ function prepareRoutes(routes) {
|
|
|
120
120
|
parts[i].endsWith("}")
|
|
121
121
|
) {
|
|
122
122
|
// param
|
|
123
|
-
parts[i] =
|
|
123
|
+
parts[i] =
|
|
124
|
+
"(?<" + parts[i].substring(1, parts[i].length - 1) + ">[^\\/#?]+)";
|
|
124
125
|
} else {
|
|
125
126
|
// regular path segment
|
|
126
127
|
parts[i] = RegExp.escape(parts[i]);
|
package/src/state.js
CHANGED
|
@@ -7,11 +7,14 @@ let cleanTimeoutId;
|
|
|
7
7
|
|
|
8
8
|
let idSym = Symbol();
|
|
9
9
|
let parentSym = Symbol();
|
|
10
|
-
let pathsResetedSym = Symbol();
|
|
11
10
|
let childrenSym = Symbol();
|
|
12
11
|
let pathsSubsSym = Symbol();
|
|
13
12
|
let unwatchedSym = Symbol();
|
|
14
13
|
let onRemoveSym = Symbol();
|
|
14
|
+
let skipSym = Symbol();
|
|
15
|
+
let evictedSym = Symbol();
|
|
16
|
+
|
|
17
|
+
let pathSeparator = "/";
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Watch registers a callback function that fires on initialization and
|
|
@@ -68,7 +71,7 @@ let onRemoveSym = Symbol();
|
|
|
68
71
|
export function watch(trackedFunc, optUntrackedFunc) {
|
|
69
72
|
let watcher = {
|
|
70
73
|
[idSym]: "_" + Math.random(),
|
|
71
|
-
}
|
|
74
|
+
};
|
|
72
75
|
|
|
73
76
|
allWatchers.set(watcher[idSym], watcher);
|
|
74
77
|
|
|
@@ -77,7 +80,7 @@ export function watch(trackedFunc, optUntrackedFunc) {
|
|
|
77
80
|
|
|
78
81
|
// nested watcher -> register previous watcher as parent
|
|
79
82
|
if (activeWatcher) {
|
|
80
|
-
oldActiveWatcher = activeWatcher
|
|
83
|
+
oldActiveWatcher = activeWatcher;
|
|
81
84
|
watcher[parentSym] = activeWatcher[idSym];
|
|
82
85
|
|
|
83
86
|
// store immediate children references for quicker cleanup
|
|
@@ -85,12 +88,44 @@ export function watch(trackedFunc, optUntrackedFunc) {
|
|
|
85
88
|
activeWatcher[childrenSym].push(watcher[idSym]);
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
// On watcher function run, resets any previous tracking paths
|
|
92
|
+
// because after this new run some of the old dependencies
|
|
93
|
+
// may no longer be reachable/evaluatable.
|
|
94
|
+
//
|
|
95
|
+
// For example, in the below code:
|
|
96
|
+
//
|
|
97
|
+
// ```js
|
|
98
|
+
// const data = store({ a: 0, b: 0, c: 0 })
|
|
99
|
+
//
|
|
100
|
+
// watch(() => {
|
|
101
|
+
// if (data.a > 0) {
|
|
102
|
+
// data.b
|
|
103
|
+
// } else {
|
|
104
|
+
// data.c
|
|
105
|
+
// }
|
|
106
|
+
// })
|
|
107
|
+
// ```
|
|
108
|
+
//
|
|
109
|
+
// initially ONLY "a" and "c" should be trackable because "b"
|
|
110
|
+
// is not reachable (aka. its getter is never invoked).
|
|
111
|
+
//
|
|
112
|
+
// If we increment `a++`, then in the new run ONLY "a" and "b" should be trackable
|
|
113
|
+
// because this time "c" is not reachable (aka. its getter is never invoked)
|
|
114
|
+
// and its previous tracking should be removed for this watcher.
|
|
115
|
+
//
|
|
116
|
+
// Note: The below code works because it reuses the same "subs" reference as in pathWatcherIds
|
|
117
|
+
// and this is intentional to avoid unnecessary iterations.
|
|
118
|
+
watcher[pathsSubsSym]?.forEach((subs) => {
|
|
119
|
+
subs.delete(watcher[idSym]);
|
|
120
|
+
});
|
|
121
|
+
|
|
88
122
|
activeWatcher = watcher;
|
|
89
|
-
|
|
90
|
-
watcher.last = trackedFunc();
|
|
123
|
+
const result = trackedFunc();
|
|
91
124
|
|
|
92
|
-
|
|
93
|
-
|
|
125
|
+
if (optUntrackedFunc) {
|
|
126
|
+
activeWatcher = null;
|
|
127
|
+
optUntrackedFunc(result);
|
|
128
|
+
}
|
|
94
129
|
|
|
95
130
|
// restore original ref (if any)
|
|
96
131
|
activeWatcher = oldActiveWatcher;
|
|
@@ -135,10 +170,8 @@ function removeWatcher(id) {
|
|
|
135
170
|
}
|
|
136
171
|
|
|
137
172
|
if (w?.[pathsSubsSym]) {
|
|
138
|
-
for (let
|
|
139
|
-
|
|
140
|
-
subset.delete(id);
|
|
141
|
-
}
|
|
173
|
+
for (let sub of w[pathsSubsSym]) {
|
|
174
|
+
sub.delete(id);
|
|
142
175
|
}
|
|
143
176
|
w[pathsSubsSym] = null;
|
|
144
177
|
}
|
|
@@ -185,12 +218,65 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
185
218
|
? Object.getOwnPropertyDescriptors(obj)
|
|
186
219
|
: {};
|
|
187
220
|
|
|
188
|
-
|
|
189
|
-
get(obj, prop,
|
|
190
|
-
if (prop
|
|
221
|
+
return new Proxy(obj, {
|
|
222
|
+
get(obj, prop, receiver) {
|
|
223
|
+
if (typeof prop == "symbol") {
|
|
224
|
+
return obj[prop];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (prop == "__raw") {
|
|
191
228
|
return obj;
|
|
192
229
|
}
|
|
193
230
|
|
|
231
|
+
// evicted child?
|
|
232
|
+
if (!obj[skipSym] && obj[parentSym]) {
|
|
233
|
+
let props = [];
|
|
234
|
+
let activeObj = obj;
|
|
235
|
+
|
|
236
|
+
let isEvicted = false;
|
|
237
|
+
|
|
238
|
+
// travel up to the root proxy
|
|
239
|
+
// (aka. x.a.b*.c -> x)
|
|
240
|
+
while (activeObj?.[parentSym]) {
|
|
241
|
+
if (activeObj[evictedSym]) {
|
|
242
|
+
isEvicted = true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
props.push(activeObj[parentSym][1]);
|
|
246
|
+
activeObj = activeObj[parentSym][0];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// try to access the original path but this time from
|
|
250
|
+
// the root point of view to ensure that we are always accessing
|
|
251
|
+
// an up-to-date store child reference
|
|
252
|
+
// (we want: x.a.b(old).c -> x -> x.a.b(new).c)
|
|
253
|
+
//
|
|
254
|
+
// note: this technically could "leak" but for our case it should be fine
|
|
255
|
+
// because the evicted object will become again garbage collectable
|
|
256
|
+
// once the related watcher(s) are removed
|
|
257
|
+
if (isEvicted) {
|
|
258
|
+
for (let i = props.length - 1; i >= 0; i--) {
|
|
259
|
+
activeObj[skipSym] = true;
|
|
260
|
+
let item = activeObj?.[props[i]];
|
|
261
|
+
activeObj[skipSym] = false;
|
|
262
|
+
|
|
263
|
+
if (i == 0) {
|
|
264
|
+
activeObj = item?.__raw;
|
|
265
|
+
} else {
|
|
266
|
+
activeObj = item;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// the original full nested path is no longer available (null/undefined)
|
|
271
|
+
if (activeObj == undefined) {
|
|
272
|
+
return activeObj;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// update the current obj with the one from the retraced path
|
|
276
|
+
obj = activeObj;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
194
280
|
// getter?
|
|
195
281
|
let getterProp;
|
|
196
282
|
if (descriptors[prop]?.get) {
|
|
@@ -202,26 +288,30 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
202
288
|
|
|
203
289
|
getterProp = prop;
|
|
204
290
|
|
|
205
|
-
// replace with an internal "
|
|
291
|
+
// replace with an internal "@@prop" property so that
|
|
206
292
|
// reactive statements can be cached
|
|
207
|
-
prop = "
|
|
293
|
+
prop = "@@" + prop;
|
|
208
294
|
}
|
|
209
295
|
|
|
210
|
-
|
|
296
|
+
let propVal = obj[prop];
|
|
211
297
|
|
|
212
|
-
// directly return
|
|
213
|
-
if (typeof
|
|
298
|
+
// directly return for functions (pop, push, etc.)
|
|
299
|
+
if (typeof propVal == "function") {
|
|
214
300
|
return propVal;
|
|
215
301
|
}
|
|
216
302
|
|
|
217
|
-
// wrap child object or array as sub store
|
|
303
|
+
// wrap child plain object or array as sub store
|
|
218
304
|
if (
|
|
219
|
-
propVal
|
|
305
|
+
propVal != null &&
|
|
306
|
+
typeof propVal == "object" &&
|
|
220
307
|
!propVal[parentSym] &&
|
|
221
|
-
|
|
308
|
+
(propVal.constructor?.name == "Object" ||
|
|
309
|
+
propVal.constructor?.name == "Array" ||
|
|
310
|
+
propVal.constructor?.name == undefined) // e.g. Object.create(null)
|
|
222
311
|
) {
|
|
223
|
-
propVal[parentSym] = [
|
|
224
|
-
|
|
312
|
+
propVal[parentSym] = [receiver, prop];
|
|
313
|
+
propVal = createProxy(propVal, pathWatcherIds);
|
|
314
|
+
obj[prop] = propVal;
|
|
225
315
|
}
|
|
226
316
|
|
|
227
317
|
// register watch subscriber (if any)
|
|
@@ -229,52 +319,36 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
229
319
|
let currentPath = getPath(obj, prop);
|
|
230
320
|
let activeWatcherId = activeWatcher[idSym];
|
|
231
321
|
|
|
322
|
+
let propPaths = [currentPath];
|
|
323
|
+
|
|
324
|
+
// always construct all parent paths ("x.a.b.c" => ["a", "a.b", "a.b.c"])
|
|
325
|
+
// because a store child object can be passed as argument to a function
|
|
326
|
+
// and in that case the parents proxy get trap will not be invoked,
|
|
327
|
+
// and their path will not be registered
|
|
328
|
+
if (obj[parentSym]) {
|
|
329
|
+
let parts = currentPath.split(pathSeparator);
|
|
330
|
+
while (parts.pop() && parts.length) {
|
|
331
|
+
propPaths.push(parts.join(pathSeparator));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// initialize a watcher paths tracking set (if not already)
|
|
232
336
|
activeWatcher[pathsSubsSym] = activeWatcher[pathsSubsSym] || new Set();
|
|
233
337
|
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
}
|
|
338
|
+
// register the paths to watch
|
|
339
|
+
for (let path of propPaths) {
|
|
340
|
+
let subs = pathWatcherIds.get(path);
|
|
341
|
+
if (!subs) {
|
|
342
|
+
subs = new Set();
|
|
343
|
+
pathWatcherIds.set(path, subs);
|
|
344
|
+
}
|
|
267
345
|
|
|
268
|
-
|
|
269
|
-
if (!subs) {
|
|
270
|
-
subs = new Set();
|
|
271
|
-
pathWatcherIds.set(currentPath, subs);
|
|
272
|
-
}
|
|
273
|
-
subs.add(activeWatcherId);
|
|
346
|
+
subs.add(activeWatcherId);
|
|
274
347
|
|
|
275
|
-
|
|
348
|
+
activeWatcher[pathsSubsSym].add(subs);
|
|
349
|
+
}
|
|
276
350
|
|
|
277
|
-
// register
|
|
351
|
+
// register an extra child watcher to update the custom getter prop replacement
|
|
278
352
|
// (should be removed automatically with the removal of the parent watcher)
|
|
279
353
|
if (
|
|
280
354
|
getterProp &&
|
|
@@ -286,7 +360,7 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
286
360
|
|
|
287
361
|
let getFunc = descriptors[getterProp].get.bind(obj);
|
|
288
362
|
|
|
289
|
-
let getWatcher = watch(() => (
|
|
363
|
+
let getWatcher = watch(() => (receiver[prop] = getFunc()));
|
|
290
364
|
|
|
291
365
|
getWatcher[onRemoveSym] = () => {
|
|
292
366
|
descriptors[getterProp]?.watchers?.delete(watcherId);
|
|
@@ -294,7 +368,7 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
294
368
|
}
|
|
295
369
|
}
|
|
296
370
|
|
|
297
|
-
return
|
|
371
|
+
return propVal;
|
|
298
372
|
},
|
|
299
373
|
set(obj, prop, value) {
|
|
300
374
|
if (typeof prop == "symbol") {
|
|
@@ -303,6 +377,17 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
303
377
|
}
|
|
304
378
|
|
|
305
379
|
let oldValue = obj[prop];
|
|
380
|
+
|
|
381
|
+
// mark as "evicted" in case a proxy child object/array is being replaced
|
|
382
|
+
if (oldValue?.[parentSym]) {
|
|
383
|
+
oldValue[evictedSym] = true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// update the stored parent reference in case of index change (e.g. unshift)
|
|
387
|
+
if (value?.[parentSym] && Array.isArray(obj) && !isNaN(prop)) {
|
|
388
|
+
value[parentSym][1] = prop;
|
|
389
|
+
}
|
|
390
|
+
|
|
306
391
|
obj[prop] = value;
|
|
307
392
|
|
|
308
393
|
// trigger only on value change
|
|
@@ -318,18 +403,22 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
318
403
|
callWatchers(obj, prop, pathWatcherIds);
|
|
319
404
|
|
|
320
405
|
let currentPath = getPath(obj, prop);
|
|
321
|
-
|
|
322
|
-
|
|
406
|
+
|
|
407
|
+
for (const item of pathWatcherIds) {
|
|
408
|
+
if (
|
|
409
|
+
// exact match
|
|
410
|
+
item[0] == currentPath ||
|
|
411
|
+
// child path
|
|
412
|
+
item[0].startsWith(currentPath + pathSeparator)
|
|
413
|
+
) {
|
|
414
|
+
pathWatcherIds.delete(item[0]);
|
|
415
|
+
}
|
|
323
416
|
}
|
|
324
417
|
}
|
|
325
418
|
|
|
326
|
-
delete obj[prop];
|
|
327
|
-
|
|
328
|
-
return true;
|
|
419
|
+
return delete obj[prop];
|
|
329
420
|
},
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
return new Proxy(obj, handler);
|
|
421
|
+
});
|
|
333
422
|
}
|
|
334
423
|
|
|
335
424
|
function getPath(obj, prop) {
|
|
@@ -337,24 +426,13 @@ function getPath(obj, prop) {
|
|
|
337
426
|
|
|
338
427
|
let parentData = obj?.[parentSym];
|
|
339
428
|
while (parentData) {
|
|
340
|
-
currentPath = parentData[1] +
|
|
429
|
+
currentPath = parentData[1] + pathSeparator + currentPath;
|
|
341
430
|
parentData = parentData[0][parentSym];
|
|
342
431
|
}
|
|
343
432
|
|
|
344
433
|
return currentPath;
|
|
345
434
|
}
|
|
346
435
|
|
|
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
|
-
|
|
358
436
|
function callWatchers(obj, prop, pathWatcherIds) {
|
|
359
437
|
let currentPath = getPath(obj, prop);
|
|
360
438
|
|
package/src/template.js
CHANGED
|
@@ -97,7 +97,9 @@ function tag(tagName, attrs = {}, ...children) {
|
|
|
97
97
|
attr = attr.substring(5);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
if (
|
|
100
|
+
if (typeof val === "undefined") {
|
|
101
|
+
el.removeAttribute(attr);
|
|
102
|
+
} else if (
|
|
101
103
|
// JS property or regular HTML attribute
|
|
102
104
|
typeof val != "function" ||
|
|
103
105
|
// event
|
|
@@ -192,6 +194,8 @@ function setChildren(el, children) {
|
|
|
192
194
|
}
|
|
193
195
|
}
|
|
194
196
|
|
|
197
|
+
// Note: Direct nested reactive functions or direct nested arrays are not supported,
|
|
198
|
+
// aka. childrenFunc must return a single element or plain array of elements.
|
|
195
199
|
function initChildrenFuncWatcher(el, childrenFunc) {
|
|
196
200
|
let endPlaceholder = document.createComment("");
|
|
197
201
|
el.appendChild(endPlaceholder);
|
|
@@ -398,7 +402,11 @@ function toArray(val) {
|
|
|
398
402
|
|
|
399
403
|
function normalizeNode(child) {
|
|
400
404
|
// wrap as TextNode so that it can be "tracked" and used with appendChild or other similar methods
|
|
401
|
-
if (
|
|
405
|
+
if (
|
|
406
|
+
typeof child == "string" ||
|
|
407
|
+
typeof child == "number" ||
|
|
408
|
+
typeof child == "boolean"
|
|
409
|
+
) {
|
|
402
410
|
let childNode = document.createTextNode(child);
|
|
403
411
|
childNode.rid = child;
|
|
404
412
|
return childNode;
|
|
@@ -406,7 +414,7 @@ function normalizeNode(child) {
|
|
|
406
414
|
|
|
407
415
|
// in case child is DOM Proxy element/array loaded from a store object
|
|
408
416
|
if (typeof child?.__raw != "undefined") {
|
|
409
|
-
return child.__raw
|
|
417
|
+
return child.__raw;
|
|
410
418
|
}
|
|
411
419
|
|
|
412
420
|
return child;
|