@ng-native/router 0.0.1 → 0.1.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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -1
  3. package/dist/bind-route-inputs.d.ts +20 -0
  4. package/dist/bind-route-inputs.js +52 -0
  5. package/dist/bind-route-inputs.js.map +1 -0
  6. package/dist/full-window-overlay.d.ts +16 -0
  7. package/dist/full-window-overlay.js +52 -0
  8. package/dist/full-window-overlay.js.map +1 -0
  9. package/dist/index.d.ts +26 -0
  10. package/dist/index.js +27 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/native-back.d.ts +38 -0
  13. package/dist/native-back.js +88 -0
  14. package/dist/native-back.js.map +1 -0
  15. package/dist/native-bar-defaults.d.ts +50 -0
  16. package/dist/native-bar-defaults.js +17 -0
  17. package/dist/native-bar-defaults.js.map +1 -0
  18. package/dist/native-header-item.d.ts +12 -0
  19. package/dist/native-header-item.js +40 -0
  20. package/dist/native-header-item.js.map +1 -0
  21. package/dist/native-header-palette.d.ts +45 -0
  22. package/dist/native-header-palette.js +42 -0
  23. package/dist/native-header-palette.js.map +1 -0
  24. package/dist/native-header.d.ts +103 -0
  25. package/dist/native-header.js +204 -0
  26. package/dist/native-header.js.map +1 -0
  27. package/dist/native-links.d.ts +30 -0
  28. package/dist/native-links.js +40 -0
  29. package/dist/native-links.js.map +1 -0
  30. package/dist/native-navigation.d.ts +84 -0
  31. package/dist/native-navigation.js +153 -0
  32. package/dist/native-navigation.js.map +1 -0
  33. package/dist/native-platform-location.d.ts +62 -0
  34. package/dist/native-platform-location.js +110 -0
  35. package/dist/native-platform-location.js.map +1 -0
  36. package/dist/native-router-link.d.ts +14 -0
  37. package/dist/native-router-link.js +51 -0
  38. package/dist/native-router-link.js.map +1 -0
  39. package/dist/native-search-bar.d.ts +44 -0
  40. package/dist/native-search-bar.js +132 -0
  41. package/dist/native-search-bar.js.map +1 -0
  42. package/dist/native-stack-outlet.d.ts +195 -0
  43. package/dist/native-stack-outlet.js +564 -0
  44. package/dist/native-stack-outlet.js.map +1 -0
  45. package/dist/native-stack-reuse-strategy.d.ts +57 -0
  46. package/dist/native-stack-reuse-strategy.js +126 -0
  47. package/dist/native-stack-reuse-strategy.js.map +1 -0
  48. package/dist/native-tab.d.ts +147 -0
  49. package/dist/native-tab.js +209 -0
  50. package/dist/native-tab.js.map +1 -0
  51. package/dist/native-tabs-outlet.d.ts +138 -0
  52. package/dist/native-tabs-outlet.js +408 -0
  53. package/dist/native-tabs-outlet.js.map +1 -0
  54. package/dist/own-host.d.ts +6 -0
  55. package/dist/own-host.js +28 -0
  56. package/dist/own-host.js.map +1 -0
  57. package/dist/provide-native-router.d.ts +41 -0
  58. package/dist/provide-native-router.js +151 -0
  59. package/dist/provide-native-router.js.map +1 -0
  60. package/dist/screen-presentation.d.ts +69 -0
  61. package/dist/screen-presentation.js +10 -0
  62. package/dist/screen-presentation.js.map +1 -0
  63. package/dist/screens.d.ts +37 -0
  64. package/dist/screens.js +108 -0
  65. package/dist/screens.js.map +1 -0
  66. package/dist/tab-routes.d.ts +16 -0
  67. package/dist/tab-routes.js +24 -0
  68. package/dist/tab-routes.js.map +1 -0
  69. package/dist/tab-safe-area-view.d.ts +15 -0
  70. package/dist/tab-safe-area-view.js +63 -0
  71. package/dist/tab-safe-area-view.js.map +1 -0
  72. package/dist/transforms.d.ts +2 -0
  73. package/dist/transforms.js +13 -0
  74. package/dist/transforms.js.map +1 -0
  75. package/package.json +59 -3
@@ -0,0 +1,84 @@
1
+ import { type NavigationExtras } from '@angular/router';
2
+ import type { ScreenPresentation, StackPresentation } from './screen-presentation.ts';
3
+ /** What `Router` takes: a url string, or the array form with segments and params. */
4
+ export type NavigationCommands = string | readonly unknown[];
5
+ /**
6
+ * Where the intent rides. `NavigationExtras.state` is the app's own object, so ours sits under
7
+ * one reserved key rather than spread through it.
8
+ */
9
+ export declare const NATIVE_INTENT = "\u0275nativeStack";
10
+ /** What a navigation asks the outlet to do with the stack, over and above rendering the route. */
11
+ export interface NativeIntent {
12
+ /** `replace` drops the screen it supersedes; `reset` empties the stack first. */
13
+ readonly stack?: 'push' | 'replace' | 'reset';
14
+ readonly presentation?: ScreenPresentation;
15
+ }
16
+ export interface NativeNavigationOptions extends NavigationExtras {
17
+ /** How the screen arrives: presentation, animation, gesture and sheet props. */
18
+ presentation?: ScreenPresentation;
19
+ }
20
+ export interface PresentOptions extends NativeNavigationOptions {
21
+ /** Shorthand for `presentation.stackPresentation`. Defaults to `modal`. */
22
+ as?: StackPresentation;
23
+ }
24
+ /** Read the intent off a navigation's state, if it carries one. */
25
+ export declare function intentOf(state: unknown): NativeIntent | null;
26
+ export declare class NativeNavigation {
27
+ private readonly router;
28
+ private readonly outlets;
29
+ /**
30
+ * Push a new screen onto the stack. The plain case, and what `nativeRouterLink` does.
31
+ *
32
+ * From a presented screen the new one is presented the same way, over it, since a pushed
33
+ * screen would slide in under it; `presentation` says otherwise, `stackAnimation` and all.
34
+ */
35
+ push(commands: NavigationCommands, options?: NativeNavigationOptions): Promise<boolean>;
36
+ /**
37
+ * Swap the current screen for another. The history entry is replaced too, so a back from the
38
+ * new screen goes where a back from the old one would have.
39
+ */
40
+ replace(commands: NavigationCommands, options?: NavigationExtras): Promise<boolean>;
41
+ /**
42
+ * Present a screen over the stack rather than pushing into it: a modal, or a sheet.
43
+ *
44
+ * `present('/filters', { as: 'formSheet', presentation: { sheetAllowedDetents: [0.5, 1] } })`.
45
+ *
46
+ * A presented screen is put on screen outside the stack's navigation controller, so it has no
47
+ * native header and no automatic safe-area insets: it needs its own way out, and its own
48
+ * `safe-area-view`. A sheet can also be dragged away, but `fullScreenModal` cannot, and with
49
+ * `gestureEnabled: false` neither can.
50
+ */
51
+ present(commands: NavigationCommands, options?: PresentOptions): Promise<boolean>;
52
+ /**
53
+ * Replace the whole stack with one screen, the way signing out or finishing onboarding does.
54
+ * Every screen below is destroyed, so there is nothing to go back to.
55
+ */
56
+ reset(commands: NavigationCommands, options?: NavigationExtras): Promise<boolean>;
57
+ /**
58
+ * Go back exactly as the Android back button does: pop the stack in front, or at the root of a
59
+ * tab's stack, go to the first tab. Not one entry back through history, which after a trip to
60
+ * another tab is that tab. At the root of the app there is nowhere to go, and this does nothing.
61
+ */
62
+ back(): void;
63
+ /**
64
+ * Pop straight back to the screen at `commands`, taking every screen above it off the stack at
65
+ * once - `popToViewController`. The screen is the one already there, with its state, not a new
66
+ * one. Resolves false, doing nothing, when no screen below the top of a stack in front is
67
+ * showing that url; a push is the way to a screen that is not there.
68
+ */
69
+ popTo(commands: NavigationCommands): Promise<boolean>;
70
+ /**
71
+ * Pop the stack in front to its first screen - `popToRootViewController`. Inside a tab, that is
72
+ * the tab's first screen; the screens below keep their state. Resolves false at the root already.
73
+ */
74
+ popToRoot(): Promise<boolean>;
75
+ private go;
76
+ /**
77
+ * The query and fragment written into a url string, as the extras `navigate` takes them: it
78
+ * reads a string command as one path segment, and would encode a `?` into it. Extras given
79
+ * alongside win over the ones in the string.
80
+ */
81
+ private queryOf;
82
+ private firstNavigationRunning;
83
+ private firstNavigation;
84
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The typed facade over `Router` for the intents a URL cannot express (ADR 0003).
3
+ *
4
+ * Angular's `Router` stays the single source of truth: each of these is a line over
5
+ * `router.navigate`, setting `replaceUrl` and a presentation on `NavigationExtras.state`. Deep
6
+ * links, guards and resolvers keep working because it is still the Router, and because state
7
+ * rides in history a back restores the presentation the screen was opened with.
8
+ *
9
+ * A navigation service that owns the stack was rejected: it either re-implements guards,
10
+ * resolvers and deep linking, or it drifts from the URL, and once the stack is not derived from
11
+ * router state the two can disagree.
12
+ *
13
+ * No decorators, so tests can import it at runtime and so it can be provided with `useClass`,
14
+ * exactly as `NativePlatformLocation` and `NativeStackReuseStrategy` are.
15
+ */
16
+ import { inject } from '@angular/core';
17
+ import { NavigationCancel, NavigationEnd, NavigationError, Router, } from '@angular/router';
18
+ import { NativeBack } from "./native-back.js";
19
+ /**
20
+ * Where the intent rides. `NavigationExtras.state` is the app's own object, so ours sits under
21
+ * one reserved key rather than spread through it.
22
+ */
23
+ export const NATIVE_INTENT = 'ɵnativeStack';
24
+ /** A url string up to its query or fragment. */
25
+ const pathOf = (url) => url.split(/[?#]/, 1)[0];
26
+ /** Read the intent off a navigation's state, if it carries one. */
27
+ export function intentOf(state) {
28
+ if (!state || typeof state !== 'object')
29
+ return null;
30
+ const intent = state[NATIVE_INTENT];
31
+ return intent && typeof intent === 'object' ? intent : null;
32
+ }
33
+ export class NativeNavigation {
34
+ router = inject(Router);
35
+ outlets = inject(NativeBack);
36
+ /**
37
+ * Push a new screen onto the stack. The plain case, and what `nativeRouterLink` does.
38
+ *
39
+ * From a presented screen the new one is presented the same way, over it, since a pushed
40
+ * screen would slide in under it; `presentation` says otherwise, `stackAnimation` and all.
41
+ */
42
+ push(commands, options = {}) {
43
+ return this.go(commands, options, { stack: 'push' });
44
+ }
45
+ /**
46
+ * Swap the current screen for another. The history entry is replaced too, so a back from the
47
+ * new screen goes where a back from the old one would have.
48
+ */
49
+ replace(commands, options = {}) {
50
+ return this.go(commands, { ...options, replaceUrl: true }, { stack: 'replace' });
51
+ }
52
+ /**
53
+ * Present a screen over the stack rather than pushing into it: a modal, or a sheet.
54
+ *
55
+ * `present('/filters', { as: 'formSheet', presentation: { sheetAllowedDetents: [0.5, 1] } })`.
56
+ *
57
+ * A presented screen is put on screen outside the stack's navigation controller, so it has no
58
+ * native header and no automatic safe-area insets: it needs its own way out, and its own
59
+ * `safe-area-view`. A sheet can also be dragged away, but `fullScreenModal` cannot, and with
60
+ * `gestureEnabled: false` neither can.
61
+ */
62
+ present(commands, options = {}) {
63
+ const { as = 'modal', presentation, ...extras } = options;
64
+ return this.go(commands, extras, {
65
+ stack: 'push',
66
+ presentation: { stackPresentation: as, ...presentation },
67
+ });
68
+ }
69
+ /**
70
+ * Replace the whole stack with one screen, the way signing out or finishing onboarding does.
71
+ * Every screen below is destroyed, so there is nothing to go back to.
72
+ */
73
+ reset(commands, options = {}) {
74
+ return this.go(commands, { ...options, replaceUrl: true }, { stack: 'reset' });
75
+ }
76
+ /**
77
+ * Go back exactly as the Android back button does: pop the stack in front, or at the root of a
78
+ * tab's stack, go to the first tab. Not one entry back through history, which after a trip to
79
+ * another tab is that tab. At the root of the app there is nowhere to go, and this does nothing.
80
+ */
81
+ back() {
82
+ this.outlets.back();
83
+ }
84
+ /**
85
+ * Pop straight back to the screen at `commands`, taking every screen above it off the stack at
86
+ * once - `popToViewController`. The screen is the one already there, with its state, not a new
87
+ * one. Resolves false, doing nothing, when no screen below the top of a stack in front is
88
+ * showing that url; a push is the way to a screen that is not there.
89
+ */
90
+ popTo(commands) {
91
+ const tree = typeof commands === 'string'
92
+ ? this.router.parseUrl(commands)
93
+ : this.router.createUrlTree([...commands]);
94
+ return this.outlets.popTo(this.router.serializeUrl(tree));
95
+ }
96
+ /**
97
+ * Pop the stack in front to its first screen - `popToRootViewController`. Inside a tab, that is
98
+ * the tab's first screen; the screens below keep their state. Resolves false at the root already.
99
+ */
100
+ popToRoot() {
101
+ return this.outlets.popToRoot();
102
+ }
103
+ go(commands, options, intent) {
104
+ const { presentation, state, ...extras } = options;
105
+ const full = presentation
106
+ ? { ...intent, presentation: { ...intent.presentation, ...presentation } }
107
+ : intent;
108
+ const navigate = () => this.router.navigate(typeof commands === 'string' ? [pathOf(commands)] : [...commands], {
109
+ ...extras,
110
+ ...(typeof commands === 'string' ? this.queryOf(commands, extras) : {}),
111
+ state: { ...state, [NATIVE_INTENT]: full },
112
+ });
113
+ // A screen stacked on the first one waits for it. Navigating while the router's first
114
+ // navigation is still running - a lazily loaded root waiting on its import - cancels that
115
+ // navigation, and the app opens on this screen with nothing beneath it. A reset replaces the
116
+ // stack anyway, so it goes at once.
117
+ return intent.stack !== 'reset' && this.firstNavigationRunning()
118
+ ? this.firstNavigation().then(navigate)
119
+ : navigate();
120
+ }
121
+ /**
122
+ * The query and fragment written into a url string, as the extras `navigate` takes them: it
123
+ * reads a string command as one path segment, and would encode a `?` into it. Extras given
124
+ * alongside win over the ones in the string.
125
+ */
126
+ queryOf(url, extras) {
127
+ const cut = url.search(/[?#]/);
128
+ if (cut < 0)
129
+ return {};
130
+ const { queryParams, fragment } = this.router.parseUrl(url.slice(cut));
131
+ return {
132
+ queryParams: { ...queryParams, ...extras.queryParams },
133
+ fragment: extras.fragment ?? fragment ?? undefined,
134
+ };
135
+ }
136
+ firstNavigationRunning() {
137
+ const router = this.router;
138
+ return router.navigated === false && !!router.currentNavigation?.();
139
+ }
140
+ firstNavigation() {
141
+ return new Promise((resolve) => {
142
+ const events = this.router.events.subscribe((event) => {
143
+ if (event instanceof NavigationEnd ||
144
+ event instanceof NavigationCancel ||
145
+ event instanceof NavigationError) {
146
+ events.unsubscribe();
147
+ resolve();
148
+ }
149
+ });
150
+ });
151
+ }
152
+ }
153
+ //# sourceMappingURL=native-navigation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-navigation.js","sourceRoot":"","sources":["../src/native-navigation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The typed facade over `Router` for the intents a URL cannot express (ADR 0003).\n *\n * Angular's `Router` stays the single source of truth: each of these is a line over\n * `router.navigate`, setting `replaceUrl` and a presentation on `NavigationExtras.state`. Deep\n * links, guards and resolvers keep working because it is still the Router, and because state\n * rides in history a back restores the presentation the screen was opened with.\n *\n * A navigation service that owns the stack was rejected: it either re-implements guards,\n * resolvers and deep linking, or it drifts from the URL, and once the stack is not derived from\n * router state the two can disagree.\n *\n * No decorators, so tests can import it at runtime and so it can be provided with `useClass`,\n * exactly as `NativePlatformLocation` and `NativeStackReuseStrategy` are.\n */\nimport { inject } from '@angular/core';\nimport {\n NavigationCancel,\n NavigationEnd,\n NavigationError,\n Router,\n type NavigationExtras,\n} from '@angular/router';\nimport { NativeBack } from './native-back.ts';\nimport type { ScreenPresentation, StackPresentation } from './screen-presentation.ts';\n\n/** What `Router` takes: a url string, or the array form with segments and params. */\nexport type NavigationCommands = string | readonly unknown[];\n\n/**\n * Where the intent rides. `NavigationExtras.state` is the app's own object, so ours sits under\n * one reserved key rather than spread through it.\n */\nexport const NATIVE_INTENT = 'ɵnativeStack';\n\n/** What a navigation asks the outlet to do with the stack, over and above rendering the route. */\nexport interface NativeIntent {\n /** `replace` drops the screen it supersedes; `reset` empties the stack first. */\n readonly stack?: 'push' | 'replace' | 'reset';\n readonly presentation?: ScreenPresentation;\n}\n\nexport interface NativeNavigationOptions extends NavigationExtras {\n /** How the screen arrives: presentation, animation, gesture and sheet props. */\n presentation?: ScreenPresentation;\n}\n\nexport interface PresentOptions extends NativeNavigationOptions {\n /** Shorthand for `presentation.stackPresentation`. Defaults to `modal`. */\n as?: StackPresentation;\n}\n\n/** A url string up to its query or fragment. */\nconst pathOf = (url: string): string => url.split(/[?#]/, 1)[0]!;\n\n/** Read the intent off a navigation's state, if it carries one. */\nexport function intentOf(state: unknown): NativeIntent | null {\n if (!state || typeof state !== 'object') return null;\n const intent = (state as Record<string, unknown>)[NATIVE_INTENT];\n return intent && typeof intent === 'object' ? (intent as NativeIntent) : null;\n}\n\nexport class NativeNavigation {\n private readonly router = inject(Router);\n private readonly outlets = inject(NativeBack);\n\n /**\n * Push a new screen onto the stack. The plain case, and what `nativeRouterLink` does.\n *\n * From a presented screen the new one is presented the same way, over it, since a pushed\n * screen would slide in under it; `presentation` says otherwise, `stackAnimation` and all.\n */\n push(commands: NavigationCommands, options: NativeNavigationOptions = {}): Promise<boolean> {\n return this.go(commands, options, { stack: 'push' });\n }\n\n /**\n * Swap the current screen for another. The history entry is replaced too, so a back from the\n * new screen goes where a back from the old one would have.\n */\n replace(commands: NavigationCommands, options: NavigationExtras = {}): Promise<boolean> {\n return this.go(commands, { ...options, replaceUrl: true }, { stack: 'replace' });\n }\n\n /**\n * Present a screen over the stack rather than pushing into it: a modal, or a sheet.\n *\n * `present('/filters', { as: 'formSheet', presentation: { sheetAllowedDetents: [0.5, 1] } })`.\n *\n * A presented screen is put on screen outside the stack's navigation controller, so it has no\n * native header and no automatic safe-area insets: it needs its own way out, and its own\n * `safe-area-view`. A sheet can also be dragged away, but `fullScreenModal` cannot, and with\n * `gestureEnabled: false` neither can.\n */\n present(commands: NavigationCommands, options: PresentOptions = {}): Promise<boolean> {\n const { as = 'modal', presentation, ...extras } = options;\n return this.go(commands, extras, {\n stack: 'push',\n presentation: { stackPresentation: as, ...presentation },\n });\n }\n\n /**\n * Replace the whole stack with one screen, the way signing out or finishing onboarding does.\n * Every screen below is destroyed, so there is nothing to go back to.\n */\n reset(commands: NavigationCommands, options: NavigationExtras = {}): Promise<boolean> {\n return this.go(commands, { ...options, replaceUrl: true }, { stack: 'reset' });\n }\n\n /**\n * Go back exactly as the Android back button does: pop the stack in front, or at the root of a\n * tab's stack, go to the first tab. Not one entry back through history, which after a trip to\n * another tab is that tab. At the root of the app there is nowhere to go, and this does nothing.\n */\n back(): void {\n this.outlets.back();\n }\n\n /**\n * Pop straight back to the screen at `commands`, taking every screen above it off the stack at\n * once - `popToViewController`. The screen is the one already there, with its state, not a new\n * one. Resolves false, doing nothing, when no screen below the top of a stack in front is\n * showing that url; a push is the way to a screen that is not there.\n */\n popTo(commands: NavigationCommands): Promise<boolean> {\n const tree =\n typeof commands === 'string'\n ? this.router.parseUrl(commands)\n : this.router.createUrlTree([...commands]);\n return this.outlets.popTo(this.router.serializeUrl(tree));\n }\n\n /**\n * Pop the stack in front to its first screen - `popToRootViewController`. Inside a tab, that is\n * the tab's first screen; the screens below keep their state. Resolves false at the root already.\n */\n popToRoot(): Promise<boolean> {\n return this.outlets.popToRoot();\n }\n\n private go(\n commands: NavigationCommands,\n options: NativeNavigationOptions,\n intent: NativeIntent,\n ): Promise<boolean> {\n const { presentation, state, ...extras } = options;\n const full: NativeIntent = presentation\n ? { ...intent, presentation: { ...intent.presentation, ...presentation } }\n : intent;\n\n const navigate = () =>\n this.router.navigate(typeof commands === 'string' ? [pathOf(commands)] : [...commands], {\n ...extras,\n ...(typeof commands === 'string' ? this.queryOf(commands, extras) : {}),\n state: { ...(state as object), [NATIVE_INTENT]: full },\n });\n // A screen stacked on the first one waits for it. Navigating while the router's first\n // navigation is still running - a lazily loaded root waiting on its import - cancels that\n // navigation, and the app opens on this screen with nothing beneath it. A reset replaces the\n // stack anyway, so it goes at once.\n return intent.stack !== 'reset' && this.firstNavigationRunning()\n ? this.firstNavigation().then(navigate)\n : navigate();\n }\n\n /**\n * The query and fragment written into a url string, as the extras `navigate` takes them: it\n * reads a string command as one path segment, and would encode a `?` into it. Extras given\n * alongside win over the ones in the string.\n */\n private queryOf(url: string, extras: NavigationExtras): NavigationExtras {\n const cut = url.search(/[?#]/);\n if (cut < 0) return {};\n const { queryParams, fragment } = this.router.parseUrl(url.slice(cut));\n return {\n queryParams: { ...queryParams, ...extras.queryParams },\n fragment: extras.fragment ?? fragment ?? undefined,\n };\n }\n\n private firstNavigationRunning(): boolean {\n const router = this.router as Partial<Router>;\n return router.navigated === false && !!router.currentNavigation?.();\n }\n\n private firstNavigation(): Promise<void> {\n return new Promise((resolve) => {\n const events = this.router.events.subscribe((event) => {\n if (\n event instanceof NavigationEnd ||\n event instanceof NavigationCancel ||\n event instanceof NavigationError\n ) {\n events.unsubscribe();\n resolve();\n }\n });\n });\n }\n}\n"]}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `PlatformLocation` over an in-memory history.
3
+ *
4
+ * Angular's `Location` reads and writes the URL through this. On the web the browser owns the
5
+ * history stack; here nothing does, so we keep it ourselves and the URL tree stays the source of
6
+ * truth in both directions: `router.navigate()` pushes an entry, and a swipe-back reaches the
7
+ * outlet as the screen's `dismissed` event, which usually calls `historyGo(-1)` here and emits
8
+ * `popstate` so the router follows. Not always: see the stack outlet's `popBy` for when the entry
9
+ * behind is another tab.
10
+ *
11
+ * No decorators, so tests can import it at runtime and so it can be provided with `useClass`.
12
+ */
13
+ import type { LocationChangeListener, PlatformLocation } from '@angular/common';
14
+ /**
15
+ * What the history needs of a deep-link source, which `DeepLinks` from
16
+ * `@ng-native/device` satisfies. Structural rather than the class itself: the router has no
17
+ * use for the rest of it, and a test has no use for a real one.
18
+ */
19
+ export interface LinkSource {
20
+ /** The path the app was launched with, or null for a normal launch. */
21
+ initialUrl(): string | null;
22
+ /** Links that arrive while the app is running. Returns an unsubscribe. */
23
+ subscribe(listener: (url: string) => void): () => void;
24
+ }
25
+ export declare class NativePlatformLocation implements PlatformLocation {
26
+ private readonly entries;
27
+ private cursor;
28
+ private readonly popStateListeners;
29
+ private readonly hashChangeListeners;
30
+ /**
31
+ * A deep link is history, not a navigation of its own: the launch url *is* the first entry,
32
+ * and one that arrives later is pushed and announced as a popstate, which is the only thing
33
+ * the router listens to. `links` is null in a test that has no interest in either.
34
+ */
35
+ constructor(links?: LinkSource | null);
36
+ /** Follow a link that arrived while the app was running. */
37
+ open(url: string): void;
38
+ /**
39
+ * The url of the entry `offset` away from the current one, or null past either end. For the
40
+ * stack outlet, which has to know whether going back reaches the screen a native pop revealed.
41
+ */
42
+ ɵurlAt(offset: number): string | null;
43
+ private get current();
44
+ getBaseHrefFromDOM(): string;
45
+ getState(): unknown;
46
+ onPopState(fn: LocationChangeListener): VoidFunction;
47
+ onHashChange(fn: LocationChangeListener): VoidFunction;
48
+ get href(): string;
49
+ /** There is no origin on a device. Angular only ever compares these, never resolves them. */
50
+ get protocol(): string;
51
+ get hostname(): string;
52
+ get port(): string;
53
+ get pathname(): string;
54
+ get search(): string;
55
+ get hash(): string;
56
+ pushState(state: unknown, _title: string, url: string): void;
57
+ replaceState(state: unknown, _title: string, url: string): void;
58
+ forward(): void;
59
+ back(): void;
60
+ historyGo(relativePosition?: number): void;
61
+ private emitPopState;
62
+ }
@@ -0,0 +1,110 @@
1
+ /** Split a url into its path, `?search` and `#hash` parts. */
2
+ function parse(url) {
3
+ const hashAt = url.indexOf('#');
4
+ const hash = hashAt === -1 ? '' : url.slice(hashAt);
5
+ const withoutHash = hashAt === -1 ? url : url.slice(0, hashAt);
6
+ const searchAt = withoutHash.indexOf('?');
7
+ const search = searchAt === -1 ? '' : withoutHash.slice(searchAt);
8
+ const pathname = searchAt === -1 ? withoutHash : withoutHash.slice(0, searchAt);
9
+ return { pathname: pathname || '/', search, hash };
10
+ }
11
+ export class NativePlatformLocation {
12
+ entries = [{ url: '/', state: null }];
13
+ cursor = 0;
14
+ popStateListeners = new Set();
15
+ hashChangeListeners = new Set();
16
+ /**
17
+ * A deep link is history, not a navigation of its own: the launch url *is* the first entry,
18
+ * and one that arrives later is pushed and announced as a popstate, which is the only thing
19
+ * the router listens to. `links` is null in a test that has no interest in either.
20
+ */
21
+ constructor(links = null) {
22
+ const initial = links?.initialUrl();
23
+ if (initial)
24
+ this.entries[0] = { url: initial, state: null };
25
+ links?.subscribe((url) => this.open(url));
26
+ }
27
+ /** Follow a link that arrived while the app was running. */
28
+ open(url) {
29
+ if (url === this.current.url)
30
+ return;
31
+ this.pushState(null, '', url);
32
+ this.emitPopState();
33
+ }
34
+ /**
35
+ * The url of the entry `offset` away from the current one, or null past either end. For the
36
+ * stack outlet, which has to know whether going back reaches the screen a native pop revealed.
37
+ */
38
+ ɵurlAt(offset) {
39
+ return this.entries[this.cursor + offset]?.url ?? null;
40
+ }
41
+ get current() {
42
+ return this.entries[this.cursor];
43
+ }
44
+ // --- PlatformLocation ---------------------------------------------------
45
+ getBaseHrefFromDOM() {
46
+ return '/';
47
+ }
48
+ getState() {
49
+ return this.current.state;
50
+ }
51
+ onPopState(fn) {
52
+ this.popStateListeners.add(fn);
53
+ return () => this.popStateListeners.delete(fn);
54
+ }
55
+ onHashChange(fn) {
56
+ this.hashChangeListeners.add(fn);
57
+ return () => this.hashChangeListeners.delete(fn);
58
+ }
59
+ get href() {
60
+ return this.current.url;
61
+ }
62
+ /** There is no origin on a device. Angular only ever compares these, never resolves them. */
63
+ get protocol() {
64
+ return 'app:';
65
+ }
66
+ get hostname() {
67
+ return '';
68
+ }
69
+ get port() {
70
+ return '';
71
+ }
72
+ get pathname() {
73
+ return parse(this.current.url).pathname;
74
+ }
75
+ get search() {
76
+ return parse(this.current.url).search;
77
+ }
78
+ get hash() {
79
+ return parse(this.current.url).hash;
80
+ }
81
+ pushState(state, _title, url) {
82
+ // A push after going back discards the forward entries, as a browser does.
83
+ this.entries.length = this.cursor + 1;
84
+ this.entries.push({ url, state });
85
+ this.cursor++;
86
+ }
87
+ replaceState(state, _title, url) {
88
+ this.entries[this.cursor] = { url, state };
89
+ }
90
+ forward() {
91
+ this.historyGo(1);
92
+ }
93
+ back() {
94
+ this.historyGo(-1);
95
+ }
96
+ historyGo(relativePosition = 0) {
97
+ const target = this.cursor + relativePosition;
98
+ // Out of range is a no-op, matching the browser rather than throwing.
99
+ if (target < 0 || target >= this.entries.length)
100
+ return;
101
+ this.cursor = target;
102
+ this.emitPopState();
103
+ }
104
+ emitPopState() {
105
+ const event = { type: 'popstate', state: this.current.state };
106
+ for (const listener of [...this.popStateListeners])
107
+ listener(event);
108
+ }
109
+ }
110
+ //# sourceMappingURL=native-platform-location.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-platform-location.js","sourceRoot":"","sources":["../src/native-platform-location.ts"],"names":[],"mappings":"AA8BA,8DAA8D;AAC9D,SAAS,KAAK,CAAC,GAAW;IACxB,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACpD,MAAM,WAAW,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAE/D,MAAM,QAAQ,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAClE,MAAM,QAAQ,GAAG,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IAEhF,OAAO,EAAE,QAAQ,EAAE,QAAQ,IAAI,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AACrD,CAAC;AAED,MAAM,OAAO,sBAAsB;IAChB,OAAO,GAAmB,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/D,MAAM,GAAG,CAAC,CAAC;IAEF,iBAAiB,GAAG,IAAI,GAAG,EAA0B,CAAC;IACtD,mBAAmB,GAAG,IAAI,GAAG,EAA0B,CAAC;IAEzE;;;;OAIG;IACH,YAAY,QAA2B,IAAI;QACzC,MAAM,OAAO,GAAG,KAAK,EAAE,UAAU,EAAE,CAAC;QACpC,IAAI,OAAO;YAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QAC7D,KAAK,EAAE,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5C,CAAC;IAED,4DAA4D;IAC5D,IAAI,CAAC,GAAW;QACd,IAAI,GAAG,KAAK,IAAI,CAAC,OAAO,CAAC,GAAG;YAAE,OAAO;QACrC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;QAC9B,IAAI,CAAC,YAAY,EAAE,CAAC;IACtB,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,MAAc;QACnB,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,IAAI,IAAI,CAAC;IACzD,CAAC;IAED,IAAY,OAAO;QACjB,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAE,CAAC;IACpC,CAAC;IAED,2EAA2E;IAE3E,kBAAkB;QAChB,OAAO,GAAG,CAAC;IACb,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;IAC5B,CAAC;IAED,UAAU,CAAC,EAA0B;QACnC,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC/B,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACjD,CAAC;IAED,YAAY,CAAC,EAA0B;QACrC,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACjC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACnD,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC;IAC1B,CAAC;IAED,6FAA6F;IAC7F,IAAI,QAAQ;QACV,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,IAAI;QACN,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC;IAC1C,CAAC;IAED,IAAI,MAAM;QACR,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;IACxC,CAAC;IAED,IAAI,IAAI;QACN,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;IACtC,CAAC;IAED,SAAS,CAAC,KAAc,EAAE,MAAc,EAAE,GAAW;QACnD,2EAA2E;QAC3E,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QACtC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;QAClC,IAAI,CAAC,MAAM,EAAE,CAAC;IAChB,CAAC;IAED,YAAY,CAAC,KAAc,EAAE,MAAc,EAAE,GAAW;QACtD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;IAC7C,CAAC;IAED,OAAO;QACL,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,IAAI;QACF,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IAED,SAAS,CAAC,gBAAgB,GAAG,CAAC;QAC5B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,GAAG,gBAAgB,CAAC;QAC9C,sEAAsE;QACtE,IAAI,MAAM,GAAG,CAAC,IAAI,MAAM,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM;YAAE,OAAO;QACxD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,YAAY,EAAE,CAAC;IACtB,CAAC;IAEO,YAAY;QAClB,MAAM,KAAK,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QAC9D,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,IAAI,CAAC,iBAAiB,CAAC;YAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IACtE,CAAC;CACF","sourcesContent":["/**\n * `PlatformLocation` over an in-memory history.\n *\n * Angular's `Location` reads and writes the URL through this. On the web the browser owns the\n * history stack; here nothing does, so we keep it ourselves and the URL tree stays the source of\n * truth in both directions: `router.navigate()` pushes an entry, and a swipe-back reaches the\n * outlet as the screen's `dismissed` event, which usually calls `historyGo(-1)` here and emits\n * `popstate` so the router follows. Not always: see the stack outlet's `popBy` for when the entry\n * behind is another tab.\n *\n * No decorators, so tests can import it at runtime and so it can be provided with `useClass`.\n */\nimport type { LocationChangeListener, PlatformLocation } from '@angular/common';\n/**\n * What the history needs of a deep-link source, which `DeepLinks` from\n * `@ng-native/device` satisfies. Structural rather than the class itself: the router has no\n * use for the rest of it, and a test has no use for a real one.\n */\nexport interface LinkSource {\n /** The path the app was launched with, or null for a normal launch. */\n initialUrl(): string | null;\n /** Links that arrive while the app is running. Returns an unsubscribe. */\n subscribe(listener: (url: string) => void): () => void;\n}\n\ninterface HistoryEntry {\n url: string;\n state: unknown;\n}\n\n/** Split a url into its path, `?search` and `#hash` parts. */\nfunction parse(url: string): { pathname: string; search: string; hash: string } {\n const hashAt = url.indexOf('#');\n const hash = hashAt === -1 ? '' : url.slice(hashAt);\n const withoutHash = hashAt === -1 ? url : url.slice(0, hashAt);\n\n const searchAt = withoutHash.indexOf('?');\n const search = searchAt === -1 ? '' : withoutHash.slice(searchAt);\n const pathname = searchAt === -1 ? withoutHash : withoutHash.slice(0, searchAt);\n\n return { pathname: pathname || '/', search, hash };\n}\n\nexport class NativePlatformLocation implements PlatformLocation {\n private readonly entries: HistoryEntry[] = [{ url: '/', state: null }];\n private cursor = 0;\n\n private readonly popStateListeners = new Set<LocationChangeListener>();\n private readonly hashChangeListeners = new Set<LocationChangeListener>();\n\n /**\n * A deep link is history, not a navigation of its own: the launch url *is* the first entry,\n * and one that arrives later is pushed and announced as a popstate, which is the only thing\n * the router listens to. `links` is null in a test that has no interest in either.\n */\n constructor(links: LinkSource | null = null) {\n const initial = links?.initialUrl();\n if (initial) this.entries[0] = { url: initial, state: null };\n links?.subscribe((url) => this.open(url));\n }\n\n /** Follow a link that arrived while the app was running. */\n open(url: string): void {\n if (url === this.current.url) return;\n this.pushState(null, '', url);\n this.emitPopState();\n }\n\n /**\n * The url of the entry `offset` away from the current one, or null past either end. For the\n * stack outlet, which has to know whether going back reaches the screen a native pop revealed.\n */\n ɵurlAt(offset: number): string | null {\n return this.entries[this.cursor + offset]?.url ?? null;\n }\n\n private get current(): HistoryEntry {\n return this.entries[this.cursor]!;\n }\n\n // --- PlatformLocation ---------------------------------------------------\n\n getBaseHrefFromDOM(): string {\n return '/';\n }\n\n getState(): unknown {\n return this.current.state;\n }\n\n onPopState(fn: LocationChangeListener): VoidFunction {\n this.popStateListeners.add(fn);\n return () => this.popStateListeners.delete(fn);\n }\n\n onHashChange(fn: LocationChangeListener): VoidFunction {\n this.hashChangeListeners.add(fn);\n return () => this.hashChangeListeners.delete(fn);\n }\n\n get href(): string {\n return this.current.url;\n }\n\n /** There is no origin on a device. Angular only ever compares these, never resolves them. */\n get protocol(): string {\n return 'app:';\n }\n\n get hostname(): string {\n return '';\n }\n\n get port(): string {\n return '';\n }\n\n get pathname(): string {\n return parse(this.current.url).pathname;\n }\n\n get search(): string {\n return parse(this.current.url).search;\n }\n\n get hash(): string {\n return parse(this.current.url).hash;\n }\n\n pushState(state: unknown, _title: string, url: string): void {\n // A push after going back discards the forward entries, as a browser does.\n this.entries.length = this.cursor + 1;\n this.entries.push({ url, state });\n this.cursor++;\n }\n\n replaceState(state: unknown, _title: string, url: string): void {\n this.entries[this.cursor] = { url, state };\n }\n\n forward(): void {\n this.historyGo(1);\n }\n\n back(): void {\n this.historyGo(-1);\n }\n\n historyGo(relativePosition = 0): void {\n const target = this.cursor + relativePosition;\n // Out of range is a no-op, matching the browser rather than throwing.\n if (target < 0 || target >= this.entries.length) return;\n this.cursor = target;\n this.emitPopState();\n }\n\n private emitPopState(): void {\n const event = { type: 'popstate', state: this.current.state };\n for (const listener of [...this.popStateListeners]) listener(event);\n }\n}\n"]}
@@ -0,0 +1,14 @@
1
+ import { type NavigationExtras } from '@angular/router';
2
+ import * as i0 from "@angular/core";
3
+ export declare class NativeRouterLink {
4
+ /** A url string, or the command array `Router.navigate` takes. */
5
+ readonly nativeRouterLink: import("@angular/core").InputSignal<string | readonly unknown[]>;
6
+ readonly extras: import("@angular/core").InputSignal<NavigationExtras | undefined>;
7
+ /** Replace the current screen rather than pushing a new one. */
8
+ readonly replace: import("@angular/core").InputSignalWithTransform<boolean, unknown>;
9
+ private readonly router;
10
+ private readonly route;
11
+ protected navigate(): void;
12
+ static ɵfac: i0.ɵɵFactoryDeclaration<NativeRouterLink, never>;
13
+ static ɵdir: i0.ɵɵDirectiveDeclaration<NativeRouterLink, "[nativeRouterLink]", never, { "nativeRouterLink": { "alias": "nativeRouterLink"; "required": true; "isSignal": true; }; "extras": { "alias": "extras"; "required": false; "isSignal": true; }; "replace": { "alias": "replace"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
14
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `routerLink` for native.
3
+ *
4
+ * Angular's `RouterLink` host-binds `href` and listens for a DOM click, neither of which exists
5
+ * here. This binds to a `<pressable>`'s `press` output instead.
6
+ *
7
+ * Because Angular resolves outputs across every directive on an element, the host listener below
8
+ * binds to `Pressable`'s `press` output without importing it, so the router package does not
9
+ * depend on the components package.
10
+ */
11
+ import { Directive, booleanAttribute, inject, input } from '@angular/core';
12
+ import { ActivatedRoute, Router } from '@angular/router';
13
+ import { NATIVE_INTENT } from "./native-navigation.js";
14
+ import * as i0 from "@angular/core";
15
+ export class NativeRouterLink {
16
+ /** A url string, or the command array `Router.navigate` takes. */
17
+ nativeRouterLink = input.required(/* @ts-ignore */
18
+ ...(ngDevMode ? [{ debugName: "nativeRouterLink" }] : /* istanbul ignore next */ []));
19
+ extras = input(/* @ts-ignore */
20
+ ...(ngDevMode ? [undefined, { debugName: "extras" }] : /* istanbul ignore next */ []));
21
+ /** Replace the current screen rather than pushing a new one. */
22
+ replace = input(false, { ...(ngDevMode ? { debugName: "replace" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
23
+ router = inject(Router);
24
+ route = inject(ActivatedRoute, { optional: true });
25
+ navigate() {
26
+ const target = this.nativeRouterLink();
27
+ const own = this.extras();
28
+ const extras = {
29
+ relativeTo: this.route,
30
+ replaceUrl: this.replace(),
31
+ ...own,
32
+ };
33
+ // `replaceUrl` only swaps the history entry. The stack reads the intent to swap the screen,
34
+ // as `NativeNavigation.replace` sets it; without it the replaced screen stays mounted
35
+ // underneath, and a swipe back lands on a page the history no longer has.
36
+ if (this.replace()) {
37
+ extras.state = { ...own?.state, [NATIVE_INTENT]: { stack: 'replace' } };
38
+ }
39
+ void this.router.navigate(Array.isArray(target) ? [...target] : [target], extras);
40
+ }
41
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: NativeRouterLink, deps: [], target: i0.ɵɵFactoryTarget.Directive });
42
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.2.0", type: NativeRouterLink, isStandalone: true, selector: "[nativeRouterLink]", inputs: { nativeRouterLink: { classPropertyName: "nativeRouterLink", publicName: "nativeRouterLink", isSignal: true, isRequired: true, transformFunction: null }, extras: { classPropertyName: "extras", publicName: "extras", isSignal: true, isRequired: false, transformFunction: null }, replace: { classPropertyName: "replace", publicName: "replace", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "press": "navigate()" } }, ngImport: i0 });
43
+ }
44
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: NativeRouterLink, decorators: [{
45
+ type: Directive,
46
+ args: [{
47
+ selector: '[nativeRouterLink]',
48
+ host: { '(press)': 'navigate()' },
49
+ }]
50
+ }], propDecorators: { nativeRouterLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "nativeRouterLink", required: true }] }], extras: [{ type: i0.Input, args: [{ isSignal: true, alias: "extras", required: false }] }], replace: [{ type: i0.Input, args: [{ isSignal: true, alias: "replace", required: false }] }] } });
51
+ //# sourceMappingURL=native-router-link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-router-link.js","sourceRoot":"","sources":["../src/native-router-link.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3E,OAAO,EAAE,cAAc,EAAE,MAAM,EAAyB,MAAM,iBAAiB,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;;AAMvD,MAAM,OAAO,gBAAgB;IAC3B,kEAAkE;IACzD,gBAAgB,GAAG,KAAK,CAAC,QAAQ;yFAA+B,CAAC;IACjE,MAAM,GAAG,KAAK;0FAAoB,CAAC;IAE5C,gEAAgE;IACvD,OAAO,GAAG,KAAK,CAAC,KAAK,+EAAI,SAAS,EAAE,gBAAgB,GAAG,CAAC;IAEhD,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IACxB,KAAK,GAAG,MAAM,CAAC,cAAc,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IAE1D,QAAQ;QAChB,MAAM,MAAM,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACvC,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAqB;YAC/B,UAAU,EAAE,IAAI,CAAC,KAAK;YACtB,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE;YAC1B,GAAG,GAAG;SACP,CAAC;QACF,4FAA4F;QAC5F,sFAAsF;QACtF,0EAA0E;QAC1E,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;YACnB,MAAM,CAAC,KAAK,GAAG,EAAE,GAAG,GAAG,EAAE,KAAK,EAAE,CAAC,aAAa,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,CAAC;QAC1E,CAAC;QAED,KAAK,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;IACpF,CAAC;uGA3BU,gBAAgB;2FAAhB,gBAAgB;;2FAAhB,gBAAgB;kBAJ5B,SAAS;mBAAC;oBACT,QAAQ,EAAE,oBAAoB;oBAC9B,IAAI,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE;iBAClC","sourcesContent":["/**\n * `routerLink` for native.\n *\n * Angular's `RouterLink` host-binds `href` and listens for a DOM click, neither of which exists\n * here. This binds to a `<pressable>`'s `press` output instead.\n *\n * Because Angular resolves outputs across every directive on an element, the host listener below\n * binds to `Pressable`'s `press` output without importing it, so the router package does not\n * depend on the components package.\n */\nimport { Directive, booleanAttribute, inject, input } from '@angular/core';\nimport { ActivatedRoute, Router, type NavigationExtras } from '@angular/router';\nimport { NATIVE_INTENT } from './native-navigation.ts';\n\n@Directive({\n selector: '[nativeRouterLink]',\n host: { '(press)': 'navigate()' },\n})\nexport class NativeRouterLink {\n /** A url string, or the command array `Router.navigate` takes. */\n readonly nativeRouterLink = input.required<string | readonly unknown[]>();\n readonly extras = input<NavigationExtras>();\n\n /** Replace the current screen rather than pushing a new one. */\n readonly replace = input(false, { transform: booleanAttribute });\n\n private readonly router = inject(Router);\n private readonly route = inject(ActivatedRoute, { optional: true });\n\n protected navigate(): void {\n const target = this.nativeRouterLink();\n const own = this.extras();\n const extras: NavigationExtras = {\n relativeTo: this.route,\n replaceUrl: this.replace(),\n ...own,\n };\n // `replaceUrl` only swaps the history entry. The stack reads the intent to swap the screen,\n // as `NativeNavigation.replace` sets it; without it the replaced screen stays mounted\n // underneath, and a swipe back lands on a page the history no longer has.\n if (this.replace()) {\n extras.state = { ...own?.state, [NATIVE_INTENT]: { stack: 'replace' } };\n }\n\n void this.router.navigate(Array.isArray(target) ? [...target] : [target], extras);\n }\n}\n"]}
@@ -0,0 +1,44 @@
1
+ import * as i0 from "@angular/core";
2
+ /** react-native-screens' placements for the navigation bar's search field. */
3
+ export type SearchBarPlacement = 'automatic' | 'inline' | 'stacked' | 'integrated' | 'integratedButton' | 'integratedCentered';
4
+ export declare class NativeSearchBar {
5
+ readonly query: import("@angular/core").ModelSignal<string>;
6
+ readonly placeholder: import("@angular/core").InputSignal<string | undefined>;
7
+ /** Scroll the field away with the content, as iOS lists do by default. */
8
+ readonly hideWhenScrolling: import("@angular/core").InputSignalWithTransform<boolean | undefined, unknown>;
9
+ readonly cancelButtonText: import("@angular/core").InputSignal<string | undefined>;
10
+ /**
11
+ * Where iOS puts the field. `automatic` lets iOS 26 fold it into a toolbar, which a presented
12
+ * sheet does not have, so there it shows nowhere; `stacked` keeps it under the title.
13
+ */
14
+ readonly placement: import("@angular/core").InputSignal<SearchBarPlacement | undefined>;
15
+ /** iOS 26: whether `automatic` may move the field into the toolbar. */
16
+ readonly allowToolbarIntegration: import("@angular/core").InputSignalWithTransform<boolean | undefined, unknown>;
17
+ readonly autoCapitalize: import("@angular/core").InputSignal<"none" | "systemDefault" | "words" | "sentences" | "characters" | undefined>;
18
+ /** Dim the content behind the field while it is being typed in. */
19
+ readonly obscureBackground: import("@angular/core").InputSignalWithTransform<boolean | undefined, unknown>;
20
+ /** Hide the navigation bar while the field is being typed in. */
21
+ readonly hideNavigationBar: import("@angular/core").InputSignalWithTransform<boolean | undefined, unknown>;
22
+ readonly tintColor: import("@angular/core").InputSignal<string | undefined>;
23
+ readonly textColor: import("@angular/core").InputSignal<string | undefined>;
24
+ /** The keyboard's search key was pressed, with the text in the field. */
25
+ readonly search: import("@angular/core").OutputEmitterRef<string>;
26
+ /** The Cancel button was pressed. */
27
+ readonly cancel: import("@angular/core").OutputEmitterRef<void>;
28
+ readonly searchFocus: import("@angular/core").OutputEmitterRef<void>;
29
+ readonly searchBlur: import("@angular/core").OutputEmitterRef<void>;
30
+ private readonly node;
31
+ private readonly engine;
32
+ /** What the field last said it holds, to tell a query set from code from the field's echo. */
33
+ private fieldText;
34
+ constructor();
35
+ focus(): void;
36
+ blur(): void;
37
+ /** Empty the field, and the query with it. */
38
+ clear(): void;
39
+ /** Close the search, as the Cancel button does. */
40
+ cancelSearch(): void;
41
+ protected onNativeText(text: string): void;
42
+ static ɵfac: i0.ɵɵFactoryDeclaration<NativeSearchBar, never>;
43
+ static ɵcmp: i0.ɵɵComponentDeclaration<NativeSearchBar, "native-search-bar", never, { "query": { "alias": "query"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "hideWhenScrolling": { "alias": "hideWhenScrolling"; "required": false; "isSignal": true; }; "cancelButtonText": { "alias": "cancelButtonText"; "required": false; "isSignal": true; }; "placement": { "alias": "placement"; "required": false; "isSignal": true; }; "allowToolbarIntegration": { "alias": "allowToolbarIntegration"; "required": false; "isSignal": true; }; "autoCapitalize": { "alias": "autoCapitalize"; "required": false; "isSignal": true; }; "obscureBackground": { "alias": "obscureBackground"; "required": false; "isSignal": true; }; "hideNavigationBar": { "alias": "hideNavigationBar"; "required": false; "isSignal": true; }; "tintColor": { "alias": "tintColor"; "required": false; "isSignal": true; }; "textColor": { "alias": "textColor"; "required": false; "isSignal": true; }; }, { "query": "queryChange"; "search": "search"; "cancel": "cancel"; "searchFocus": "searchFocus"; "searchBlur": "searchBlur"; }, never, never, true, never>;
44
+ }