shablon 0.0.1-rc.2 → 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 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
 
@@ -166,12 +166,16 @@ data.activity = "rest"
166
166
 
167
167
 
168
168
  <details>
169
- <summary><strong id="api.watch">watch(callback)</strong></summary>
169
+ <summary><strong id="api.watch">watch(trackedFunc, optUntrackedFunc)</strong></summary>
170
170
 
171
- Watch registers a callback function that fires once on initialization and
172
- every time any of its `store` reactive dependencies change.
171
+ Watch registers a callback function that fires on initialization and
172
+ every time any of its evaluated `store` reactive properties change.
173
173
 
174
- 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)._
175
179
 
176
180
  For example:
177
181
 
@@ -187,7 +191,31 @@ w.unwatch()
187
191
  data.count++ // doesn't trigger watch update
188
192
  ```
189
193
 
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.
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.
191
219
 
192
220
  </details>
193
221
 
@@ -218,8 +246,8 @@ When a reactive function is set as attribute value or child, it is invoked only
218
246
 
219
247
  Each constructed tag has 3 additional optional lifecycle attributes:
220
248
 
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
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
223
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 `==`
224
252
 
225
253
  </details>
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.1-rc.2",
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.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) {
@@ -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 (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
  }