@equinor/fusion-framework-module-navigation 7.0.0-next.1 → 7.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/CHANGELOG.md +35 -8
- package/README.md +97 -614
- package/dist/esm/NavigationProvider.js +57 -23
- package/dist/esm/NavigationProvider.js.map +1 -1
- package/dist/esm/events.js +20 -3
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/index.js +23 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/BaseHistory.js +23 -6
- package/dist/esm/lib/BaseHistory.js.map +1 -1
- package/dist/esm/lib/BrowserHistoryStack.js +5 -6
- package/dist/esm/lib/BrowserHistoryStack.js.map +1 -1
- package/dist/esm/lib/MemoryHistory.js +13 -6
- package/dist/esm/lib/MemoryHistory.js.map +1 -1
- package/dist/esm/lib/MemoryStack.js +1 -0
- package/dist/esm/lib/MemoryStack.js.map +1 -1
- package/dist/esm/lib/create-history.js +7 -13
- package/dist/esm/lib/create-history.js.map +1 -1
- package/dist/esm/lib/index.js +9 -0
- package/dist/esm/lib/index.js.map +1 -1
- package/dist/esm/lib/state/history.flows.js +7 -12
- package/dist/esm/lib/state/history.flows.js.map +1 -1
- package/dist/esm/lib/state/history.reducer.js +8 -4
- package/dist/esm/lib/state/history.reducer.js.map +1 -1
- package/dist/esm/lib/state/history.state.js +9 -3
- package/dist/esm/lib/state/history.state.js.map +1 -1
- package/dist/esm/lib/utils/resolve-browser-location.js +2 -2
- package/dist/esm/lib/utils/resolve-browser-location.js.map +1 -1
- package/dist/esm/module.js +4 -4
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/NavigationConfigurator.interface.d.ts +25 -5
- package/dist/types/NavigationProvider.d.ts +57 -23
- package/dist/types/NavigationProvider.interface.d.ts +20 -7
- package/dist/types/events.d.ts +20 -3
- package/dist/types/index.d.ts +24 -1
- package/dist/types/lib/BaseHistory.d.ts +23 -6
- package/dist/types/lib/BrowserHistoryStack.d.ts +5 -6
- package/dist/types/lib/MemoryHistory.d.ts +5 -5
- package/dist/types/lib/create-history.d.ts +7 -13
- package/dist/types/lib/index.d.ts +9 -0
- package/dist/types/lib/state/history.flows.d.ts +6 -11
- package/dist/types/lib/state/history.reducer.d.ts +8 -4
- package/dist/types/lib/state/history.state.d.ts +9 -3
- package/dist/types/lib/types.d.ts +39 -8
- package/dist/types/version.d.ts +1 -1
- package/package.json +11 -12
- package/src/NavigationConfigurator.interface.ts +28 -5
- package/src/NavigationProvider.interface.ts +20 -7
- package/src/NavigationProvider.ts +57 -23
- package/src/events.ts +20 -3
- package/src/index.ts +25 -1
- package/src/lib/BaseHistory.ts +23 -6
- package/src/lib/BrowserHistoryStack.ts +5 -6
- package/src/lib/MemoryHistory.ts +13 -6
- package/src/lib/MemoryStack.ts +1 -0
- package/src/lib/create-history.ts +7 -13
- package/src/lib/index.ts +10 -0
- package/src/lib/state/history.flows.ts +7 -12
- package/src/lib/state/history.reducer.ts +8 -4
- package/src/lib/state/history.state.ts +9 -3
- package/src/lib/types.ts +39 -8
- package/src/lib/utils/resolve-browser-location.ts +2 -2
- package/src/module.ts +4 -4
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -1,694 +1,177 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @equinor/fusion-framework-module-navigation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Routing and navigation module for **Fusion Framework** with observable state management and automatic basename localization.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **Path localization** — consumers work with clean paths (`/users`) while the history stack receives full paths (`/apps/my-app/users`).
|
|
8
|
+
- **Observable state** — navigation state exposed as an RxJS observable (`state$`) with `shareReplay` semantics.
|
|
9
|
+
- **Multiple history types** — browser (pathname), hash (`#/path`), and memory (no URL changes).
|
|
10
|
+
- **Router compatibility** — `createRouter()` creates `@remix-run/router` instances wired to the framework history.
|
|
11
|
+
- **Navigation blocking** — intercept navigations with `history.block()` and optionally retry.
|
|
12
|
+
- **Telemetry & events** — dispatches `NavigateEvent` / `NavigatedEvent` and tracks actions via the telemetry module.
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
## Installation
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
2. **Observable State**: Exposes navigation state as an RxJS observable, enabling reactive programming patterns throughout your application
|
|
14
|
-
|
|
15
|
-
3. **History Abstraction**: Supports multiple history types (browser, hash, memory) with a unified API, making it easy to switch between deployment scenarios
|
|
16
|
-
|
|
17
|
-
4. **Router Compatibility**: Provides a `createRouter()` method that creates router instances compatible with industry-standard routers (Remix/React Router)
|
|
18
|
-
|
|
19
|
-
### How It Works
|
|
20
|
-
|
|
21
|
-
```mermaid
|
|
22
|
-
flowchart TD
|
|
23
|
-
A[Your Application<br/>Clean paths: /users] -->|push/replace| B[NavigationProvider]
|
|
24
|
-
B -->|Localizes paths<br/>Adds basename| C[History<br/>Browser/Hash/Memory]
|
|
25
|
-
C -->|Full paths| D[Router<br/>/apps/my-app/users]
|
|
26
|
-
|
|
27
|
-
B -->|state$ observable| A
|
|
28
|
-
B -->|path getter| A
|
|
29
|
-
C -->|Navigation events| B
|
|
16
|
+
```sh
|
|
17
|
+
pnpm add @equinor/fusion-framework-module-navigation
|
|
30
18
|
```
|
|
31
19
|
|
|
20
|
+
## Usage
|
|
32
21
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
## Quick Start
|
|
22
|
+
### Enable the module
|
|
36
23
|
|
|
37
24
|
```ts
|
|
38
|
-
import { enableNavigation
|
|
39
|
-
import { ModulesConfigurator } from '@equinor/fusion-framework-module';
|
|
40
|
-
|
|
41
|
-
const configurator = new ModulesConfigurator();
|
|
25
|
+
import { enableNavigation } from '@equinor/fusion-framework-module-navigation';
|
|
42
26
|
|
|
43
|
-
//
|
|
27
|
+
// Minimal — basename only
|
|
44
28
|
enableNavigation(configurator, '/apps/my-app');
|
|
45
29
|
|
|
46
|
-
// Advanced
|
|
30
|
+
// Advanced — full configuration
|
|
47
31
|
enableNavigation(configurator, {
|
|
48
32
|
configure: (config) => {
|
|
49
33
|
config.setBasename('/apps/my-app');
|
|
50
34
|
config.setHistory(createHistory('browser'));
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## Configuration
|
|
56
|
-
|
|
57
|
-
### Basic Configuration
|
|
58
|
-
|
|
59
|
-
The simplest way to enable navigation is by providing a basename string:
|
|
60
|
-
|
|
61
|
-
```ts
|
|
62
|
-
enableNavigation(configurator, '/apps/my-app');
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
This will:
|
|
66
|
-
- Use browser history (default)
|
|
67
|
-
- Configure basename for path localization
|
|
68
|
-
- Set up the navigation module with sensible defaults
|
|
69
|
-
|
|
70
|
-
### Advanced Configuration
|
|
71
|
-
|
|
72
|
-
For more control, use the configuration callback:
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
import { enableNavigation, createHistory } from '@equinor/fusion-framework-module-navigation';
|
|
76
|
-
|
|
77
|
-
enableNavigation(configurator, {
|
|
78
|
-
configure: (config) => {
|
|
79
|
-
// Set the basename (required when app is served from subdirectory)
|
|
80
|
-
config.setBasename('/apps/my-app');
|
|
81
|
-
|
|
82
|
-
// Set custom history type
|
|
83
|
-
config.setHistory(createHistory('browser')); // or 'hash', 'memory'
|
|
84
|
-
}
|
|
35
|
+
},
|
|
85
36
|
});
|
|
86
37
|
```
|
|
87
38
|
|
|
88
|
-
###
|
|
89
|
-
|
|
90
|
-
#### basename
|
|
91
|
-
|
|
92
|
-
The base pathname for your application. This is **required** when your app is served from a subdirectory.
|
|
93
|
-
|
|
94
|
-
**Why basename matters:**
|
|
95
|
-
- The URL pathname must start with the basename when your app is served from a subdirectory
|
|
96
|
-
- The navigation module automatically handles adding/removing the basename prefix
|
|
97
|
-
- Consumers receive localized paths (basename removed), while the underlying history receives full paths
|
|
98
|
-
|
|
99
|
-
**Example:**
|
|
100
|
-
```ts
|
|
101
|
-
// If your app is served at: https://example.com/apps/my-app
|
|
102
|
-
config.setBasename('/apps/my-app');
|
|
103
|
-
|
|
104
|
-
// When you navigate to '/users':
|
|
105
|
-
// - Consumer sees: '/users'
|
|
106
|
-
// - History receives: '/apps/my-app/users'
|
|
107
|
-
// - Browser URL: 'https://example.com/apps/my-app/users'
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
**When to set basename:**
|
|
111
|
-
- ✅ App is served from a subdirectory (e.g., `/apps/my-app`)
|
|
112
|
-
- ✅ App is part of a larger portal or micro-frontend architecture
|
|
113
|
-
- ❌ App is served from root (e.g., `https://example.com/`) - leave basename undefined
|
|
114
|
-
|
|
115
|
-
#### history
|
|
116
|
-
|
|
117
|
-
The history instance to use for navigation. The module provides three history types:
|
|
118
|
-
|
|
119
|
-
| Type | Description | Use Case |
|
|
120
|
-
|------|-------------|----------|
|
|
121
|
-
| `'browser'` | Uses browser History API with normal URLs | **Default** - Most web applications |
|
|
122
|
-
| `'hash'` | Uses hash-based routing (e.g., `#/path`) | Legacy browser support, static hosting without server config |
|
|
123
|
-
| `'memory'` | Uses in-memory history (no URL changes) | **Widget applications**, testing, SSR, or when URL shouldn't change |
|
|
39
|
+
### Programmatic navigation
|
|
124
40
|
|
|
125
|
-
If not provided, defaults to browser history.
|
|
126
|
-
|
|
127
|
-
**Example:**
|
|
128
41
|
```ts
|
|
129
|
-
|
|
130
|
-
config.setHistory(createHistory('browser'));
|
|
42
|
+
const navigation = framework.modules.navigation;
|
|
131
43
|
|
|
132
|
-
//
|
|
133
|
-
|
|
44
|
+
navigation.push('/users'); // adds history entry
|
|
45
|
+
navigation.replace('/login'); // replaces current entry
|
|
46
|
+
navigation.push('/users', { id: 1 }); // with state
|
|
134
47
|
|
|
135
|
-
//
|
|
136
|
-
config.setHistory(createHistory('memory'));
|
|
48
|
+
console.log(navigation.path.pathname); // '/users' — basename removed
|
|
137
49
|
```
|
|
138
50
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
### Browser History
|
|
142
|
-
|
|
143
|
-
Uses the browser's native History API with clean URLs. This is the default and recommended option for most applications.
|
|
51
|
+
### Observable navigation state
|
|
144
52
|
|
|
145
|
-
**Characteristics:**
|
|
146
|
-
- Clean URLs: `https://example.com/apps/my-app/users`
|
|
147
|
-
- Requires server configuration for client-side routing
|
|
148
|
-
- Full browser history support (back/forward buttons work)
|
|
149
|
-
- Best SEO and user experience
|
|
150
|
-
|
|
151
|
-
**When to use:**
|
|
152
|
-
- ✅ Modern web applications
|
|
153
|
-
- ✅ Server can be configured for client-side routing
|
|
154
|
-
- ✅ You want clean, SEO-friendly URLs
|
|
155
|
-
|
|
156
|
-
**Example:**
|
|
157
53
|
```ts
|
|
158
|
-
|
|
159
|
-
// Navigates to: https://example.com/apps/my-app/users
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
**Server Configuration:**
|
|
163
|
-
For browser history to work, your server must serve `index.html` for all routes. Example nginx config:
|
|
164
|
-
|
|
165
|
-
```nginx
|
|
166
|
-
location / {
|
|
167
|
-
try_files $uri $uri/ /index.html;
|
|
168
|
-
}
|
|
169
|
-
```
|
|
54
|
+
import { filter } from 'rxjs';
|
|
170
55
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
Uses hash-based routing where the path is stored in the URL hash fragment.
|
|
174
|
-
|
|
175
|
-
**Characteristics:**
|
|
176
|
-
- URLs include hash: `https://example.com/#/apps/my-app/users`
|
|
177
|
-
- No server configuration needed
|
|
178
|
-
- Works with static hosting
|
|
179
|
-
- Slightly less clean URLs
|
|
180
|
-
|
|
181
|
-
**When to use:**
|
|
182
|
-
- ✅ Static hosting without server configuration
|
|
183
|
-
- ✅ Legacy browser support required
|
|
184
|
-
- ✅ You can't configure server routing rules
|
|
185
|
-
- ❌ SEO is important (hash fragments are not crawled)
|
|
186
|
-
|
|
187
|
-
**Example:**
|
|
188
|
-
```ts
|
|
189
|
-
config.setHistory(createHistory('hash'));
|
|
190
|
-
// Navigates to: https://example.com/#/apps/my-app/users
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
### Memory History
|
|
194
|
-
|
|
195
|
-
Uses in-memory history storage without modifying the browser URL.
|
|
196
|
-
|
|
197
|
-
**Characteristics:**
|
|
198
|
-
- No URL changes (URL stays the same)
|
|
199
|
-
- History stored in memory only
|
|
200
|
-
- Perfect for testing
|
|
201
|
-
- Ideal for widget/embedded applications
|
|
202
|
-
|
|
203
|
-
**When to use:**
|
|
204
|
-
- ✅ **Widget applications** - When serving an application as a widget embedded in another page
|
|
205
|
-
- ✅ Testing (unit tests, integration tests)
|
|
206
|
-
- ✅ SSR scenarios where URL shouldn't change
|
|
207
|
-
- ✅ Embedded applications where URL changes are not desired
|
|
208
|
-
- ❌ Production web applications (use browser or hash instead)
|
|
209
|
-
|
|
210
|
-
**Example:**
|
|
211
|
-
```ts
|
|
212
|
-
// Widget application - history in memory, no URL changes
|
|
213
|
-
config.setHistory(createHistory('memory'));
|
|
214
|
-
// No URL changes, history managed in memory
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
## Usage
|
|
218
|
-
|
|
219
|
-
### Accessing Navigation Provider
|
|
220
|
-
|
|
221
|
-
Once configured, access the navigation provider through the framework:
|
|
222
|
-
|
|
223
|
-
```ts
|
|
224
|
-
import { useFramework } from '@equinor/fusion-framework-react';
|
|
225
|
-
|
|
226
|
-
function MyComponent() {
|
|
227
|
-
const framework = useFramework();
|
|
228
|
-
const navigation = framework.navigation;
|
|
229
|
-
|
|
230
|
-
// Get current path (localized, basename removed)
|
|
231
|
-
const currentPath = navigation.path;
|
|
232
|
-
console.log(currentPath.pathname); // '/users' (not '/apps/my-app/users')
|
|
233
|
-
|
|
234
|
-
// Navigate programmatically
|
|
235
|
-
navigation.push('/users');
|
|
236
|
-
navigation.replace('/dashboard');
|
|
237
|
-
}
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
### Navigation Methods
|
|
241
|
-
|
|
242
|
-
#### push(to, state?)
|
|
243
|
-
|
|
244
|
-
Pushes a new navigation entry onto the history stack. Adds a new entry to the browser history, so the back button will return to the previous location.
|
|
245
|
-
|
|
246
|
-
```ts
|
|
247
|
-
// Navigate to a new path
|
|
248
|
-
navigation.push('/users');
|
|
249
|
-
|
|
250
|
-
// Navigate with state data
|
|
251
|
-
navigation.push('/users', { userId: 123 });
|
|
252
|
-
|
|
253
|
-
// Navigate with Path object (includes search/hash)
|
|
254
|
-
navigation.push({
|
|
255
|
-
pathname: '/users',
|
|
256
|
-
search: '?sort=name',
|
|
257
|
-
hash: '#section'
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// Use current path (no argument)
|
|
261
|
-
navigation.push(); // Pushes current path again
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
#### replace(to, state?)
|
|
265
|
-
|
|
266
|
-
Replaces the current history entry. Unlike `push`, this doesn't add a new entry to history, so the back button won't return to the previous location.
|
|
267
|
-
|
|
268
|
-
```ts
|
|
269
|
-
// Replace current location
|
|
270
|
-
navigation.replace('/login');
|
|
271
|
-
|
|
272
|
-
// Replace with state
|
|
273
|
-
navigation.replace('/login', { from: '/dashboard' });
|
|
274
|
-
|
|
275
|
-
// Replace with Path object
|
|
276
|
-
navigation.replace({
|
|
277
|
-
pathname: '/login',
|
|
278
|
-
search: '?redirect=/dashboard'
|
|
56
|
+
navigation.state$.subscribe(({ action, location }) => {
|
|
57
|
+
console.log(action, location.pathname); // 'PUSH' '/users'
|
|
279
58
|
});
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
**When to use `replace` vs `push`:**
|
|
283
|
-
- Use `push` when you want to add to history (normal navigation)
|
|
284
|
-
- Use `replace` when you don't want to add to history (redirects, auth flows)
|
|
285
59
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
```ts
|
|
291
|
-
// Create href for a path
|
|
292
|
-
const href = navigation.createHref('/users');
|
|
293
|
-
console.log(href); // '/apps/my-app/users'
|
|
294
|
-
|
|
295
|
-
// Create href for current path
|
|
296
|
-
const currentHref = navigation.createHref();
|
|
297
|
-
console.log(currentHref); // '/apps/my-app/users' (current path)
|
|
298
|
-
|
|
299
|
-
// Create href with Path object
|
|
300
|
-
const hrefWithSearch = navigation.createHref({
|
|
301
|
-
pathname: '/users',
|
|
302
|
-
search: '?id=1',
|
|
303
|
-
hash: '#section'
|
|
60
|
+
navigation.state$.pipe(
|
|
61
|
+
filter(({ action }) => action === 'POP'),
|
|
62
|
+
).subscribe(({ location }) => {
|
|
63
|
+
console.log('Back/forward to', location.pathname);
|
|
304
64
|
});
|
|
305
|
-
console.log(hrefWithSearch); // '/apps/my-app/users?id=1#section'
|
|
306
65
|
```
|
|
307
66
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
Creates a full URL object with basename included. Similar to `createHref` but returns a URL object.
|
|
67
|
+
### Create href / URL
|
|
311
68
|
|
|
312
69
|
```ts
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
70
|
+
// basename = '/apps/my-app'
|
|
71
|
+
navigation.createHref('/users');
|
|
72
|
+
// → '/apps/my-app/users'
|
|
316
73
|
|
|
317
|
-
|
|
318
|
-
|
|
74
|
+
navigation.createURL('/users');
|
|
75
|
+
// → URL { pathname: '/apps/my-app/users', ... }
|
|
319
76
|
```
|
|
320
77
|
|
|
321
|
-
|
|
78
|
+
### Router integration (legacy)
|
|
322
79
|
|
|
323
|
-
|
|
80
|
+
> **Note:** Prefer `@equinor/fusion-framework-react-router` for new applications.
|
|
324
81
|
|
|
325
82
|
```ts
|
|
326
|
-
import {
|
|
83
|
+
import type { AgnosticRouteObject } from '@remix-run/router';
|
|
327
84
|
|
|
328
85
|
const routes: AgnosticRouteObject[] = [
|
|
329
86
|
{ path: '/', element: <Home /> },
|
|
330
|
-
{ path: '/users', element: <
|
|
331
|
-
{ path: '/users/:id', element: <UserDetail /> }
|
|
87
|
+
{ path: '/users/:id', element: <UserDetail /> },
|
|
332
88
|
];
|
|
333
89
|
|
|
334
90
|
const router = navigation.createRouter(routes);
|
|
335
|
-
|
|
336
|
-
// Use the router with your routing library
|
|
337
|
-
router.subscribe((state) => {
|
|
338
|
-
// Handle route state
|
|
339
|
-
});
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
> [!CAUTION]
|
|
343
|
-
> **Important**: Always use `navigation.createRouter()` instead of creating router instances directly. Creating routers directly will create their own history wrapper that is **NOT** connected to the framework, causing unpredictable behavior and broken links.
|
|
344
|
-
|
|
345
|
-
## Path Localization
|
|
346
|
-
|
|
347
|
-
Path localization is a core feature of the Navigation Module. It ensures that:
|
|
348
|
-
|
|
349
|
-
1. **Consumers work with clean paths**: Your application code uses paths like `/users` without worrying about basename
|
|
350
|
-
2. **History receives full paths**: The underlying history gets paths like `/apps/my-app/users` with basename included
|
|
351
|
-
3. **Automatic translation**: The module automatically adds/removes basename as needed
|
|
352
|
-
|
|
353
|
-
### How Path Localization Works
|
|
354
|
-
|
|
355
|
-
```ts
|
|
356
|
-
// Configuration
|
|
357
|
-
config.setBasename('/apps/my-app');
|
|
358
|
-
|
|
359
|
-
// Consumer code (your application)
|
|
360
|
-
navigation.push('/users');
|
|
361
|
-
// → NavigationProvider adds basename
|
|
362
|
-
// → History receives: '/apps/my-app/users'
|
|
363
|
-
// → Browser URL: 'https://example.com/apps/my-app/users'
|
|
364
|
-
|
|
365
|
-
// Reading path (consumer receives localized version)
|
|
366
|
-
console.log(navigation.path.pathname); // '/users' (basename removed)
|
|
367
|
-
console.log(history.location.pathname); // '/apps/my-app/users' (full path)
|
|
368
|
-
|
|
369
|
-
// Observable state (localized paths)
|
|
370
|
-
navigation.state$.subscribe(({ location }) => {
|
|
371
|
-
console.log(location.pathname); // '/users' (basename removed)
|
|
372
|
-
});
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
### Path Localization Rules
|
|
376
|
-
|
|
377
|
-
1. **When pushing/replacing**: Paths are automatically prefixed with basename
|
|
378
|
-
2. **When reading paths**: The `path` getter returns localized paths (basename removed)
|
|
379
|
-
3. **In state$ observable**: All emitted paths are localized
|
|
380
|
-
4. **In createHref/createURL**: Basename is automatically included
|
|
381
|
-
|
|
382
|
-
### Path Localization Examples
|
|
383
|
-
|
|
384
|
-
```ts
|
|
385
|
-
// Example: Basename is '/apps/my-app'
|
|
386
|
-
|
|
387
|
-
// Push navigation
|
|
388
|
-
navigation.push('/users');
|
|
389
|
-
// → History: '/apps/my-app/users'
|
|
390
|
-
// → navigation.path: '/users'
|
|
391
|
-
|
|
392
|
-
// Push with search and hash
|
|
393
|
-
navigation.push('/users?id=1#section');
|
|
394
|
-
// → History: '/apps/my-app/users?id=1#section'
|
|
395
|
-
// → navigation.path: '/users?id=1#section'
|
|
396
|
-
|
|
397
|
-
// Push root path
|
|
398
|
-
navigation.push('/');
|
|
399
|
-
// → History: '/apps/my-app'
|
|
400
|
-
// → navigation.path: '/' or ''
|
|
401
|
-
|
|
402
|
-
// Create href
|
|
403
|
-
const href = navigation.createHref('/users');
|
|
404
|
-
// → Returns: '/apps/my-app/users'
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
## Observable Navigation State
|
|
408
|
-
|
|
409
|
-
The navigation module provides an observable stream of navigation updates using RxJS:
|
|
410
|
-
|
|
411
|
-
```ts
|
|
412
|
-
import { map, filter } from 'rxjs';
|
|
413
|
-
|
|
414
|
-
// Subscribe to all navigation changes
|
|
415
|
-
navigation.state$.subscribe(({ action, location }) => {
|
|
416
|
-
console.log('Action:', action); // 'PUSH', 'REPLACE', or 'POP'
|
|
417
|
-
console.log('Location:', location.pathname); // Localized path
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
// Filter for specific actions
|
|
421
|
-
navigation.state$.pipe(
|
|
422
|
-
filter(({ action }) => action === 'POP')
|
|
423
|
-
).subscribe(({ location }) => {
|
|
424
|
-
console.log('User navigated back to:', location.pathname);
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
// Map to pathname only
|
|
428
|
-
navigation.state$.pipe(
|
|
429
|
-
map(({ location }) => location.pathname)
|
|
430
|
-
).subscribe(pathname => {
|
|
431
|
-
console.log('Current path:', pathname);
|
|
432
|
-
});
|
|
433
91
|
```
|
|
434
92
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
- **Shareable**: Multiple subscribers share the same observable (uses `shareReplay`)
|
|
438
|
-
- **Replays last value**: Late subscribers immediately receive the last navigation state
|
|
439
|
-
- **Localized paths**: All emitted paths have basename removed
|
|
440
|
-
- **Filtered by basename**: Only paths within the basename scope are emitted
|
|
93
|
+
> Always use `navigation.createRouter()` instead of `createBrowserRouter()` directly—creating a router outside the provider breaks basename handling and state synchronisation.
|
|
441
94
|
|
|
442
|
-
|
|
95
|
+
### History types
|
|
443
96
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
// Type-safe navigation usage
|
|
450
|
-
function navigateToUsers(navigation: INavigationProvider) {
|
|
451
|
-
navigation.push('/users'); // ✅ Type-safe
|
|
452
|
-
// navigation.push(123); // ❌ Type error
|
|
453
|
-
}
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
**Available types:**
|
|
457
|
-
- `INavigationProvider` - Navigation provider interface
|
|
458
|
-
- `INavigationConfigurator` - Configuration interface (for helpers)
|
|
459
|
-
- `History` - History instance type
|
|
460
|
-
- `Path` - Path object type
|
|
461
|
-
- `To` - Navigation destination type
|
|
462
|
-
|
|
463
|
-
## Examples
|
|
464
|
-
|
|
465
|
-
### Complete Application Setup
|
|
466
|
-
|
|
467
|
-
```ts
|
|
468
|
-
import { enableNavigation, createHistory } from '@equinor/fusion-framework-module-navigation';
|
|
469
|
-
import { ModulesConfigurator } from '@equinor/fusion-framework-module';
|
|
470
|
-
|
|
471
|
-
export const configure = async (configurator: ModulesConfigurator) => {
|
|
472
|
-
enableNavigation(configurator, {
|
|
473
|
-
configure: (config) => {
|
|
474
|
-
// Set basename if app is served from subdirectory
|
|
475
|
-
config.setBasename('/apps/my-app');
|
|
476
|
-
|
|
477
|
-
// Use browser history (default)
|
|
478
|
-
config.setHistory(createHistory('browser'));
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
};
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
### React Component Usage
|
|
485
|
-
|
|
486
|
-
```tsx
|
|
487
|
-
import { useEffect, useState } from 'react';
|
|
488
|
-
|
|
489
|
-
function NavigationExample() {
|
|
490
|
-
const navigation = useModule('navigation')
|
|
491
|
-
const [currentPath, setCurrentPath] = useState(navigation.path.pathname);
|
|
492
|
-
|
|
493
|
-
useEffect(() => {
|
|
494
|
-
const subscription = navigation.state$.subscribe(({ location }) => {
|
|
495
|
-
setCurrentPath(location.pathname);
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
return () => subscription.unsubscribe();
|
|
499
|
-
}, [navigation]);
|
|
500
|
-
|
|
501
|
-
return (
|
|
502
|
-
<div>
|
|
503
|
-
<p>Current path: {currentPath}</p>
|
|
504
|
-
<button onClick={() => navigation.push('/users')}>
|
|
505
|
-
Go to Users
|
|
506
|
-
</button>
|
|
507
|
-
<button onClick={() => navigation.replace('/dashboard')}>
|
|
508
|
-
Go to Dashboard
|
|
509
|
-
</button>
|
|
510
|
-
</div>
|
|
511
|
-
);
|
|
512
|
-
}
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
### Router Integration
|
|
516
|
-
|
|
517
|
-
```tsx
|
|
518
|
-
import { useFramework } from '@equinor/fusion-framework-react';
|
|
519
|
-
import { RouterProvider } from '@remix-run/react';
|
|
520
|
-
|
|
521
|
-
function App() {
|
|
522
|
-
const framework = useFramework();
|
|
523
|
-
const navigation = framework.navigation;
|
|
524
|
-
|
|
525
|
-
const routes = [
|
|
526
|
-
{ path: '/', element: <Home /> },
|
|
527
|
-
{ path: '/users', element: <Users /> },
|
|
528
|
-
{ path: '/users/:id', element: <UserDetail /> }
|
|
529
|
-
];
|
|
530
|
-
|
|
531
|
-
const router = navigation.createRouter(routes);
|
|
532
|
-
|
|
533
|
-
return <RouterProvider router={router} />;
|
|
534
|
-
}
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
### Testing with Memory History
|
|
97
|
+
| Factory argument | Class | Description |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `'browser'` | `BrowserHistory` | Pathname-based routing via the History API. **Default.** |
|
|
100
|
+
| `'hash'` | `BrowserHistory` (hash stack) | Hash-fragment routing (`#/path`). No server config needed. |
|
|
101
|
+
| `'memory'` | `MemoryHistory` | In-memory history. Ideal for widgets, tests, and SSR. |
|
|
538
102
|
|
|
539
103
|
```ts
|
|
540
104
|
import { createHistory } from '@equinor/fusion-framework-module-navigation';
|
|
541
|
-
import { enableNavigation } from '@equinor/fusion-framework-module-navigation';
|
|
542
|
-
|
|
543
|
-
// In tests, use memory history
|
|
544
|
-
const configurator = new ModulesConfigurator();
|
|
545
|
-
enableNavigation(configurator, {
|
|
546
|
-
configure: (config) => {
|
|
547
|
-
config.setHistory(createHistory('memory'));
|
|
548
|
-
}
|
|
549
|
-
});
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
## Important Notes
|
|
553
|
-
|
|
554
|
-
### ⚠️ Router Integration
|
|
555
|
-
|
|
556
|
-
> [!CAUTION]
|
|
557
|
-
> **Never create router instances directly** in Fusion Framework applications. Always use `navigation.createRouter()` to ensure proper integration with the framework's navigation system.
|
|
558
105
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
- Links and navigation will behave unpredictably
|
|
563
|
-
- State synchronization issues will occur
|
|
564
|
-
|
|
565
|
-
**✅ Correct:**
|
|
566
|
-
```ts
|
|
567
|
-
const router = navigation.createRouter(routes);
|
|
568
|
-
```
|
|
569
|
-
|
|
570
|
-
**❌ Incorrect:**
|
|
571
|
-
```ts
|
|
572
|
-
import { createBrowserRouter } from '@remix-run/router';
|
|
573
|
-
const router = createBrowserRouter(routes); // Don't do this!
|
|
106
|
+
const browserHistory = createHistory('browser');
|
|
107
|
+
const hashHistory = createHistory('hash');
|
|
108
|
+
const memoryHistory = createHistory('memory');
|
|
574
109
|
```
|
|
575
110
|
|
|
576
|
-
### Navigation
|
|
111
|
+
### Navigation blocking
|
|
577
112
|
|
|
578
|
-
The `history.block()` method is available for intercepting navigation attempts. It uses a callback-based approach where the blocker receives a transition object with a `retry()` method.
|
|
579
|
-
|
|
580
|
-
**Important:** You must unblock before retrying. The block must be removed before the navigation can proceed.
|
|
581
|
-
|
|
582
|
-
**Example:**
|
|
583
113
|
```ts
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
// Show confirmation dialog
|
|
590
|
-
showConfirmationDialog(() => {
|
|
591
|
-
unblock(); // Remove the block first
|
|
592
|
-
transition.retry(); // Then allow navigation to proceed
|
|
114
|
+
const unblock = navigation.history.block((transition) => {
|
|
115
|
+
if (hasUnsavedChanges) {
|
|
116
|
+
showConfirmDialog(() => {
|
|
117
|
+
unblock(); // remove blocker first
|
|
118
|
+
transition.retry(); // then retry navigation
|
|
593
119
|
});
|
|
594
120
|
} else {
|
|
595
|
-
unblock();
|
|
596
|
-
transition.retry();
|
|
121
|
+
unblock();
|
|
122
|
+
transition.retry();
|
|
597
123
|
}
|
|
598
124
|
});
|
|
599
|
-
|
|
600
|
-
// Clean up when done
|
|
601
|
-
unblock();
|
|
602
125
|
```
|
|
603
126
|
|
|
604
|
-
### Path Localization Best Practices
|
|
605
|
-
|
|
606
|
-
1. **Always use the provider's methods**: Use `navigation.push()` instead of directly calling `history.push()`
|
|
607
|
-
2. **Read paths from the provider**: Use `navigation.path` instead of `history.location` for consumer-facing code
|
|
608
|
-
3. **Subscribe to state$**: Use `navigation.state$` instead of listening to history events directly
|
|
609
|
-
4. **Create routers properly**: Always use `navigation.createRouter()` for router integration
|
|
610
|
-
|
|
611
|
-
### Basename Configuration
|
|
612
|
-
|
|
613
|
-
- **Required when**: App is served from a subdirectory
|
|
614
|
-
- **Not required when**: App is served from root (`/`)
|
|
615
|
-
- **Must match**: The URL path prefix where your app is served
|
|
616
|
-
- **Example**: If app is at `https://example.com/apps/my-app`, basename should be `/apps/my-app`
|
|
617
|
-
|
|
618
127
|
## API Reference
|
|
619
128
|
|
|
620
|
-
###
|
|
129
|
+
### Functions
|
|
621
130
|
|
|
622
|
-
|
|
131
|
+
| Export | Description |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `enableNavigation(configurator, opts?)` | Registers the navigation module on a configurator. |
|
|
134
|
+
| `createHistory(type, ...args)` | Factory for `BrowserHistory`, hash-based `BrowserHistory`, or `MemoryHistory`. |
|
|
623
135
|
|
|
624
|
-
|
|
625
|
-
enableNavigation(
|
|
626
|
-
configurator: IModulesConfigurator,
|
|
627
|
-
basenameOrOptions?: string | {
|
|
628
|
-
configure: (config: NavigationConfigurator, ref?: unknown) => void
|
|
629
|
-
}
|
|
630
|
-
): void
|
|
631
|
-
```
|
|
136
|
+
### Classes
|
|
632
137
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
138
|
+
| Export | Description |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `NavigationProvider` | Module provider — manages state, localization, and lifecycle. |
|
|
141
|
+
| `NavigationConfigurator` | Fluent config builder with Zod validation. |
|
|
142
|
+
| `BrowserHistory` | History backed by browser `pushState` / `replaceState`. |
|
|
143
|
+
| `MemoryHistory` | History backed by in-memory storage. |
|
|
636
144
|
|
|
637
|
-
|
|
638
|
-
```ts
|
|
639
|
-
// Simple usage
|
|
640
|
-
enableNavigation(configurator, '/apps/my-app');
|
|
145
|
+
### Interfaces & Types
|
|
641
146
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
147
|
+
| Export | Description |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `INavigationProvider` | Public contract for the navigation provider. |
|
|
150
|
+
| `INavigationConfigurator` | Configuration shape (`basename`, `history`, `telemetry`, `eventProvider`). |
|
|
151
|
+
| `History` | History instance contract (observable state + navigation methods). |
|
|
152
|
+
| `Path` | `{ pathname, search, hash }` |
|
|
153
|
+
| `Location` | `Path` extended with `state` and `key`. |
|
|
154
|
+
| `To` | `string \| Partial<Path>` — target for navigation operations. |
|
|
155
|
+
| `Action` | Enum: `Pop`, `Push`, `Replace`. |
|
|
156
|
+
| `NavigationUpdate` | `{ delta, action, location }` emitted by `state$`. |
|
|
650
157
|
|
|
651
|
-
###
|
|
158
|
+
### Events
|
|
652
159
|
|
|
653
|
-
|
|
160
|
+
| Event | When | Cancelable |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `NavigateEvent` (`onNavigate`) | Before navigation | Yes |
|
|
163
|
+
| `NavigatedEvent` (`onNavigated`) | After navigation | No |
|
|
654
164
|
|
|
655
|
-
|
|
656
|
-
createHistory(type?: 'browser' | 'hash' | 'memory'): History
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
**Parameters:**
|
|
660
|
-
- `type`: The type of history to create (defaults to `'browser'`)
|
|
661
|
-
|
|
662
|
-
**Returns:**
|
|
663
|
-
- A `History` instance for navigation management
|
|
664
|
-
|
|
665
|
-
**Examples:**
|
|
666
|
-
```ts
|
|
667
|
-
const browserHistory = createHistory('browser');
|
|
668
|
-
const hashHistory = createHistory('hash');
|
|
669
|
-
const memoryHistory = createHistory('memory');
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
### NavigationProvider
|
|
673
|
-
|
|
674
|
-
The main provider class that manages navigation state and provides navigation methods.
|
|
675
|
-
|
|
676
|
-
**Properties:**
|
|
677
|
-
- `state$`: Observable stream of navigation updates
|
|
678
|
-
- `path`: Current localized path (basename removed)
|
|
679
|
-
- `history`: The underlying history instance
|
|
680
|
-
- `navigator`: (deprecated) Alias for `history`
|
|
165
|
+
## Configuration
|
|
681
166
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
- `dispose()`: Clean up resources
|
|
167
|
+
| Option | Type | Default | Description |
|
|
168
|
+
|---|---|---|---|
|
|
169
|
+
| `basename` | `string` | `undefined` | URL path prefix stripped from / prepended to consumer paths. |
|
|
170
|
+
| `history` | `History` | Browser history (or memory in Node) | History instance created via `createHistory()`. |
|
|
171
|
+
| `telemetry` | `ITelemetryProvider` | Auto-resolved | Tracks navigation events and errors. |
|
|
172
|
+
| `eventProvider` | `IEventModuleProvider` | Auto-resolved | Dispatches `onNavigate` / `onNavigated` events. |
|
|
689
173
|
|
|
690
174
|
## See Also
|
|
691
175
|
|
|
692
|
-
- [
|
|
693
|
-
- [Navigation
|
|
694
|
-
- [RxJS Documentation](https://rxjs.dev/) (for observable patterns)
|
|
176
|
+
- [`@equinor/fusion-framework-react-router`](https://github.com/equinor/fusion-framework/tree/main/packages/react/router) — React Router integration
|
|
177
|
+
- [Navigation Cookbook](https://github.com/equinor/fusion-framework/tree/main/cookbooks/app-react-router) — Example application with routing
|