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 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(callback)`
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(callback)</strong></summary>
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
- Watch registers a callback function that fires once on initialization and
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
- It returns a "watcher" object that could be used to `unwatch` the registered listener.
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
- Note that for reactive getters, initially the watch callback will be invoked twice because we register a second internal watcher to cache the getter value.
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",
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.44"
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 || options.fallbackPath;
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
- console.warn("no route found");
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("Route navigation failed:", err);
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) + ">\\w+)";
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 dependencies changes.
18
+ * every time any of its evaluated `store` reactive properties change.
18
19
  *
19
- * Returns a "watcher" object that could be used to `unwatch()` the registered listener.
20
+ * It returns a "watcher" object that could be used to `unwatch()` the registered listener.
20
21
  *
21
- * Example:
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
- * @param {Function} callback
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(callback) {
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
- watcher.last = callback();
57
- activeWatcher = allWatchers.get([watcher[parentSym]]); // restore parent ref (if any)
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 unnecessery reactive update.
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 obj[prop] == "function") {
169
- return obj[prop];
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 obj[prop] == "object" &&
175
- obj[prop] !== null &&
176
- !obj[prop][parentSym]
219
+ propVal !== null && typeof propVal == "object" &&
220
+ !propVal[parentSym] &&
221
+ !isExcludedInstance(propVal)
177
222
  ) {
178
- obj[prop][parentSym] = [obj, prop];
179
- obj[prop] = createProxy(obj[prop], pathWatcherIds);
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 (Array.isArray(childOrFunc)) {
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
- // plain elem
187
- el.appendChild(normalizeNode(childOrFunc));
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
  }