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 CHANGED
@@ -1,50 +1,278 @@
1
- # ui5.guard.router (Library)
1
+ # ui5-lib-guard-router
2
2
 
3
- The core library providing `ui5.guard.router.Router` -- an extension of `sap.m.routing.Router` with async navigation guards.
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
- ## Structure
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
- src/
9
- Router.ts Router implementation (overrides parse())
10
- types.ts TypeScript types (GuardFn, LeaveGuardFn, GuardContext, GuardResult, GuardRedirect, RouteGuardConfig, GuardRouter)
11
- library.ts UI5 library registration
12
- manifest.json Library manifest
13
- .library XML library descriptor
14
- themes/ Theme placeholder (noLibraryCSS)
15
- test/
16
- qunit/ QUnit unit tests (Router.qunit.ts, NativeRouterCompat.qunit.ts)
17
- wdio-qunit.conf.ts wdio config for running QUnit tests via wdio-qunit-service
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
- ## Scripts
43
+ All existing routes, targets, and navigation calls continue to work unchanged.
21
44
 
22
- ```bash
23
- # Serve the library (for running QUnit tests in browser)
24
- npm start
25
- # => http://localhost:8080/test-resources/ui5/guard/router/qunit/testsuite.qunit.html
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
- # Build
28
- npm run build
61
+ // Route-specific guard — redirect when not logged in
62
+ router.addRouteGuard("protected", (context) => {
63
+ return isLoggedIn() ? true : "home";
64
+ });
29
65
 
30
- # Type check
31
- npm run typecheck
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
- ## Running QUnit tests
79
+ ## How it works
35
80
 
36
- From the monorepo root:
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
- ```bash
39
- npm run test:qunit
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
- This uses `wdio-qunit-service` to launch the QUnit test suite in a headless Chrome browser and report results.
270
+ ## Compatibility
43
271
 
44
- To run tests interactively in a browser, start the library server (`npm start` in this directory) and open the testsuite URL above.
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
- ## Framework
276
+ ## License
47
277
 
48
- - OpenUI5 1.144.0
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>0.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>
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>