slicejs-web-framework 2.1.0 → 2.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.
@@ -4,12 +4,16 @@ export default class Router {
4
4
  this.activeRoute = null;
5
5
  this.pathToRouteMap = this.createPathToRouteMap(routes);
6
6
 
7
- // NUEVO: Sistema de caché optimizado
7
+ // Navigation Guards
8
+ this._beforeEachGuard = null;
9
+ this._afterEachGuard = null;
10
+
11
+ // Sistema de caché optimizado
8
12
  this.routeContainersCache = new Map();
9
13
  this.lastCacheUpdate = 0;
10
14
  this.CACHE_DURATION = 100; // ms - caché muy corto pero efectivo
11
15
 
12
- // NUEVO: Observer para invalidar caché automáticamente
16
+ // Observer para invalidar caché automáticamente
13
17
  this.setupMutationObserver();
14
18
  }
15
19
 
@@ -18,7 +22,276 @@ export default class Router {
18
22
  window.addEventListener('popstate', this.onRouteChange.bind(this));
19
23
  }
20
24
 
21
- // NUEVO: Observer para detectar cambios en el DOM
25
+ // ============================================
26
+ // NAVIGATION GUARDS API
27
+ // ============================================
28
+
29
+ /**
30
+ * Registra un guard que se ejecuta ANTES de cada navegación
31
+ * Puede bloquear o redirigir la navegación
32
+ * @param {Function} guard - Función (to, from, next) => {}
33
+ */
34
+ beforeEach(guard) {
35
+ if (typeof guard !== 'function') {
36
+ slice.logger.logError('Router', 'beforeEach expects a function');
37
+ return;
38
+ }
39
+ this._beforeEachGuard = guard;
40
+ }
41
+
42
+ /**
43
+ * Registra un guard que se ejecuta DESPUÉS de cada navegación
44
+ * No puede bloquear la navegación
45
+ * @param {Function} guard - Función (to, from) => {}
46
+ */
47
+ afterEach(guard) {
48
+ if (typeof guard !== 'function') {
49
+ slice.logger.logError('Router', 'afterEach expects a function');
50
+ return;
51
+ }
52
+ this._afterEachGuard = guard;
53
+ }
54
+
55
+ /**
56
+ * Crea un objeto con información de ruta para los guards
57
+ * @param {Object} route - Objeto de ruta
58
+ * @param {Object} params - Parámetros de la ruta
59
+ * @returns {Object} Objeto con path, component, params, query, metadata
60
+ */
61
+ _createRouteInfo(route, params = {}) {
62
+ if (!route) {
63
+ return {
64
+ path: '/404',
65
+ component: 'NotFound',
66
+ params: {},
67
+ query: this._parseQueryParams(),
68
+ metadata: {}
69
+ };
70
+ }
71
+
72
+ return {
73
+ path: route.fullPath || route.path,
74
+ component: route.parentRoute ? route.parentRoute.component : route.component,
75
+ params: params,
76
+ query: this._parseQueryParams(),
77
+ metadata: route.metadata || {}
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Parsea los query parameters de la URL actual
83
+ * @returns {Object} Objeto con los query params
84
+ */
85
+ _parseQueryParams() {
86
+ const queryString = window.location.search;
87
+ if (!queryString) return {};
88
+
89
+ const params = {};
90
+ const urlParams = new URLSearchParams(queryString);
91
+
92
+ for (const [key, value] of urlParams) {
93
+ params[key] = value;
94
+ }
95
+
96
+ return params;
97
+ }
98
+
99
+ /**
100
+ * Ejecuta el beforeEach guard si existe
101
+ * @param {Object} to - Información de ruta destino
102
+ * @param {Object} from - Información de ruta origen
103
+ * @returns {String|null} Path de redirección o null si continúa
104
+ */
105
+ async _executeBeforeEachGuard(to, from) {
106
+ if (!this._beforeEachGuard) {
107
+ return null;
108
+ }
109
+
110
+ let redirectPath = null;
111
+ let nextCalled = false;
112
+
113
+ const next = (path) => {
114
+ if (nextCalled) {
115
+ slice.logger.logWarning('Router', 'next() called multiple times in guard');
116
+ return;
117
+ }
118
+ nextCalled = true;
119
+
120
+ if (path && typeof path === 'string') {
121
+ redirectPath = path;
122
+ }
123
+ };
124
+
125
+ try {
126
+ await this._beforeEachGuard(to, from, next);
127
+
128
+ // Si no se llamó next(), loguear advertencia pero continuar
129
+ if (!nextCalled) {
130
+ slice.logger.logWarning(
131
+ 'Router',
132
+ 'beforeEach guard did not call next(). Navigation will continue.'
133
+ );
134
+ }
135
+
136
+ return redirectPath;
137
+ } catch (error) {
138
+ slice.logger.logError('Router', 'Error in beforeEach guard', error);
139
+ return null; // En caso de error, continuar con la navegación
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Ejecuta el afterEach guard si existe
145
+ * @param {Object} to - Información de ruta destino
146
+ * @param {Object} from - Información de ruta origen
147
+ */
148
+ _executeAfterEachGuard(to, from) {
149
+ if (!this._afterEachGuard) {
150
+ return;
151
+ }
152
+
153
+ try {
154
+ this._afterEachGuard(to, from);
155
+ } catch (error) {
156
+ slice.logger.logError('Router', 'Error in afterEach guard', error);
157
+ }
158
+ }
159
+
160
+ // ============================================
161
+ // ROUTING CORE (MODIFICADO CON GUARDS)
162
+ // ============================================
163
+
164
+ async navigate(path) {
165
+ const currentPath = window.location.pathname;
166
+
167
+ // Obtener información de ruta actual
168
+ const { route: fromRoute, params: fromParams } = this.matchRoute(currentPath);
169
+ const from = this._createRouteInfo(fromRoute, fromParams);
170
+
171
+ // Obtener información de ruta destino
172
+ const { route: toRoute, params: toParams } = this.matchRoute(path);
173
+ const to = this._createRouteInfo(toRoute, toParams);
174
+
175
+ // EJECUTAR BEFORE EACH GUARD
176
+ const redirectPath = await this._executeBeforeEachGuard(to, from);
177
+
178
+ // Si el guard redirige, navegar a la nueva ruta
179
+ if (redirectPath) {
180
+ // Evitar loops infinitos
181
+ if (redirectPath === path) {
182
+ slice.logger.logError('Router', `Guard redirection loop detected: ${path}`);
183
+ return;
184
+ }
185
+ return this.navigate(redirectPath);
186
+ }
187
+
188
+ // Continuar con la navegación normal
189
+ window.history.pushState({}, path, window.location.origin + path);
190
+ await this._performNavigation(to, from);
191
+ }
192
+
193
+ /**
194
+ * Método interno para ejecutar la navegación después de pasar los guards
195
+ * @param {Object} to - Información de ruta destino
196
+ * @param {Object} from - Información de ruta origen
197
+ */
198
+ async _performNavigation(to, from) {
199
+ // Renderizar la nueva ruta
200
+ await this.onRouteChange();
201
+
202
+ // EJECUTAR AFTER EACH GUARD
203
+ this._executeAfterEachGuard(to, from);
204
+ }
205
+
206
+ async onRouteChange() {
207
+ // Cancelar el timeout anterior si existe
208
+ if (this.routeChangeTimeout) {
209
+ clearTimeout(this.routeChangeTimeout);
210
+ }
211
+
212
+ // Debounce de 10ms para evitar múltiples llamadas seguidas
213
+ this.routeChangeTimeout = setTimeout(async () => {
214
+ const path = window.location.pathname;
215
+ const routeContainersFlag = await this.renderRoutesComponentsInPage();
216
+
217
+ if (routeContainersFlag) {
218
+ return;
219
+ }
220
+
221
+ const { route, params } = this.matchRoute(path);
222
+ if (route) {
223
+ await this.handleRoute(route, params);
224
+ }
225
+ }, 10);
226
+ }
227
+
228
+ async handleRoute(route, params) {
229
+ const targetElement = document.querySelector('#app');
230
+
231
+ const componentName = route.parentRoute ? route.parentRoute.component : route.component;
232
+ const sliceId = `route-${componentName}`;
233
+
234
+ const existingComponent = slice.controller.getComponent(sliceId);
235
+
236
+ if (slice.loading) {
237
+ slice.loading.start();
238
+ }
239
+
240
+ if (existingComponent) {
241
+ targetElement.innerHTML = '';
242
+ if (existingComponent.update) {
243
+ existingComponent.props = { ...existingComponent.props, ...params };
244
+ await existingComponent.update();
245
+ }
246
+ targetElement.appendChild(existingComponent);
247
+ await this.renderRoutesInComponent(existingComponent);
248
+ } else {
249
+ const component = await slice.build(componentName, {
250
+ params,
251
+ sliceId: sliceId,
252
+ });
253
+
254
+ targetElement.innerHTML = '';
255
+ targetElement.appendChild(component);
256
+
257
+ await this.renderRoutesInComponent(component);
258
+ }
259
+
260
+ // Invalidar caché después de cambios importantes en el DOM
261
+ this.invalidateCache();
262
+
263
+ if (slice.loading) {
264
+ slice.loading.stop();
265
+ }
266
+
267
+ slice.router.activeRoute = route;
268
+ }
269
+
270
+ async loadInitialRoute() {
271
+ const path = window.location.pathname;
272
+ const { route, params } = this.matchRoute(path);
273
+
274
+ // Para la carga inicial, también ejecutar guards
275
+ const from = this._createRouteInfo(null, {});
276
+ const to = this._createRouteInfo(route, params);
277
+
278
+ // EJECUTAR BEFORE EACH GUARD en carga inicial
279
+ const redirectPath = await this._executeBeforeEachGuard(to, from);
280
+
281
+ if (redirectPath) {
282
+ return this.navigate(redirectPath);
283
+ }
284
+
285
+ await this.handleRoute(route, params);
286
+
287
+ // EJECUTAR AFTER EACH GUARD en carga inicial
288
+ this._executeAfterEachGuard(to, from);
289
+ }
290
+
291
+ // ============================================
292
+ // MÉTODOS EXISTENTES (SIN CAMBIOS)
293
+ // ============================================
294
+
22
295
  setupMutationObserver() {
23
296
  if (typeof MutationObserver !== 'undefined') {
24
297
  this.observer = new MutationObserver((mutations) => {
@@ -26,7 +299,6 @@ export default class Router {
26
299
 
27
300
  mutations.forEach((mutation) => {
28
301
  if (mutation.type === 'childList') {
29
- // Solo invalidar si se añadieron/removieron nodos que podrían ser rutas
30
302
  const addedNodes = Array.from(mutation.addedNodes);
31
303
  const removedNodes = Array.from(mutation.removedNodes);
32
304
 
@@ -55,7 +327,6 @@ export default class Router {
55
327
  }
56
328
  }
57
329
 
58
- // NUEVO: Invalidar caché
59
330
  invalidateCache() {
60
331
  this.routeContainersCache.clear();
61
332
  this.lastCacheUpdate = 0;
@@ -92,14 +363,12 @@ export default class Router {
92
363
  return pathToRouteMap;
93
364
  }
94
365
 
95
- // OPTIMIZADO: Sistema de caché inteligente
96
366
  async renderRoutesComponentsInPage(searchContainer = document) {
97
367
  let routerContainersFlag = false;
98
368
  const routeContainers = this.getCachedRouteContainers(searchContainer);
99
369
 
100
370
  for (const routeContainer of routeContainers) {
101
371
  try {
102
- // Verificar que el componente aún esté conectado al DOM
103
372
  if (!routeContainer.isConnected) {
104
373
  this.invalidateCache();
105
374
  continue;
@@ -118,18 +387,15 @@ export default class Router {
118
387
  return routerContainersFlag;
119
388
  }
120
389
 
121
- // NUEVO: Obtener contenedores con caché
122
390
  getCachedRouteContainers(container) {
123
391
  const containerKey = container === document ? 'document' : container.sliceId || 'anonymous';
124
392
  const now = Date.now();
125
393
 
126
- // Verificar si el caché es válido
127
394
  if (this.routeContainersCache.has(containerKey) &&
128
395
  (now - this.lastCacheUpdate) < this.CACHE_DURATION) {
129
396
  return this.routeContainersCache.get(containerKey);
130
397
  }
131
398
 
132
- // Regenerar caché
133
399
  const routeContainers = this.findAllRouteContainersOptimized(container);
134
400
  this.routeContainersCache.set(containerKey, routeContainers);
135
401
  this.lastCacheUpdate = now;
@@ -137,17 +403,14 @@ export default class Router {
137
403
  return routeContainers;
138
404
  }
139
405
 
140
- // OPTIMIZADO: Búsqueda más eficiente usando TreeWalker
141
406
  findAllRouteContainersOptimized(container) {
142
407
  const routeContainers = [];
143
408
 
144
- // Usar TreeWalker para una búsqueda más eficiente
145
409
  const walker = document.createTreeWalker(
146
410
  container,
147
411
  NodeFilter.SHOW_ELEMENT,
148
412
  {
149
413
  acceptNode: (node) => {
150
- // Solo aceptar nodos que sean slice-route o slice-multi-route
151
414
  if (node.tagName === 'SLICE-ROUTE' || node.tagName === 'SLICE-MULTI-ROUTE') {
152
415
  return NodeFilter.FILTER_ACCEPT;
153
416
  }
@@ -164,7 +427,6 @@ export default class Router {
164
427
  return routeContainers;
165
428
  }
166
429
 
167
- // NUEVO: Método específico para renderizar rutas dentro de un componente
168
430
  async renderRoutesInComponent(component) {
169
431
  if (!component) {
170
432
  slice.logger.logWarning('Router', 'No component provided for route rendering');
@@ -174,85 +436,6 @@ export default class Router {
174
436
  return await this.renderRoutesComponentsInPage(component);
175
437
  }
176
438
 
177
- // OPTIMIZADO: Debouncing para evitar múltiples llamadas seguidas
178
- async onRouteChange() {
179
- // Cancelar el timeout anterior si existe
180
- if (this.routeChangeTimeout) {
181
- clearTimeout(this.routeChangeTimeout);
182
- }
183
-
184
- // Debounce de 10ms para evitar múltiples llamadas seguidas
185
- this.routeChangeTimeout = setTimeout(async () => {
186
- const path = window.location.pathname;
187
- const routeContainersFlag = await this.renderRoutesComponentsInPage();
188
-
189
- if (routeContainersFlag) {
190
- return;
191
- }
192
-
193
- const { route, params } = this.matchRoute(path);
194
- if (route) {
195
- await this.handleRoute(route, params);
196
- }
197
- }, 10);
198
- }
199
-
200
- async navigate(path) {
201
- window.history.pushState({}, path, window.location.origin + path);
202
- await this.onRouteChange();
203
- }
204
-
205
- async handleRoute(route, params) {
206
- const targetElement = document.querySelector('#app');
207
-
208
- const componentName = route.parentRoute ? route.parentRoute.component : route.component;
209
- const sliceId = `route-${componentName}`;
210
-
211
- const existingComponent = slice.controller.getComponent(sliceId);
212
-
213
- if (slice.loading) {
214
- slice.loading.start();
215
- }
216
-
217
- if (existingComponent) {
218
- targetElement.innerHTML = '';
219
- if (existingComponent.update) {
220
- existingComponent.props = { ...existingComponent.props, ...params };
221
- await existingComponent.update();
222
- }
223
- targetElement.appendChild(existingComponent);
224
- // Renderizar DESPUÉS de insertar (pero antes de mostrar)
225
- await this.renderRoutesInComponent(existingComponent);
226
- } else {
227
- const component = await slice.build(componentName, {
228
- params,
229
- sliceId: sliceId,
230
- });
231
-
232
- targetElement.innerHTML = '';
233
- targetElement.appendChild(component);
234
-
235
- // Renderizar INMEDIATAMENTE después de insertar
236
- await this.renderRoutesInComponent(component);
237
- }
238
-
239
- // Invalidar caché después de cambios importantes en el DOM
240
- this.invalidateCache();
241
-
242
- if (slice.loading) {
243
- slice.loading.stop();
244
- }
245
-
246
- slice.router.activeRoute = route;
247
- }
248
-
249
- async loadInitialRoute() {
250
- const path = window.location.pathname;
251
- const { route, params } = this.matchRoute(path);
252
-
253
- await this.handleRoute(route, params);
254
- }
255
-
256
439
  matchRoute(path) {
257
440
  const exactMatch = this.pathToRouteMap.get(path);
258
441
  if (exactMatch) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slicejs-web-framework",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "",
5
5
  "engines": {
6
6
  "node": ">=20"