@erickxavier/no-js 1.1.0 → 1.2.0

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.
@@ -55,7 +55,8 @@ registerDirective("on:*", {
55
55
  }
56
56
  });
57
57
  if (el.parentElement)
58
- observer.observe(el.parentElement, { childList: true });
58
+ observer.observe(el.parentElement, { childList: true, subtree: true });
59
+ _onDispose(() => observer.disconnect());
59
60
  return;
60
61
  }
61
62
 
@@ -4,7 +4,6 @@
4
4
 
5
5
  import {
6
6
  _config,
7
- _log,
8
7
  _warn,
9
8
  _stores,
10
9
  _notifyStoreWatchers,
@@ -124,16 +123,20 @@ for (const method of HTTP_METHODS) {
124
123
  const savedRetryDelay = _config.retryDelay;
125
124
  _config.retries = retryCount;
126
125
  _config.retryDelay = retryDelay;
127
- const data = await _doFetch(
128
- resolvedUrl,
129
- method,
130
- reqBody,
131
- extraHeaders,
132
- el,
133
- _activeAbort.signal,
134
- );
135
- _config.retries = savedRetries;
136
- _config.retryDelay = savedRetryDelay;
126
+ let data;
127
+ try {
128
+ data = await _doFetch(
129
+ resolvedUrl,
130
+ method,
131
+ reqBody,
132
+ extraHeaders,
133
+ el,
134
+ _activeAbort.signal,
135
+ );
136
+ } finally {
137
+ _config.retries = savedRetries;
138
+ _config.retryDelay = savedRetryDelay;
139
+ }
137
140
 
138
141
  // Cache response
139
142
  if (method === "get") _cacheSet(cacheKey, data, cacheStrategy);
@@ -4,6 +4,7 @@
4
4
  // ═══════════════════════════════════════════════════════════════════════
5
5
 
6
6
  import { _i18n, _watchI18n, _loadI18nNamespace, _notifyI18n } from "../i18n.js";
7
+ import { _watchExpr } from "../globals.js";
7
8
  import { evaluate } from "../evaluate.js";
8
9
  import { findContext } from "../dom.js";
9
10
  import { registerDirective, processTree } from "../registry.js";
@@ -30,7 +31,7 @@ registerDirective("t", {
30
31
  }
31
32
  }
32
33
 
33
- ctx.$watch(update);
34
+ _watchExpr(key, ctx, update);
34
35
  _watchI18n(update);
35
36
  update();
36
37
  },
@@ -39,6 +40,9 @@ registerDirective("t", {
39
40
  registerDirective("i18n-ns", {
40
41
  priority: 1,
41
42
  init(el, name, ns) {
43
+ // Empty ns = marker attribute (e.g. route-view); skip loading
44
+ if (!ns) return;
45
+
42
46
  // Save children to prevent premature t resolution
43
47
  const saved = document.createDocumentFragment();
44
48
  while (el.firstChild) saved.appendChild(el.firstChild);
@@ -3,6 +3,7 @@
3
3
  // ═══════════════════════════════════════════════════════════════════════
4
4
 
5
5
  import { createContext } from "../context.js";
6
+ import { _watchExpr } from "../globals.js";
6
7
  import { evaluate, resolve } from "../evaluate.js";
7
8
  import { findContext, _cloneTemplate } from "../dom.js";
8
9
  import { registerDirective, processTree } from "../registry.js";
@@ -23,11 +24,26 @@ registerDirective("each", {
23
24
  const animLeave = el.getAttribute("animate-leave");
24
25
  const stagger = parseInt(el.getAttribute("animate-stagger")) || 0;
25
26
  const animDuration = parseInt(el.getAttribute("animate-duration")) || 0;
27
+ let prevList = null;
26
28
 
27
29
  function update() {
28
- let list = resolve(listPath, ctx);
30
+ let list = /[\[\]()\s+\-*\/!?:&|]/.test(listPath)
31
+ ? evaluate(listPath, ctx)
32
+ : resolve(listPath, ctx);
29
33
  if (!Array.isArray(list)) return;
30
34
 
35
+ // If same list reference and items are rendered, skip re-render
36
+ // and just propagate the notification to child contexts so their
37
+ // watchers (bind, show, model, etc.) can react to parent changes
38
+ // without destroying/recreating the DOM (preserves input focus).
39
+ if (list === prevList && list.length > 0 && el.children.length > 0) {
40
+ for (const child of el.children) {
41
+ if (child.__ctx && child.__ctx.$notify) child.__ctx.$notify();
42
+ }
43
+ return;
44
+ }
45
+ prevList = list;
46
+
31
47
  // Empty state
32
48
  if (list.length === 0 && elseTpl) {
33
49
  const clone = _cloneTemplate(elseTpl);
@@ -90,6 +106,7 @@ registerDirective("each", {
90
106
  const firstChild = wrapper.firstElementChild;
91
107
  if (firstChild) {
92
108
  firstChild.classList.add(animEnter);
109
+ firstChild.addEventListener("animationend", () => firstChild.classList.remove(animEnter), { once: true });
93
110
  // Stagger animation — delay must be on the child, not the wrapper
94
111
  if (stagger) {
95
112
  firstChild.style.animationDelay = i * stagger + "ms";
@@ -99,7 +116,7 @@ registerDirective("each", {
99
116
  });
100
117
  }
101
118
 
102
- ctx.$watch(update);
119
+ _watchExpr(expr, ctx, update);
103
120
  update();
104
121
  },
105
122
  });
@@ -204,6 +221,7 @@ registerDirective("foreach", {
204
221
  const firstChild = wrapper.firstElementChild;
205
222
  if (firstChild) {
206
223
  firstChild.classList.add(animEnter);
224
+ firstChild.addEventListener("animationend", () => firstChild.classList.remove(animEnter), { once: true });
207
225
  // Stagger animation — delay must be on the child, not the wrapper
208
226
  if (stagger) {
209
227
  firstChild.style.animationDelay = (i * stagger) + "ms";
@@ -233,7 +251,7 @@ registerDirective("foreach", {
233
251
  }
234
252
  }
235
253
 
236
- ctx.$watch(update);
254
+ _watchExpr(fromPath, ctx, update);
237
255
  update();
238
256
  },
239
257
  });
@@ -6,6 +6,7 @@ import {
6
6
  _refs,
7
7
  _stores,
8
8
  _notifyStoreWatchers,
9
+ _onDispose,
9
10
  } from "../globals.js";
10
11
  import { createContext } from "../context.js";
11
12
  import { evaluate, _execStatement, _interpolate } from "../evaluate.js";
@@ -17,6 +18,9 @@ registerDirective("ref", {
17
18
  priority: 5,
18
19
  init(el, name, refName) {
19
20
  _refs[refName] = el;
21
+ _onDispose(() => {
22
+ if (_refs[refName] === el) delete _refs[refName];
23
+ });
20
24
  },
21
25
  });
22
26
 
@@ -2,7 +2,7 @@
2
2
  // DIRECTIVES: state, store, computed, watch
3
3
  // ═══════════════════════════════════════════════════════════════════════
4
4
 
5
- import { _stores, _log } from "../globals.js";
5
+ import { _stores, _log, _watchExpr } from "../globals.js";
6
6
  import { createContext } from "../context.js";
7
7
  import { evaluate, _execStatement } from "../evaluate.js";
8
8
  import { findContext } from "../dom.js";
@@ -78,7 +78,7 @@ registerDirective("computed", {
78
78
  const val = evaluate(expr, ctx);
79
79
  ctx.$set(computedName, val);
80
80
  }
81
- ctx.$watch(update);
81
+ _watchExpr(expr, ctx, update);
82
82
  update();
83
83
  },
84
84
  });
@@ -89,7 +89,7 @@ registerDirective("watch", {
89
89
  const ctx = findContext(el);
90
90
  const onChange = el.getAttribute("on:change");
91
91
  let lastVal = evaluate(watchExpr, ctx);
92
- ctx.$watch(() => {
92
+ _watchExpr(watchExpr, ctx, () => {
93
93
  const newVal = evaluate(watchExpr, ctx);
94
94
  if (newVal !== lastVal) {
95
95
  const oldVal = lastVal;
@@ -2,6 +2,7 @@
2
2
  // DIRECTIVES: class-*, style-*
3
3
  // ═══════════════════════════════════════════════════════════════════════
4
4
 
5
+ import { _watchExpr } from "../globals.js";
5
6
  import { evaluate } from "../evaluate.js";
6
7
  import { findContext } from "../dom.js";
7
8
  import { registerDirective } from "../registry.js";
@@ -14,16 +15,18 @@ registerDirective("class-*", {
14
15
  const ctx = findContext(el);
15
16
 
16
17
  // class-map="{ active: x, bold: y }"
18
+ // Supports space-separated keys: class-map="{ 'bg-sky-500 text-white': x }"
17
19
  if (suffix === "map") {
18
20
  function update() {
19
21
  const obj = evaluate(expr, ctx);
20
22
  if (obj && typeof obj === "object") {
21
23
  for (const [cls, cond] of Object.entries(obj)) {
22
- el.classList.toggle(cls, !!cond);
24
+ const parts = cls.split(/\s+/).filter(Boolean);
25
+ parts.forEach((c) => el.classList.toggle(c, !!cond));
23
26
  }
24
27
  }
25
28
  }
26
- ctx.$watch(update);
29
+ _watchExpr(expr, ctx, update);
27
30
  update();
28
31
  return;
29
32
  }
@@ -42,7 +45,7 @@ registerDirective("class-*", {
42
45
  prevClasses = next;
43
46
  }
44
47
  }
45
- ctx.$watch(update);
48
+ _watchExpr(expr, ctx, update);
46
49
  update();
47
50
  return;
48
51
  }
@@ -51,8 +54,8 @@ registerDirective("class-*", {
51
54
  function update() {
52
55
  el.classList.toggle(suffix, !!evaluate(expr, ctx));
53
56
  }
54
- ctx.$watch(update);
55
- if (expr.includes("NoJS.locale")) _watchI18n(update);
57
+ _watchExpr(expr, ctx, update);
58
+ if (expr.includes("$i18n") || expr.includes("NoJS.locale")) _watchI18n(update);
56
59
  update();
57
60
  },
58
61
  });
@@ -73,7 +76,7 @@ registerDirective("style-*", {
73
76
  }
74
77
  }
75
78
  }
76
- ctx.$watch(update);
79
+ _watchExpr(expr, ctx, update);
77
80
  update();
78
81
  return;
79
82
  }
@@ -84,7 +87,7 @@ registerDirective("style-*", {
84
87
  const val = evaluate(expr, ctx);
85
88
  el.style[cssProp] = val != null ? String(val) : "";
86
89
  }
87
- ctx.$watch(update);
90
+ _watchExpr(expr, ctx, update);
88
91
  update();
89
92
  },
90
93
  });
package/src/evaluate.js CHANGED
@@ -265,22 +265,28 @@ export function _execStatement(expr, ctx, extraVars = {}) {
265
265
  // For each key in any ancestor context, find the owning context at runtime
266
266
  // and call $set on it — so mutations inside `each` loops correctly
267
267
  // propagate back to parent state (e.g. cart updated from a loop's on:click).
268
+ // Only write back values that actually changed locally, to avoid
269
+ // overwriting proxy mutations made by called functions.
268
270
  const chainKeys = new Set();
269
271
  let _wCtx = ctx;
270
272
  while (_wCtx && _wCtx.__isProxy) {
271
273
  for (const k of Object.keys(_wCtx.__raw)) chainKeys.add(k);
272
274
  _wCtx = _wCtx.$parent;
273
275
  }
276
+ const origObj = {};
277
+ for (const k of chainKeys) {
278
+ if (!k.startsWith("$") && k in vals) origObj[k] = vals[k];
279
+ }
274
280
  const setters = [...chainKeys]
275
281
  .filter((k) => !k.startsWith("$"))
276
282
  .map(
277
283
  (k) =>
278
- `{let _c=__ctx;while(_c&&_c.__isProxy){if('${k}'in _c.__raw){_c.$set('${k}',typeof ${k}!=='undefined'?${k}:_c.__raw['${k}']);break;}_c=_c.$parent;}}`,
284
+ `{let _c=__ctx;while(_c&&_c.__isProxy){if('${k}'in _c.__raw){if(typeof ${k}!=='undefined'){if(${k}!==__orig['${k}'])_c.$set('${k}',${k});else if(typeof ${k}==='object'&&${k}!==null)_c.$notify();}break;}_c=_c.$parent;}}`,
279
285
  )
280
286
  .join("\n");
281
287
 
282
- const fn = new Function("__ctx", ...keyArr, `${expr};\n${setters}`);
283
- fn(ctx, ...valArr);
288
+ const fn = new Function("__ctx", "__orig", ...keyArr, `${expr};\n${setters}`);
289
+ fn(ctx, origObj, ...valArr);
284
290
 
285
291
  // Notify global store watchers when expression touches $store
286
292
  if (typeof expr === "string" && expr.includes("$store")) {
package/src/i18n.js CHANGED
@@ -47,13 +47,14 @@ export async function _loadLocale(locale, ns) {
47
47
 
48
48
  let url = _config.i18n.loadPath.replace("{locale}", locale);
49
49
  if (ns) url = url.replace("{ns}", ns);
50
+ else if (url.includes("{ns}")) return; // no namespace to substitute
51
+
50
52
 
51
53
  try {
52
54
  const res = await fetch(url);
53
55
  if (!res.ok) { _warn(`i18n: failed to load ${url} (${res.status})`); return; }
54
56
  const data = await res.json();
55
- const toMerge = ns ? { [ns]: data } : data;
56
- _i18n.locales[locale] = _deepMerge(_i18n.locales[locale] || {}, toMerge);
57
+ _i18n.locales[locale] = _deepMerge(_i18n.locales[locale] || {}, data);
57
58
  if (_config.i18n.cache) _i18nCache.set(cacheKey, data);
58
59
  } catch (e) {
59
60
  _warn(`i18n: error loading ${url}`, e);
package/src/index.js CHANGED
@@ -37,6 +37,7 @@ import "./directives/events.js";
37
37
  import "./directives/refs.js";
38
38
  import "./directives/validation.js";
39
39
  import "./directives/i18n.js";
40
+ import "./directives/dnd.js";
40
41
 
41
42
  // ═══════════════════════════════════════════════════════════════════════
42
43
  // PUBLIC API
@@ -215,7 +216,7 @@ const NoJS = {
215
216
  resolve,
216
217
 
217
218
  // Version
218
- version: "1.1.0",
219
+ version: "1.2.0",
219
220
  };
220
221
 
221
222
  export default NoJS;
package/src/router.js CHANGED
@@ -315,6 +315,9 @@ export function _createRouter() {
315
315
  }
316
316
  return;
317
317
  }
318
+ // Skip if path unchanged (prevents double-processing from programmatic hash set)
319
+ const [p] = raw.split("?");
320
+ if (p === current.path) return;
318
321
  navigate(raw, true);
319
322
  });
320
323
  // Initial route