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 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 ~5KB JS framework that comes with deeply reactive state management, plain JS extendable templates and hash-based router.
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 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`.
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.1-rc.4",
2
+ "version": "0.0.1-rc.5",
3
3
  "name": "shablon",
4
4
  "description": "No-build JavaScript framework for Single-page applications",
5
5
  "author": "Gani Georgiev",
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] = "(?<" + parts[i].substring(1, parts[i].length - 1) + ">[^\\/#?]+)";
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
- activeWatcher[pathsResetedSym] = false;
90
- watcher.last = trackedFunc();
123
+ const result = trackedFunc();
91
124
 
92
- activeWatcher = null;
93
- optUntrackedFunc?.();
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 subset of w[pathsSubsSym]) {
139
- if (subset.has(id)) {
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
- let handler = {
189
- get(obj, prop, target) {
190
- if (prop === "__raw") {
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 "@prop" property so that
291
+ // replace with an internal "@@prop" property so that
206
292
  // reactive statements can be cached
207
- prop = "@" + prop;
293
+ prop = "@@" + prop;
208
294
  }
209
295
 
210
- const propVal = obj[prop]
296
+ let propVal = obj[prop];
211
297
 
212
- // directly return symbols and functions (pop, push, etc.)
213
- if (typeof prop == "symbol" || typeof propVal == "function") {
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 !== null && typeof propVal == "object" &&
305
+ propVal != null &&
306
+ typeof propVal == "object" &&
220
307
  !propVal[parentSym] &&
221
- !isExcludedInstance(propVal)
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] = [obj, prop];
224
- obj[prop] = createProxy(propVal, pathWatcherIds);
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
- // 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
- }
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
- let subs = pathWatcherIds.get(currentPath);
269
- if (!subs) {
270
- subs = new Set();
271
- pathWatcherIds.set(currentPath, subs);
272
- }
273
- subs.add(activeWatcherId);
346
+ subs.add(activeWatcherId);
274
347
 
275
- activeWatcher[pathsSubsSym].add(subs);
348
+ activeWatcher[pathsSubsSym].add(subs);
349
+ }
276
350
 
277
- // register a child watcher to update the custom getter prop replacement
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(() => (target[prop] = getFunc()));
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 obj[prop];
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
- if (pathWatcherIds.has(currentPath)) {
322
- pathWatcherIds.delete(currentPath);
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] + "." + currentPath;
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 (typeof child == "string" || typeof child == "number" || typeof child == "boolean") {
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;