ui5-lib-guard-router 0.0.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ui5-lib-guard-router might be problematic. Click here for more details.
- package/README.md +260 -32
- package/dist/index.d.ts +1 -1
- package/dist/resources/ui5/guard/router/.library +17 -17
- package/dist/resources/ui5/guard/router/Router-dbg.js +430 -430
- package/dist/resources/ui5/guard/router/Router.d.ts.map +1 -1
- package/dist/resources/ui5/guard/router/library-dbg.js +16 -16
- package/dist/resources/ui5/guard/router/library-dbg.js.map +1 -1
- package/dist/resources/ui5/guard/router/library-preload.js +2 -2
- package/dist/resources/ui5/guard/router/library-preload.js.map +1 -1
- package/dist/resources/ui5/guard/router/library.d.ts.map +1 -1
- package/dist/resources/ui5/guard/router/library.js +1 -1
- package/dist/resources/ui5/guard/router/library.js.map +1 -1
- package/dist/resources/ui5/guard/router/manifest.json +1 -1
- package/dist/resources/ui5/guard/router/types-dbg.js +1 -1
- package/dist/resources/ui5/guard/router/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/.library +17 -17
- package/src/manifest.json +1 -1
package/README.md
CHANGED
|
@@ -1,50 +1,278 @@
|
|
|
1
|
-
# ui5
|
|
1
|
+
# ui5-lib-guard-router
|
|
2
2
|
|
|
3
|
-
|
|
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
4
|
|
|
5
|
-
|
|
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
6
|
|
|
7
|
+
> [!WARNING]
|
|
8
|
+
> This library is **experimental**. The API may change without notice. Pin your version and review changes before upgrading.
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
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 duplicated checks across controllers.
|
|
13
|
+
|
|
14
|
+
This library solves all three by intercepting at the router level, before any route matching begins.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install ui5-lib-guard-router
|
|
7
20
|
```
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
**1. Add the library dependency and set the router class in your `manifest.json`:**
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"sap.ui5": {
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"libs": {
|
|
31
|
+
"ui5.guard.router": {}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"routing": {
|
|
35
|
+
"config": {
|
|
36
|
+
"routerClass": "ui5.guard.router.Router"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
18
41
|
```
|
|
19
42
|
|
|
20
|
-
|
|
43
|
+
All existing routes, targets, and navigation calls continue to work unchanged.
|
|
21
44
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
45
|
+
**2. Register guards in your Component:**
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import UIComponent from "sap/ui/core/UIComponent";
|
|
49
|
+
import type { GuardRouter } from "ui5/guard/router/types";
|
|
50
|
+
|
|
51
|
+
export default class Component extends UIComponent {
|
|
52
|
+
static metadata = {
|
|
53
|
+
manifest: "json",
|
|
54
|
+
interfaces: ["sap.ui.core.IAsyncContentCreation"],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
init(): void {
|
|
58
|
+
super.init();
|
|
59
|
+
const router = this.getRouter() as unknown as GuardRouter;
|
|
26
60
|
|
|
27
|
-
|
|
28
|
-
|
|
61
|
+
// Route-specific guard — redirect when not logged in
|
|
62
|
+
router.addRouteGuard("protected", (context) => {
|
|
63
|
+
return isLoggedIn() ? true : "home";
|
|
64
|
+
});
|
|
29
65
|
|
|
30
|
-
|
|
31
|
-
|
|
66
|
+
// Global guard — runs for every navigation
|
|
67
|
+
router.addGuard((context) => {
|
|
68
|
+
if (context.toRoute === "admin" && !isAdmin()) {
|
|
69
|
+
return "home";
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
router.initialize();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
32
77
|
```
|
|
33
78
|
|
|
34
|
-
##
|
|
79
|
+
## How it works
|
|
35
80
|
|
|
36
|
-
|
|
81
|
+
The library extends `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.
|
|
37
82
|
|
|
38
|
-
|
|
39
|
-
|
|
83
|
+
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.
|
|
84
|
+
|
|
85
|
+
## API
|
|
86
|
+
|
|
87
|
+
All methods return `this` for chaining.
|
|
88
|
+
|
|
89
|
+
### Guard registration
|
|
90
|
+
|
|
91
|
+
| Method | Description |
|
|
92
|
+
| ---------------------------------------------------------- | ---------------------------------------------- |
|
|
93
|
+
| `addGuard(fn)` | Global enter guard (runs for every navigation) |
|
|
94
|
+
| `addRouteGuard(routeName, fn)` | Enter guard for a specific route |
|
|
95
|
+
| `addRouteGuard(routeName, { beforeEnter?, beforeLeave? })` | Enter and/or leave guards via object form |
|
|
96
|
+
| `addLeaveGuard(routeName, fn)` | Leave guard (runs when leaving the route) |
|
|
97
|
+
|
|
98
|
+
### Guard removal
|
|
99
|
+
|
|
100
|
+
| Method | Description |
|
|
101
|
+
| ------------------------------------------------------------- | ------------------------------------------------ |
|
|
102
|
+
| `removeGuard(fn)` | Remove a global enter guard |
|
|
103
|
+
| `removeRouteGuard(routeName, fn)` | Remove an enter guard |
|
|
104
|
+
| `removeRouteGuard(routeName, { beforeEnter?, beforeLeave? })` | Remove enter and/or leave guards via object form |
|
|
105
|
+
| `removeLeaveGuard(routeName, fn)` | Remove a leave guard |
|
|
106
|
+
|
|
107
|
+
### GuardContext
|
|
108
|
+
|
|
109
|
+
Every guard receives a `GuardContext` object:
|
|
110
|
+
|
|
111
|
+
| Property | Type | Description |
|
|
112
|
+
| ------------- | -------------------------------------------------- | --------------------------------------------------- |
|
|
113
|
+
| `toRoute` | `string` | Target route name (empty if no match) |
|
|
114
|
+
| `toHash` | `string` | Raw hash being navigated to |
|
|
115
|
+
| `toArguments` | `Record<string, string \| Record<string, string>>` | Parsed route parameters |
|
|
116
|
+
| `fromRoute` | `string` | Current route name (empty on first navigation) |
|
|
117
|
+
| `fromHash` | `string` | Current hash |
|
|
118
|
+
| `signal` | `AbortSignal` | Aborted when a newer navigation supersedes this one |
|
|
119
|
+
|
|
120
|
+
### Return values
|
|
121
|
+
|
|
122
|
+
**Enter guards** (`addGuard`, `addRouteGuard`):
|
|
123
|
+
|
|
124
|
+
| Return | Effect |
|
|
125
|
+
| ---------------------------------------------- | ----------------------------------------------- |
|
|
126
|
+
| `true` | Allow navigation |
|
|
127
|
+
| `false` | Block (stay on current route, no history entry) |
|
|
128
|
+
| `"routeName"` | Redirect to named route (replaces history) |
|
|
129
|
+
| `{ route, parameters?, componentTargetInfo? }` | Redirect with route parameters |
|
|
130
|
+
| anything else (`null`, `undefined`) | Treated as block |
|
|
131
|
+
|
|
132
|
+
Only strict `true` allows navigation — no truthy coercion.
|
|
133
|
+
|
|
134
|
+
**Leave guards** (`addLeaveGuard`):
|
|
135
|
+
|
|
136
|
+
| Return | Effect |
|
|
137
|
+
| --------------------------------- | ------------------------------- |
|
|
138
|
+
| `true` | Allow leaving the current route |
|
|
139
|
+
| `false` (or any non-`true` value) | Block |
|
|
140
|
+
|
|
141
|
+
Leave guards cannot redirect. For redirection logic, use enter guards on the target route.
|
|
142
|
+
|
|
143
|
+
### Execution order
|
|
144
|
+
|
|
145
|
+
1. **Leave guards** for the current route (registration order)
|
|
146
|
+
2. **Global enter guards** (registration order)
|
|
147
|
+
3. **Route-specific enter guards** for the target (registration order)
|
|
148
|
+
4. Pipeline **short-circuits** at the first non-`true` result
|
|
149
|
+
|
|
150
|
+
## Examples
|
|
151
|
+
|
|
152
|
+
### Async guard with AbortSignal
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
router.addRouteGuard("dashboard", async (context) => {
|
|
156
|
+
const res = await fetch(`/api/access/${context.toRoute}`, {
|
|
157
|
+
signal: context.signal,
|
|
158
|
+
});
|
|
159
|
+
const { allowed } = await res.json();
|
|
160
|
+
return allowed ? true : "forbidden";
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Redirect with parameters
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
router.addGuard((context) => {
|
|
168
|
+
if (context.toRoute === "old-detail") {
|
|
169
|
+
return {
|
|
170
|
+
route: "detail",
|
|
171
|
+
parameters: { id: context.toArguments.id },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return true;
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Guard factories
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// guards.ts
|
|
182
|
+
import JSONModel from "sap/ui/model/json/JSONModel";
|
|
183
|
+
import type { GuardFn, LeaveGuardFn, GuardContext, GuardResult } from "ui5/guard/router/types";
|
|
184
|
+
|
|
185
|
+
export function createAuthGuard(authModel: JSONModel): GuardFn {
|
|
186
|
+
return (context: GuardContext): GuardResult => {
|
|
187
|
+
return authModel.getProperty("/isLoggedIn") ? true : "home";
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function createDirtyFormGuard(formModel: JSONModel): LeaveGuardFn {
|
|
192
|
+
return (context: GuardContext): boolean => {
|
|
193
|
+
return !formModel.getProperty("/isDirty");
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Object form (enter + leave)
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
router.addRouteGuard("editOrder", {
|
|
202
|
+
beforeEnter: createAuthGuard(authModel),
|
|
203
|
+
beforeLeave: createDirtyFormGuard(formModel),
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Leave guard with controller lifecycle
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import type { GuardRouter, LeaveGuardFn } from "ui5/guard/router/types";
|
|
211
|
+
import { createDirtyFormGuard } from "./guards";
|
|
212
|
+
|
|
213
|
+
export default class EditOrderController extends Controller {
|
|
214
|
+
private _leaveGuard: LeaveGuardFn;
|
|
215
|
+
|
|
216
|
+
onInit(): void {
|
|
217
|
+
const formModel = new JSONModel({ isDirty: false });
|
|
218
|
+
this.getView()!.setModel(formModel, "form");
|
|
219
|
+
|
|
220
|
+
const router = (this.getOwnerComponent() as UIComponent).getRouter() as unknown as GuardRouter;
|
|
221
|
+
this._leaveGuard = createDirtyFormGuard(formModel);
|
|
222
|
+
router.addLeaveGuard("editOrder", this._leaveGuard);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
onExit(): void {
|
|
226
|
+
const router = (this.getOwnerComponent() as UIComponent).getRouter() as unknown as GuardRouter;
|
|
227
|
+
router.removeLeaveGuard("editOrder", this._leaveGuard);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
> **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.
|
|
233
|
+
|
|
234
|
+
> **Guard cleanup**: The router's `destroy()` method automatically clears all guards when the component is destroyed. Controller-registered guards persist across in-app navigations (since UI5 caches views), which is typically desired for route-specific guards tied to view state.
|
|
235
|
+
|
|
236
|
+
## Limitations
|
|
237
|
+
|
|
238
|
+
### Redirect targets bypass guards
|
|
239
|
+
|
|
240
|
+
When a guard redirects from route A to route B, route B's guards are **not** evaluated. This prevents infinite redirect loops. In practice, redirect targets are typically "safe" routes (`home`, `login`) without guards. If you need guard logic on a redirect target, run the check inline:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
router.addRouteGuard("dashboard", (context) => {
|
|
244
|
+
if (!hasPermission()) {
|
|
245
|
+
return isOnboarded() ? "profile" : "onboarding";
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### URL bar flickers during async guards
|
|
252
|
+
|
|
253
|
+
When a guard returns a Promise, the browser's URL bar shows the target hash while the guard resolves. If it blocks or redirects, the URL reverts. This is a UI5 architecture constraint — `HashChanger` updates the URL before `parse()` is called. Sync guards are not affected.
|
|
254
|
+
|
|
255
|
+
Show a busy indicator while async guards resolve to communicate that navigation is in progress:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
router.addRouteGuard("dashboard", async (context) => {
|
|
259
|
+
app.setBusy(true);
|
|
260
|
+
try {
|
|
261
|
+
const res = await fetch(`/api/access/${context.toRoute}`, { signal: context.signal });
|
|
262
|
+
const { allowed } = await res.json();
|
|
263
|
+
return allowed ? true : "home";
|
|
264
|
+
} finally {
|
|
265
|
+
app.setBusy(false);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
40
268
|
```
|
|
41
269
|
|
|
42
|
-
|
|
270
|
+
## Compatibility
|
|
43
271
|
|
|
44
|
-
|
|
272
|
+
- **Minimum UI5 version**: 1.118 (requires [`sap.ui.core.Lib`](https://sdk.openui5.org/api/sap.ui.core.Lib))
|
|
273
|
+
- **Router APIs**: depends on [`getRouteInfoByHash`](https://sdk.openui5.org/api/sap.ui.core.routing.Router%23methods/getRouteInfoByHash) (since 1.75)
|
|
274
|
+
- **Developed and tested against**: OpenUI5 1.144.0
|
|
45
275
|
|
|
46
|
-
##
|
|
276
|
+
## License
|
|
47
277
|
|
|
48
|
-
-
|
|
49
|
-
- TypeScript via `ui5-tooling-transpile`
|
|
50
|
-
- UI5 Tooling specVersion 4.0
|
|
278
|
+
[MIT](https://github.com/wridgeu/ui5-lib-guard-router/blob/main/LICENSE)
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,6 @@
|
|
|
22
22
|
// - @types/istanbul-lib-report@3.0.3
|
|
23
23
|
// - @types/istanbul-lib-coverage@2.0.6
|
|
24
24
|
// - @types/qunit@2.19.13
|
|
25
|
-
/// <reference path="./resources/ui5/guard/router/Router.d.ts"/>
|
|
26
25
|
/// <reference path="./resources/ui5/guard/router/library.d.ts"/>
|
|
26
|
+
/// <reference path="./resources/ui5/guard/router/Router.d.ts"/>
|
|
27
27
|
/// <reference path="./resources/ui5/guard/router/types.d.ts"/>
|
|
@@ -1,17 +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>
|
|
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>
|
|
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.0</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>
|