vaderjs-native 1.0.32 → 1.0.34

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/router/index.tsx +117 -283
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vaderjs-native",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
4
4
  "binaryVersion": "1.0.26",
5
5
  "description": "Build Native Applications using Vaderjs framework.",
6
6
  "bin": {
package/router/index.tsx CHANGED
@@ -11,9 +11,7 @@ export interface Route {
11
11
 
12
12
  export interface RouteConfig {
13
13
  routes: Route[];
14
- mode?: "hash" | "history";
15
- base?: string;
16
- fallback?: any; // Fallback component for 404
14
+ fallback?: any;
17
15
  }
18
16
 
19
17
  export interface RouteMatch {
@@ -28,406 +26,242 @@ class Router {
28
26
  private routeMap: Map<string, Route> = new Map();
29
27
  private currentMatch: RouteMatch | null = null;
30
28
  private listeners: Array<(match: RouteMatch | null) => void> = [];
31
- private mode: "hash" | "history" = "history";
32
- private base: string = "";
33
29
  private fallback: any = null;
34
- private AppComponent: any = null;
30
+ private ready = false;
35
31
 
36
32
  constructor(config: RouteConfig) {
37
- this.mode = Vader.platform() === "web" ? (config.mode || "history") : "hash";
38
- this.base = config.base || "";
39
- this.fallback = config.fallback || null;
40
33
  this.routes = config.routes;
41
-
42
- // Build route map with nested routes
43
- this.buildRouteMap(config.routes);
44
-
45
- // Initialize the router
34
+ this.fallback = config.fallback || null;
35
+
36
+ this.buildRouteMap(this.routes);
46
37
  this.initializeRouter();
47
38
  }
48
39
 
49
- /**
50
- * Set the root App component
51
- */
52
- public setAppComponent(App: any): void {
53
- this.AppComponent = App;
54
- }
40
+ /* ----------------------- ROUTE MAP ----------------------- */
55
41
 
56
- /**
57
- * Get the root App component
58
- */
59
- public getAppComponent(): any {
60
- return this.AppComponent;
61
- }
62
-
63
- /**
64
- * Recursively build route map including nested routes
65
- */
66
- private buildRouteMap(routes: Route[], parentPath: string = ""): void {
67
- routes.forEach((route) => {
42
+ private buildRouteMap(routes: Route[], parentPath = ""): void {
43
+ routes.forEach(route => {
68
44
  const fullPath = this.normalizePath(parentPath + route.path);
69
45
  this.routeMap.set(fullPath, route);
70
-
71
- // Register children routes recursively
72
- if (route.children && route.children.length > 0) {
46
+
47
+ if (route.children?.length) {
73
48
  this.buildRouteMap(route.children, fullPath);
74
49
  }
75
50
  });
76
51
  }
77
52
 
78
- /**
79
- * Normalize path
80
- */
81
53
  private normalizePath(path: string): string {
82
54
  if (path === "/") return "/";
83
-
84
- // Remove duplicate slashes
85
- let normalized = path.replace(/\/+/g, "/");
86
-
87
- // Ensure it doesn't end with slash (except root)
88
- if (normalized !== "/" && normalized.endsWith("/")) {
89
- normalized = normalized.slice(0, -1);
90
- }
91
-
92
- // Ensure it starts with slash
93
- if (!normalized.startsWith("/")) {
94
- normalized = "/" + normalized;
95
- }
96
-
97
- return normalized;
55
+ let p = path.replace(/\/+/g, "/");
56
+ if (!p.startsWith("/")) p = "/" + p;
57
+ if (p !== "/" && p.endsWith("/")) p = p.slice(0, -1);
58
+ return p;
98
59
  }
99
60
 
100
- /**
101
- * Match path against routes with dynamic parameters
102
- */
61
+ /* ----------------------- MATCHING ----------------------- */
62
+
103
63
  private matchPath(path: string): RouteMatch | null {
104
- const normalizedPath = this.normalizePath(path);
105
-
106
- // Try exact match first
107
- const exactRoute = this.routeMap.get(normalizedPath);
108
- if (exactRoute) {
64
+ const normalized = this.normalizePath(path);
65
+
66
+ const exact = this.routeMap.get(normalized);
67
+ if (exact) {
109
68
  return {
110
- route: exactRoute,
111
- path: normalizedPath,
69
+ route: exact,
70
+ path: normalized,
112
71
  params: {},
113
- query: this.getQueryParams(),
72
+ query: this.getQueryParams()
114
73
  };
115
74
  }
116
-
117
- // Try pattern matching for dynamic routes
118
- for (const [routePath, route] of this.routeMap.entries()) {
119
- const match = this.matchPattern(routePath, normalizedPath);
75
+
76
+ for (const [pattern, route] of this.routeMap.entries()) {
77
+ const match = this.matchPattern(pattern, normalized);
120
78
  if (match) {
121
79
  return {
122
80
  route,
123
- path: normalizedPath,
81
+ path: normalized,
124
82
  params: match.params,
125
- query: this.getQueryParams(),
83
+ query: this.getQueryParams()
126
84
  };
127
85
  }
128
86
  }
129
-
87
+
130
88
  return null;
131
89
  }
132
90
 
133
- /**
134
- * Match path pattern with parameters (e.g., /users/:id)
135
- */
136
- private matchPattern(pattern: string, path: string): { params: Record<string, string> } | null {
137
- const patternParts = pattern.split("/");
138
- const pathParts = path.split("/");
139
-
140
- if (patternParts.length !== pathParts.length) {
141
- return null;
142
- }
143
-
91
+ private matchPattern(pattern: string, path: string) {
92
+ const p1 = pattern.split("/");
93
+ const p2 = path.split("/");
94
+
95
+ if (p1.length !== p2.length) return null;
96
+
144
97
  const params: Record<string, string> = {};
145
-
146
- for (let i = 0; i < patternParts.length; i++) {
147
- const patternPart = patternParts[i];
148
- const pathPart = pathParts[i];
149
-
150
- if (patternPart.startsWith(":")) {
151
- // Dynamic segment
152
- const paramName = patternPart.slice(1);
153
- params[paramName] = decodeURIComponent(pathPart);
154
- } else if (patternPart !== pathPart) {
155
- // Static segment doesn't match
98
+
99
+ for (let i = 0; i < p1.length; i++) {
100
+ if (p1[i].startsWith(":")) {
101
+ params[p1[i].slice(1)] = decodeURIComponent(p2[i]);
102
+ } else if (p1[i] !== p2[i]) {
156
103
  return null;
157
104
  }
158
105
  }
159
-
106
+
160
107
  return { params };
161
108
  }
162
109
 
163
- /**
164
- * Initialize router with event listeners
165
- */
166
- private initializeRouter(): void {
167
- if (typeof window === "undefined") return;
168
-
169
- // Handle back / forward
170
- window.addEventListener("popstate", () => {
171
- this.handleLocationChange();
172
- });
110
+ /* ----------------------- INIT ----------------------- */
173
111
 
174
- // 🔥 PATCH pushState / replaceState
175
- const originalPush = history.pushState;
176
- const originalReplace = history.replaceState;
112
+ private initializeRouter(): void {
113
+ if (typeof window === "undefined") return;
177
114
 
178
- history.pushState = (...args) => {
179
- originalPush.apply(history, args as any);
180
- this.handleLocationChange();
181
- };
115
+ window.addEventListener("hashchange", () => {
116
+ this.handleLocationChange();
117
+ });
182
118
 
183
- history.replaceState = (...args) => {
184
- originalReplace.apply(history, args as any);
185
119
  this.handleLocationChange();
186
- };
187
-
188
- // Initial match
189
- this.handleLocationChange();
190
- }
191
-
120
+ this.ready = true;
121
+ }
192
122
 
193
- /**
194
- * Handle location change
195
- */
196
123
  private handleLocationChange(): void {
197
124
  const path = this.getCurrentPath();
198
125
  const match = this.matchPath(path);
199
-
200
- if (match) {
201
- this.currentMatch = match;
202
- } else {
203
- this.currentMatch = null;
204
- }
205
-
206
- // Notify all listeners
207
- this.listeners.forEach((listener) => listener(this.currentMatch));
126
+ this.currentMatch = match;
127
+
128
+ this.listeners.forEach(l => l(this.currentMatch));
208
129
  }
209
130
 
210
- /**
211
- * Get current path from URL
212
- */
213
131
  private getCurrentPath(): string {
214
132
  if (typeof window === "undefined") return "/";
215
-
216
- if (this.mode === "hash") {
217
- return window.location.hash.slice(1) || "/";
218
- } else {
219
- let path = window.location.pathname;
220
-
221
- // Remove base path if configured
222
- if (this.base && path.startsWith(this.base)) {
223
- path = path.slice(this.base.length);
224
- }
225
-
226
- return this.normalizePath(path) || "/";
227
- }
133
+ return this.normalizePath(window.location.hash.slice(1) || "/");
228
134
  }
229
135
 
230
- /**
231
- * Navigate to a new path
232
- */
136
+ /* ----------------------- NAVIGATION ----------------------- */
137
+
233
138
  public navigate(
234
139
  path: string,
235
- options: {
236
- replace?: boolean;
237
- state?: any;
238
- query?: Record<string, string>;
239
- } = {}
140
+ options: { replace?: boolean; query?: Record<string, string> } = {}
240
141
  ): void {
241
- const normalizedPath = this.normalizePath(path);
242
-
243
- // Build URL with query params
244
- let url = normalizedPath;
245
- if (options.query && Object.keys(options.query).length > 0) {
246
- const queryString = new URLSearchParams(options.query).toString();
247
- url += `?${queryString}`;
142
+ let url = this.normalizePath(path);
143
+
144
+ if (options.query && Object.keys(options.query).length) {
145
+ url += "?" + new URLSearchParams(options.query).toString();
248
146
  }
249
-
250
- if (typeof window !== "undefined") {
251
- const fullUrl = this.mode === "hash" ? `#${url}` : this.base + url;
252
-
253
- if (options.replace) {
254
- window.history.replaceState(options.state || {}, "", fullUrl);
255
- } else {
256
- window.history.pushState(options.state || {}, "", fullUrl);
257
- }
258
-
259
- this.handleLocationChange();
147
+
148
+ const full = `#${url}`;
149
+
150
+ if (options.replace) {
151
+ window.location.replace(full);
152
+ } else {
153
+ window.location.hash = full;
260
154
  }
155
+ this.handleLocationChange()
261
156
  }
262
157
 
263
- /**
264
- * Subscribe to route changes
265
- */
266
- public on(callback: (match: RouteMatch | null) => void): () => void {
267
- this.listeners.push(callback);
158
+ /* ----------------------- SUBSCRIPTIONS ----------------------- */
268
159
 
269
- // 🔥 Always emit current state (even null)
270
- callback(this.currentMatch);
160
+ public on(cb: (match: RouteMatch | null) => void): () => void {
161
+ this.listeners.push(cb);
162
+ if (this.ready) cb(this.currentMatch);
271
163
 
272
- return () => {
273
- this.listeners = this.listeners.filter(l => l !== callback);
274
- };
275
- }
164
+ return () => {
165
+ this.listeners = this.listeners.filter(l => l !== cb);
166
+ };
167
+ }
276
168
 
169
+ /* ----------------------- HELPERS ----------------------- */
277
170
 
171
+ public isReady(): boolean {
172
+ return this.ready;
173
+ }
278
174
 
279
- /**
280
- * Get current route match
281
- */
282
175
  public getCurrentMatch(): RouteMatch | null {
283
176
  return this.currentMatch;
284
177
  }
285
178
 
286
- /**
287
- * Get current route
288
- */
289
179
  public getCurrentRoute(): Route | null {
290
180
  return this.currentMatch?.route || null;
291
181
  }
292
182
 
293
- /**
294
- * Get query parameters from URL
295
- */
183
+ public getFallback(): any {
184
+ return this.fallback;
185
+ }
186
+
296
187
  public getQueryParams(): Record<string, string> {
297
188
  if (typeof window === "undefined") return {};
298
-
299
189
  const params: Record<string, string> = {};
300
- const queryString = window.location.search.slice(1);
301
-
302
- if (!queryString) return params;
303
-
304
- queryString.split("&").forEach((param) => {
305
- const [key, value] = param.split("=");
306
- if (key) {
307
- params[decodeURIComponent(key)] = decodeURIComponent(value || "");
308
- }
190
+ const qs = window.location.search.slice(1);
191
+ if (!qs) return params;
192
+
193
+ qs.split("&").forEach(p => {
194
+ const [k, v] = p.split("=");
195
+ if (k) params[decodeURIComponent(k)] = decodeURIComponent(v || "");
309
196
  });
310
-
311
- return params;
312
- }
313
197
 
314
- /**
315
- * Check if a path is active
316
- */
317
- public isActive(path: string, exact: boolean = false): boolean {
318
- const currentPath = this.currentMatch?.path || "/";
319
- const normalizedPath = this.normalizePath(path);
320
-
321
- if (exact) {
322
- return currentPath === normalizedPath;
323
- }
324
-
325
- return currentPath.startsWith(normalizedPath);
198
+ return params;
326
199
  }
327
200
 
328
- /**
329
- * Go back in history
330
- */
331
- public back(): void {
332
- if (typeof window !== "undefined") {
333
- window.history.back();
334
- }
201
+ public isActive(path: string, exact = false): boolean {
202
+ const current = this.currentMatch?.path || "/";
203
+ const target = this.normalizePath(path);
204
+ return exact ? current === target : current.startsWith(target);
335
205
  }
336
206
 
337
- /**
338
- * Go forward in history
339
- */
340
- public forward(): void {
341
- if (typeof window !== "undefined") {
342
- window.history.forward();
343
- }
207
+ public back() {
208
+ window.history.back();
344
209
  }
345
210
 
346
- /**
347
- * Get the fallback component for 404
348
- */
349
- public getFallback(): any {
350
- return this.fallback;
211
+ public forward() {
212
+ window.history.forward();
351
213
  }
352
214
  }
353
215
 
354
- // Create a singleton instance
216
+ /* ----------------------- SINGLETON ----------------------- */
217
+
355
218
  let routerInstance: Router | null = null;
356
219
 
357
- /**
358
- * Create and initialize router
359
- */
360
220
  export function createRouter(config: RouteConfig): Router {
361
221
  routerInstance = new Router(config);
362
222
  return routerInstance;
363
223
  }
364
224
 
365
- /**
366
- * Get router instance
367
- */
368
225
  export function useRouter(): Router {
369
226
  if (!routerInstance) {
370
- throw new Error("Router not initialized. Call createRouter() first.");
227
+ throw new Error("Router not initialized");
371
228
  }
372
229
  return routerInstance;
373
230
  }
374
231
 
375
- /**
376
- * Hook for getting current route
377
- */
378
- export function useRoute(): RouteMatch | null {
232
+ /* ----------------------- HOOKS ----------------------- */
233
+
234
+ export function useRoute(): RouteMatch | null | "loading" {
379
235
  const router = useRouter();
380
- const [match, setMatch] = Vader.useState<RouteMatch | null>(
381
- router.getCurrentMatch()
236
+ const [match, setMatch] = Vader.useState<RouteMatch | null | "loading">(
237
+ router.isReady() ? router.getCurrentMatch() : "loading"
382
238
  );
383
-
384
- console.log("useRoute - current match:", match);
385
239
 
386
240
  Vader.useEffect(() => {
387
- const unsubscribe = router.on((newMatch) => {
388
- setMatch(newMatch);
389
- });
390
-
391
- return unsubscribe;
241
+ return router.on(setMatch);
392
242
  }, []);
393
243
 
394
244
  return match;
395
245
  }
396
246
 
397
- /**
398
- * Hook for navigation
399
- */
400
247
  export function useNavigate() {
401
248
  const router = useRouter();
402
-
403
- return (
404
- path: string,
405
- options?: {
406
- replace?: boolean;
407
- state?: any;
408
- query?: Record<string, string>;
409
- }
410
- ) => {
411
- router.navigate(path, options);
412
- };
249
+ return router.navigate.bind(router);
413
250
  }
414
251
 
415
- /**
416
- * Hook for checking active route
417
- */
418
- export function useActiveRoute(path: string, exact: boolean = false): boolean {
252
+ export function useActiveRoute(path: string, exact = false): boolean {
419
253
  const router = useRouter();
420
- const [isActive, setIsActive] = Vader.useState(() => router.isActive(path, exact));
254
+ const [active, setActive] = Vader.useState(
255
+ router.isActive(path, exact)
256
+ );
421
257
 
422
258
  Vader.useEffect(() => {
423
- const unsubscribe = router.on(() => {
424
- setIsActive(router.isActive(path, exact));
259
+ return router.on(() => {
260
+ setActive(router.isActive(path, exact));
425
261
  });
426
-
427
- return unsubscribe;
428
262
  }, [path, exact]);
429
263
 
430
- return isActive;
264
+ return active;
431
265
  }
432
266
 
433
- export default Router;
267
+ export default Router;