@erickxavier/no-js 1.0.2 → 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.
package/src/registry.js CHANGED
@@ -2,6 +2,9 @@
2
2
  // DIRECTIVE REGISTRY & DOM PROCESSING
3
3
  // ═══════════════════════════════════════════════════════════════════════
4
4
 
5
+ import { _currentEl, _setCurrentEl, _storeWatchers } from "./globals.js";
6
+ import { _i18nListeners } from "./i18n.js";
7
+
5
8
  const _directives = new Map();
6
9
 
7
10
  export function registerDirective(name, handler) {
@@ -43,9 +46,12 @@ export function processElement(el) {
43
46
  }
44
47
 
45
48
  matched.sort((a, b) => a.priority - b.priority);
49
+ const prev = _currentEl;
46
50
  for (const m of matched) {
51
+ _setCurrentEl(el);
47
52
  m.init(el, m.name, m.value);
48
53
  }
54
+ _setCurrentEl(prev);
49
55
  }
50
56
 
51
57
  export function processTree(root) {
@@ -58,3 +64,27 @@ export function processTree(root) {
58
64
  if (!node.__declared) processElement(node);
59
65
  }
60
66
  }
67
+
68
+ // ─── Disposal: proactive cleanup of watchers/listeners/disposers ────────
69
+
70
+ function _disposeElement(node) {
71
+ if (node.__ctx && node.__ctx.__listeners) {
72
+ for (const fn of node.__ctx.__listeners) {
73
+ _storeWatchers.delete(fn);
74
+ _i18nListeners.delete(fn);
75
+ }
76
+ node.__ctx.__listeners.clear();
77
+ }
78
+ if (node.__disposers) {
79
+ node.__disposers.forEach((fn) => fn());
80
+ node.__disposers = null;
81
+ }
82
+ node.__declared = false;
83
+ }
84
+
85
+ export function _disposeTree(root) {
86
+ if (!root) return;
87
+ _disposeElement(root);
88
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
89
+ while (walker.nextNode()) _disposeElement(walker.currentNode);
90
+ }
package/src/router.js CHANGED
@@ -6,13 +6,14 @@ import { _config, _stores, _log } from "./globals.js";
6
6
  import { createContext } from "./context.js";
7
7
  import { evaluate } from "./evaluate.js";
8
8
  import { findContext, _clearDeclared, _loadTemplateElement, _processTemplateIncludes } from "./dom.js";
9
- import { processTree } from "./registry.js";
9
+ import { processTree, _disposeTree } from "./registry.js";
10
10
  import { _animateIn } from "./animations.js";
11
11
 
12
12
  export function _createRouter() {
13
13
  const routes = [];
14
14
  let current = { path: "", params: {}, query: {}, hash: "" };
15
15
  const listeners = new Set();
16
+ const _autoTemplateCache = new Map();
16
17
 
17
18
  function _getOrCreateEntry(path) {
18
19
  let entry = routes.find((r) => r.path === path);
@@ -118,9 +119,38 @@ export function _createRouter() {
118
119
  const outletName = outletAttr && outletAttr.trim() !== "" ? outletAttr.trim() : "default";
119
120
 
120
121
  // Find the template for this outlet in the matched route
121
- const tpl = matched?.route?.outlets?.[outletName];
122
+ let tpl = matched?.route?.outlets?.[outletName];
123
+
124
+ // ── File-based routing: auto-resolve from route-view[src] or config ──
125
+ const configTemplates = _config.router.templates || "";
126
+ if (!tpl && (outletEl.hasAttribute("src") || configTemplates)) {
127
+ const rawSrc = outletEl.getAttribute("src") || configTemplates;
128
+ const baseSrc = rawSrc.replace(/\/?$/, "/");
129
+ const ext = outletEl.getAttribute("ext") || _config.router.ext || ".html";
130
+ const indexName = outletEl.getAttribute("route-index") || "index";
131
+ const segment = current.path === "/" ? indexName : current.path.replace(/^\//, "");
132
+ const fullSrc = baseSrc + segment + ext;
133
+ const cacheKey = outletName + ":" + fullSrc;
134
+
135
+ if (_autoTemplateCache.has(cacheKey)) {
136
+ tpl = _autoTemplateCache.get(cacheKey);
137
+ } else {
138
+ tpl = document.createElement("template");
139
+ tpl.setAttribute("src", fullSrc);
140
+ tpl.setAttribute("route", current.path);
141
+ document.body.appendChild(tpl);
142
+ _autoTemplateCache.set(cacheKey, tpl);
143
+ _log("[ROUTER] File-based route:", current.path, "→", fullSrc);
144
+ }
145
+
146
+ // Auto i18n namespace (convention: filename = namespace)
147
+ if (outletEl.hasAttribute("i18n-ns") && !tpl.getAttribute("i18n-ns")) {
148
+ tpl.setAttribute("i18n-ns", segment);
149
+ }
150
+ }
122
151
 
123
- // Always clear first
152
+ // Always clear first — dispose watchers/listeners before wiping DOM
153
+ _disposeTree(outletEl);
124
154
  outletEl.innerHTML = "";
125
155
 
126
156
  if (tpl) {
@@ -130,6 +160,13 @@ export function _createRouter() {
130
160
  await _loadTemplateElement(tpl);
131
161
  }
132
162
 
163
+ // i18n namespace loading for route template
164
+ const i18nNs = tpl.getAttribute("i18n-ns");
165
+ if (i18nNs) {
166
+ const { _loadI18nNamespace } = await import("./i18n.js");
167
+ await _loadI18nNamespace(i18nNs);
168
+ }
169
+
133
170
  const clone = tpl.content.cloneNode(true);
134
171
 
135
172
  const routeCtx = createContext(
@@ -176,10 +213,10 @@ export function _createRouter() {
176
213
  if (exactClass) {
177
214
  link.classList.toggle(exactClass, current.path === routePath);
178
215
  } else if (activeClass && !link.hasAttribute("route-active-exact")) {
179
- link.classList.toggle(
180
- activeClass,
181
- current.path.startsWith(routePath),
182
- );
216
+ const isActive = routePath === "/"
217
+ ? current.path === "/"
218
+ : current.path.startsWith(routePath);
219
+ link.classList.toggle(activeClass, isActive);
183
220
  }
184
221
  });
185
222
 
@@ -278,6 +315,9 @@ export function _createRouter() {
278
315
  }
279
316
  return;
280
317
  }
318
+ // Skip if path unchanged (prevents double-processing from programmatic hash set)
319
+ const [p] = raw.split("?");
320
+ if (p === current.path) return;
281
321
  navigate(raw, true);
282
322
  });
283
323
  // Initial route