ui5-lib-guard-router 1.0.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/LICENSE +21 -0
- package/README.md +382 -0
- package/dist/index.d.ts +27 -0
- package/dist/resources/ui5/guard/router/.library +17 -0
- package/dist/resources/ui5/guard/router/Router-dbg.js +431 -0
- package/dist/resources/ui5/guard/router/Router-dbg.js.map +1 -0
- package/dist/resources/ui5/guard/router/Router.d.ts +28 -0
- package/dist/resources/ui5/guard/router/Router.d.ts.map +1 -0
- package/dist/resources/ui5/guard/router/Router.js +2 -0
- package/dist/resources/ui5/guard/router/Router.js.map +1 -0
- package/dist/resources/ui5/guard/router/library-dbg.js +17 -0
- package/dist/resources/ui5/guard/router/library-dbg.js.map +1 -0
- package/dist/resources/ui5/guard/router/library-preload.js +10 -0
- package/dist/resources/ui5/guard/router/library-preload.js.map +1 -0
- package/dist/resources/ui5/guard/router/library.d.ts +7 -0
- package/dist/resources/ui5/guard/router/library.d.ts.map +1 -0
- package/dist/resources/ui5/guard/router/library.js +2 -0
- package/dist/resources/ui5/guard/router/library.js.map +1 -0
- package/dist/resources/ui5/guard/router/manifest.json +33 -0
- package/dist/resources/ui5/guard/router/types-dbg.js +2 -0
- package/dist/resources/ui5/guard/router/types-dbg.js.map +1 -0
- package/dist/resources/ui5/guard/router/types.d.ts +118 -0
- package/dist/resources/ui5/guard/router/types.d.ts.map +1 -0
- package/dist/resources/ui5/guard/router/types.js +2 -0
- package/dist/resources/ui5/guard/router/types.js.map +1 -0
- package/package.json +52 -0
- package/src/.library +17 -0
- package/src/Router.ts +540 -0
- package/src/library.ts +17 -0
- package/src/manifest.json +33 -0
- package/src/types.ts +136 -0
- package/tsconfig.json +13 -0
- package/ui5.yaml +24 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Marco
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# ui5-lib-guard-router
|
|
2
|
+
|
|
3
|
+
Drop-in replacement for `sap.m.routing.Router` that intercepts navigation **before** route matching, target loading, or view creation — preventing flashes of unauthorized content and polluted browser history.
|
|
4
|
+
|
|
5
|
+
> Born from [SAP/openui5#3411](https://github.com/SAP/openui5/issues/3411), an open request since 2021 for native navigation guard support in UI5.
|
|
6
|
+
>
|
|
7
|
+
> **Related resources**:
|
|
8
|
+
>
|
|
9
|
+
> - [Stack Overflow: Preventing router from navigating](https://stackoverflow.com/questions/29165700/preventing-router-from-navigating/29167292#29167292) (native NavContainer `navigate` event, sync-only, fires after route match)
|
|
10
|
+
> - [Research: Native NavContainer navigate event](../../docs/research-native-router-navigate-event.md) (detailed comparison with this library)
|
|
11
|
+
|
|
12
|
+
> [!WARNING]
|
|
13
|
+
> This library is **experimental**. It is not battle-tested in production environments, and the API may change without notice. If you choose to consume it, you do so at your own risk — make sure to pin your version and review changes before upgrading.
|
|
14
|
+
|
|
15
|
+
## Why
|
|
16
|
+
|
|
17
|
+
UI5's router has no way to block or redirect navigation before views render. The usual workaround — scattering guard logic across `attachPatternMatched` callbacks — causes flashes of unauthorized content, polluted browser history, and scattered guard logic across controllers.
|
|
18
|
+
|
|
19
|
+
This library solves all three by intercepting at the router level, before any route matching begins.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install ui5-lib-guard-router
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
**1. Add the library dependency and set the router class in your `manifest.json`:**
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"sap.ui5": {
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"libs": {
|
|
36
|
+
"ui5.guard.router": {}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"routing": {
|
|
40
|
+
"config": {
|
|
41
|
+
"routerClass": "ui5.guard.router.Router"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
All existing routes, targets, and navigation calls continue to work unchanged.
|
|
49
|
+
|
|
50
|
+
**2. Register guards in your Component:**
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import UIComponent from "sap/ui/core/UIComponent";
|
|
54
|
+
import type { GuardRouter } from "ui5/guard/router/types";
|
|
55
|
+
|
|
56
|
+
export default class Component extends UIComponent {
|
|
57
|
+
static metadata = {
|
|
58
|
+
manifest: "json",
|
|
59
|
+
interfaces: ["sap.ui.core.IAsyncContentCreation"],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
init(): void {
|
|
63
|
+
super.init();
|
|
64
|
+
const router = this.getRouter() as unknown as GuardRouter;
|
|
65
|
+
|
|
66
|
+
// Route-specific guard — redirect when not logged in
|
|
67
|
+
router.addRouteGuard("protected", (context) => {
|
|
68
|
+
return isLoggedIn() ? true : "home";
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Global guard — runs for every navigation
|
|
72
|
+
router.addGuard((context) => {
|
|
73
|
+
if (context.toRoute === "admin" && !isAdmin()) {
|
|
74
|
+
return "home";
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
router.initialize();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## How it works
|
|
85
|
+
|
|
86
|
+
The library extends [`sap.m.routing.Router`](https://sdk.openui5.org/api/sap.m.routing.Router) and overrides `parse()`, the single method through which all navigation flows (programmatic `navTo`, browser back/forward, direct URL changes). Guards run before any route matching, target loading, or view creation.
|
|
87
|
+
|
|
88
|
+
Because it extends the mobile router directly, all existing `sap.m.routing.Router` behavior (Targets, route events, `navTo`, back navigation) works unchanged.
|
|
89
|
+
|
|
90
|
+
The guard pipeline stays **synchronous when all guards return plain values** and only becomes async when a guard returns a Promise. A generation counter discards stale async results when navigations overlap, and an `AbortSignal` is passed to each guard so async work (like `fetch`) can be cancelled early.
|
|
91
|
+
|
|
92
|
+
## API
|
|
93
|
+
|
|
94
|
+
All methods return `this` for chaining.
|
|
95
|
+
|
|
96
|
+
### Guard registration
|
|
97
|
+
|
|
98
|
+
| Method | Description |
|
|
99
|
+
| ---------------------------------------------------------- | ---------------------------------------------- |
|
|
100
|
+
| `addGuard(fn)` | Global enter guard (runs for every navigation) |
|
|
101
|
+
| `addRouteGuard(routeName, fn)` | Enter guard for a specific route |
|
|
102
|
+
| `addRouteGuard(routeName, { beforeEnter?, beforeLeave? })` | Enter and/or leave guards via object form |
|
|
103
|
+
| `addLeaveGuard(routeName, fn)` | Leave guard (runs when leaving the route) |
|
|
104
|
+
|
|
105
|
+
### Guard removal
|
|
106
|
+
|
|
107
|
+
| Method | Description |
|
|
108
|
+
| ------------------------------------------------------------- | ------------------------------------------------ |
|
|
109
|
+
| `removeGuard(fn)` | Remove a global enter guard |
|
|
110
|
+
| `removeRouteGuard(routeName, fn)` | Remove an enter guard |
|
|
111
|
+
| `removeRouteGuard(routeName, { beforeEnter?, beforeLeave? })` | Remove enter and/or leave guards via object form |
|
|
112
|
+
| `removeLeaveGuard(routeName, fn)` | Remove a leave guard |
|
|
113
|
+
|
|
114
|
+
### GuardContext
|
|
115
|
+
|
|
116
|
+
Every guard receives a `GuardContext` object:
|
|
117
|
+
|
|
118
|
+
| Property | Type | Description |
|
|
119
|
+
| ------------- | -------------------------------------------------- | --------------------------------------------------- |
|
|
120
|
+
| `toRoute` | `string` | Target route name (empty if no match) |
|
|
121
|
+
| `toHash` | `string` | Raw hash being navigated to |
|
|
122
|
+
| `toArguments` | `Record<string, string \| Record<string, string>>` | Parsed route parameters |
|
|
123
|
+
| `fromRoute` | `string` | Current route name (empty on first navigation) |
|
|
124
|
+
| `fromHash` | `string` | Current hash |
|
|
125
|
+
| `signal` | `AbortSignal` | Aborted when a newer navigation supersedes this one |
|
|
126
|
+
|
|
127
|
+
### Return values
|
|
128
|
+
|
|
129
|
+
**Enter guards** (`addGuard`, `addRouteGuard`):
|
|
130
|
+
|
|
131
|
+
| Return | Effect |
|
|
132
|
+
| ---------------------------------------------- | ----------------------------------------------- |
|
|
133
|
+
| `true` | Allow navigation |
|
|
134
|
+
| `false` | Block (stay on current route, no history entry) |
|
|
135
|
+
| `"routeName"` | Redirect to named route (replaces history) |
|
|
136
|
+
| `{ route, parameters?, componentTargetInfo? }` | Redirect with route parameters |
|
|
137
|
+
| anything else (`null`, `undefined`) | Treated as block |
|
|
138
|
+
|
|
139
|
+
Only strict `true` allows navigation — no truthy coercion.
|
|
140
|
+
|
|
141
|
+
**Leave guards** (`addLeaveGuard`):
|
|
142
|
+
|
|
143
|
+
| Return | Effect |
|
|
144
|
+
| --------------------------------- | ------------------------------- |
|
|
145
|
+
| `true` | Allow leaving the current route |
|
|
146
|
+
| `false` (or any non-`true` value) | Block |
|
|
147
|
+
|
|
148
|
+
Leave guards cannot redirect. For redirection logic, use enter guards on the target route.
|
|
149
|
+
|
|
150
|
+
### Execution order
|
|
151
|
+
|
|
152
|
+
1. **Leave guards** for the current route (registration order)
|
|
153
|
+
2. **Global enter guards** (registration order)
|
|
154
|
+
3. **Route-specific enter guards** for the target (registration order)
|
|
155
|
+
4. Pipeline **short-circuits** at the first non-`true` result
|
|
156
|
+
|
|
157
|
+
## Examples
|
|
158
|
+
|
|
159
|
+
### Async guard with AbortSignal
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
router.addRouteGuard("dashboard", async (context) => {
|
|
163
|
+
const res = await fetch(`/api/access/${context.toRoute}`, {
|
|
164
|
+
signal: context.signal,
|
|
165
|
+
});
|
|
166
|
+
const { allowed } = await res.json();
|
|
167
|
+
return allowed ? true : "forbidden";
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Redirect with parameters
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
router.addGuard((context) => {
|
|
175
|
+
if (context.toRoute === "old-detail") {
|
|
176
|
+
return {
|
|
177
|
+
route: "detail",
|
|
178
|
+
parameters: { id: context.toArguments.id },
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Guard factories
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// guards.ts
|
|
189
|
+
import JSONModel from "sap/ui/model/json/JSONModel";
|
|
190
|
+
import type { GuardFn, LeaveGuardFn, GuardContext, GuardResult } from "ui5/guard/router/types";
|
|
191
|
+
|
|
192
|
+
export function createAuthGuard(authModel: JSONModel): GuardFn {
|
|
193
|
+
return (context: GuardContext): GuardResult => {
|
|
194
|
+
return authModel.getProperty("/isLoggedIn") ? true : "home";
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function createDirtyFormGuard(formModel: JSONModel): LeaveGuardFn {
|
|
199
|
+
return (context: GuardContext): boolean => {
|
|
200
|
+
return !formModel.getProperty("/isDirty");
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Object form (enter + leave)
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
router.addRouteGuard("editOrder", {
|
|
209
|
+
beforeEnter: createAuthGuard(authModel),
|
|
210
|
+
beforeLeave: createDirtyFormGuard(formModel),
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Dynamic guard registration
|
|
215
|
+
|
|
216
|
+
Guards can be added or removed at any point during the router's lifetime:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
const logGuard: GuardFn = (ctx) => {
|
|
220
|
+
console.log(`Navigation: ${ctx.fromRoute} → ${ctx.toRoute}`);
|
|
221
|
+
return true;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
router.addGuard(logGuard);
|
|
225
|
+
// later...
|
|
226
|
+
router.removeGuard(logGuard);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Leave guard with controller lifecycle
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import type { GuardRouter, LeaveGuardFn } from "ui5/guard/router/types";
|
|
233
|
+
import { createDirtyFormGuard } from "./guards";
|
|
234
|
+
|
|
235
|
+
export default class EditOrderController extends Controller {
|
|
236
|
+
private _leaveGuard: LeaveGuardFn;
|
|
237
|
+
|
|
238
|
+
onInit(): void {
|
|
239
|
+
const formModel = new JSONModel({ isDirty: false });
|
|
240
|
+
this.getView()!.setModel(formModel, "form");
|
|
241
|
+
|
|
242
|
+
const router = (this.getOwnerComponent() as UIComponent).getRouter() as unknown as GuardRouter;
|
|
243
|
+
this._leaveGuard = createDirtyFormGuard(formModel);
|
|
244
|
+
router.addLeaveGuard("editOrder", this._leaveGuard);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
onExit(): void {
|
|
248
|
+
const router = (this.getOwnerComponent() as UIComponent).getRouter() as unknown as GuardRouter;
|
|
249
|
+
router.removeLeaveGuard("editOrder", this._leaveGuard);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
> [!TIP]
|
|
255
|
+
> **User feedback on blocked navigation**: When a leave guard blocks, the router silently restores the previous hash. There is no built-in confirmation dialog. Show a `sap.m.MessageBox.confirm()` inside your leave guard (returning the user's choice as a `Promise<boolean>`) to make the block visible.
|
|
256
|
+
|
|
257
|
+
> [!NOTE]
|
|
258
|
+
> **Guard cleanup and lifecycle**
|
|
259
|
+
>
|
|
260
|
+
> **Component level**: The router's `destroy()` method automatically clears all registered guards when the component is destroyed (including during FLP navigation).
|
|
261
|
+
>
|
|
262
|
+
> **Controller level**: UI5's routing caches views indefinitely, so `onExit` is called only when the component is destroyed, not on every navigation away. Controller-registered guards therefore persist across in-app navigations. This is typically the desired behavior for route-specific guards tied to view state.
|
|
263
|
+
>
|
|
264
|
+
> In FLP apps with `sap-keep-alive` enabled, the component persists when navigating to other apps. Guards remain registered since the same instance is reused.
|
|
265
|
+
|
|
266
|
+
### Native alternative for leave guards: Fiori Launchpad data loss prevention
|
|
267
|
+
|
|
268
|
+
If your app runs inside SAP Fiori Launchpad (FLP), the shell provides built-in data loss protection through two public APIs on `sap.ushell.Container`:
|
|
269
|
+
|
|
270
|
+
**`setDirtyFlag(bDirty)`** (since 1.27.0): A simple boolean flag. When set to `true`, FLP shows a browser `confirm()` dialog when the user attempts cross-app navigation (home button, other tiles), browser back/forward out of the app, or page refresh/close:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
sap.ushell.Container.setDirtyFlag(true); // mark unsaved changes
|
|
274
|
+
sap.ushell.Container.setDirtyFlag(false); // clear after save
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**`registerDirtyStateProvider(fn)`** (since 1.31.0): Registers a callback that FLP calls during navigation to dynamically determine dirty state. The callback receives a `NavigationContext` with `isCrossAppNavigation` (boolean) and `innerAppRoute` (string), allowing the provider to distinguish between cross-app and in-app navigation:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
const dirtyProvider = (navigationContext) => {
|
|
281
|
+
if (navigationContext?.isCrossAppNavigation) {
|
|
282
|
+
return formModel.getProperty("/isDirty");
|
|
283
|
+
}
|
|
284
|
+
return false; // let in-app routing handle it
|
|
285
|
+
};
|
|
286
|
+
sap.ushell.Container.registerDirtyStateProvider(dirtyProvider);
|
|
287
|
+
|
|
288
|
+
// Clean up (since 1.67.0)
|
|
289
|
+
sap.ushell.Container.deregisterDirtyStateProvider(dirtyProvider);
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
> **Note**: `getDirtyFlag()` is deprecated since UI5 1.120. FLP internally uses `getDirtyFlagsAsync()` (private) which combines the flag with all registered providers. The synchronous `getDirtyFlag()` still works but should not be relied upon in new code.
|
|
293
|
+
|
|
294
|
+
**How the two approaches complement each other**: FLP's data loss protection operates at the shell navigation filter level, intercepting navigation _before_ the hash change reaches your app's router. Leave guards operate _inside_ your app's router, intercepting route-to-route navigation. For complete coverage:
|
|
295
|
+
|
|
296
|
+
- Use **leave guards** for in-app route changes (e.g., navigating from an edit form to a list within your app)
|
|
297
|
+
- Use **`setDirtyFlag`** or **`registerDirtyStateProvider`** for FLP-level navigation (cross-app, browser close, home button)
|
|
298
|
+
|
|
299
|
+
See the [FLP Dirty State Research](../../docs/research-flp-dirty-state.md) for a detailed analysis of the FLP internals.
|
|
300
|
+
|
|
301
|
+
## Limitations
|
|
302
|
+
|
|
303
|
+
### Redirect targets bypass guards
|
|
304
|
+
|
|
305
|
+
When a guard redirects navigation from route A to route B, route B's guards are **not** evaluated. The redirect commits immediately.
|
|
306
|
+
|
|
307
|
+
This matters when the redirect target has its own guards. For example:
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
User navigates to "dashboard"
|
|
311
|
+
→ dashboard guard checks permissions, returns "profile"
|
|
312
|
+
→ profile guard checks onboarding status ← this guard is SKIPPED
|
|
313
|
+
→ profile view renders
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
This is intentional. Evaluating guards on redirect targets introduces the risk of infinite loops (`A → B → A → B → ...`). While solvable with a visited-set that detects cycles, the implementation adds significant complexity. This is particularly true when redirect targets have **async** guards, since the redirect chain can no longer be bracketed in a single synchronous call stack. The chain state must then persist across async boundaries and be cleared only by terminal events (commit, block, or loop detection).
|
|
317
|
+
|
|
318
|
+
In practice, redirect targets are typically "safe" routes like `home` or `login` that don't have guards of their own. If you need guard logic on a redirect target, run the check inline before returning the redirect:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
router.addRouteGuard("dashboard", (context) => {
|
|
322
|
+
if (!hasPermission()) {
|
|
323
|
+
return isOnboarded() ? "profile" : "onboarding";
|
|
324
|
+
}
|
|
325
|
+
return true;
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### URL bar shows target hash during async guards
|
|
330
|
+
|
|
331
|
+
When a guard returns a Promise (e.g., a `fetch` call to check permissions), the browser's URL bar shows the target hash while the guard is resolving. If the guard ultimately blocks or redirects, the URL reverts. However, there is a brief window where the displayed URL doesn't match the active route.
|
|
332
|
+
|
|
333
|
+
This does **not** affect sync guards, which resolve in the same tick as the hash change (the URL flicker is imperceptible).
|
|
334
|
+
|
|
335
|
+
**Why the router doesn't handle this**: UI5's `HashChanger` updates the URL and fires `hashChanged` _before_ `parse()` is called. The router cannot prevent the URL change; it can only react to it. Frameworks like Vue Router and Angular Router avoid this by controlling the URL update themselves (calling `history.pushState` only after guards resolve), but UI5's architecture doesn't allow this without intercepting at the HashChanger level, which is globally scoped and fragile.
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
User clicks link / navTo()
|
|
339
|
+
↓
|
|
340
|
+
HashChanger updates browser URL ← URL changes HERE
|
|
341
|
+
↓
|
|
342
|
+
HashChanger fires hashChanged
|
|
343
|
+
↓
|
|
344
|
+
Router.parse() called ← guards run HERE
|
|
345
|
+
↓
|
|
346
|
+
┌────┴────┐
|
|
347
|
+
allowed blocked
|
|
348
|
+
↓ ↓
|
|
349
|
+
views _restoreHash()
|
|
350
|
+
load reverts URL
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Show a busy indicator while async guards resolve. This communicates to the user that navigation is in progress, making the URL bar state a non-issue:
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
router.addRouteGuard("dashboard", async (context) => {
|
|
357
|
+
const app = rootView.byId("app") as App;
|
|
358
|
+
app.setBusy(true);
|
|
359
|
+
try {
|
|
360
|
+
const res = await fetch(`/api/access/${context.toRoute}`, {
|
|
361
|
+
signal: context.signal,
|
|
362
|
+
});
|
|
363
|
+
const { allowed } = await res.json();
|
|
364
|
+
return allowed ? true : "home";
|
|
365
|
+
} finally {
|
|
366
|
+
app.setBusy(false);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
This follows the same pattern as [TanStack Router's `pendingComponent`](https://tanstack.com/router/latest/docs/framework/react/guide/navigation-blocking#handling-blocked-navigations): the URL reflects the intent while a loading state signals that the navigation hasn't committed yet.
|
|
372
|
+
|
|
373
|
+
## Compatibility
|
|
374
|
+
|
|
375
|
+
> [!IMPORTANT]
|
|
376
|
+
> **Minimum UI5 version: 1.118**
|
|
377
|
+
>
|
|
378
|
+
> The library uses [`sap.ui.core.Lib`](https://sdk.openui5.org/api/sap.ui.core.Lib) for library initialization, which was introduced in **UI5 1.118**. The Router itself only depends on APIs available since 1.75 (notably [`getRouteInfoByHash`](https://sdk.openui5.org/api/sap.ui.core.routing.Router%23methods/getRouteInfoByHash)), but the library packaging sets the effective floor. Developed and tested against OpenUI5 1.144.0.
|
|
379
|
+
|
|
380
|
+
## License
|
|
381
|
+
|
|
382
|
+
[MIT](../../LICENSE)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Generated with TypeScript 5.7.3 / OpenUI5 1.144.0 using:
|
|
2
|
+
// - yargs-parser@20.2.9
|
|
3
|
+
// - typescript@5.7.3
|
|
4
|
+
// - string_decoder@1.3.0
|
|
5
|
+
// - buffer@6.0.3
|
|
6
|
+
// - events@3.3.0
|
|
7
|
+
// - undici-types@6.21.0
|
|
8
|
+
// - @types/yauzl@2.10.3
|
|
9
|
+
// - @types/yargs-parser@21.0.3
|
|
10
|
+
// - @types/yargs@17.0.35
|
|
11
|
+
// - @types/ws@8.18.1
|
|
12
|
+
// - @types/which@2.0.2
|
|
13
|
+
// - @types/stack-utils@2.0.3
|
|
14
|
+
// - @types/sizzle@2.3.10
|
|
15
|
+
// - @types/sinonjs__fake-timers@8.1.5
|
|
16
|
+
// - @types/qunit@2.5.4
|
|
17
|
+
// - @types/normalize-package-data@2.4.4
|
|
18
|
+
// - @types/node@20.19.32
|
|
19
|
+
// - @types/mocha@10.0.10
|
|
20
|
+
// - @types/jquery@3.5.13
|
|
21
|
+
// - @types/istanbul-reports@3.0.4
|
|
22
|
+
// - @types/istanbul-lib-report@3.0.3
|
|
23
|
+
// - @types/istanbul-lib-coverage@2.0.6
|
|
24
|
+
// - @types/qunit@2.19.13
|
|
25
|
+
/// <reference path="./resources/ui5/guard/router/library.d.ts"/>
|
|
26
|
+
/// <reference path="./resources/ui5/guard/router/types.d.ts"/>
|
|
27
|
+
/// <reference path="./resources/ui5/guard/router/Router.d.ts"/>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" ?>
|
|
2
|
+
<library xmlns="http://www.sap.com/sap.ui.library.xsd">
|
|
3
|
+
<name>ui5.guard.router</name>
|
|
4
|
+
<vendor>Marco</vendor>
|
|
5
|
+
<version>1.0.1</version>
|
|
6
|
+
<copyright></copyright>
|
|
7
|
+
<title>UI5 Router extension with async navigation guards</title>
|
|
8
|
+
<documentation>Extends sap.m.routing.Router with async navigation guards, running before route matching begins.</documentation>
|
|
9
|
+
<dependencies>
|
|
10
|
+
<dependency>
|
|
11
|
+
<libraryName>sap.ui.core</libraryName>
|
|
12
|
+
</dependency>
|
|
13
|
+
<dependency>
|
|
14
|
+
<libraryName>sap.m</libraryName>
|
|
15
|
+
</dependency>
|
|
16
|
+
</dependencies>
|
|
17
|
+
</library>
|