@stacknav/angular 0.1.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 +198 -0
- package/fesm2022/stacknav-angular.mjs +724 -0
- package/fesm2022/stacknav-angular.mjs.map +1 -0
- package/package.json +43 -0
- package/types/stacknav-angular.d.ts +260 -0
- package/types/stacknav-angular.d.ts.map +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# @stacknav/angular
|
|
2
|
+
|
|
3
|
+
`<sn-outlet />`: a router outlet with the iOS push/pop transition, built on
|
|
4
|
+
[`@stacknav/core`](../core).
|
|
5
|
+
|
|
6
|
+
It is an outlet, not a router. Angular Router keeps doing everything it does:
|
|
7
|
+
routes, guards, resolvers, `routerLink`, `router.navigate`, lazy loading,
|
|
8
|
+
component input binding and browser history. The outlet only changes what
|
|
9
|
+
happens when the router activates a route: the page that was showing stays alive
|
|
10
|
+
beneath the new one, the change is animated, and a swipe from the leading edge
|
|
11
|
+
pops.
|
|
12
|
+
|
|
13
|
+
- **Works alongside the router.** There is no navigation API of its own. You
|
|
14
|
+
navigate with the router, go back with `Location`, and read params as you
|
|
15
|
+
already do.
|
|
16
|
+
- **Pages stay alive.** The page you came from is kept beneath the top one,
|
|
17
|
+
hidden. Its scroll position, form state, signals and subscriptions are intact
|
|
18
|
+
when you pop back, with nothing to restore.
|
|
19
|
+
- **Swipe back.** Drag from the leading edge and the page follows the pointer;
|
|
20
|
+
the router follows the gesture, through `history.back()` when that lands on the
|
|
21
|
+
right page. A `canDeactivate` guard that rejects puts the page back.
|
|
22
|
+
- **Configurable direction.** Whether a navigation is a push, a pop or a replace
|
|
23
|
+
comes from strategies you order: an explicit hint, the browser's back/forward,
|
|
24
|
+
the kept stack, numbers on your routes, or the route tree.
|
|
25
|
+
|
|
26
|
+
## Use
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// main.ts
|
|
30
|
+
bootstrapApplication(App, {
|
|
31
|
+
providers: [
|
|
32
|
+
provideRouter(routes, withComponentInputBinding(), withRouterConfig({ canceledNavigationResolution: 'computed' })),
|
|
33
|
+
provideStackNav(),
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```html
|
|
39
|
+
<!-- app.html: the outlet needs a height; it is the pages' scroll container -->
|
|
40
|
+
<sn-outlet style="height: 100dvh" />
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```css
|
|
44
|
+
/* styles.css: the transition's options are custom properties, all optional */
|
|
45
|
+
:root { --sn-duration: 340ms; --sn-parallax: 20%; }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A variable that is set wins over the matching `provideStackNav({ transition })`
|
|
49
|
+
option, so the stylesheet has the final say on how the animation feels.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// a page, using nothing from this library
|
|
53
|
+
@Component({
|
|
54
|
+
imports: [RouterLink],
|
|
55
|
+
template: `
|
|
56
|
+
<button (click)="location.back()">‹ Back</button>
|
|
57
|
+
<h1>{{ id() }}</h1>
|
|
58
|
+
<a routerLink="reviews">Reviews</a>`,
|
|
59
|
+
})
|
|
60
|
+
export class Item {
|
|
61
|
+
readonly id = input.required<string>(); // bound by withComponentInputBinding()
|
|
62
|
+
readonly location = inject(Location);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`canceledNavigationResolution: 'computed'` is optional but recommended. With the
|
|
67
|
+
router's default, a back navigation refused by a guard rewrites the history entry
|
|
68
|
+
the browser landed on.
|
|
69
|
+
|
|
70
|
+
## Deciding the direction
|
|
71
|
+
|
|
72
|
+
### Implicit: number your routes
|
|
73
|
+
|
|
74
|
+
Put a number on each route and the outlet does the rest. Navigating to a higher
|
|
75
|
+
number pushes, a lower one pops, and the same number replaces. This needs no
|
|
76
|
+
hints and no extra calls: use `routerLink` and `router.navigate` as usual.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
export const routes: Routes = [
|
|
80
|
+
{ path: '', component: Home, data: { stackLevel: 1 } },
|
|
81
|
+
{ path: 'settings', component: Settings, data: { stackLevel: 2 } },
|
|
82
|
+
{ path: 'about', component: About, data: { stackLevel: 3 } },
|
|
83
|
+
];
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The property name is configurable:
|
|
87
|
+
`provideStackNav({ levelOf: (snapshot) => snapshot.data['depth'] })`. Routes
|
|
88
|
+
without a number fall through to the route tree, where a descendant pushes and an
|
|
89
|
+
ancestor pops, so you only need to number the screens the tree gets wrong.
|
|
90
|
+
|
|
91
|
+
### Explicit: a hint on the navigation
|
|
92
|
+
|
|
93
|
+
For a navigation that should go against the numbers, pass a hint through the
|
|
94
|
+
router's own `NavigationExtras.info`, under the `stacknav` key:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
router.navigate(['/items', 2], { info: { stacknav: 'push' } }); // force a push
|
|
98
|
+
router.navigate(['/login'], { info: { stacknav: 'replace' } }); // swap the top page
|
|
99
|
+
router.navigate(['/x'], { info: { stacknav: { direction: 'pop', animated: false } } }); // no animation
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### The full order
|
|
103
|
+
|
|
104
|
+
The defaults are
|
|
105
|
+
`[fromHint(), fromHistory(), fromStack(), fromLevel(), fromTree()]`, which give:
|
|
106
|
+
|
|
107
|
+
| Navigation | Direction | Because |
|
|
108
|
+
| --- | --- | --- |
|
|
109
|
+
| `/items` → `/items/42` | push | descendant in the route tree |
|
|
110
|
+
| `/items/42` → `/items` | pop | ancestor |
|
|
111
|
+
| browser back / forward | pop / push | history |
|
|
112
|
+
| `routerLink` to a page still kept beneath | pop | the stack |
|
|
113
|
+
| `/settings` (`data.stackLevel: 2`) → `/about` (`stackLevel: 3`) | push | numbering |
|
|
114
|
+
| `/items/1` → `/items/2` via `routerLink` | replace | siblings, once `StackNavRouteReuseStrategy` is provided (see below) |
|
|
115
|
+
| `router.navigate(['/items', 2], { info: { stacknav: 'push' } })` | push | explicit hint |
|
|
116
|
+
|
|
117
|
+
Change the order, drop a strategy, or add your own:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
provideStackNav({
|
|
121
|
+
direction: [fromHint(), fromHistory(), myTabStrategy, fromTree({ sameDepth: 'push' })],
|
|
122
|
+
levelOf: (snapshot) => snapshot.data['order'], // where numbers live (default data.stackLevel)
|
|
123
|
+
keyOf: (snapshot) => snapshot.data['pageId'] ?? defaultKeyOf(snapshot), // what identifies a page
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A strategy receives `{ from, to, trigger, historyDelta, hint, stack }`, where
|
|
128
|
+
`from` and `to` carry `{ key, segments, level, data, snapshot }`.
|
|
129
|
+
|
|
130
|
+
### Back buttons
|
|
131
|
+
|
|
132
|
+
A back button is `Location.back()`. After a deep link there is nothing to go back
|
|
133
|
+
to, so an app typically falls back to a route as a pop. That is a few lines of
|
|
134
|
+
app code using `Router`, `Location` and the browser's `navigation.canGoBack`; see
|
|
135
|
+
[`apps/angular-demo/src/app/back.ts`](../../apps/angular-demo/src/app/back.ts).
|
|
136
|
+
|
|
137
|
+
### Siblings
|
|
138
|
+
|
|
139
|
+
The router's default `RouteReuseStrategy` reuses the component when only params
|
|
140
|
+
change (`/items/1` → `/items/2`), so the outlet is never activated and nothing
|
|
141
|
+
animates. To make those separate pages, provide the strategy this package
|
|
142
|
+
exports, like any other:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
{ provide: RouteReuseStrategy, useClass: StackNavRouteReuseStrategy }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Routes opt out of it with `data: { reuseRoute: true }`.
|
|
149
|
+
|
|
150
|
+
## API
|
|
151
|
+
|
|
152
|
+
### `provideStackNav(config?)`
|
|
153
|
+
|
|
154
|
+
| Option | Default | Description |
|
|
155
|
+
| --- | --- | --- |
|
|
156
|
+
| `direction` | core defaults | strategies in order, or one resolver function |
|
|
157
|
+
| `fallbackDirection` | `'push'` | used when no strategy has an answer |
|
|
158
|
+
| `levelOf(snapshot)` | `data.stackLevel` | the route's number |
|
|
159
|
+
| `keyOf(snapshot)` | the route's URL path | identity of a page |
|
|
160
|
+
| `infoKey` | `'stacknav'` | key in `NavigationExtras.info` for hints |
|
|
161
|
+
| `transition` | `{}` | `createIOSTransition` options for every outlet. The same options are CSS variables (`--sn-duration`, `--sn-easing`, `--sn-parallax`, `--sn-dim-max`, `--sn-shadow`, …) read off the outlet, so a stylesheet can retune them. See the [core README](../core#tuning-from-css) |
|
|
162
|
+
| `gesture` | `{}` | `createEdgePanGesture` options; `false` disables swiping |
|
|
163
|
+
| `detachInactiveViews` | `false` | detach change detection from hidden pages |
|
|
164
|
+
| `injectStyles` | `true` | insert the core stylesheet at runtime |
|
|
165
|
+
| `animated` | `true` | animate at all |
|
|
166
|
+
|
|
167
|
+
### `<sn-outlet>` (`StackNavOutlet`)
|
|
168
|
+
|
|
169
|
+
Inputs: `name`, `transition`, `gesture`, `routerOutletData`.
|
|
170
|
+
|
|
171
|
+
Outputs: `activate`, `deactivate`, `attach`, `detach` (as on `router-outlet`) and
|
|
172
|
+
`navigated` with `{ view, direction, animated, reused }`.
|
|
173
|
+
|
|
174
|
+
Properties: `stack` (the core `NavigationStack`, for `progress` events), `pages`
|
|
175
|
+
(kept pages, bottom to top), `canPop`, `lastDirection`.
|
|
176
|
+
|
|
177
|
+
Component inputs are bound when the router is configured with
|
|
178
|
+
`withComponentInputBinding()`: query params, params and data, in that order of
|
|
179
|
+
precedence, with unmatched inputs set to `undefined`. The router only binds
|
|
180
|
+
inputs for its own outlet, so this outlet does it itself and cannot see the
|
|
181
|
+
options passed to `withComponentInputBinding()`. Those options, and route
|
|
182
|
+
`resources`, are not honoured.
|
|
183
|
+
|
|
184
|
+
### `StackNavRouteReuseStrategy`
|
|
185
|
+
|
|
186
|
+
Opt-in; see [Siblings](#siblings) above.
|
|
187
|
+
|
|
188
|
+
## How it works
|
|
189
|
+
|
|
190
|
+
The outlet implements `RouterOutletContract`. When the router activates a route,
|
|
191
|
+
the outlet creates the component (or finds the kept one for that key), resolves
|
|
192
|
+
the direction, and asks the core stack to push, pop onto, or replace. Deactivated
|
|
193
|
+
pages are not destroyed until the stack drops them.
|
|
194
|
+
|
|
195
|
+
Each page gets an `ActivatedRoute` proxy whose observables switch to the route
|
|
196
|
+
object the router hands over when the page is reached again. Its nested outlet
|
|
197
|
+
contexts are saved and restored, so a `<router-outlet>` inside a kept page keeps
|
|
198
|
+
working.
|