@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 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.