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.
- package/package.json +1 -1
- package/router/index.tsx +117 -283
package/package.json
CHANGED
package/router/index.tsx
CHANGED
|
@@ -11,9 +11,7 @@ export interface Route {
|
|
|
11
11
|
|
|
12
12
|
export interface RouteConfig {
|
|
13
13
|
routes: Route[];
|
|
14
|
-
|
|
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
|
|
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
|
-
|
|
43
|
-
this.buildRouteMap(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
102
|
-
*/
|
|
61
|
+
/* ----------------------- MATCHING ----------------------- */
|
|
62
|
+
|
|
103
63
|
private matchPath(path: string): RouteMatch | null {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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:
|
|
111
|
-
path:
|
|
69
|
+
route: exact,
|
|
70
|
+
path: normalized,
|
|
112
71
|
params: {},
|
|
113
|
-
query: this.getQueryParams()
|
|
72
|
+
query: this.getQueryParams()
|
|
114
73
|
};
|
|
115
74
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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:
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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 <
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
const originalReplace = history.replaceState;
|
|
112
|
+
private initializeRouter(): void {
|
|
113
|
+
if (typeof window === "undefined") return;
|
|
177
114
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
270
|
-
|
|
160
|
+
public on(cb: (match: RouteMatch | null) => void): () => void {
|
|
161
|
+
this.listeners.push(cb);
|
|
162
|
+
if (this.ready) cb(this.currentMatch);
|
|
271
163
|
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
348
|
-
*/
|
|
349
|
-
public getFallback(): any {
|
|
350
|
-
return this.fallback;
|
|
211
|
+
public forward() {
|
|
212
|
+
window.history.forward();
|
|
351
213
|
}
|
|
352
214
|
}
|
|
353
215
|
|
|
354
|
-
|
|
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
|
|
227
|
+
throw new Error("Router not initialized");
|
|
371
228
|
}
|
|
372
229
|
return routerInstance;
|
|
373
230
|
}
|
|
374
231
|
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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 [
|
|
254
|
+
const [active, setActive] = Vader.useState(
|
|
255
|
+
router.isActive(path, exact)
|
|
256
|
+
);
|
|
421
257
|
|
|
422
258
|
Vader.useEffect(() => {
|
|
423
|
-
|
|
424
|
-
|
|
259
|
+
return router.on(() => {
|
|
260
|
+
setActive(router.isActive(path, exact));
|
|
425
261
|
});
|
|
426
|
-
|
|
427
|
-
return unsubscribe;
|
|
428
262
|
}, [path, exact]);
|
|
429
263
|
|
|
430
|
-
return
|
|
264
|
+
return active;
|
|
431
265
|
}
|
|
432
266
|
|
|
433
|
-
export default Router;
|
|
267
|
+
export default Router;
|