@ossy/router-react 0.0.1-beta.1
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 +301 -0
- package/build/cjs/esm/router-react.d.ts +51 -0
- package/build/cjs/index.js +1 -0
- package/build/esm/index.js +1 -0
- package/build/esm/router-react.d.ts +51 -0
- package/package.json +51 -0
- package/rollup.config.js +35 -0
- package/src/router-react.tsx +253 -0
- package/tsconfig.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# @ossy/router-react
|
|
2
|
+
|
|
3
|
+
A React router built for server side rendering and multi-language support with localized paths.
|
|
4
|
+
|
|
5
|
+
What this package tries to do is to lift the responsibility of creating language aware links to a single provider, the `<Router/>` component.
|
|
6
|
+
|
|
7
|
+
The components that want to trigger a navigation only need to be aware of the pageId (with eventual params) they want to navigate to.
|
|
8
|
+
|
|
9
|
+
Loading of appropriate data is still the responsibility of the component.
|
|
10
|
+
|
|
11
|
+
This Router is aimed towards websites that want to prerender as much as possible on the server and therefore doesn't include many client side functionality that you might want for SPAs, like setting search params without doing a full navigation.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
- Easy to use
|
|
15
|
+
- Supports single language websites
|
|
16
|
+
- Supports multi language websites with localized paths
|
|
17
|
+
- Supports prerendering (API is only hooks based ATM, API that works for server components is comming)
|
|
18
|
+
|
|
19
|
+
## Roadmap
|
|
20
|
+
- [ ] API that works on server components (non hooks API)
|
|
21
|
+
- [ ] Trigger navigation programaticly on the client (`router.navigate()`)
|
|
22
|
+
- [ ] Update search params without doing full navigation, for times you keep state in the url
|
|
23
|
+
- [ ] Redirects
|
|
24
|
+
|
|
25
|
+
## Table of Contents
|
|
26
|
+
|
|
27
|
+
- [@ossy/router-react](#ossyrouter-react)
|
|
28
|
+
- [Features](#features)
|
|
29
|
+
- [What problem does this package solve?](#what-problem-does-this-package-solve)
|
|
30
|
+
- [Installation](#installation)
|
|
31
|
+
- [Usage](#usage)
|
|
32
|
+
- [Single language website setup](#single-language-website-setup)
|
|
33
|
+
- [Multi language website setup](#multi-language-website-setup)
|
|
34
|
+
- [Server side rendering](#server-side-rendering)
|
|
35
|
+
- [Navigation](#navigation)
|
|
36
|
+
- [Navigation with params](#navigation-with-params)
|
|
37
|
+
- [Note on default params](#note-on-default-params)
|
|
38
|
+
- [Navigating to specific page in a specific language](#navigating-to-specific-page-in-a-specific-language)
|
|
39
|
+
- [Switching language (same page)](#switching-language-same-page)
|
|
40
|
+
- [Navigating back](#navigating-back)
|
|
41
|
+
- [Redirects](#redirects)
|
|
42
|
+
|
|
43
|
+
## What problem does this package solve?
|
|
44
|
+
When creating links in your app it's not uncommon to hard code the path you want to navigate to.
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
const MyComponent = () => {
|
|
48
|
+
const { org } = useParams()
|
|
49
|
+
return (
|
|
50
|
+
<nav>
|
|
51
|
+
<a href={`/${org}`}>Org home</a>
|
|
52
|
+
<a href={`/${org}/users`}>Org users</a>
|
|
53
|
+
</nav>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
This works pretty OK when you only have to worry about one language.
|
|
58
|
+
But what happens when you want to add another language with localized paths?
|
|
59
|
+
How would that look like inside a component?
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
const MyComponent = () => {
|
|
63
|
+
const { language, org } = useParams()
|
|
64
|
+
|
|
65
|
+
const paths = {
|
|
66
|
+
en: {
|
|
67
|
+
home: `/${org}`,
|
|
68
|
+
users: `/${org}/users`
|
|
69
|
+
},
|
|
70
|
+
sv: {
|
|
71
|
+
home: `/${org}`,
|
|
72
|
+
users: `/${org}/anvandare`
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<nav>
|
|
78
|
+
<a href={paths[language].home}>Org home</a>
|
|
79
|
+
<a href={paths[language].users}>Org users</a>
|
|
80
|
+
</nav>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
The example above doesn't look too bad, but keep in mind it's only for one extra language and in one component. Imagine doing that everytime you want to navigate?
|
|
85
|
+
|
|
86
|
+
What this package tries to do is to lift the responsibility of creating language aware links to a single provider, the `<Router/>` component.
|
|
87
|
+
|
|
88
|
+
The components that want to trigger a navigation only need to be aware of the pageId (with eventual params) they want to navigate to.
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
const MyComponent = () => {
|
|
92
|
+
const router = useRouter()
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<nav>
|
|
96
|
+
<a href={router.getHref('home')}>Org home</a>
|
|
97
|
+
<a href={router.getHref('users')}>Org users</a>
|
|
98
|
+
</nav>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
To take this one step further, if you were to integrate with a cms you could add the pageId to the data, and you wouldn't need to update the component or data if the paths changes.
|
|
103
|
+
|
|
104
|
+
In the below example we pretend to load data from a CMS and itterate through navigation links.
|
|
105
|
+
In this made up case, the `data.navigationLinks.href` refers to the pageId.
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
const MyComponent = () => {
|
|
109
|
+
const router = useRouter()
|
|
110
|
+
const { data } = useDataFromSomeCms()
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<nav>
|
|
114
|
+
{data.navigationLinks(link => (
|
|
115
|
+
<a href={router.getHref(link.href)} key={link.href}>{link.label}</a>)
|
|
116
|
+
)}
|
|
117
|
+
</nav>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## Installation
|
|
124
|
+
```
|
|
125
|
+
npm install @ossy/router-react
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Usage
|
|
129
|
+
|
|
130
|
+
### Single language website setup
|
|
131
|
+
```tsx
|
|
132
|
+
import { Router } from '@ossy/router-react'
|
|
133
|
+
|
|
134
|
+
const pages = [
|
|
135
|
+
{ id: 'home', path: '/', element: <h1>Home page<h1> },
|
|
136
|
+
{ id: 'users', path: '/users', element: <h1>Users page<h1> },
|
|
137
|
+
{ id: 'user', path: '/users/:userId', element: <h1>User page<h1> },
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
export const App = () => (
|
|
141
|
+
<html>
|
|
142
|
+
<head>
|
|
143
|
+
<title>My app</title>
|
|
144
|
+
</head>
|
|
145
|
+
<body>
|
|
146
|
+
<Router pages={pages} />
|
|
147
|
+
</body>
|
|
148
|
+
</html>
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Multi language website setup
|
|
153
|
+
|
|
154
|
+
Setting up a multi language website is just a matter of providing the `<Router/>` component with some additional information. Both `defaultLanguage` and `supportedLanguages` are required for multi language websites. To specify paths for each supported language, provide the `page.path` key with an object containing key value pairs that corresponds with the language and it's associated path for that page.
|
|
155
|
+
|
|
156
|
+
- When navigating to a page on a multi language site, the language needs to be at the root.
|
|
157
|
+
So the urls for a page with the config below becomes `/en/users` and `/sv/anvandare`, both rendering the same element.
|
|
158
|
+
```tsx
|
|
159
|
+
{
|
|
160
|
+
id: 'users',
|
|
161
|
+
path: {
|
|
162
|
+
en: '/users',
|
|
163
|
+
sv: '/anvandare'
|
|
164
|
+
},
|
|
165
|
+
element: <h1>Users page<h1>
|
|
166
|
+
},
|
|
167
|
+
```
|
|
168
|
+
- It's then up to the rendered element to read the language from the `useRouter()` hook and load appropriate data.
|
|
169
|
+
- If users navigate to the root url, the router will redirect to the default language
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import { Router } from '@ossy/router-react'
|
|
173
|
+
|
|
174
|
+
const routerConfig = {
|
|
175
|
+
defaultLanguage: ['en'],
|
|
176
|
+
supportedLanguages: ['en', 'sv'],
|
|
177
|
+
pages: [
|
|
178
|
+
{
|
|
179
|
+
id: 'home',
|
|
180
|
+
path: {
|
|
181
|
+
en: '/',
|
|
182
|
+
sv: '/',
|
|
183
|
+
},
|
|
184
|
+
element: <h1>Home page<h1>
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: 'users',
|
|
188
|
+
path: {
|
|
189
|
+
en: '/users',
|
|
190
|
+
sv: '/anvandare'
|
|
191
|
+
},
|
|
192
|
+
element: <h1>Users page<h1>
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: 'user',
|
|
196
|
+
path: {
|
|
197
|
+
en: '/users/:userId',
|
|
198
|
+
sv: '/anvandare/:userId'
|
|
199
|
+
},
|
|
200
|
+
element: <h1>User page<h1>
|
|
201
|
+
},
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export const App = () => (
|
|
206
|
+
<html>
|
|
207
|
+
<head>
|
|
208
|
+
<title>My app</title>
|
|
209
|
+
</head>
|
|
210
|
+
<body>
|
|
211
|
+
<Router {...routerConfig} />
|
|
212
|
+
</body>
|
|
213
|
+
</html>
|
|
214
|
+
)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Server side rendering
|
|
218
|
+
- `@ossy/react-router` provides no server on it's own.
|
|
219
|
+
- Meaning it's up to you wich server framework you choose to use, be it Expressjs or something else.
|
|
220
|
+
- Then you can use any of the React dom static methods to actually render the page.
|
|
221
|
+
- To provide the active url use the url prop on the router.
|
|
222
|
+
|
|
223
|
+
### Navigation
|
|
224
|
+
|
|
225
|
+
Navigation is done by getting the `href` through `router.getHref(<pageId>)` and using regular `<a/>` tags to do the actual navigation.
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
const router = useRouter()
|
|
229
|
+
<a href={router.getHref('users')}>Users</a>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### Navigation with params
|
|
233
|
+
To create an href with params, provide `getHref()` with an options objet.
|
|
234
|
+
```tsx
|
|
235
|
+
const router = useRouter()
|
|
236
|
+
const userId = '111'
|
|
237
|
+
<a href={router.getHref({ id: 'user', params: { userId } })}>User ${userId}</a>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
##### Note on default params
|
|
241
|
+
The default params for the `getHref()` call will be taken from the url.
|
|
242
|
+
This means that if a user have navigated to `/users/:userId` and you want to build a link to
|
|
243
|
+
`/users/:userId/details`, you don't need to supply the userId to the `getHref()` call since it already exists in the url.
|
|
244
|
+
|
|
245
|
+
#### Navigating to specific page in a specific language
|
|
246
|
+
For multi language websites, the default behaviour is that `getHref()` returns the href for the active language, This can be overriden by providing the target language in the options object.
|
|
247
|
+
```tsx
|
|
248
|
+
const router = useRouter()
|
|
249
|
+
<a href={router.getHref({ id: 'home', language: 'en' })}>Home</a>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### Hash and search params
|
|
253
|
+
To add hash and search params, add them to the end of the pageId.
|
|
254
|
+
```tsx
|
|
255
|
+
const router = useRouter()
|
|
256
|
+
<a href={router.getHref('@collection#main?sort=desc')}>Collection</a>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### Switching language (same page)
|
|
260
|
+
Switching the language of the current page is done by navigating to that route.
|
|
261
|
+
To make it easier, if pageId is left out of the getHref call, it will assume you want the current page.
|
|
262
|
+
So to make a language switcher could look as easy as this:
|
|
263
|
+
```
|
|
264
|
+
const router = useRouter()
|
|
265
|
+
<a href={router.getHref({ language: 'en' })}>English</a>
|
|
266
|
+
<a href={router.getHref({ language: 'sv' })}>Swedish</a>
|
|
267
|
+
<a href={router.getHref({ language: 'es' })}>Spanish</a>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
#### Navigating back
|
|
271
|
+
Backwards navigation can be done with the `router.back()` methods.
|
|
272
|
+
It just calls `window.history.back()` under the hood.
|
|
273
|
+
```tsx
|
|
274
|
+
const router = useRouter()
|
|
275
|
+
<Button href={router.back()}>Back</Button>
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
#### Redirects
|
|
279
|
+
You can redirect by setting the redirect field on the page defenition to the pageId you want to redirect to.
|
|
280
|
+
The redirect will take place on the client.
|
|
281
|
+
In the example below, the `/services` will redirect to `/contact`.
|
|
282
|
+
```tsx
|
|
283
|
+
const pages = [
|
|
284
|
+
{
|
|
285
|
+
id: 'services',
|
|
286
|
+
redirect: '@contact',
|
|
287
|
+
path: '/services'
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: 'contact',
|
|
291
|
+
path: '/contact'
|
|
292
|
+
}
|
|
293
|
+
]
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Why not open source?
|
|
297
|
+
|
|
298
|
+
TLDR: It might be in the future.
|
|
299
|
+
|
|
300
|
+
Right now the source is in a private monorepo with other private packages and workflows.
|
|
301
|
+
It's to much of a hassle to break it out right now.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { MultiLanguagePage, RouterOptions, SingleLanguagePage } from '@ossy-se/router';
|
|
3
|
+
export declare const RouterContext: React.Context<{
|
|
4
|
+
pages: RouterPage[];
|
|
5
|
+
defaultLanguage: string | undefined;
|
|
6
|
+
supportedLanguages: string[];
|
|
7
|
+
}>;
|
|
8
|
+
export declare const useRouter: () => {
|
|
9
|
+
pages: RouterPage[];
|
|
10
|
+
defaultLanguage: string | undefined;
|
|
11
|
+
supportedLanguages: string[];
|
|
12
|
+
};
|
|
13
|
+
export declare const Router: <T extends RouterPage>({ url, pages, defaultLanguage, supportedLanguages }: RouterProps<T>) => React.JSX.Element;
|
|
14
|
+
export interface RouterProps<T extends RouterPage> extends RouterOptions<T> {
|
|
15
|
+
/**
|
|
16
|
+
* Render page associated with the given url.
|
|
17
|
+
* Used to render the page on server side where window.location is not available
|
|
18
|
+
* @type string
|
|
19
|
+
* @default undefined
|
|
20
|
+
*/
|
|
21
|
+
url?: string;
|
|
22
|
+
}
|
|
23
|
+
export type RouterPage = SingleLanguageRouterPage | MultiLanguageRouterPage;
|
|
24
|
+
export interface SingleLanguageRouterPage extends SingleLanguagePage {
|
|
25
|
+
/** If provided the page will redirect to the given page id
|
|
26
|
+
* @default undefined
|
|
27
|
+
* @type {string}
|
|
28
|
+
* @example { redirect: '@home'}
|
|
29
|
+
*/
|
|
30
|
+
redirect?: string;
|
|
31
|
+
element?: React.ReactNode;
|
|
32
|
+
render: () => React.ReactNode;
|
|
33
|
+
}
|
|
34
|
+
export interface MultiLanguageRouterPage extends MultiLanguagePage {
|
|
35
|
+
/** If provided the page will redirect to the given page id
|
|
36
|
+
* @default undefined
|
|
37
|
+
* @type {string}
|
|
38
|
+
* @example { redirect: '@home'}
|
|
39
|
+
*/
|
|
40
|
+
redirect?: string;
|
|
41
|
+
element?: React.ReactNode;
|
|
42
|
+
render: () => React.ReactNode;
|
|
43
|
+
}
|
|
44
|
+
export interface GetHrefRequest {
|
|
45
|
+
id?: string;
|
|
46
|
+
language?: string;
|
|
47
|
+
params?: Record<string, string>;
|
|
48
|
+
}
|
|
49
|
+
export interface GetHrefOfActivePageByLanguageRequest {
|
|
50
|
+
language: string;
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("react"),n=require("@ossy-se/router"),r=function(){return r=Object.assign||function(e){for(var n,r=1,t=arguments.length;r<t;r++)for(var a in n=arguments[r])Object.prototype.hasOwnProperty.call(n,a)&&(e[a]=n[a]);return e},r.apply(this,arguments)};"function"==typeof SuppressedError&&SuppressedError;var t=e.createContext({pages:[],defaultLanguage:void 0,supportedLanguages:[]}),a=function(e){return void 0===e&&(e=""),function(e){return"/"===e[e.length-1]?e:"".concat(e,"/")}(function(e){return"/"===e[0]?e:"/".concat(e)}(e))};exports.Router=function(o){var i,u,l,d,s,c=o.url,f=o.pages,g=void 0===f?[]:f,p=o.defaultLanguage,v=void 0===p?void 0:p,w=o.supportedLanguages,h=void 0===w?[]:w;if(c){var m=c.match(/([^?#]*)(\?[^#]*)?(#.*)?/);u=c,l=(null==m?void 0:m[1])||"",d=(null==m?void 0:m[3])||"",s=(null==m?void 0:m[2])||""}else{if("undefined"==typeof window)return e.createElement(e.Fragment,null);u=window.location.href,l=window.location.pathname,s=(null==(d=window.location.hash)?void 0:d.includes("?"))?d.split("?")[1]:window.location.search}var y=h.length>1&&!!v,b=l.split("/")[1]||"",L=h.includes(b)?b:v,k=n.Router.of({pages:g,defaultLanguage:v,supportedLanguages:h}),P=k.getParamsFromUrl(u),R=new URLSearchParams(s),S=k.getPageByUrl(u);if(S||(y&&"/"===l&&"undefined"!=typeof window&&(window.location.href="/"+v),S=y?g.find((function(e){return"*"===a(e.path[L])})):g.find((function(e){return"*"===a(e.path)}))),!S)return console.warn("[RouterReact] No active page found for",l),e.createElement(e.Fragment,null);e.useCallback((function(e,n){if("undefined"!=typeof window){window.history.pushState({},"","".concat(l,"?").concat(R.toString()));var r=new URLSearchParams(R);r.set(e,n),history.pushState({},"","?".concat(r.toString()))}}),[R]);var C=e.useCallback((function(e){var n,r,t,a,o,i={};if("string"==typeof e?(n=e,i=P,r=L):(o=e).id||void 0===o.language?(n=(null==e?void 0:e.id)||S.id,i=(null==e?void 0:e.params)||P,r=(null==e?void 0:e.language)||L):(n=S.id,i=P,r=e.language),n.includes("#")){var u=n.split("#");if(n=u[0],(t=u[1]).includes("?")){var l=n.split("?");t=l[0],a=l[1]}}else if(n.includes("?")){var d=n.split("?");n=d[0],a=d[1]}var s=k.getPathname({id:n.replace("@",""),params:i,language:r});return t&&(s=s+"#"+t),a&&(s=s+"?"+a),s}),[k,L]),x=e.useCallback((function(e){if("undefined"!=typeof window&&e)if("@back"!==e){var n=C(e);n&&(window.location.href=n)}else window.history.back()}),[C]),E=e.useCallback((function(){"undefined"!=typeof window&&window.history.back()}),[]),U={href:u,pages:g,language:L,defaultLanguage:v,supportedLanguages:h,params:P,searchParams:r({},Array.from(R.entries()).reduce((function(e,n){var t,a=n[0],o=n[1];return r(r({},e),((t={})[a]=o,t))}),{})),navigate:x,getHref:C,back:E};(null==S?void 0:S.redirect)&&x({id:S.redirect});var F=(null==S?void 0:S.element)||(null===(i=null==S?void 0:S.render)||void 0===i?void 0:i.call(S));return e.createElement(t.Provider,{value:U},F)},exports.RouterContext=t,exports.useRouter=function(){return e.useContext(t)};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e,{createContext as n,useContext as r,useCallback as a}from"react";import{Router as t}from"@ossy-se/router";var o=function(){return o=Object.assign||function(e){for(var n,r=1,a=arguments.length;r<a;r++)for(var t in n=arguments[r])Object.prototype.hasOwnProperty.call(n,t)&&(e[t]=n[t]);return e},o.apply(this,arguments)};"function"==typeof SuppressedError&&SuppressedError;var i=n({pages:[],defaultLanguage:void 0,supportedLanguages:[]}),u=function(){return r(i)},d=function(e){return void 0===e&&(e=""),function(e){return"/"===e[e.length-1]?e:"".concat(e,"/")}(function(e){return"/"===e[0]?e:"/".concat(e)}(e))},l=function(n){var r,u,l,c,s,f=n.url,g=n.pages,p=void 0===g?[]:g,v=n.defaultLanguage,w=void 0===v?void 0:v,h=n.supportedLanguages,m=void 0===h?[]:h;if(f){var y=f.match(/([^?#]*)(\?[^#]*)?(#.*)?/);u=f,l=(null==y?void 0:y[1])||"",c=(null==y?void 0:y[3])||"",s=(null==y?void 0:y[2])||""}else{if("undefined"==typeof window)return e.createElement(e.Fragment,null);u=window.location.href,l=window.location.pathname,s=(null==(c=window.location.hash)?void 0:c.includes("?"))?c.split("?")[1]:window.location.search}var L=m.length>1&&!!w,P=l.split("/")[1]||"",S=m.includes(P)?P:w,b=t.of({pages:p,defaultLanguage:w,supportedLanguages:m}),E=b.getParamsFromUrl(u),R=new URLSearchParams(s),k=b.getPageByUrl(u);if(k||(L&&"/"===l&&"undefined"!=typeof window&&(window.location.href="/"+w),k=L?p.find((function(e){return"*"===d(e.path[S])})):p.find((function(e){return"*"===d(e.path)}))),!k)return console.warn("[RouterReact] No active page found for",l),e.createElement(e.Fragment,null);a((function(e,n){if("undefined"!=typeof window){window.history.pushState({},"","".concat(l,"?").concat(R.toString()));var r=new URLSearchParams(R);r.set(e,n),history.pushState({},"","?".concat(r.toString()))}}),[R]);var U=a((function(e){var n,r,a,t,o,i={};if("string"==typeof e?(n=e,i=E,r=S):(o=e).id||void 0===o.language?(n=(null==e?void 0:e.id)||k.id,i=(null==e?void 0:e.params)||E,r=(null==e?void 0:e.language)||S):(n=k.id,i=E,r=e.language),n.includes("#")){var u=n.split("#");if(n=u[0],(a=u[1]).includes("?")){var d=n.split("?");a=d[0],t=d[1]}}else if(n.includes("?")){var l=n.split("?");n=l[0],t=l[1]}var c=b.getPathname({id:n.replace("@",""),params:i,language:r});return a&&(c=c+"#"+a),t&&(c=c+"?"+t),c}),[b,S]),F=a((function(e){if("undefined"!=typeof window&&e)if("@back"!==e){var n=U(e);n&&(window.location.href=n)}else window.history.back()}),[U]),O=a((function(){"undefined"!=typeof window&&window.history.back()}),[]),j={href:u,pages:p,language:S,defaultLanguage:w,supportedLanguages:m,params:E,searchParams:o({},Array.from(R.entries()).reduce((function(e,n){var r,a=n[0],t=n[1];return o(o({},e),((r={})[a]=t,r))}),{})),navigate:F,getHref:U,back:O};(null==k?void 0:k.redirect)&&F({id:k.redirect});var x=(null==k?void 0:k.element)||(null===(r=null==k?void 0:k.render)||void 0===r?void 0:r.call(k));return e.createElement(i.Provider,{value:j},x)};export{l as Router,i as RouterContext,u as useRouter};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { MultiLanguagePage, RouterOptions, SingleLanguagePage } from '@ossy-se/router';
|
|
3
|
+
export declare const RouterContext: React.Context<{
|
|
4
|
+
pages: RouterPage[];
|
|
5
|
+
defaultLanguage: string | undefined;
|
|
6
|
+
supportedLanguages: string[];
|
|
7
|
+
}>;
|
|
8
|
+
export declare const useRouter: () => {
|
|
9
|
+
pages: RouterPage[];
|
|
10
|
+
defaultLanguage: string | undefined;
|
|
11
|
+
supportedLanguages: string[];
|
|
12
|
+
};
|
|
13
|
+
export declare const Router: <T extends RouterPage>({ url, pages, defaultLanguage, supportedLanguages }: RouterProps<T>) => React.JSX.Element;
|
|
14
|
+
export interface RouterProps<T extends RouterPage> extends RouterOptions<T> {
|
|
15
|
+
/**
|
|
16
|
+
* Render page associated with the given url.
|
|
17
|
+
* Used to render the page on server side where window.location is not available
|
|
18
|
+
* @type string
|
|
19
|
+
* @default undefined
|
|
20
|
+
*/
|
|
21
|
+
url?: string;
|
|
22
|
+
}
|
|
23
|
+
export type RouterPage = SingleLanguageRouterPage | MultiLanguageRouterPage;
|
|
24
|
+
export interface SingleLanguageRouterPage extends SingleLanguagePage {
|
|
25
|
+
/** If provided the page will redirect to the given page id
|
|
26
|
+
* @default undefined
|
|
27
|
+
* @type {string}
|
|
28
|
+
* @example { redirect: '@home'}
|
|
29
|
+
*/
|
|
30
|
+
redirect?: string;
|
|
31
|
+
element?: React.ReactNode;
|
|
32
|
+
render: () => React.ReactNode;
|
|
33
|
+
}
|
|
34
|
+
export interface MultiLanguageRouterPage extends MultiLanguagePage {
|
|
35
|
+
/** If provided the page will redirect to the given page id
|
|
36
|
+
* @default undefined
|
|
37
|
+
* @type {string}
|
|
38
|
+
* @example { redirect: '@home'}
|
|
39
|
+
*/
|
|
40
|
+
redirect?: string;
|
|
41
|
+
element?: React.ReactNode;
|
|
42
|
+
render: () => React.ReactNode;
|
|
43
|
+
}
|
|
44
|
+
export interface GetHrefRequest {
|
|
45
|
+
id?: string;
|
|
46
|
+
language?: string;
|
|
47
|
+
params?: Record<string, string>;
|
|
48
|
+
}
|
|
49
|
+
export interface GetHrefOfActivePageByLanguageRequest {
|
|
50
|
+
language: string;
|
|
51
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ossy/router-react",
|
|
3
|
+
"repository": "github:ossy-se/ossy",
|
|
4
|
+
"description": "React bindings for the @ossy-se/router",
|
|
5
|
+
"version": "0.0.1-beta.1",
|
|
6
|
+
"source": "src/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "build/cjs/index.js",
|
|
9
|
+
"module": "build/esm/index.js",
|
|
10
|
+
"author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "rollup -c rollup.config.js",
|
|
13
|
+
"test": ""
|
|
14
|
+
},
|
|
15
|
+
"browserslist": {
|
|
16
|
+
"production": [
|
|
17
|
+
">0.2%",
|
|
18
|
+
"not dead",
|
|
19
|
+
"not op_mini all"
|
|
20
|
+
],
|
|
21
|
+
"development": [
|
|
22
|
+
"last 1 chrome version",
|
|
23
|
+
"last 1 firefox version",
|
|
24
|
+
"last 1 safari version"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@babel/cli": "^7.21.5",
|
|
29
|
+
"@babel/core": "^7.26.0",
|
|
30
|
+
"@babel/eslint-parser": "^7.15.8",
|
|
31
|
+
"@babel/preset-env": "^7.26.0",
|
|
32
|
+
"@babel/preset-react": "^7.24.1",
|
|
33
|
+
"@babel/preset-typescript": "^7.26.0",
|
|
34
|
+
"@rollup/plugin-babel": "6.0.4",
|
|
35
|
+
"@rollup/plugin-commonjs": "^25.0.5",
|
|
36
|
+
"@rollup/plugin-node-resolve": "^15.3.0",
|
|
37
|
+
"@rollup/plugin-terser": "0.4.4",
|
|
38
|
+
"@rollup/plugin-typescript": "^11.1.5",
|
|
39
|
+
"@types/react": "^18.2.79",
|
|
40
|
+
"rollup": "^4.24.3",
|
|
41
|
+
"rollup-plugin-dts": "^6.1.0",
|
|
42
|
+
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
43
|
+
"rollup-plugin-postcss-modules": "^2.1.1",
|
|
44
|
+
"rollup-plugin-preserve-directives": "^0.4.0",
|
|
45
|
+
"tslib": "^2.8.1"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@ossy/router": "0.0.1-beta.1",
|
|
49
|
+
"react": "^19.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import babel from '@rollup/plugin-babel'
|
|
2
|
+
import { nodeResolve as resolveDependencies } from '@rollup/plugin-node-resolve'
|
|
3
|
+
import resolveCommonJsDependencies from '@rollup/plugin-commonjs'
|
|
4
|
+
import removeOwnPeerDependencies from 'rollup-plugin-peer-deps-external'
|
|
5
|
+
import minifyJS from '@rollup/plugin-terser'
|
|
6
|
+
import typescript from '@rollup/plugin-typescript'
|
|
7
|
+
import preserveDirectives from "rollup-plugin-preserve-directives"
|
|
8
|
+
|
|
9
|
+
export default [
|
|
10
|
+
{
|
|
11
|
+
input: 'src/router-react.tsx',
|
|
12
|
+
output: [
|
|
13
|
+
{
|
|
14
|
+
file: 'build/cjs/index.js',
|
|
15
|
+
format: 'cjs'
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
file: 'build/esm/index.js',
|
|
19
|
+
format: 'esm'
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
plugins: [
|
|
23
|
+
resolveCommonJsDependencies(),
|
|
24
|
+
resolveDependencies(),
|
|
25
|
+
removeOwnPeerDependencies(),
|
|
26
|
+
babel({
|
|
27
|
+
exclude: ['**/node_modules/**/*'],
|
|
28
|
+
presets: ['@babel/preset-react']
|
|
29
|
+
}),
|
|
30
|
+
minifyJS(),
|
|
31
|
+
preserveDirectives(),
|
|
32
|
+
typescript({ tsconfig: "./tsconfig.json" }),
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
]
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { createContext, useContext, useCallback } from 'react'
|
|
3
|
+
import { MultiLanguagePage, Router as OssyRouter, RouterOptions, SingleLanguagePage, Page as _Page } from '@ossy-se/router'
|
|
4
|
+
|
|
5
|
+
export const RouterContext = createContext({
|
|
6
|
+
pages: [] as RouterPage[],
|
|
7
|
+
defaultLanguage: undefined as string | undefined,
|
|
8
|
+
supportedLanguages: [] as string[]
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const useRouter = () => useContext(RouterContext)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const appendSlash = (string: string) => string[string.length - 1] === '/'
|
|
15
|
+
? string : `${string}/`
|
|
16
|
+
|
|
17
|
+
const prependSlash = (string: string) => string[0] === '/'
|
|
18
|
+
? string : `/${string}`
|
|
19
|
+
|
|
20
|
+
const padWithSlash = (string = '') => appendSlash(prependSlash(string))
|
|
21
|
+
|
|
22
|
+
export const Router = <T extends RouterPage>({
|
|
23
|
+
url,
|
|
24
|
+
pages = [],
|
|
25
|
+
defaultLanguage = undefined,
|
|
26
|
+
supportedLanguages = []
|
|
27
|
+
}: RouterProps<T> ) => {
|
|
28
|
+
let href: string;
|
|
29
|
+
let pathname: string;
|
|
30
|
+
let hash: string;
|
|
31
|
+
let search: string;
|
|
32
|
+
|
|
33
|
+
if (url) {
|
|
34
|
+
const urlParts = url.match(/([^?#]*)(\?[^#]*)?(#.*)?/);
|
|
35
|
+
href = url;
|
|
36
|
+
pathname = urlParts?.[1] || '';
|
|
37
|
+
hash = urlParts?.[3] || '';
|
|
38
|
+
search = urlParts?.[2] || '';
|
|
39
|
+
} else if (typeof window !== 'undefined') {
|
|
40
|
+
href = window.location.href
|
|
41
|
+
pathname = window.location.pathname
|
|
42
|
+
hash = window.location.hash
|
|
43
|
+
search = hash?.includes('?') ? hash.split('?')[1] : window.location.search
|
|
44
|
+
} else {
|
|
45
|
+
return <></>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isMultiLanguage = supportedLanguages.length > 1 && !!defaultLanguage
|
|
49
|
+
const potentialLanguage = pathname.split('/')[1] || ''
|
|
50
|
+
const language = supportedLanguages.includes(potentialLanguage) ? potentialLanguage : defaultLanguage
|
|
51
|
+
const router = OssyRouter.of<T>({ pages, defaultLanguage, supportedLanguages })
|
|
52
|
+
const params = router.getParamsFromUrl(href)
|
|
53
|
+
const searchParams = new URLSearchParams(search)
|
|
54
|
+
|
|
55
|
+
let activePage: T = router.getPageByUrl(href) as T
|
|
56
|
+
|
|
57
|
+
if (!activePage) {
|
|
58
|
+
if (isMultiLanguage) {
|
|
59
|
+
if (pathname === '/') {
|
|
60
|
+
if (typeof window !== 'undefined') {
|
|
61
|
+
window.location.href = '/' + defaultLanguage
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
activePage = isMultiLanguage
|
|
67
|
+
? pages.find(page => padWithSlash((page.path as Record<string, string>)[(language as string)]) === '*') as T
|
|
68
|
+
: pages.find(page => padWithSlash(page.path as string) === '*') as T
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!activePage) {
|
|
72
|
+
console.warn('[RouterReact] No active page found for', pathname)
|
|
73
|
+
return <></>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @deprecated router do not support setting client side only search params.
|
|
78
|
+
* Use getHref with search params instead
|
|
79
|
+
*
|
|
80
|
+
* Note: That migh not be true though because we have the back() and navigate() functions....
|
|
81
|
+
*/
|
|
82
|
+
const setSearchParam = useCallback((name: string, value: any) => {
|
|
83
|
+
if (typeof window === 'undefined') return
|
|
84
|
+
window.history.pushState({}, '', `${pathname}?${searchParams.toString()}`)
|
|
85
|
+
const newSearchParams = new URLSearchParams(searchParams)
|
|
86
|
+
newSearchParams.set(name, value)
|
|
87
|
+
history.pushState({ }, "", `?${newSearchParams.toString()}`)
|
|
88
|
+
}, [searchParams])
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* takes either a string id or an object with id, params and language
|
|
92
|
+
* - params.id - id of the page to get the href for
|
|
93
|
+
* - param.language - language to use for the href, defaults to current language
|
|
94
|
+
* - param.params - params to use for the href, defaults to current params
|
|
95
|
+
* @param {string|object}
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
const getHref = useCallback((getHrefRequest: string | GetHrefRequest | GetHrefOfActivePageByLanguageRequest) => {
|
|
99
|
+
// Can be supplied from params, if not will be taken from active page
|
|
100
|
+
let _pageId: string;
|
|
101
|
+
let _params: Record<string, string> = {}
|
|
102
|
+
let _language: string | undefined;
|
|
103
|
+
|
|
104
|
+
// Can only be supplied from params, will not be taken from active page
|
|
105
|
+
let hash: string | undefined;
|
|
106
|
+
let search: string | undefined;
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if (typeof getHrefRequest === 'string') {
|
|
110
|
+
_pageId = getHrefRequest
|
|
111
|
+
_params = params
|
|
112
|
+
_language = language
|
|
113
|
+
} else if (isGetHrefOfActivePageByLanguageRequest(getHrefRequest)) {
|
|
114
|
+
_pageId = activePage.id
|
|
115
|
+
_params = params
|
|
116
|
+
_language = getHrefRequest.language
|
|
117
|
+
} else {
|
|
118
|
+
_pageId = getHrefRequest?.id || activePage.id
|
|
119
|
+
_params = getHrefRequest?.params || params
|
|
120
|
+
_language = getHrefRequest?.language || language
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (_pageId.includes('#')) {
|
|
124
|
+
const [_id, _hash] = _pageId.split('#')
|
|
125
|
+
_pageId = _id
|
|
126
|
+
hash = _hash
|
|
127
|
+
|
|
128
|
+
if (hash.includes('?')) {
|
|
129
|
+
const [_hash, _search] = _pageId.split('?')
|
|
130
|
+
hash = _hash
|
|
131
|
+
search = _search
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
} else if (_pageId.includes('?')) {
|
|
135
|
+
const [_id, _search] = _pageId.split('?')
|
|
136
|
+
_pageId = _id
|
|
137
|
+
search = _search
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let pathname = router.getPathname({
|
|
141
|
+
id: _pageId.replace('@', ''),
|
|
142
|
+
params: _params,
|
|
143
|
+
language: _language
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
if (hash) {
|
|
147
|
+
pathname = pathname + '#' + hash
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (search) {
|
|
151
|
+
pathname = pathname + '?' + search
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return pathname
|
|
155
|
+
}, [router, language])
|
|
156
|
+
|
|
157
|
+
const navigate = useCallback((navigationRequest: string | '@back' | GetHrefRequest | GetHrefOfActivePageByLanguageRequest) => {
|
|
158
|
+
if (typeof window === 'undefined') return
|
|
159
|
+
if (!navigationRequest) return
|
|
160
|
+
|
|
161
|
+
if (navigationRequest === '@back') {
|
|
162
|
+
window.history.back()
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const href = getHref(navigationRequest)
|
|
167
|
+
if (!href) return
|
|
168
|
+
window.location.href = href
|
|
169
|
+
|
|
170
|
+
}, [getHref])
|
|
171
|
+
|
|
172
|
+
const back = useCallback(() => {
|
|
173
|
+
if (typeof window === 'undefined') return
|
|
174
|
+
window.history.back()
|
|
175
|
+
}, [])
|
|
176
|
+
|
|
177
|
+
const context = {
|
|
178
|
+
href,
|
|
179
|
+
pages,
|
|
180
|
+
language,
|
|
181
|
+
defaultLanguage,
|
|
182
|
+
supportedLanguages,
|
|
183
|
+
params,
|
|
184
|
+
searchParams: {
|
|
185
|
+
...Array.from(searchParams.entries()).reduce((all, [key, value]) => ({ ...all, [key]: value }), {}),
|
|
186
|
+
// set: setSearchParam
|
|
187
|
+
},
|
|
188
|
+
navigate,
|
|
189
|
+
getHref,
|
|
190
|
+
back,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (activePage?.redirect) {
|
|
194
|
+
navigate({ id: activePage.redirect })
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const renderedPage = activePage?.element || activePage?.render?.()
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<RouterContext.Provider value={context}>
|
|
201
|
+
{ renderedPage }
|
|
202
|
+
</RouterContext.Provider>
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface RouterProps<T extends RouterPage> extends RouterOptions<T> {
|
|
208
|
+
/**
|
|
209
|
+
* Render page associated with the given url.
|
|
210
|
+
* Used to render the page on server side where window.location is not available
|
|
211
|
+
* @type string
|
|
212
|
+
* @default undefined
|
|
213
|
+
*/
|
|
214
|
+
url?: string;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export type RouterPage = SingleLanguageRouterPage | MultiLanguageRouterPage
|
|
218
|
+
|
|
219
|
+
export interface SingleLanguageRouterPage extends SingleLanguagePage {
|
|
220
|
+
/** If provided the page will redirect to the given page id
|
|
221
|
+
* @default undefined
|
|
222
|
+
* @type {string}
|
|
223
|
+
* @example { redirect: '@home'}
|
|
224
|
+
*/
|
|
225
|
+
redirect?: string;
|
|
226
|
+
element?: React.ReactNode;
|
|
227
|
+
render: () => React.ReactNode
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface MultiLanguageRouterPage extends MultiLanguagePage {
|
|
231
|
+
/** If provided the page will redirect to the given page id
|
|
232
|
+
* @default undefined
|
|
233
|
+
* @type {string}
|
|
234
|
+
* @example { redirect: '@home'}
|
|
235
|
+
*/
|
|
236
|
+
redirect?: string;
|
|
237
|
+
element?: React.ReactNode;
|
|
238
|
+
render: () => React.ReactNode
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface GetHrefRequest {
|
|
242
|
+
id?: string;
|
|
243
|
+
language?: string;
|
|
244
|
+
params?: Record<string, string>;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface GetHrefOfActivePageByLanguageRequest {
|
|
248
|
+
language: string;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function isGetHrefOfActivePageByLanguageRequest(request: any): request is GetHrefOfActivePageByLanguageRequest {
|
|
252
|
+
return !request.id && request.language !== undefined
|
|
253
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es5",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "node",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "react",
|
|
15
|
+
"incremental": false,
|
|
16
|
+
"declaration": true,
|
|
17
|
+
"declarationDir": "./build/esm"
|
|
18
|
+
},
|
|
19
|
+
"exclude": [
|
|
20
|
+
"build",
|
|
21
|
+
"node_modules",
|
|
22
|
+
"src/**/*.test.tsx",
|
|
23
|
+
"src/**/*.stories.tsx",
|
|
24
|
+
"rollup.config.js",
|
|
25
|
+
"src/data.mock.ts"
|
|
26
|
+
]
|
|
27
|
+
}
|