@aminnairi/react-router 0.1.0 → 1.0.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/README.md +481 -90
- package/index.tsx +149 -62
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -40,7 +40,11 @@ import { createPage } from "@aminnairi/react-router";
|
|
|
40
40
|
|
|
41
41
|
export const home = createPage({
|
|
42
42
|
path: "/",
|
|
43
|
-
element: ()
|
|
43
|
+
element: function Home() {
|
|
44
|
+
return (
|
|
45
|
+
<h1>Home page</h1>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
44
48
|
});
|
|
45
49
|
```
|
|
46
50
|
|
|
@@ -49,11 +53,14 @@ touch src/router/fallback.tsx
|
|
|
49
53
|
```
|
|
50
54
|
|
|
51
55
|
```tsx
|
|
56
|
+
import { useNavigateToPage } from "@aminnairi/react-router";
|
|
52
57
|
import { home } from "./pages/home";
|
|
53
58
|
|
|
54
59
|
export const Fallback = () => {
|
|
60
|
+
const navigateToHomePage = useNavigateToPage(home);
|
|
61
|
+
|
|
55
62
|
return (
|
|
56
|
-
<button onClick={
|
|
63
|
+
<button onClick={navigateToHomePage}>
|
|
57
64
|
Go back home
|
|
58
65
|
</button>
|
|
59
66
|
);
|
|
@@ -66,9 +73,12 @@ touch src/router/issue.tsx
|
|
|
66
73
|
|
|
67
74
|
```tsx
|
|
68
75
|
import { Fragment } from "react";
|
|
76
|
+
import { useNavigateToPage } from "@aminnairi/react-router";
|
|
69
77
|
import { home } from "./pages/home";
|
|
70
78
|
|
|
71
79
|
export const Issue = () => {
|
|
80
|
+
const navigateToHomePage = useNavigateToPage(home);
|
|
81
|
+
|
|
72
82
|
return (
|
|
73
83
|
<Fragment>
|
|
74
84
|
<h1>An issue occurred</h1>
|
|
@@ -93,8 +103,8 @@ import { home } from "./router/pages/home";
|
|
|
93
103
|
export const router = createRouter({
|
|
94
104
|
fallback: Fallback,
|
|
95
105
|
issue: Issue,
|
|
96
|
-
|
|
97
|
-
home
|
|
106
|
+
pages: [
|
|
107
|
+
home
|
|
98
108
|
]
|
|
99
109
|
});
|
|
100
110
|
```
|
|
@@ -113,6 +123,31 @@ export default function App() {
|
|
|
113
123
|
}
|
|
114
124
|
```
|
|
115
125
|
|
|
126
|
+
```bash
|
|
127
|
+
touch src/main.tsx
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { StrictMode } from "react";
|
|
132
|
+
import { createRoot } from "react-dom/client";
|
|
133
|
+
import { router } from "./router";
|
|
134
|
+
import App from "./App";
|
|
135
|
+
|
|
136
|
+
const rootElement = document.getElementById("root");
|
|
137
|
+
|
|
138
|
+
if (!rootElement) {
|
|
139
|
+
throw new Error("Root element not found");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
createRoot(rootElement).render(
|
|
143
|
+
<StrictMode>
|
|
144
|
+
<router.Provider>
|
|
145
|
+
<App />
|
|
146
|
+
</router.Provider>
|
|
147
|
+
</StrictMode>
|
|
148
|
+
);
|
|
149
|
+
```
|
|
150
|
+
|
|
116
151
|
### Startup
|
|
117
152
|
|
|
118
153
|
```bash
|
|
@@ -130,9 +165,11 @@ import { createPage } from "@aminnairi/react-router";
|
|
|
130
165
|
|
|
131
166
|
createPage({
|
|
132
167
|
path: "/",
|
|
133
|
-
element: ()
|
|
134
|
-
|
|
135
|
-
|
|
168
|
+
element: function Home() {
|
|
169
|
+
return (
|
|
170
|
+
<h1>Home</h1>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
136
173
|
});
|
|
137
174
|
```
|
|
138
175
|
|
|
@@ -143,11 +180,11 @@ import { createPage, createRouter } from "@aminnairi/react-router";
|
|
|
143
180
|
|
|
144
181
|
const home = createPage({
|
|
145
182
|
path: "/",
|
|
146
|
-
element: ()
|
|
147
|
-
|
|
148
|
-
Home
|
|
149
|
-
|
|
150
|
-
|
|
183
|
+
element: function Home() {
|
|
184
|
+
return (
|
|
185
|
+
<h1>Home</h1>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
151
188
|
});
|
|
152
189
|
|
|
153
190
|
createRouter({
|
|
@@ -161,9 +198,7 @@ createRouter({
|
|
|
161
198
|
An error occurred
|
|
162
199
|
</h1>
|
|
163
200
|
),
|
|
164
|
-
pages: [
|
|
165
|
-
home.page
|
|
166
|
-
]
|
|
201
|
+
pages: [home]
|
|
167
202
|
});
|
|
168
203
|
```
|
|
169
204
|
|
|
@@ -174,69 +209,80 @@ import { createPage } from "@aminnairi/react-router";
|
|
|
174
209
|
|
|
175
210
|
createPage({
|
|
176
211
|
path: "/users/:user",
|
|
177
|
-
element: ({ parameters: { user }})
|
|
178
|
-
|
|
179
|
-
User#{user}
|
|
180
|
-
|
|
181
|
-
|
|
212
|
+
element: function User({ parameters: { user }}) {
|
|
213
|
+
return (
|
|
214
|
+
<h1>User#{user}</h1>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
182
217
|
});
|
|
183
218
|
```
|
|
184
219
|
|
|
185
|
-
And
|
|
220
|
+
And you can have of course more than one dynamic parameter.
|
|
186
221
|
|
|
187
222
|
```tsx
|
|
188
223
|
import { createPage } from "@aminnairi/react-router";
|
|
189
224
|
|
|
190
225
|
createPage({
|
|
191
226
|
path: "/users/:user/articles/:article",
|
|
192
|
-
element: ({ parameters: { user, article }})
|
|
193
|
-
|
|
194
|
-
Article#{article } of user#{user}
|
|
195
|
-
|
|
196
|
-
|
|
227
|
+
element: function UserArticle({ parameters: { user, article }}) {
|
|
228
|
+
return (
|
|
229
|
+
<h1>Article#{article } of user#{user}</h1>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
197
232
|
});
|
|
198
233
|
```
|
|
199
234
|
|
|
200
|
-
|
|
235
|
+
### useNavigateToPage
|
|
236
|
+
|
|
237
|
+
You can navigate from one page from another.
|
|
201
238
|
|
|
202
239
|
```tsx
|
|
203
240
|
import { Fragment } from "react";
|
|
204
|
-
import { createPage } from "@aminnairi/react-router";
|
|
241
|
+
import { createPage, useNavigateToPage } from "@aminnairi/react-router";
|
|
205
242
|
|
|
206
243
|
const login = createPage({
|
|
207
244
|
path: "/login",
|
|
208
|
-
element: ()
|
|
209
|
-
|
|
210
|
-
Login
|
|
211
|
-
|
|
212
|
-
|
|
245
|
+
element: function Login() {
|
|
246
|
+
return (
|
|
247
|
+
<h1>Login</h1>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
213
250
|
});
|
|
214
251
|
|
|
215
252
|
const about = createPage({
|
|
216
253
|
path: "/about",
|
|
217
|
-
element: ()
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
254
|
+
element: function About() {
|
|
255
|
+
const navigateToLoginPage = useNavigateToPage(login);
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<Fragment>
|
|
259
|
+
<h1>
|
|
260
|
+
About Us
|
|
261
|
+
</h1>
|
|
262
|
+
<button onClick={navigateToLoginPage}>
|
|
263
|
+
Login
|
|
264
|
+
</button>
|
|
265
|
+
</Fragment>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
226
268
|
});
|
|
227
269
|
|
|
228
270
|
createPage({
|
|
229
271
|
path: "/",
|
|
230
|
-
element: ()
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
272
|
+
element: function Home() {
|
|
273
|
+
const navigateToAboutPage = useNavigateToPage(about);
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<Fragment>
|
|
277
|
+
<h1>
|
|
278
|
+
Home
|
|
279
|
+
</h1>
|
|
280
|
+
<button onClick={navigateToAboutPage}>
|
|
281
|
+
About Us
|
|
282
|
+
</button>
|
|
283
|
+
</Fragment>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
240
286
|
});
|
|
241
287
|
```
|
|
242
288
|
|
|
@@ -244,29 +290,33 @@ And you can of course navigate to pages that have dynamic parameters as well.
|
|
|
244
290
|
|
|
245
291
|
```tsx
|
|
246
292
|
import { Fragment } from "react";
|
|
247
|
-
import { createPage } from "@aminnairi/react-router";
|
|
293
|
+
import { createPage, useNavigateToPage } from "@aminnairi/react-router";
|
|
248
294
|
|
|
249
295
|
const user = createPage({
|
|
250
296
|
path: "/users/:user",
|
|
251
|
-
element: ({ parameters: { user }})
|
|
252
|
-
|
|
253
|
-
User#{user}
|
|
254
|
-
|
|
255
|
-
|
|
297
|
+
element: function User({ parameters: { user }}) {
|
|
298
|
+
return (
|
|
299
|
+
<h1>User#{user}</h1>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
256
302
|
});
|
|
257
303
|
|
|
258
304
|
createPage({
|
|
259
305
|
path: "/",
|
|
260
|
-
element: ()
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
306
|
+
element: function Home() {
|
|
307
|
+
const navigateToUserPage = useNavigateToPage(user);
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<Fragment>
|
|
311
|
+
<h1>
|
|
312
|
+
Home
|
|
313
|
+
</h1>
|
|
314
|
+
<button onClick={() => navigateToUserPage({ user: "123" })}>
|
|
315
|
+
User#123
|
|
316
|
+
</button>
|
|
317
|
+
</Fragment>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
270
320
|
});
|
|
271
321
|
```
|
|
272
322
|
|
|
@@ -281,9 +331,11 @@ import { createRouter, createPage } from "@aminnairi/react-router";
|
|
|
281
331
|
|
|
282
332
|
const home = createPage({
|
|
283
333
|
path: "/",
|
|
284
|
-
element: ()
|
|
285
|
-
|
|
286
|
-
|
|
334
|
+
element: function Home() {
|
|
335
|
+
return (
|
|
336
|
+
<h1>Home</h1>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
287
339
|
});
|
|
288
340
|
|
|
289
341
|
const router = createRouter({
|
|
@@ -294,7 +346,7 @@ const router = createRouter({
|
|
|
294
346
|
<h1>An error occurred</h1>
|
|
295
347
|
),
|
|
296
348
|
pages: [
|
|
297
|
-
home
|
|
349
|
+
home
|
|
298
350
|
]
|
|
299
351
|
});
|
|
300
352
|
|
|
@@ -324,7 +376,9 @@ const App = () => {
|
|
|
324
376
|
|
|
325
377
|
root.render(
|
|
326
378
|
<StrictMode>
|
|
327
|
-
<
|
|
379
|
+
<router.Provider>
|
|
380
|
+
<App />
|
|
381
|
+
</router.Provider>
|
|
328
382
|
</StrictMode>
|
|
329
383
|
);
|
|
330
384
|
```
|
|
@@ -340,9 +394,11 @@ import { createRouter, createPage } from "@aminnairi/react-router";
|
|
|
340
394
|
|
|
341
395
|
const home = createPage({
|
|
342
396
|
path: "/",
|
|
343
|
-
element: ()
|
|
344
|
-
|
|
345
|
-
|
|
397
|
+
element: function Page() {
|
|
398
|
+
return (
|
|
399
|
+
<h1>Home</h1>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
346
402
|
});
|
|
347
403
|
|
|
348
404
|
const router = createRouter({
|
|
@@ -354,7 +410,7 @@ const router = createRouter({
|
|
|
354
410
|
<h1>An error occurred</h1>
|
|
355
411
|
),
|
|
356
412
|
pages: [
|
|
357
|
-
home
|
|
413
|
+
home
|
|
358
414
|
]
|
|
359
415
|
});
|
|
360
416
|
|
|
@@ -384,7 +440,9 @@ const App = () => {
|
|
|
384
440
|
|
|
385
441
|
root.render(
|
|
386
442
|
<StrictMode>
|
|
387
|
-
<
|
|
443
|
+
<router.Provider>
|
|
444
|
+
<App />
|
|
445
|
+
</router.Provider>
|
|
388
446
|
</StrictMode>
|
|
389
447
|
);
|
|
390
448
|
```
|
|
@@ -398,9 +456,11 @@ import { createRouter, createPage } from "@aminnairi/react-router";
|
|
|
398
456
|
|
|
399
457
|
const home = createPage({
|
|
400
458
|
path: "/",
|
|
401
|
-
element: ()
|
|
402
|
-
|
|
403
|
-
|
|
459
|
+
element: function Home() {
|
|
460
|
+
return (
|
|
461
|
+
<h1>Home</h1>
|
|
462
|
+
);
|
|
463
|
+
}
|
|
404
464
|
});
|
|
405
465
|
|
|
406
466
|
const router = createRouter({
|
|
@@ -418,7 +478,7 @@ const router = createRouter({
|
|
|
418
478
|
);
|
|
419
479
|
),
|
|
420
480
|
pages: [
|
|
421
|
-
home
|
|
481
|
+
home
|
|
422
482
|
]
|
|
423
483
|
});
|
|
424
484
|
|
|
@@ -448,7 +508,9 @@ const App = () => {
|
|
|
448
508
|
|
|
449
509
|
root.render(
|
|
450
510
|
<StrictMode>
|
|
451
|
-
<
|
|
511
|
+
<router.Provider>
|
|
512
|
+
<App />
|
|
513
|
+
</router.Provider>
|
|
452
514
|
</StrictMode>
|
|
453
515
|
);
|
|
454
516
|
```
|
|
@@ -462,9 +524,11 @@ import { createRouter, createPage, createIssue } from "@aminnairi/react-router";
|
|
|
462
524
|
|
|
463
525
|
const home = createPage({
|
|
464
526
|
path: "/",
|
|
465
|
-
element: ()
|
|
466
|
-
|
|
467
|
-
|
|
527
|
+
element: function Home() {
|
|
528
|
+
return (
|
|
529
|
+
<h1>Home</h1>
|
|
530
|
+
);
|
|
531
|
+
}
|
|
468
532
|
});
|
|
469
533
|
|
|
470
534
|
const Fallback = () => {
|
|
@@ -488,7 +552,89 @@ const router = createRouter({
|
|
|
488
552
|
fallback: Fallback,
|
|
489
553
|
issue: Issue,
|
|
490
554
|
pages: [
|
|
491
|
-
home
|
|
555
|
+
home
|
|
556
|
+
]
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const rootElement = document.getElementById("root");
|
|
560
|
+
|
|
561
|
+
if (!rootElement) {
|
|
562
|
+
throw new Error("Root element not found");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const root = createRoot(rootElement);
|
|
566
|
+
|
|
567
|
+
const App = () => {
|
|
568
|
+
return (
|
|
569
|
+
<Fragment>
|
|
570
|
+
<header>
|
|
571
|
+
<h1>App</h1>
|
|
572
|
+
</header>
|
|
573
|
+
<main>
|
|
574
|
+
<router.View />
|
|
575
|
+
</main>
|
|
576
|
+
<footer>
|
|
577
|
+
Credit © Yourself 2025
|
|
578
|
+
</footer>
|
|
579
|
+
</Fragment>
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
root.render(
|
|
584
|
+
<StrictMode>
|
|
585
|
+
<App />
|
|
586
|
+
</StrictMode>
|
|
587
|
+
);
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
You can use a prefix for your routes, useful if you need to publish this app in a scope like GitHub Pages.
|
|
591
|
+
|
|
592
|
+
You don't have to manually append this prefix when creating pages, its automatically added for you.
|
|
593
|
+
|
|
594
|
+
```tsx
|
|
595
|
+
import { Fragment, StrictMode } from "react";
|
|
596
|
+
import { createRoot } from "react-dom/client";
|
|
597
|
+
import { createRouter, createPage, createIssue, useNavigateToPage } from "@aminnairi/react-router";
|
|
598
|
+
|
|
599
|
+
const home = createPage({
|
|
600
|
+
path: "/",
|
|
601
|
+
element: function Home() {
|
|
602
|
+
return (
|
|
603
|
+
<h1>Home</h1>
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
const Fallback = () => {
|
|
609
|
+
const navigateToHomePage = useNavigateToPage(home);
|
|
610
|
+
|
|
611
|
+
return (
|
|
612
|
+
<Fragment>
|
|
613
|
+
<h1>Not found</h1>
|
|
614
|
+
<button onClick={navigateToHomePage}>
|
|
615
|
+
Go Back Home
|
|
616
|
+
</button>
|
|
617
|
+
</Fragment>
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const Issue = createIssue(({ error, reset }) => (
|
|
622
|
+
return (
|
|
623
|
+
<Fragment>
|
|
624
|
+
<h1>Error</h1>
|
|
625
|
+
<p>{error.message}</p>
|
|
626
|
+
<button onClick={reset}>Reset</button>
|
|
627
|
+
</Fragment>
|
|
628
|
+
);
|
|
629
|
+
));
|
|
630
|
+
|
|
631
|
+
const router = createRouter({
|
|
632
|
+
prefix: "/portfolio",
|
|
633
|
+
transition: true,
|
|
634
|
+
fallback: Fallback,
|
|
635
|
+
issue: Issue,
|
|
636
|
+
pages: [
|
|
637
|
+
home
|
|
492
638
|
]
|
|
493
639
|
});
|
|
494
640
|
|
|
@@ -523,6 +669,119 @@ root.render(
|
|
|
523
669
|
);
|
|
524
670
|
```
|
|
525
671
|
|
|
672
|
+
### useNavigateToPage
|
|
673
|
+
|
|
674
|
+
Allow you to create a function that can then be called to navigate to another page inside a React component.
|
|
675
|
+
|
|
676
|
+
It accepts a page that has been created using `createPage`.
|
|
677
|
+
|
|
678
|
+
```tsx
|
|
679
|
+
import { Fragment } from "react";
|
|
680
|
+
import { createPage, useNavigateToPage } from "@aminnairi/react-router";
|
|
681
|
+
|
|
682
|
+
const home = createPath({
|
|
683
|
+
path: "/",
|
|
684
|
+
element: function Home() {
|
|
685
|
+
return (
|
|
686
|
+
<h1>Home</h1>
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
createPage({
|
|
692
|
+
path: "/about",
|
|
693
|
+
element: function About() {
|
|
694
|
+
const navigateToHomePage = useNavigateToPage(home);
|
|
695
|
+
|
|
696
|
+
return (
|
|
697
|
+
<Fragment>
|
|
698
|
+
<h1>About</h1>
|
|
699
|
+
<button onClick={navigateToHomePage}>Home</button>
|
|
700
|
+
</Fragment>
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
If this page has dynamic parameters, it forces you to provide them when called inside your component.
|
|
707
|
+
|
|
708
|
+
The parameters should always be provided as string, as they are the only data type that can be used inside a URL.
|
|
709
|
+
|
|
710
|
+
```tsx
|
|
711
|
+
import { Fragment } from "react";
|
|
712
|
+
import { createPage, useNavigateToPage } from "@aminnairi/react-router";
|
|
713
|
+
|
|
714
|
+
const user = createPath({
|
|
715
|
+
path: "/users/:user",
|
|
716
|
+
element: function User({ parameters: { user }}) {
|
|
717
|
+
return (
|
|
718
|
+
<h1>User#{user}</h1>
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
createPage({
|
|
724
|
+
path: "/about",
|
|
725
|
+
element: function About() {
|
|
726
|
+
const navigateToUserPage = useNavigateToPage(user);
|
|
727
|
+
|
|
728
|
+
return (
|
|
729
|
+
<Fragment>
|
|
730
|
+
<h1>About</h1>
|
|
731
|
+
<button onClick={() => navigateToUserPage({ user: "123" })}>Home</button>
|
|
732
|
+
</Fragment>
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
### useSearch
|
|
739
|
+
|
|
740
|
+
Allow you to get one or more search query from the URL.
|
|
741
|
+
|
|
742
|
+
This will return an instance of the `URLSearchParams` Web API so that you can use you existing knowledge to manipulate the search queries easily.
|
|
743
|
+
|
|
744
|
+
```tsx
|
|
745
|
+
import { useMemo } from "react";
|
|
746
|
+
import { createPage, useSearch } from "@aminnairi/react-router";
|
|
747
|
+
|
|
748
|
+
createPage({
|
|
749
|
+
path: "/users",
|
|
750
|
+
element: function Home() {
|
|
751
|
+
const [search] = useSearch();
|
|
752
|
+
const sortedByDate = useMemo(() => search.get("sort-by") === "date", [search]);
|
|
753
|
+
|
|
754
|
+
return (
|
|
755
|
+
<h1>Users</h1>
|
|
756
|
+
<p>Sorted by date: {sortedByDate ? "yes" : "no"}</p>
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
You cannot set the search queries for now, this will be added in future release of this library.
|
|
763
|
+
|
|
764
|
+
### useHash
|
|
765
|
+
|
|
766
|
+
Allow you to get the hash, also called fragment, from the URL which is everything after the `#` symbol.
|
|
767
|
+
|
|
768
|
+
```tsx
|
|
769
|
+
import { createPage, useHash } from "@aminnairi/react-router";
|
|
770
|
+
|
|
771
|
+
createPage({
|
|
772
|
+
path: "/oauth/callback",
|
|
773
|
+
element: function OauthCallback() {
|
|
774
|
+
const token = useHash();
|
|
775
|
+
|
|
776
|
+
return (
|
|
777
|
+
<h1>You token is {token}</h1>
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
## Internal API
|
|
784
|
+
|
|
526
785
|
### doesRouteMatchPath
|
|
527
786
|
|
|
528
787
|
Return a boolean in case a route matches a path. A route is a URI that looks something like `/users/:user/articles` and a path is the browser's location pathname that looks something like `/users/123/articles`.
|
|
@@ -541,6 +800,20 @@ doesRoutePatchPath("/users/:user", "/users/123"); // true
|
|
|
541
800
|
doesRoutePatchPath("/users/:user", "/users/123/articles"); // false
|
|
542
801
|
```
|
|
543
802
|
|
|
803
|
+
You can also optionally provide a prefix.
|
|
804
|
+
|
|
805
|
+
```typescript
|
|
806
|
+
import { doesRouteMatchPath } from "@aminnairi/react-router";
|
|
807
|
+
|
|
808
|
+
doesRoutePatchPath("/", "/github", "/github"); // true
|
|
809
|
+
|
|
810
|
+
doesRoutePatchPath("/", "/github/about", "/github"); // false
|
|
811
|
+
|
|
812
|
+
doesRoutePatchPath("/users/:user", "/github/users/123", "/github"); // true
|
|
813
|
+
|
|
814
|
+
doesRoutePatchPath("/users/:user", "/github/users/123/articles", "/github"); // false
|
|
815
|
+
```
|
|
816
|
+
|
|
544
817
|
### getParameters
|
|
545
818
|
|
|
546
819
|
Return an object in case a route matches a path, with its dynamic parameters as output. It returns a generic `object` type in case no dynamic parameters are found in the URI. Note that the parameters are always strings, if you need to, convert them to other types explicitely.
|
|
@@ -559,6 +832,20 @@ getParameters("/users/:user", "/users/123"); // { user: "123" }
|
|
|
559
832
|
getParameters("/users/:user", "/users/123/articles"); // { user: "123" }
|
|
560
833
|
```
|
|
561
834
|
|
|
835
|
+
You can also provide an optional prefix.
|
|
836
|
+
|
|
837
|
+
```typescript
|
|
838
|
+
import { getParameters } from "@aminnairi/react-router";
|
|
839
|
+
|
|
840
|
+
getParameters("/", "/github", "/github"); // object
|
|
841
|
+
|
|
842
|
+
getParameters("/", "/github/about", "/github"); // object
|
|
843
|
+
|
|
844
|
+
getParameters("/users/:user", "/github/users/123", "/github"); // { user: "123" }
|
|
845
|
+
|
|
846
|
+
getParameters("/users/:user", "/github/users/123/articles", "/github"); // { user: "123" }
|
|
847
|
+
```
|
|
848
|
+
|
|
562
849
|
### findPage
|
|
563
850
|
|
|
564
851
|
Return a page that matches the `window.location.pathname` property containing the current URI of the page from an array of pages.
|
|
@@ -591,10 +878,44 @@ const pages = [
|
|
|
591
878
|
login.page
|
|
592
879
|
];
|
|
593
880
|
|
|
594
|
-
const foundPage = findPage(
|
|
595
|
-
|
|
881
|
+
const foundPage = findPage(pages, "/login");
|
|
882
|
+
|
|
883
|
+
if (foundPage) {
|
|
884
|
+
console.log("Found a page matching the current location");
|
|
885
|
+
console.log(foundPage.path);
|
|
886
|
+
} else {
|
|
887
|
+
console.log("No page matching the current location.");
|
|
888
|
+
}
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
You can also provide an optional prefix.
|
|
892
|
+
|
|
893
|
+
```tsx
|
|
894
|
+
import { findPage, createPage } from "@aminnairi/react-router";
|
|
895
|
+
|
|
896
|
+
const home = createPage({
|
|
897
|
+
path: "/",
|
|
898
|
+
element: () => <h1>Home</h1>
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
const about = createPage({
|
|
902
|
+
path: "/about",
|
|
903
|
+
element: () => <h1>About</h1>
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
const login = createPage({
|
|
907
|
+
path: "/login",
|
|
908
|
+
element: () => <h1>Login</h1>
|
|
596
909
|
});
|
|
597
910
|
|
|
911
|
+
const pages = [
|
|
912
|
+
home.page,
|
|
913
|
+
about.page,
|
|
914
|
+
login.page
|
|
915
|
+
];
|
|
916
|
+
|
|
917
|
+
const foundPage = findPage(pages, "/github/login", "/github");
|
|
918
|
+
|
|
598
919
|
if (foundPage) {
|
|
599
920
|
console.log("Found a page matching the current location");
|
|
600
921
|
console.log(foundPage.path);
|
|
@@ -603,6 +924,22 @@ if (foundPage) {
|
|
|
603
924
|
}
|
|
604
925
|
```
|
|
605
926
|
|
|
927
|
+
### sanitizePath
|
|
928
|
+
|
|
929
|
+
Internal function that helps normalizing the URL by removing trailing and leading slashes as well as removing any duplicate and unecessary slashes.
|
|
930
|
+
|
|
931
|
+
```ts
|
|
932
|
+
import { sanitizePath } from "@aminnairi/react-router";
|
|
933
|
+
|
|
934
|
+
sanitizePath("/"); // "/"
|
|
935
|
+
|
|
936
|
+
sanitizePath("users"); // "/users"
|
|
937
|
+
|
|
938
|
+
sanitizePath("users/"); // "/users"
|
|
939
|
+
|
|
940
|
+
sanitizePath("users//123///articles"); // "/users/123/articles"
|
|
941
|
+
```
|
|
942
|
+
|
|
606
943
|
## Features
|
|
607
944
|
|
|
608
945
|
### TypeScript
|
|
@@ -633,4 +970,58 @@ Never fear having a blank page again when a component throws. This library lets
|
|
|
633
970
|
|
|
634
971
|
## License
|
|
635
972
|
|
|
636
|
-
See [`LICENSE`](./LICENSE).
|
|
973
|
+
See [`LICENSE`](./LICENSE).
|
|
974
|
+
|
|
975
|
+
## Changelogs
|
|
976
|
+
|
|
977
|
+
### Versions
|
|
978
|
+
|
|
979
|
+
- [`1.0.0`](#100)
|
|
980
|
+
- [`0.1.1`](#011)
|
|
981
|
+
- [`0.1.0`](#010)
|
|
982
|
+
|
|
983
|
+
### 1.0.0
|
|
984
|
+
|
|
985
|
+
#### Major changes
|
|
986
|
+
|
|
987
|
+
- The arguments of `findPage` have moved from an object to regular arguments, with the first one being the path, and the second being the current route
|
|
988
|
+
- Removed the `page.navigate` property in favor of the new `useNavigateTo` hook
|
|
989
|
+
- The `createPage` now returns the page directly instead of exposing it in an object
|
|
990
|
+
|
|
991
|
+
#### Minor changes
|
|
992
|
+
|
|
993
|
+
- Added a `Provider` component from the created `router` which exposes variables for the children such as the location
|
|
994
|
+
- Added a new `useIsActivePage` hook for the created `router` which helps computing if a given page is active or not
|
|
995
|
+
- Added a new `useSearch` hook for the created `router` for getting search parameters from the current URL
|
|
996
|
+
- Added a new `useHash` hook for the created `router` for getting the URL fragment
|
|
997
|
+
- Added a new `sanitizePath` function for removing unecessary and extra slashes in a given string
|
|
998
|
+
- Added a new `useNavigateTo` hook that replaces the old `page.navigate` function
|
|
999
|
+
- Added a `prefix` property in order to use prefix for routes that need it like GitHub Pages
|
|
1000
|
+
|
|
1001
|
+
### 0.1.1
|
|
1002
|
+
|
|
1003
|
+
#### Major changes
|
|
1004
|
+
|
|
1005
|
+
None.
|
|
1006
|
+
|
|
1007
|
+
#### Minor changes
|
|
1008
|
+
|
|
1009
|
+
None.
|
|
1010
|
+
|
|
1011
|
+
#### Bug & security fixes
|
|
1012
|
+
|
|
1013
|
+
Fixed peer dependency for react.
|
|
1014
|
+
|
|
1015
|
+
### 0.1.0
|
|
1016
|
+
|
|
1017
|
+
#### Major changes
|
|
1018
|
+
|
|
1019
|
+
None.
|
|
1020
|
+
|
|
1021
|
+
#### Minor changes
|
|
1022
|
+
|
|
1023
|
+
None.
|
|
1024
|
+
|
|
1025
|
+
#### Bug & security fixes
|
|
1026
|
+
|
|
1027
|
+
None.
|
package/index.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState, FunctionComponent, useMemo, Component, PropsWithChildren } from "react";
|
|
1
|
+
import { useEffect, useState, FunctionComponent, useMemo, Component, PropsWithChildren, createContext, SetStateAction, Dispatch, ReactNode, useContext } from "react";
|
|
2
2
|
|
|
3
3
|
export type AbsolutePath<Path extends string> =
|
|
4
4
|
Path extends `${infer Start}:${string}/${infer Rest}`
|
|
@@ -25,46 +25,32 @@ export interface Page<Path extends string> {
|
|
|
25
25
|
element: FunctionComponent<PageComponentProps<Path>>
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export interface CreateRouterOptions {
|
|
28
|
+
export interface CreateRouterOptions<Path extends string> {
|
|
29
29
|
transition?: boolean,
|
|
30
|
-
|
|
30
|
+
prefix?: string,
|
|
31
|
+
pages: Array<Page<Path>>
|
|
31
32
|
fallback: FunctionComponent
|
|
32
33
|
issue: FunctionComponent<IssueProps>
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
export interface CreateRouteOutput<Path extends string> {
|
|
36
|
-
page: Page<Path>,
|
|
37
|
-
navigate: (parameters: Parameters<Path>) => void
|
|
38
|
-
}
|
|
39
|
-
|
|
40
36
|
export interface FindPageOptions {
|
|
41
37
|
pages: Array<Page<string>>
|
|
38
|
+
path: string
|
|
42
39
|
}
|
|
43
40
|
|
|
44
|
-
export const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (replace) {
|
|
51
|
-
window.history.replaceState(null, pathWithParameters, pathWithParameters);
|
|
52
|
-
} else {
|
|
53
|
-
window.history.pushState(null, pathWithParameters, pathWithParameters);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
window.dispatchEvent(new CustomEvent("popstate"));
|
|
57
|
-
};
|
|
41
|
+
export const sanitizePath = (path: string): string => {
|
|
42
|
+
return "/" + path
|
|
43
|
+
.replace(/^\/|\/$/g, "")
|
|
44
|
+
.replace(/\/+/g, "/");
|
|
45
|
+
}
|
|
58
46
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
navigate
|
|
62
|
-
};
|
|
47
|
+
export const createPage = <Path extends string>(page: Page<Path>) => {
|
|
48
|
+
return page
|
|
63
49
|
}
|
|
64
50
|
|
|
65
|
-
export const doesRouteMatchPath = (path: string, route: string): boolean => {
|
|
66
|
-
const pathParts = path.split("/").filter(Boolean);
|
|
67
|
-
const routeParts = route.split("/").filter(Boolean);
|
|
51
|
+
export const doesRouteMatchPath = (path: string, route: string, prefix?: string): boolean => {
|
|
52
|
+
const pathParts = sanitizePath(`${prefix ?? ""}/${path}`).split("/").filter(Boolean);
|
|
53
|
+
const routeParts = sanitizePath(route).split("/").filter(Boolean);
|
|
68
54
|
|
|
69
55
|
return (
|
|
70
56
|
pathParts.length === routeParts.length &&
|
|
@@ -72,18 +58,35 @@ export const doesRouteMatchPath = (path: string, route: string): boolean => {
|
|
|
72
58
|
);
|
|
73
59
|
}
|
|
74
60
|
|
|
75
|
-
export const getParameters = <Path extends string>(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
61
|
+
export const getParameters = <Path extends string>(path: Path, route: string, prefix?: string): Parameters<Path> => {
|
|
62
|
+
if (!doesRouteMatchPath(path, route, prefix)) {
|
|
63
|
+
return {} as Parameters<Path>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const pathParts = sanitizePath(`${prefix ?? ""}/${path}`).split("/").filter(Boolean);
|
|
67
|
+
const routeParts = sanitizePath(route).split("/").filter(Boolean);
|
|
68
|
+
|
|
69
|
+
return pathParts.reduce((parameters, pathPart, pathPartIndex) => {
|
|
70
|
+
const routePart = routeParts[pathPartIndex];
|
|
71
|
+
|
|
72
|
+
if (!routePart) {
|
|
73
|
+
return parameters;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!pathPart.startsWith(":")) {
|
|
77
|
+
return parameters;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
...parameters,
|
|
82
|
+
[`${pathPart.slice(1)}`]: routePart
|
|
83
|
+
}
|
|
84
|
+
}, {} as Parameters<Path>);
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
const findPage = (
|
|
87
|
+
const findPage = (pages: Array<Page<string>>, path: string, prefix?: string) => {
|
|
85
88
|
const foundPage = pages.find(route => {
|
|
86
|
-
return doesRouteMatchPath(route.path,
|
|
89
|
+
return doesRouteMatchPath(sanitizePath(`${prefix ?? ""}/${route.path}`), sanitizePath(path));
|
|
87
90
|
});
|
|
88
91
|
|
|
89
92
|
return foundPage;
|
|
@@ -150,34 +153,95 @@ export const createIssue = (issue: FunctionComponent<IssueProps>) => {
|
|
|
150
153
|
return issue;
|
|
151
154
|
}
|
|
152
155
|
|
|
153
|
-
export
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
export interface ContextInterface {
|
|
157
|
+
prefix: string,
|
|
158
|
+
pathname: string,
|
|
159
|
+
setPathname: Dispatch<SetStateAction<string>>,
|
|
160
|
+
search: URLSearchParams,
|
|
161
|
+
setSearch: Dispatch<SetStateAction<URLSearchParams>>,
|
|
162
|
+
hash: string,
|
|
163
|
+
setHash: Dispatch<SetStateAction<string>>
|
|
164
|
+
}
|
|
158
165
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
path: page.path,
|
|
163
|
-
route: window.location.pathname
|
|
164
|
-
})
|
|
165
|
-
}
|
|
166
|
+
export interface ProviderProps {
|
|
167
|
+
children: ReactNode
|
|
168
|
+
}
|
|
166
169
|
|
|
167
|
-
|
|
168
|
-
|
|
170
|
+
const Context = createContext<ContextInterface>({
|
|
171
|
+
prefix: "",
|
|
172
|
+
pathname: sanitizePath(window.location.pathname),
|
|
173
|
+
setPathname: () => { },
|
|
174
|
+
search: new URLSearchParams(),
|
|
175
|
+
setSearch: () => { },
|
|
176
|
+
hash: window.location.hash,
|
|
177
|
+
setHash: () => { }
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
export const useNavigateToPage = <Path extends string>(page: Page<Path>) => {
|
|
181
|
+
const { prefix } = useContext(Context);
|
|
182
|
+
|
|
183
|
+
return (parameters: Parameters<Path>, replace: boolean = false) => {
|
|
184
|
+
const pathWithParameters = Object.entries(parameters).reduce((path, [parameterName, parameterValue]) => {
|
|
185
|
+
return path.replace(`:${parameterName}`, parameterValue);
|
|
186
|
+
}, sanitizePath(`${prefix ?? ""}/${page.path}`));
|
|
187
|
+
|
|
188
|
+
if (replace) {
|
|
189
|
+
window.history.replaceState(null, pathWithParameters, pathWithParameters);
|
|
190
|
+
} else {
|
|
191
|
+
window.history.pushState(null, pathWithParameters, pathWithParameters);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
window.dispatchEvent(new CustomEvent("popstate"));
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export const useIsActivePage = (page: Page<string>) => {
|
|
199
|
+
const { pathname, prefix } = useContext(Context);
|
|
200
|
+
|
|
201
|
+
return doesRouteMatchPath(sanitizePath(page.path), sanitizePath(pathname), prefix);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export const useSearch = () => {
|
|
205
|
+
const { search } = useContext(Context);
|
|
206
|
+
|
|
207
|
+
return search;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export const useHash = () => {
|
|
211
|
+
const { hash } = useContext(Context);
|
|
212
|
+
return hash;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export const createRouter = <Path extends string>({ pages, fallback, transition: withViewTransition, issue, prefix }: CreateRouterOptions<Path>) => {
|
|
216
|
+
const Provider = ({ children }: ProviderProps) => {
|
|
217
|
+
const [pathname, setPathname] = useState(sanitizePath(window.location.pathname));
|
|
218
|
+
const [search, setSearch] = useState(new URLSearchParams(sanitizePath(window.location.search)));
|
|
219
|
+
const [hash, setHash] = useState(window.location.hash);
|
|
220
|
+
const shouldTransitionBetweenPages = useMemo(() => typeof document.startViewTransition === "function" && withViewTransition ? true : false, [withViewTransition]);
|
|
221
|
+
|
|
222
|
+
const value = useMemo(() => {
|
|
223
|
+
return {
|
|
224
|
+
prefix: prefix ?? "",
|
|
225
|
+
pathname,
|
|
226
|
+
search,
|
|
227
|
+
hash,
|
|
228
|
+
setPathname,
|
|
229
|
+
setSearch,
|
|
230
|
+
setHash
|
|
231
|
+
};
|
|
232
|
+
}, [prefix, pathname, search, hash]);
|
|
169
233
|
|
|
170
234
|
useEffect(() => {
|
|
171
235
|
const onWindowPopstate = () => {
|
|
172
|
-
const foundPage = findPage({ pages });
|
|
173
|
-
|
|
174
236
|
if (shouldTransitionBetweenPages) {
|
|
175
237
|
document.startViewTransition(() => {
|
|
176
|
-
|
|
238
|
+
setPathname(sanitizePath(window.location.pathname));
|
|
177
239
|
});
|
|
178
|
-
|
|
179
|
-
|
|
240
|
+
|
|
241
|
+
return;
|
|
180
242
|
}
|
|
243
|
+
|
|
244
|
+
setPathname(sanitizePath(window.location.pathname));
|
|
181
245
|
};
|
|
182
246
|
|
|
183
247
|
window.addEventListener("popstate", onWindowPopstate);
|
|
@@ -187,18 +251,41 @@ export const createRouter = ({ pages, fallback, transition: withViewTransition,
|
|
|
187
251
|
}
|
|
188
252
|
}, []);
|
|
189
253
|
|
|
190
|
-
|
|
191
|
-
|
|
254
|
+
return (
|
|
255
|
+
<Context.Provider value={value}>
|
|
192
256
|
<ErrorBoundary fallback={issue} transition={shouldTransitionBetweenPages}>
|
|
193
|
-
{
|
|
257
|
+
{children}
|
|
194
258
|
</ErrorBoundary>
|
|
259
|
+
</Context.Provider>
|
|
260
|
+
);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const View = () => {
|
|
264
|
+
const Fallback = useMemo(() => fallback, []);
|
|
265
|
+
const { pathname } = useContext(Context);
|
|
266
|
+
const page = useMemo(() => findPage(pages, pathname, prefix), [pathname]);
|
|
267
|
+
|
|
268
|
+
const parameters = useMemo(() => {
|
|
269
|
+
if (page) {
|
|
270
|
+
return getParameters(sanitizePath(page.path), sanitizePath(window.location.pathname), prefix);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {};
|
|
274
|
+
}, [page]);
|
|
275
|
+
|
|
276
|
+
if (page) {
|
|
277
|
+
return (
|
|
278
|
+
<page.element parameters={parameters} />
|
|
195
279
|
);
|
|
196
280
|
}
|
|
197
281
|
|
|
198
|
-
return
|
|
282
|
+
return (
|
|
283
|
+
<Fallback />
|
|
284
|
+
);
|
|
199
285
|
};
|
|
200
286
|
|
|
201
287
|
return {
|
|
202
|
-
View
|
|
288
|
+
View,
|
|
289
|
+
Provider,
|
|
203
290
|
};
|
|
204
291
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@aminnairi/react-router",
|
|
4
4
|
"description": "Type-safe router for the React library",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "1.0.0",
|
|
6
6
|
"homepage": "https://github.com/aminnairi/react-router#readme",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"bugs": {
|
|
@@ -20,9 +20,11 @@
|
|
|
20
20
|
"router",
|
|
21
21
|
"hook",
|
|
22
22
|
"typescript",
|
|
23
|
-
"transition"
|
|
23
|
+
"transition",
|
|
24
|
+
"navigation",
|
|
25
|
+
"history"
|
|
24
26
|
],
|
|
25
27
|
"peerDependencies": {
|
|
26
|
-
"react": "
|
|
28
|
+
"react": ">=18.0.0"
|
|
27
29
|
}
|
|
28
30
|
}
|