@erickxavier/no-js 1.8.1 → 1.8.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erickxavier/no-js",
3
- "version": "1.8.1",
3
+ "version": "1.8.2",
4
4
  "description": "The HTML-first reactive framework — build dynamic web apps with just HTML attributes, no JavaScript required",
5
5
  "main": "dist/cjs/no.js",
6
6
  "module": "dist/esm/no.js",
@@ -2,7 +2,7 @@
2
2
  // DIRECTIVES: bind, bind-html, bind-*, model
3
3
  // ═══════════════════════════════════════════════════════════════════════
4
4
 
5
- import { _watchExpr } from "../globals.js";
5
+ import { _watchExpr, _onDispose } from "../globals.js";
6
6
  import { evaluate, _execStatement } from "../evaluate.js";
7
7
  import { findContext, _sanitizeHtml } from "../dom.js";
8
8
  import { registerDirective } from "../registry.js";
@@ -46,10 +46,12 @@ registerDirective("bind-*", {
46
46
  el.tagName === "TEXTAREA" ||
47
47
  el.tagName === "SELECT")
48
48
  ) {
49
- el.addEventListener("input", () => {
49
+ const inputHandler = () => {
50
50
  const val = el.type === "number" ? Number(el.value) : el.value;
51
51
  _execStatement(`${expr} = ${JSON.stringify(val)}`, ctx);
52
- });
52
+ };
53
+ el.addEventListener("input", inputHandler);
54
+ _onDispose(() => el.removeEventListener("input", inputHandler));
53
55
  }
54
56
 
55
57
  function update() {
@@ -104,13 +106,15 @@ registerDirective("model", {
104
106
  tag === "SELECT" || type === "checkbox" || type === "radio"
105
107
  ? "change"
106
108
  : "input";
107
- el.addEventListener(event, () => {
109
+ const domHandler = () => {
108
110
  let val;
109
111
  if (type === "checkbox") val = el.checked;
110
112
  else if (type === "number" || type === "range") val = Number(el.value);
111
113
  else val = el.value;
112
114
  _execStatement(`${expr} = __val`, ctx, { __val: val });
113
- });
115
+ };
116
+ el.addEventListener(event, domHandler);
117
+ _onDispose(() => el.removeEventListener(event, domHandler));
114
118
 
115
119
  _watchExpr(expr, ctx, update);
116
120
  update();
@@ -5,7 +5,7 @@
5
5
  import { _watchExpr } from "../globals.js";
6
6
  import { evaluate } from "../evaluate.js";
7
7
  import { findContext, _clearDeclared, _cloneTemplate } from "../dom.js";
8
- import { registerDirective, processTree } from "../registry.js";
8
+ import { registerDirective, processTree, _disposeChildren } from "../registry.js";
9
9
  import { _animateIn, _animateOut } from "../animations.js";
10
10
 
11
11
  registerDirective("if", {
@@ -36,6 +36,7 @@ registerDirective("if", {
36
36
  }
37
37
 
38
38
  function render(result) {
39
+ _disposeChildren(el);
39
40
  if (result) {
40
41
  if (thenId) {
41
42
  const clone = _cloneTemplate(thenId);
@@ -92,6 +93,7 @@ registerDirective("else-if", {
92
93
  prev.getAttribute("if") || prev.getAttribute("else-if");
93
94
  if (prevExpr) {
94
95
  if (evaluate(prevExpr, ctx)) {
96
+ _disposeChildren(el);
95
97
  el.innerHTML = "";
96
98
  el.style.display = "none";
97
99
  return;
@@ -106,10 +108,12 @@ registerDirective("else-if", {
106
108
  if (thenId) {
107
109
  const clone = _cloneTemplate(thenId);
108
110
  if (clone) {
111
+ _disposeChildren(el);
109
112
  el.innerHTML = "";
110
113
  el.appendChild(clone);
111
114
  }
112
115
  } else {
116
+ _disposeChildren(el);
113
117
  el.innerHTML = "";
114
118
  for (const child of originalChildren)
115
119
  el.appendChild(child.cloneNode(true));
@@ -117,6 +121,7 @@ registerDirective("else-if", {
117
121
  _clearDeclared(el);
118
122
  processTree(el);
119
123
  } else {
124
+ _disposeChildren(el);
120
125
  el.innerHTML = "";
121
126
  }
122
127
  }
@@ -142,6 +147,7 @@ registerDirective("else", {
142
147
  prev.getAttribute("if") || prev.getAttribute("else-if");
143
148
  if (prevExpr) {
144
149
  if (evaluate(prevExpr, ctx)) {
150
+ _disposeChildren(el);
145
151
  el.innerHTML = "";
146
152
  el.style.display = "none";
147
153
  return;
@@ -155,10 +161,12 @@ registerDirective("else", {
155
161
  if (thenId) {
156
162
  const clone = _cloneTemplate(thenId);
157
163
  if (clone) {
164
+ _disposeChildren(el);
158
165
  el.innerHTML = "";
159
166
  el.appendChild(clone);
160
167
  }
161
168
  } else {
169
+ _disposeChildren(el);
162
170
  el.innerHTML = "";
163
171
  for (const child of originalChildren)
164
172
  el.appendChild(child.cloneNode(true));
@@ -258,6 +266,7 @@ registerDirective("switch", {
258
266
  if (thenTpl) {
259
267
  const clone = _cloneTemplate(thenTpl);
260
268
  if (clone) {
269
+ _disposeChildren(child);
261
270
  child.innerHTML = "";
262
271
  child.appendChild(clone);
263
272
  }
@@ -272,6 +281,7 @@ registerDirective("switch", {
272
281
  if (!matched && thenTpl) {
273
282
  const clone = _cloneTemplate(thenTpl);
274
283
  if (clone) {
284
+ _disposeChildren(child);
275
285
  child.innerHTML = "";
276
286
  child.appendChild(clone);
277
287
  }
@@ -5,7 +5,7 @@
5
5
  import { evaluate, _execStatement, resolve } from "../evaluate.js";
6
6
  import { findContext } from "../dom.js";
7
7
  import { createContext } from "../context.js";
8
- import { registerDirective, processTree } from "../registry.js";
8
+ import { registerDirective, processTree, _disposeChildren } from "../registry.js";
9
9
  import { _onDispose, _warn } from "../globals.js";
10
10
 
11
11
  // ─── Module-scoped DnD coordination state ─────────────────────────────
@@ -414,7 +414,8 @@ registerDirective("drag", {
414
414
  if (disabled) el.removeAttribute("aria-grabbed");
415
415
  else el.setAttribute("aria-grabbed", "false");
416
416
  }
417
- ctx.$watch(updateDisabled);
417
+ const unwatchDisabled = ctx.$watch(updateDisabled);
418
+ _onDispose(unwatchDisabled);
418
419
  }
419
420
 
420
421
  // Keyboard DnD support
@@ -724,6 +725,7 @@ registerDirective("drag-list", {
724
725
  const tpl = tplId ? document.getElementById(tplId) : null;
725
726
  if (!tpl) return;
726
727
 
728
+ _disposeChildren(el);
727
729
  el.innerHTML = "";
728
730
  const count = list.length;
729
731
 
@@ -1074,7 +1076,8 @@ registerDirective("drag-list", {
1074
1076
  });
1075
1077
 
1076
1078
  // ─── Reactive rendering ──────────────────────────────────────────
1077
- ctx.$watch(renderItems);
1079
+ const unwatchList = ctx.$watch(renderItems);
1080
+ _onDispose(unwatchList);
1078
1081
  renderItems();
1079
1082
  },
1080
1083
  });
@@ -142,6 +142,9 @@ registerDirective("on:*", {
142
142
  if (modifiers.has("once")) opts.once = true;
143
143
 
144
144
  el.addEventListener(event, handler, opts);
145
+ if (!opts.once) {
146
+ _onDispose(() => el.removeEventListener(event, handler, opts));
147
+ }
145
148
  },
146
149
  });
147
150
 
@@ -150,9 +153,11 @@ registerDirective("trigger", {
150
153
  init(el, name, eventName) {
151
154
  const ctx = findContext(el);
152
155
  const dataExpr = el.getAttribute("trigger-data");
153
- el.addEventListener("click", () => {
156
+ const clickHandler = () => {
154
157
  const detail = dataExpr ? evaluate(dataExpr, ctx) : null;
155
158
  el.dispatchEvent(new CustomEvent(eventName, { detail, bubbles: true }));
156
- });
159
+ };
160
+ el.addEventListener("click", clickHandler);
161
+ _onDispose(() => el.removeEventListener("click", clickHandler));
157
162
  },
158
163
  });
@@ -15,7 +15,7 @@ import { createContext } from "../context.js";
15
15
  import { evaluate, _execStatement, _interpolate } from "../evaluate.js";
16
16
  import { _doFetch, _cacheGet, _cacheSet } from "../fetch.js";
17
17
  import { findContext, _clearDeclared, _cloneTemplate } from "../dom.js";
18
- import { registerDirective, processTree } from "../registry.js";
18
+ import { registerDirective, processTree, _disposeChildren } from "../registry.js";
19
19
  import { _devtoolsEmit } from "../devtools.js";
20
20
 
21
21
  const HTTP_METHODS = ["get", "post", "put", "patch", "delete"];
@@ -96,6 +96,7 @@ for (const method of HTTP_METHODS) {
96
96
  if (loadingTpl) {
97
97
  const clone = _cloneTemplate(loadingTpl);
98
98
  if (clone) {
99
+ _disposeChildren(el);
99
100
  el.innerHTML = "";
100
101
  el.appendChild(clone);
101
102
  processTree(el);
@@ -149,6 +150,7 @@ for (const method of HTTP_METHODS) {
149
150
  ) {
150
151
  const clone = _cloneTemplate(emptyTpl);
151
152
  if (clone) {
153
+ _disposeChildren(el);
152
154
  el.innerHTML = "";
153
155
  el.appendChild(clone);
154
156
  processTree(el);
@@ -169,6 +171,7 @@ for (const method of HTTP_METHODS) {
169
171
  if (successTpl) {
170
172
  const clone = _cloneTemplate(successTpl);
171
173
  if (clone) {
174
+ _disposeChildren(el);
172
175
  el.innerHTML = "";
173
176
  // Inject var
174
177
  const tplEl = document.getElementById(
@@ -185,6 +188,7 @@ for (const method of HTTP_METHODS) {
185
188
  }
186
189
  } else {
187
190
  // Restore original children and re-process
191
+ _disposeChildren(el);
188
192
  el.innerHTML = "";
189
193
  for (const child of originalChildren)
190
194
  el.appendChild(child.cloneNode(true));
@@ -216,6 +220,7 @@ for (const method of HTTP_METHODS) {
216
220
  if (errorTpl) {
217
221
  const clone = _cloneTemplate(errorTpl);
218
222
  if (clone) {
223
+ _disposeChildren(el);
219
224
  el.innerHTML = "";
220
225
  const tplEl = document.getElementById(
221
226
  errorTpl.replace("#", ""),
@@ -244,18 +249,22 @@ for (const method of HTTP_METHODS) {
244
249
 
245
250
  // For forms, intercept submit
246
251
  if (el.tagName === "FORM" && method !== "get") {
247
- el.addEventListener("submit", (e) => {
252
+ const submitHandler = (e) => {
248
253
  e.preventDefault();
249
254
  doRequest();
250
- });
255
+ };
256
+ el.addEventListener("submit", submitHandler);
257
+ _onDispose(() => el.removeEventListener("submit", submitHandler));
251
258
  } else if (method === "get") {
252
259
  doRequest();
253
260
  } else {
254
261
  // Non-GET on non-FORM: attach click listener
255
- el.addEventListener("click", (e) => {
262
+ const clickHandler = (e) => {
256
263
  e.preventDefault();
257
264
  doRequest();
258
- });
265
+ };
266
+ el.addEventListener("click", clickHandler);
267
+ _onDispose(() => el.removeEventListener("click", clickHandler));
259
268
  }
260
269
 
261
270
  // Reactive URL watching: re-fetch when {expressions} in URL change
@@ -278,14 +287,23 @@ for (const method of HTTP_METHODS) {
278
287
  }
279
288
  }
280
289
 
290
+ _onDispose(() => {
291
+ if (_debounceTimer) clearTimeout(_debounceTimer);
292
+ });
293
+
281
294
  // Watch all ancestor contexts for changes
282
295
  let ancestor = parentCtx;
283
296
  while (ancestor && ancestor.__isProxy) {
284
- ancestor.$watch(onAncestorChange);
297
+ const unwatch = ancestor.$watch(onAncestorChange);
298
+ _onDispose(unwatch);
285
299
  ancestor = ancestor.$parent;
286
300
  }
287
301
  }
288
302
 
303
+ // Expose doRequest for programmatic re-fetch via $refs
304
+ el.refresh = doRequest;
305
+ _onDispose(() => { delete el.refresh; });
306
+
289
307
  // Polling
290
308
  if (refreshInterval > 0) {
291
309
  const id = setInterval(doRequest, refreshInterval);
@@ -6,7 +6,7 @@ import { createContext } from "../context.js";
6
6
  import { _watchExpr } from "../globals.js";
7
7
  import { evaluate, resolve } from "../evaluate.js";
8
8
  import { findContext, _cloneTemplate } from "../dom.js";
9
- import { registerDirective, processTree } from "../registry.js";
9
+ import { registerDirective, processTree, _disposeChildren } from "../registry.js";
10
10
  import { _animateOut } from "../animations.js";
11
11
 
12
12
  registerDirective("each", {
@@ -48,6 +48,7 @@ registerDirective("each", {
48
48
  if (list.length === 0 && elseTpl) {
49
49
  const clone = _cloneTemplate(elseTpl);
50
50
  if (clone) {
51
+ _disposeChildren(el);
51
52
  el.innerHTML = "";
52
53
  el.appendChild(clone);
53
54
  processTree(el);
@@ -80,6 +81,7 @@ registerDirective("each", {
80
81
 
81
82
  function renderItems(tpl, list) {
82
83
  const count = list.length;
84
+ _disposeChildren(el);
83
85
  el.innerHTML = "";
84
86
 
85
87
  list.forEach((item, i) => {
@@ -178,6 +180,7 @@ registerDirective("foreach", {
178
180
  if (list.length === 0 && elseTpl) {
179
181
  const clone = _cloneTemplate(elseTpl);
180
182
  if (clone) {
183
+ _disposeChildren(el);
181
184
  el.innerHTML = "";
182
185
  el.appendChild(clone);
183
186
  processTree(el);
@@ -189,6 +192,7 @@ registerDirective("foreach", {
189
192
  const count = list.length;
190
193
 
191
194
  function renderForeachItems() {
195
+ _disposeChildren(el);
192
196
  el.innerHTML = "";
193
197
  list.forEach((item, i) => {
194
198
  const childData = {
@@ -16,7 +16,7 @@ import { createContext } from "../context.js";
16
16
  import { evaluate, _execStatement, _interpolate } from "../evaluate.js";
17
17
  import { _doFetch } from "../fetch.js";
18
18
  import { findContext, _cloneTemplate } from "../dom.js";
19
- import { registerDirective, processTree } from "../registry.js";
19
+ import { registerDirective, processTree, _disposeChildren } from "../registry.js";
20
20
 
21
21
  registerDirective("ref", {
22
22
  priority: 5,
@@ -64,6 +64,7 @@ registerDirective("use", {
64
64
  }
65
65
  }
66
66
 
67
+ _disposeChildren(el);
67
68
  el.innerHTML = "";
68
69
  const wrapper = document.createElement("div");
69
70
  wrapper.style.display = "contents";
@@ -93,7 +94,7 @@ registerDirective("call", {
93
94
  const originalChildren = [...el.childNodes].map((n) => n.cloneNode(true));
94
95
  let _activeAbort = null;
95
96
 
96
- el.addEventListener("click", async (e) => {
97
+ const clickHandler = async (e) => {
97
98
  e.preventDefault();
98
99
  if (confirmMsg && !window.confirm(confirmMsg)) return;
99
100
 
@@ -107,6 +108,7 @@ registerDirective("call", {
107
108
  if (loadingTpl) {
108
109
  const clone = _cloneTemplate(loadingTpl);
109
110
  if (clone) {
111
+ _disposeChildren(el);
110
112
  el.innerHTML = "";
111
113
  el.appendChild(clone);
112
114
  processTree(el);
@@ -137,6 +139,7 @@ registerDirective("call", {
137
139
 
138
140
  // Restore original children
139
141
  if (loadingTpl) {
142
+ _disposeChildren(el);
140
143
  el.innerHTML = "";
141
144
  for (const child of originalChildren)
142
145
  el.appendChild(child.cloneNode(true));
@@ -181,6 +184,7 @@ registerDirective("call", {
181
184
 
182
185
  // Restore original children
183
186
  if (loadingTpl) {
187
+ _disposeChildren(el);
184
188
  el.innerHTML = "";
185
189
  for (const child of originalChildren)
186
190
  el.appendChild(child.cloneNode(true));
@@ -210,6 +214,8 @@ registerDirective("call", {
210
214
  }
211
215
  }
212
216
  }
213
- });
217
+ };
218
+ el.addEventListener("click", clickHandler);
219
+ _onDispose(() => el.removeEventListener("click", clickHandler));
214
220
  },
215
221
  });
@@ -6,7 +6,7 @@
6
6
  import { _validators, _onDispose } from "../globals.js";
7
7
  import { createContext } from "../context.js";
8
8
  import { findContext, _cloneTemplate } from "../dom.js";
9
- import { registerDirective, processTree } from "../registry.js";
9
+ import { registerDirective, processTree, _disposeChildren } from "../registry.js";
10
10
  import { evaluate } from "../evaluate.js";
11
11
 
12
12
  // ── ValidityState → rule name mapping ────────────────────────────────
@@ -544,6 +544,7 @@ registerDirective("error-boundary", {
544
544
  { err: { message } },
545
545
  ctx,
546
546
  );
547
+ _disposeChildren(el);
547
548
  el.innerHTML = "";
548
549
  const wrapper = document.createElement("div");
549
550
  wrapper.style.display = "contents";
package/src/globals.js CHANGED
@@ -62,7 +62,11 @@ export function _notifyStoreWatchers() {
62
62
  }
63
63
 
64
64
  export function _watchExpr(expr, ctx, fn) {
65
- ctx.$watch(fn);
65
+ const unwatch = ctx.$watch(fn);
66
+ _onDispose(() => {
67
+ unwatch();
68
+ _storeWatchers.delete(fn);
69
+ });
66
70
  if (typeof expr === "string" && expr.includes("$store")) {
67
71
  _storeWatchers.add(fn);
68
72
  }
package/src/index.js CHANGED
@@ -240,7 +240,7 @@ const NoJS = {
240
240
  resolve,
241
241
 
242
242
  // Version
243
- version: "1.8.1",
243
+ version: "1.8.2",
244
244
  };
245
245
 
246
246
  export default NoJS;
package/src/registry.js CHANGED
@@ -106,3 +106,9 @@ export function _disposeTree(root) {
106
106
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
107
107
  while (walker.nextNode()) _disposeElement(walker.currentNode);
108
108
  }
109
+
110
+ export function _disposeChildren(parent) {
111
+ if (!parent) return;
112
+ const walker = document.createTreeWalker(parent, NodeFilter.SHOW_ELEMENT);
113
+ while (walker.nextNode()) _disposeElement(walker.currentNode);
114
+ }