ngx-stack 22.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.
- package/LICENSE +21 -0
- package/README.md +760 -0
- package/fesm2022/ngx-stack.mjs +2391 -0
- package/fesm2022/ngx-stack.mjs.map +1 -0
- package/package.json +56 -0
- package/schematics/migration.json +10 -0
- package/schematics/migrations/v23/index.js +62 -0
- package/schematics/package.json +3 -0
- package/types/ngx-stack.d.ts +1075 -0
package/README.md
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
<h1 align="center">ngx-stack</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
Native-feeling page stacks for Angular — push/pop transitions and an interactive
|
|
5
|
+
<br />iOS swipe-to-go-back, without pulling in a UI framework.
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<a href="https://www.npmjs.com/package/ngx-stack"><img alt="npm" src="https://img.shields.io/npm/v/ngx-stack?color=%23007aff"></a>
|
|
10
|
+
<a href="https://www.npmjs.com/package/ngx-stack"><img alt="downloads" src="https://img.shields.io/npm/dm/ngx-stack"></a>
|
|
11
|
+
<a href="https://github.com/AppsGanin/ngx-stack/actions/workflows/ci.yml"><img alt="ci" src="https://github.com/AppsGanin/ngx-stack/actions/workflows/ci.yml/badge.svg"></a>
|
|
12
|
+
<a href="https://bundlephobia.com/package/ngx-stack"><img alt="bundle size" src="https://img.shields.io/bundlephobia/minzip/ngx-stack"></a>
|
|
13
|
+
<img alt="license" src="https://img.shields.io/npm/l/ngx-stack">
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="https://appsganin.github.io/ngx-stack/"><b>Live demo</b></a> ·
|
|
18
|
+
<a href="#quick-start">Quick start</a> ·
|
|
19
|
+
<a href="#api">API</a> ·
|
|
20
|
+
<a href="#configuration">Configuration</a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<!-- Absolute, not relative: this README is also the npm package page, and npm does not resolve
|
|
24
|
+
relative image paths the way GitHub does. -->
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="https://raw.githubusercontent.com/AppsGanin/ngx-stack/main/projects/ngx-stack/docs/swipe.png" alt="A swipe-back caught mid-gesture: the detail page follows the finger while the list behind it parallaxes at a third of the speed and dims" width="360">
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<sub>Caught mid-swipe. The page follows the finger 1:1; the one behind parallaxes at a third of<br />the speed and dims — and it kept its scroll position, because it was never unmounted.</sub>
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
- 🎯 **The gesture follows your finger.** Not an approximation of it — the transition is scrubbed by
|
|
36
|
+
the drag, and can be abandoned halfway.
|
|
37
|
+
- 🍎 **The UIKit push on iOS, Material everywhere else** — or a transition you write yourself, per
|
|
38
|
+
platform.
|
|
39
|
+
- 📱 **Capacitor and Cordova**, including the Android hardware back button.
|
|
40
|
+
- 🗂 **Tabs with independent stacks.** Drill three deep in one, switch away, come back to exactly
|
|
41
|
+
where you were.
|
|
42
|
+
- 🔗 **Deep links that behave.** Open the app three screens in and the pages that should be beneath
|
|
43
|
+
are there, so the swipe works from the first frame.
|
|
44
|
+
- ♿️ **Focus and screen-reader announcements** on every page change; `prefers-reduced-motion` honoured.
|
|
45
|
+
- 🌍 **RTL** — transitions mirror, the gesture moves to the right edge.
|
|
46
|
+
- 🪶 **Unstyled and zoneless.** Signals throughout. No UI framework, no CSS to override.
|
|
47
|
+
|
|
48
|
+
## Compatibility
|
|
49
|
+
|
|
50
|
+
The library's major version **is** the Angular major version. There is no table to consult and no
|
|
51
|
+
guessing — `npm i ngx-stack` on Angular 22 installs 22.x.
|
|
52
|
+
|
|
53
|
+
| Angular | ngx-stack | npm tag |
|
|
54
|
+
| ------- | --------- | -------- |
|
|
55
|
+
| 22.x | 22.x | `latest` |
|
|
56
|
+
|
|
57
|
+
`ng update ngx-stack` runs migrations, so a major bump doesn't leave you reading a changelog to find
|
|
58
|
+
out why the build broke.
|
|
59
|
+
|
|
60
|
+
## Why this exists
|
|
61
|
+
|
|
62
|
+
Angular's router destroys the outgoing component the moment you navigate. That is the right default,
|
|
63
|
+
and completely incompatible with a swipe-back: the page you are swiping back to has to already be
|
|
64
|
+
mounted and painted **before** the navigation that reveals it, or there is nothing to drag into view.
|
|
65
|
+
|
|
66
|
+
The other half of the problem is the animation. A CSS transition or a View Transition is
|
|
67
|
+
fire-and-forget — it runs on its own clock. A swipe-back is the opposite: the finger owns the clock,
|
|
68
|
+
the transition has to follow it, and the user may change their mind halfway and drag right back. So
|
|
69
|
+
every transition here is a set of paused Web Animations that the gesture seeks by writing
|
|
70
|
+
`currentTime` directly. That is the whole trick.
|
|
71
|
+
|
|
72
|
+
## Install
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm i ngx-stack
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Quick start
|
|
79
|
+
|
|
80
|
+
Four things. Three of them are one line.
|
|
81
|
+
|
|
82
|
+
**1.** Add the provider.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// app.config.ts
|
|
86
|
+
import { provideRouter } from '@angular/router';
|
|
87
|
+
import { provideNgxStack } from 'ngx-stack';
|
|
88
|
+
|
|
89
|
+
export const appConfig: ApplicationConfig = {
|
|
90
|
+
providers: [
|
|
91
|
+
provideRouter(routes),
|
|
92
|
+
provideNgxStack(), // iOS gets the UIKit push + swipe-back; Android and web get their own
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**2.** Swap the outlet.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// app.ts
|
|
101
|
+
import { NgxStackOutlet } from 'ngx-stack';
|
|
102
|
+
|
|
103
|
+
@Component({
|
|
104
|
+
selector: 'app-root',
|
|
105
|
+
imports: [NgxStackOutlet],
|
|
106
|
+
template: `<ngx-stack-outlet />`,
|
|
107
|
+
styles: `
|
|
108
|
+
:host {
|
|
109
|
+
display: block;
|
|
110
|
+
height: 100%;
|
|
111
|
+
}
|
|
112
|
+
`,
|
|
113
|
+
})
|
|
114
|
+
export class App {}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**3.** Give it a height. This is the one thing that will silently waste your afternoon: the outlet is
|
|
118
|
+
`position: relative` with absolutely positioned pages inside it, so if nothing above it has a size,
|
|
119
|
+
it collapses to zero and you get a blank screen with **no error**.
|
|
120
|
+
|
|
121
|
+
```scss
|
|
122
|
+
// styles.scss
|
|
123
|
+
html,
|
|
124
|
+
body {
|
|
125
|
+
height: 100%;
|
|
126
|
+
margin: 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
app-root {
|
|
130
|
+
display: block;
|
|
131
|
+
height: 100dvh;
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**4.** Routes stay exactly as they were. Ordinary components, ordinary `router.navigate()`.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
export const routes: Routes = [
|
|
139
|
+
{ path: 'list', component: ListPage },
|
|
140
|
+
{ path: 'item/:id', component: ItemPage }, // or loadComponent
|
|
141
|
+
];
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Done. Tapping through to `/item/42` pushes; a drag from the left edge pops.
|
|
145
|
+
|
|
146
|
+
## API
|
|
147
|
+
|
|
148
|
+
| Export | What |
|
|
149
|
+
| -------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
|
150
|
+
| `provideNgxStack(config?)` | Sets it all up. See [Configuration](#configuration). |
|
|
151
|
+
| `<ngx-stack-outlet>` | Replaces `<router-outlet>`. Holds one stack per tab. |
|
|
152
|
+
| `<ngx-stack>` | A stack with no URLs — wizards, sheets. [Details](#imperative-stack). |
|
|
153
|
+
| `NgxStackNav` | `forward()`, `back()`, `root()` — navigation with an explicit stack intent. |
|
|
154
|
+
| `NgxStackTabs` | Which tab is active, and where each one was last seen. |
|
|
155
|
+
| `NgxStackSwipe` | The app-wide swipe switch: `enable()`, `disable()`, `enabled()`. |
|
|
156
|
+
| `NgxStackPage` | Page lifecycle hooks, and `ngxCanSwipeBack()`. |
|
|
157
|
+
| `provideCapacitorBack(App)` | Android hardware back → the stack. |
|
|
158
|
+
| `provideCordovaBack()` | The same, for Cordova. |
|
|
159
|
+
| `iosTransition` · `androidTransition` · `webTransition` · `noneTransition` | One per platform. [Transitions](#transitions). |
|
|
160
|
+
| `slideTransition(o)` · `riseTransition(o)` | The two shapes the built-ins are made of. Retune either. |
|
|
161
|
+
|
|
162
|
+
## Router-driven stack
|
|
163
|
+
|
|
164
|
+
Pages get real URLs, deep links work, and the browser's back button is just another way to
|
|
165
|
+
pop the stack.
|
|
166
|
+
|
|
167
|
+
Navigate with the plain router — no special API:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
router.navigate(['/item', 42]); // pushes — /item/42 isn't on the stack
|
|
171
|
+
location.back(); // pops — and so does a swipe from the left edge
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The direction is inferred: if the incoming URL is already on the stack, it's a pop back to that
|
|
175
|
+
page; otherwise it's a push. When that inference is wrong, say so explicitly with `NgxStackNav`.
|
|
176
|
+
|
|
177
|
+
### The plain Angular way keeps working
|
|
178
|
+
|
|
179
|
+
You don't have to learn a navigation API. `routerLink`, `router.navigate()`, `router.navigateByUrl()`,
|
|
180
|
+
the browser's back button and `location.back()` all work, and so do resolvers, guards, lazy
|
|
181
|
+
`loadComponent`, and relative navigation.
|
|
182
|
+
|
|
183
|
+
| What you write | What the stack does |
|
|
184
|
+
| -------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
185
|
+
| `[routerLink]="['/item', 42]"` | Pushes. `/item/42` isn't on the stack. |
|
|
186
|
+
| `router.navigate(['/list'])` when `/list` is below | **Pops back to it**, destroying what was above. A stack unwinds to a page it already has. |
|
|
187
|
+
| `router.navigate([…], { replaceUrl: true })` | **Swaps the top page.** History didn't grow, so neither does the stack. |
|
|
188
|
+
| `?query` change on the same route | Nothing. Same page, same component, `ActivatedRoute` emits — as it always did. |
|
|
189
|
+
| Browser back / `location.back()` | Pops. Prefer `nav.back()` once you have tabs — see below. |
|
|
190
|
+
|
|
191
|
+
Two of those are worth pausing on, because they're where a stack genuinely differs from a router:
|
|
192
|
+
|
|
193
|
+
**Navigating to a URL already on the stack unwinds to it.** Home → Details → `navigate(['/home'])`
|
|
194
|
+
takes you back to the _existing_ Home and throws Details away, rather than stacking a second Home
|
|
195
|
+
on top. That's what a stack is for, and it's what makes the browser back button work for free. If
|
|
196
|
+
you really want a second copy, say so: `nav.forward(['/home'])`.
|
|
197
|
+
|
|
198
|
+
**`nav.back()` is not the same as `location.back()`.** They agree in a plain app. They part company
|
|
199
|
+
once you have tabs, or a cold deep link — see those sections for why.
|
|
200
|
+
|
|
201
|
+
The outlet needs a height. It is `position: relative` with absolutely positioned pages
|
|
202
|
+
inside, so give its parent a real size (`height: 100dvh` on the host is usually it).
|
|
203
|
+
|
|
204
|
+
### `NgxStackNav`
|
|
205
|
+
|
|
206
|
+
Injectable. Navigation with an explicit stack intent — for the cases where the inference
|
|
207
|
+
above gets it wrong.
|
|
208
|
+
|
|
209
|
+
| Method | What it does |
|
|
210
|
+
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
211
|
+
| `forward(commands, extras?)` | **Always pushes**, even if the URL is already on the stack. Without this, navigating Home → Details → Home unwinds to the existing Home and throws Details away. Use it when you genuinely want a second copy on top. |
|
|
212
|
+
| `back(commands?, extras?)` | With no arguments, a real `history.back()` — so the URL, the browser's back button and the stack stay in agreement. This is what the swipe gesture calls. Pass commands to pop to a specific URL instead. |
|
|
213
|
+
| `root(commands, extras?)` | **Replaces the entire stack** with one page, destroying everything else. After login, after logout, after a flow completes. Uses `replaceUrl`, so the discarded pages don't linger in browser history either. |
|
|
214
|
+
|
|
215
|
+
All three return `Promise<boolean>` from the router, and take the usual `NavigationExtras`.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
const nav = inject(NgxStackNav);
|
|
219
|
+
|
|
220
|
+
nav.forward(['/list']); // push a *second* /list rather than unwinding to the first
|
|
221
|
+
nav.back(); // pop one page
|
|
222
|
+
nav.root(['/home']); // wipe the stack, start again
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Imperative stack
|
|
226
|
+
|
|
227
|
+
For navigation that has no business being in the address bar — a wizard inside a modal, a
|
|
228
|
+
drill-down in one tab. Same transition, same swipe-back, no URLs.
|
|
229
|
+
|
|
230
|
+
```html
|
|
231
|
+
<ngx-stack [root]="Step1" #wizard />
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const stack = inject(NgxStack); // from any page on the stack
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Methods
|
|
239
|
+
|
|
240
|
+
Every one returns a `Promise<void>` that resolves when the transition has finished.
|
|
241
|
+
|
|
242
|
+
| Method | What it does |
|
|
243
|
+
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
244
|
+
| `push(component, inputs?)` | Mount `component` and slide it in on top. `inputs` is a plain object applied with `setInput`, so signal inputs and `@Input()` both work. The page below stays mounted and keeps its state. |
|
|
245
|
+
| `pop()` | Drop the top page and go back one. Destroys the popped component. A no-op at the root — safe to call blindly. |
|
|
246
|
+
| `popTo(index)` | Unwind to the page at `index` (0 is the root), destroying every page above it. Skips whole sections of a flow in a single transition instead of popping them one at a time. |
|
|
247
|
+
| `popToRoot()` | `popTo(0)`. Back to the first page, everything above it destroyed. The "start over" button of a wizard. |
|
|
248
|
+
| `setRoot(component, inputs?)` | Throw the whole stack away and start again from `component`. The imperative twin of `NgxStackNav.root()`. |
|
|
249
|
+
|
|
250
|
+
### Inputs
|
|
251
|
+
|
|
252
|
+
| Input | Type | What it does |
|
|
253
|
+
| ------------ | ------------------------- | ------------------------------------------------------------------------------------------ |
|
|
254
|
+
| `root` | `Type<unknown> \| null` | The bottom page, mounted on init without a transition. Use `setRoot()` to change it later. |
|
|
255
|
+
| `rootInputs` | `Record<string, unknown>` | Inputs for that first page. |
|
|
256
|
+
| `swipeBack` | `boolean \| null` | Swipe-back for this stack only. `null` (default) defers to the app-wide `NgxStackSwipe`. |
|
|
257
|
+
|
|
258
|
+
### Signals
|
|
259
|
+
|
|
260
|
+
`pages` (the mounted `StackEntry[]`, bottom first), `depth`, `canGoBack`, `animating`. The
|
|
261
|
+
routed outlet exposes the same four, so `<ngx-stack-outlet #outlet>` plus
|
|
262
|
+
`@if (outlet.canGoBack())` is how you show a back button.
|
|
263
|
+
|
|
264
|
+
Stacks nest. A swipe inside an inner stack pops the inner stack; when the inner stack is at
|
|
265
|
+
its own root it declines the gesture and the swipe falls through to the outer one.
|
|
266
|
+
|
|
267
|
+
## Tabs
|
|
268
|
+
|
|
269
|
+
Set `tabs` to your top-level path prefixes and each one gets its own independent stack:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
provideNgxStack({ tabs: ['inbox', 'search', 'settings'] });
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Why you have to say this
|
|
276
|
+
|
|
277
|
+
Nothing in a route config distinguishes _"these are siblings you switch between, each keeping its
|
|
278
|
+
own history"_ from _"these are just different URLs"_. `/settings` is a tab and `/settings/sheet` is a
|
|
279
|
+
page inside it — that's a fact about your app's shape, and only you have it.
|
|
280
|
+
|
|
281
|
+
It isn't repeated anywhere, though. `NgxStackTabs` exposes the same list as `tabs()`, so the one
|
|
282
|
+
declaration also renders your tab bar:
|
|
283
|
+
|
|
284
|
+
```html
|
|
285
|
+
@for (tab of tabs.tabs(); track tab) { <button (click)="tabs.select(tab)">{{ tab }}</button> }
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Routes need nothing.** A page is filed by its URL: everything under `/inbox/…` belongs to the
|
|
289
|
+
Inbox stack. Write your routes exactly as you would have anyway.
|
|
290
|
+
|
|
291
|
+
The one exception is a page whose URL _doesn't_ say which tab it's in — a flat `/starred` that
|
|
292
|
+
belongs to Inbox. That one says so:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
{ path: 'starred', component: StarredPage, data: { tab: 'inbox' } }
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Without it, the page lands in the no-tab stack: mounted, outside the tab bar, and stranded the
|
|
299
|
+
moment you switch tabs. (A `tab` naming something that isn't in the list throws, rather than quietly
|
|
300
|
+
filing the page somewhere no button can reach.)
|
|
301
|
+
|
|
302
|
+
If you write **nested** routes, declare it once on the tab's root and every page under it inherits —
|
|
303
|
+
whether or not the root has a component:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
{
|
|
307
|
+
path: 'settings',
|
|
308
|
+
data: { tab: 'settings' }, // once, for the whole subtree
|
|
309
|
+
children: [
|
|
310
|
+
{ path: '', component: SettingsPage },
|
|
311
|
+
{ path: 'sheet', component: SheetPage },
|
|
312
|
+
],
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
A route that genuinely belongs to no tab — a login screen, a full-screen modal — gets `tab: ''` and
|
|
317
|
+
its own stack, which is normally exactly what you want.
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
export const routes: Routes = [
|
|
321
|
+
{ path: 'inbox', component: InboxPage },
|
|
322
|
+
{ path: 'inbox/item/:id', component: ItemPage }, // → the Inbox stack
|
|
323
|
+
{ path: 'search', component: SearchPage },
|
|
324
|
+
{ path: 'search/result/:id', component: ResultPage }, // → the Search stack
|
|
325
|
+
];
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Drill three deep in Inbox, switch to Search, come back: Inbox is exactly as you left it, still
|
|
329
|
+
three deep, still scrolled where it was. Nothing was unmounted — a tab switch is a cut, not a
|
|
330
|
+
push, so it doesn't animate and it doesn't destroy anything.
|
|
331
|
+
|
|
332
|
+
The tab _bar_ is yours to draw; the library ships no visual design. `NgxStackTabs` is the only
|
|
333
|
+
part you need, and it exists to remember where each tab was:
|
|
334
|
+
|
|
335
|
+
| Member | What it does |
|
|
336
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
337
|
+
| `active()` | Signal. The tab on screen. |
|
|
338
|
+
| `tabs()` | Signal. The tabs you configured, in order. |
|
|
339
|
+
| `select(tab)` | Go to `tab`, landing back on the page you last had open there. Selecting the tab you're already on pops it to its root — as every native tab bar does. |
|
|
340
|
+
| `urlOf(tab)` | Where that tab was last seen. |
|
|
341
|
+
|
|
342
|
+
```html
|
|
343
|
+
@for (tab of tabs.tabs(); track tab) {
|
|
344
|
+
<button [class.active]="tabs.active() === tab" (click)="tabs.select(tab)">{{ tab }}</button>
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**One thing to know.** Browser history is a single linear thread; tabs are several stacks. Once
|
|
349
|
+
you switch tabs, the history entry behind you belongs to a _different_ tab, so a plain
|
|
350
|
+
`history.back()` would jump sideways out of the stack you're in. `nav.back()` and the swipe both
|
|
351
|
+
notice this: they use real `history.back()` when it happens to land on the page below, and
|
|
352
|
+
navigate to the page they actually mean when it doesn't. A route outside every tab — a login
|
|
353
|
+
screen, a full-screen modal — gets `tab: ''` and its own stack, which is usually what you want.
|
|
354
|
+
|
|
355
|
+
## Deep links: opening the app partway in
|
|
356
|
+
|
|
357
|
+
A push notification, a shared URL, a hard refresh three screens deep. The router puts you straight
|
|
358
|
+
on the detail page and the stack has exactly **one** entry. Nothing is beneath it — so there's
|
|
359
|
+
nothing for a swipe to drag into view, and `history.back()` walks straight out of the app.
|
|
360
|
+
|
|
361
|
+
### The back button — usually nothing to do
|
|
362
|
+
|
|
363
|
+
Where a page sits is normally obvious from its URL, and the route config already knows it:
|
|
364
|
+
`/inbox/item/12/notes` → `/inbox/item/12` → `/inbox`, dropping segments until what's left is a real
|
|
365
|
+
page. So the back button just works. `canGoBack()` is true even with one page on the stack, and
|
|
366
|
+
`nav.back()` builds the parent and animates to it as a _back_ — exactly as though it had been there
|
|
367
|
+
all along. Chains unwind a level at a time.
|
|
368
|
+
|
|
369
|
+
Only when the URL doesn't tell the truth do you have to say so:
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
// Flat: nothing about /ticket/42 says it sits under the inbox.
|
|
373
|
+
{ path: 'ticket/:id', component: TicketPage, data: { parent: '/inbox' } }
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
What none of this gives you is the _gesture_: a swipe has to reveal a page that is already mounted
|
|
377
|
+
and painted before the finger lands, and the parent is only built on demand. For that, read on.
|
|
378
|
+
|
|
379
|
+
### The gesture too — `deepLinks: true`
|
|
380
|
+
|
|
381
|
+
A swipe has to reveal a page that is already mounted and painted before the finger lands, so for the
|
|
382
|
+
_gesture_ the ancestors have to genuinely be there. The only honest way to get them there is to have
|
|
383
|
+
navigated to them — so that's what this does, at startup, silently, before the app is shown:
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
provideNgxStack({
|
|
387
|
+
deepLinks: true,
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
That's the whole setup — one flag in the config you already have. Nothing to keep in sync either:
|
|
392
|
+
the ancestors come from your route config. `data: { parent }` where you've declared one, otherwise
|
|
393
|
+
the URL's own nesting (`/inbox/item/12/notes` → `/inbox/item/12` → `/inbox`, dropping segments until
|
|
394
|
+
what's left is a real page). Open that URL cold and the stack comes up as `[inbox, item/12, notes]`,
|
|
395
|
+
with `notes` on screen and the swipe live from the first frame.
|
|
396
|
+
|
|
397
|
+
The deep page is still constructed **exactly once**. The first navigation is intercepted at
|
|
398
|
+
`NavigationStart` — before anything is recognised or activated — so asking for a different URL there
|
|
399
|
+
supersedes it and it never lands. (Correcting _after_ it lands would build the deep page, throw it
|
|
400
|
+
away, and build it again, running its resolvers twice for the privilege.)
|
|
401
|
+
|
|
402
|
+
Two things it can't work out on its own, both fixed by `data: { parent }`:
|
|
403
|
+
|
|
404
|
+
- **A flat URL.** `/ticket/42` doesn't say where it sits; `data: { parent: '/inbox' }` does.
|
|
405
|
+
- **Routes behind `loadChildren`.** The children live inside a dynamic import and don't exist yet, so
|
|
406
|
+
rather than guess, it declines.
|
|
407
|
+
|
|
408
|
+
Or pass your own `deepLinks: (url) => string | null` to override the lot.
|
|
409
|
+
|
|
410
|
+
What it does cost: the ancestors are built and **their resolvers run**, for pages the user may never
|
|
411
|
+
look at. If your list page does something expensive on load, leave `deepLinks` off, keep
|
|
412
|
+
`data: { parent }`, and let the first back be a tap.
|
|
413
|
+
|
|
414
|
+
## Turning the swipe on and off
|
|
415
|
+
|
|
416
|
+
`provideNgxStack({ swipeBack })` only sets the starting value. Three layers can change or
|
|
417
|
+
override it at runtime, narrowest last, **and any of them can veto**:
|
|
418
|
+
|
|
419
|
+
**1. App-wide** — `NgxStackSwipe`. Reach for this when something global is eating the drag: a
|
|
420
|
+
modal is open, a map has the screen, a payment is in flight.
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
const swipe = inject(NgxStackSwipe);
|
|
424
|
+
|
|
425
|
+
swipe.enabled(); // Signal<boolean> — read it in a template
|
|
426
|
+
swipe.disable();
|
|
427
|
+
swipe.enable();
|
|
428
|
+
swipe.set(isEnabled);
|
|
429
|
+
swipe.reset(); // back to whatever the config resolved to on this device
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**2. One stack** — `[swipeBack]` on the outlet or on `<ngx-stack>`. `true` or `false` overrides
|
|
433
|
+
the app-wide switch in either direction; `null` (the default) defers to it.
|
|
434
|
+
|
|
435
|
+
```html
|
|
436
|
+
<ngx-stack-outlet [swipeBack]="false" />
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**3. One page** — a veto only; it can't switch the gesture on where the layers above turned it
|
|
440
|
+
off. Either declare it on the route, for a page that is simply never swipeable:
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
{ path: 'checkout', component: CheckoutPage, data: { swipeBack: false } }
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
…or decide in the moment. `ngxCanSwipeBack()` is asked on _every_ touch that lands in the edge
|
|
447
|
+
zone, so it can read live state:
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
export class EditorPage implements NgxStackPage {
|
|
451
|
+
readonly dirty = signal(false);
|
|
452
|
+
|
|
453
|
+
ngxCanSwipeBack(): boolean {
|
|
454
|
+
return !this.dirty(); // no swiping away from unsaved work
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Note that a veto stops the _gesture_, not navigation. Your back button and `nav.back()` still
|
|
460
|
+
work — which is usually what you want, since the button is where you can put a confirm dialog.
|
|
461
|
+
|
|
462
|
+
## Transitions
|
|
463
|
+
|
|
464
|
+
Three built-ins, one per platform — built out of two shapes:
|
|
465
|
+
|
|
466
|
+
| Platform | Default | Shape | What it does |
|
|
467
|
+
| -------- | ------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
468
|
+
| iOS | `iosTransition` | `slideTransition()` | The UIKit push. The incoming page slides the full width over the outgoing one, which drifts the other way at a third of the speed and dims beneath it. |
|
|
469
|
+
| Android | `androidTransition` | `riseTransition()` | Material's rise-and-fade. The page below stays put and is simply covered. |
|
|
470
|
+
| Web | `webTransition` | `riseTransition()` | **Identical to Android's today** — same numbers, still its own transition. |
|
|
471
|
+
|
|
472
|
+
The web gets Material's rather than a slide of its own, because a full-width slide is a phone idiom:
|
|
473
|
+
it says _"this screen came from over there"_, which is true of something you swiped into view and not
|
|
474
|
+
of something you clicked — and on a wide monitor it is a great many pixels moving for no reason.
|
|
475
|
+
|
|
476
|
+
`androidTransition` and `webTransition` stay **two transitions rather than one**, even though they
|
|
477
|
+
are the same animation right now. "What a phone does" and "what a browser does" are two decisions
|
|
478
|
+
that merely happen to agree today; if they were a single object, the first person to want a different
|
|
479
|
+
feel on the desktop would have to change both.
|
|
480
|
+
|
|
481
|
+
Override any of them:
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
provideNgxStack({
|
|
485
|
+
transitions: {
|
|
486
|
+
ios: iosTransition,
|
|
487
|
+
android: androidTransition,
|
|
488
|
+
web: noneTransition, // no animation in the browser at all
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Or pass a single function to use everywhere: `transitions: myTransition`.
|
|
494
|
+
|
|
495
|
+
Both shapes are exported, so re-tuning one is a few numbers rather than a new transition:
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
import { riseTransition } from 'ngx-stack';
|
|
499
|
+
|
|
500
|
+
const snappierWeb = riseTransition({ travel: 6, easing: 'ease-out', durationScale: 0.4 });
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
> **If you want the swipe gesture on Android or the web**, that platform needs a transition that
|
|
504
|
+
> moves **horizontally**. Material's doesn't, so a finger dragging sideways would scrub a page moving
|
|
505
|
+
> vertically — which is exactly why `swipeBack: 'auto'` only arms the gesture on iOS.
|
|
506
|
+
> `slideTransition()` is there for it.
|
|
507
|
+
|
|
508
|
+
### Writing one
|
|
509
|
+
|
|
510
|
+
A transition is a pure function from "which two pages, which direction" to keyframes:
|
|
511
|
+
|
|
512
|
+
```ts
|
|
513
|
+
import { scrimOf, type StackTransition } from 'ngx-stack';
|
|
514
|
+
|
|
515
|
+
export const myTransition: StackTransition = (ctx) => {
|
|
516
|
+
const forward = ctx.direction === 'forward';
|
|
517
|
+
// The page that ends up on top; on 'back' this is the one being uncovered.
|
|
518
|
+
const { enteringEl, leavingEl, width, duration } = ctx;
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
duration,
|
|
522
|
+
easing: 'cubic-bezier(0.32, 0.72, 0, 1)',
|
|
523
|
+
animations: [
|
|
524
|
+
{
|
|
525
|
+
el: enteringEl,
|
|
526
|
+
keyframes: forward
|
|
527
|
+
? [{ transform: 'translateX(100%)' }, { transform: 'translateX(0)' }]
|
|
528
|
+
: [{ transform: 'translateX(-33%)' }, { transform: 'translateX(0)' }],
|
|
529
|
+
},
|
|
530
|
+
// scrimOf(el) is the dim overlay we put on every page for you.
|
|
531
|
+
],
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
One rule: **it must be scrubbable.** The swipe-back seeks your `back` keyframes to arbitrary
|
|
537
|
+
progress, so they have to be continuous and reversible — no discrete steps, nothing that
|
|
538
|
+
can't be interpolated. The engine handles the rest: during a drag it runs your keyframes on
|
|
539
|
+
a linear clock so the page tracks the finger exactly, then on release it snapshots what's on
|
|
540
|
+
screen and animates from there to the target with your easing. (Re-applying an easing curve
|
|
541
|
+
to a half-finished timeline would snap the page at the moment the user lets go — hence the
|
|
542
|
+
handoff.)
|
|
543
|
+
|
|
544
|
+
## Swipe-back
|
|
545
|
+
|
|
546
|
+
On by default on iOS, off elsewhere (`swipeBack: 'auto'`). Android's back gesture belongs to
|
|
547
|
+
the OS, and the default web transition is too subtle to scrub usefully.
|
|
548
|
+
|
|
549
|
+
```ts
|
|
550
|
+
provideNgxStack({
|
|
551
|
+
swipeBack: true,
|
|
552
|
+
swipeEdgeWidth: 50, // px from the left edge where a drag can start
|
|
553
|
+
swipeThreshold: 0.5, // release past halfway completes the pop
|
|
554
|
+
swipeVelocityThreshold: 0.35, // px/ms — a fast flick decides on its own
|
|
555
|
+
swipeWithMouse: true, // drag with a mouse. Development only.
|
|
556
|
+
});
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
## Capacitor and Cordova
|
|
560
|
+
|
|
561
|
+
Both are the easy target: their webviews have no back gesture of their own, so nothing fights us for
|
|
562
|
+
the screen edge. Everything here is plain web — the swipe, the transitions, the stacks, the tabs all
|
|
563
|
+
work in either shell unchanged.
|
|
564
|
+
|
|
565
|
+
The one thing that isn't web is the **Android hardware back button**, and each shell delivers it its
|
|
566
|
+
own way:
|
|
567
|
+
|
|
568
|
+
```ts
|
|
569
|
+
// Capacitor — pass the plugin in, so @capacitor/app stays out of the dependency graph of
|
|
570
|
+
// anyone shipping only to the web.
|
|
571
|
+
import { App } from '@capacitor/app';
|
|
572
|
+
providers: [provideRouter(routes), provideNgxStack(), provideCapacitorBack(App)];
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
// Cordova / PhoneGap — nothing to pass in; its API is a global.
|
|
577
|
+
providers: [provideRouter(routes), provideNgxStack(), provideCordovaBack()];
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
Both pop the stack, and both exit the app at the root — back on the first screen closes it rather
|
|
581
|
+
than doing nothing, which is what Android users expect.
|
|
582
|
+
|
|
583
|
+
Neither asks the _shell_ whether it can go back. Capacitor's `backButton` event and Cordova's both
|
|
584
|
+
report the **webview's history**, and history is a single linear thread while tabs are several
|
|
585
|
+
stacks — so it says yes when the only thing behind you is a different tab. They ask the stack.
|
|
586
|
+
|
|
587
|
+
Platform detection knows both shells (`platform.isCapacitor`, `isCordova`, `isNative`), and that
|
|
588
|
+
matters for more than the back button: `hasSystemBackGesture` keys off `isNative`, so an iOS
|
|
589
|
+
**Cordova** app doesn't needlessly concede the first 16px of the screen edge to a gesture its
|
|
590
|
+
webview doesn't have.
|
|
591
|
+
|
|
592
|
+
## The iOS browser caveat, honestly
|
|
593
|
+
|
|
594
|
+
In **iOS Safari and iOS PWAs** — not Capacitor — WebKit reserves the screen edge for its own
|
|
595
|
+
interactive back navigation, and there is no API to turn that off. This is a real,
|
|
596
|
+
long-standing limitation, not something this library is failing to work around. Your options
|
|
597
|
+
are all imperfect, so pick one:
|
|
598
|
+
|
|
599
|
+
| `systemGesture` | What it does |
|
|
600
|
+
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
601
|
+
| `'inset'` | **Default.** Start our zone `systemEdgeInset` px (16) inland so the two don't fight. WebKit still owns the outermost pixels; when it navigates, we detect it via `hasUAVisualTransition` and skip our animation rather than double-animating. |
|
|
602
|
+
| `'suppress'` | `preventDefault()` the touchstart in the edge zone, which does stop WebKit. But it also suppresses the synthetic `click` for touches starting there — don't use it if you have tappable UI near the left edge. |
|
|
603
|
+
| `'ignore'` | Do nothing and let both gestures coexist. |
|
|
604
|
+
|
|
605
|
+
None of this applies under Capacitor.
|
|
606
|
+
|
|
607
|
+
## Page lifecycle
|
|
608
|
+
|
|
609
|
+
Stacked pages are not destroyed when you navigate away, which is the point — it's what
|
|
610
|
+
preserves their state and lets a swipe reveal them instantly. It also means `ngOnDestroy` no
|
|
611
|
+
longer means "the user left this page". These hooks do:
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
export class ItemPage implements NgxStackPage {
|
|
615
|
+
ngxViewWillEnter() {}
|
|
616
|
+
ngxViewDidEnter() {}
|
|
617
|
+
ngxViewWillLeave() {}
|
|
618
|
+
ngxViewDidLeave() {} // still alive — unless it was popped
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Scroll position needs no work on your part: buried pages get `content-visibility: hidden`,
|
|
623
|
+
which skips their rendering but preserves their internal state, scroll offsets included.
|
|
624
|
+
|
|
625
|
+
## Styling
|
|
626
|
+
|
|
627
|
+
The library ships no visual design. Four custom properties, no `!important` needed anywhere —
|
|
628
|
+
the page defaults are wrapped in `:where()`, so a plain `:host { display: flex }` in your page
|
|
629
|
+
component takes the layout over.
|
|
630
|
+
|
|
631
|
+
```css
|
|
632
|
+
--ngx-stack-page-background: #fff;
|
|
633
|
+
--ngx-stack-page-shadow: -6px 0 20px rgb(0 0 0 / 12%);
|
|
634
|
+
--ngx-stack-scrim-color: #000;
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
## RTL
|
|
638
|
+
|
|
639
|
+
Nothing to configure. `direction: 'auto'` reads the computed direction off the host, so
|
|
640
|
+
`<html dir="rtl">` is the entire integration — and because it's read per transition rather than
|
|
641
|
+
cached, a language switcher that flips `dir` at runtime works without a reload.
|
|
642
|
+
|
|
643
|
+
Everything horizontal mirrors: pages arrive from the left, the parallax runs the other way, and
|
|
644
|
+
the swipe moves to the **right** edge and pulls left. Custom transitions get `ctx.rtl` and should
|
|
645
|
+
respect it, or they'll feel backwards in Arabic and Hebrew — the new page appearing to come from
|
|
646
|
+
where the user just came from.
|
|
647
|
+
|
|
648
|
+
## Memory: `maxDepth`
|
|
649
|
+
|
|
650
|
+
Pages are cheap but not free, and a stack you can descend forever — a chat, a wiki, a file
|
|
651
|
+
browser — will happily hold fifty live components.
|
|
652
|
+
|
|
653
|
+
```ts
|
|
654
|
+
provideNgxStack({ maxDepth: 10 }); // 0, the default, means no cap
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
Pages that fall off the bottom are destroyed. Walking back past that horizon still works: the
|
|
658
|
+
page is rebuilt from its URL and **still animates as a back**, because that's what actually
|
|
659
|
+
happened, whatever the DOM currently remembers. What it loses is its state. That's the trade you
|
|
660
|
+
asked for by setting a cap, and it's why the default is off.
|
|
661
|
+
|
|
662
|
+
## Accessibility
|
|
663
|
+
|
|
664
|
+
A stack navigation is invisible to assistive tech — no document load, no focus change it can
|
|
665
|
+
infer anything from, just the DOM quietly rearranging. So by default (`manageFocus: true`) the
|
|
666
|
+
stack moves focus into the page that arrived and announces its route `title` through a polite
|
|
667
|
+
live region. Buried pages are `inert`, so nothing off-screen is reachable by keyboard.
|
|
668
|
+
|
|
669
|
+
Focus lands on the page container unless you point it somewhere better:
|
|
670
|
+
|
|
671
|
+
```html
|
|
672
|
+
<h1 ngxStackAutofocus tabindex="-1">Message 42</h1>
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
`prefers-reduced-motion: reduce` makes transitions instant. The swipe keeps its animation, on
|
|
676
|
+
purpose: motion the user is dragging with their own finger is direct manipulation, which the
|
|
677
|
+
guidance exempts — and a gesture with no visual response isn't reduced motion, it's broken.
|
|
678
|
+
|
|
679
|
+
## Events
|
|
680
|
+
|
|
681
|
+
```html
|
|
682
|
+
<ngx-stack-outlet (transitionStart)="…" (transitionEnd)="onDone($event)" />
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
Both carry a `StackTransitionEvent`: `direction`, `entering`, `leaving`, `tab`, `animated`, and
|
|
686
|
+
`interactive` — true when a finger drove it rather than a navigation.
|
|
687
|
+
|
|
688
|
+
## Known limits
|
|
689
|
+
|
|
690
|
+
- **`withComponentInputBinding()` is not supported.** Angular's router binds route params to
|
|
691
|
+
component inputs through a private token that a third-party outlet cannot reach. Read params
|
|
692
|
+
from `ActivatedRoute` instead.
|
|
693
|
+
- **`RouteReuseStrategy` is claimed.** `provideNgxStack()` installs `NgxStackRouteReuseStrategy`,
|
|
694
|
+
which is required — Angular's default recycles a component when only params change, quietly
|
|
695
|
+
merging `/item/1` and `/item/2` into one page. A strategy of your own that returns
|
|
696
|
+
`shouldDetach: true` will fight the stack, and the outlet throws if it's asked to detach.
|
|
697
|
+
- **Component-less and redirect routes** can't be pages. `loadComponent` is fine.
|
|
698
|
+
- **No SSR.** The stack is DOM-driven throughout.
|
|
699
|
+
- **Android's predictive back is not wired up, and can't be.** Capacitor forwards the back
|
|
700
|
+
_event_, which `provideCapacitorBack()` handles — but the OS never forwards the gesture's
|
|
701
|
+
progress into the webview, so there is nothing to drive an interactive animation with. Anything
|
|
702
|
+
claiming otherwise is faking it. On Android the back gesture stays the OS's, and it's
|
|
703
|
+
instantaneous.
|
|
704
|
+
- **Async `canDeactivate` guards can't gate the gesture.** A touch handler cannot wait for a
|
|
705
|
+
promise. The default `guardPolicy: 'block'` refuses the swipe on any guarded route for that
|
|
706
|
+
reason; `ngxCanSwipeBack()` is the synchronous way back in.
|
|
707
|
+
|
|
708
|
+
## Configuration
|
|
709
|
+
|
|
710
|
+
Every option, with its default. All of them are optional — `provideNgxStack()` with no arguments is
|
|
711
|
+
a working setup.
|
|
712
|
+
|
|
713
|
+
```ts
|
|
714
|
+
provideNgxStack({/* … */});
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
**Look and feel**
|
|
718
|
+
|
|
719
|
+
| Option | Default | What |
|
|
720
|
+
| ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------- |
|
|
721
|
+
| `platform` | `'auto'` | `'ios'` · `'android'` · `'web'`. Forced, for testing an iOS build on a laptop. `'auto'` reads the OS. |
|
|
722
|
+
| `direction` | `'auto'` | `'ltr'` · `'rtl'`. `'auto'` reads the computed direction off the host, so `<html dir="rtl">` just works. |
|
|
723
|
+
| `transitions` | built-in | One `StackTransition`, or `{ ios, android, web }`. See [Transitions](#transitions). |
|
|
724
|
+
| `duration` | `420` | Base ms. The built-ins scale it — Android and web run shorter than iOS. |
|
|
725
|
+
| `animateRoot` | `false` | Animate the very first page onto an empty stack. |
|
|
726
|
+
| `respectReducedMotion` | `true` | `prefers-reduced-motion` makes transitions instant. The swipe keeps its animation — it's direct manipulation. |
|
|
727
|
+
|
|
728
|
+
**Swipe-back**
|
|
729
|
+
|
|
730
|
+
| Option | Default | What |
|
|
731
|
+
| ------------------------ | --------- | ------------------------------------------------------------------------------------------------------- |
|
|
732
|
+
| `swipeBack` | `'auto'` | `'auto'` arms it on iOS only. Changed at runtime with `NgxStackSwipe`. |
|
|
733
|
+
| `swipeEdgeWidth` | `50` | px of edge in which a drag can begin. Mirrored in RTL. |
|
|
734
|
+
| `swipeThreshold` | `0.5` | Release past this fraction and the pop completes. |
|
|
735
|
+
| `swipeVelocityThreshold` | `0.35` | px/ms. A faster flick decides on its own, whatever the distance. |
|
|
736
|
+
| `swipeWithMouse` | `false` | Drag with a mouse. For developing the gesture on a laptop — leave it off in production. |
|
|
737
|
+
| `guardPolicy` | `'block'` | A route with a `canDeactivate` guard isn't swipeable unless it implements `ngxCanSwipeBack()`. |
|
|
738
|
+
| `systemGesture` | `'inset'` | What to do about WebKit's own edge gesture. [The iOS browser caveat](#the-ios-browser-caveat-honestly). |
|
|
739
|
+
| `systemEdgeInset` | `16` | With `'inset'`, px at the very edge conceded to the browser. |
|
|
740
|
+
|
|
741
|
+
**Structure**
|
|
742
|
+
|
|
743
|
+
| Option | Default | What |
|
|
744
|
+
| ------------- | ------- | ---------------------------------------------------------------------------------------------------------- |
|
|
745
|
+
| `tabs` | — | `['inbox', 'search']` — path prefixes, each with its own stack. [Tabs](#tabs). |
|
|
746
|
+
| `deepLinks` | `false` | Rebuild the ancestors when the app opens partway in. [Deep links](#deep-links-opening-the-app-partway-in). |
|
|
747
|
+
| `maxDepth` | `0` | Cap on pages kept mounted per stack. `0` is no cap. [Memory](#memory-maxdepth). |
|
|
748
|
+
| `manageFocus` | `true` | Move focus into the entering page and announce it. [Accessibility](#accessibility). |
|
|
749
|
+
|
|
750
|
+
## Contributing
|
|
751
|
+
|
|
752
|
+
Issues and pull requests are welcome. The development setup, the release process, and how a new
|
|
753
|
+
Angular major gets supported are all in the [workspace README](https://github.com/AppsGanin/ngx-stack).
|
|
754
|
+
|
|
755
|
+
Commits follow [Conventional Commits](https://www.conventionalcommits.org/) — the convention Angular
|
|
756
|
+
itself invented — and a `pre-commit` hook runs ESLint and Prettier over what you staged.
|
|
757
|
+
|
|
758
|
+
## License
|
|
759
|
+
|
|
760
|
+
[MIT](https://github.com/AppsGanin/ngx-stack/blob/main/LICENSE) © Dmitry Ganin
|