@benjavicente/angular-router-experimental 1.142.11

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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +15 -0
  3. package/dist/fesm2022/tanstack-angular-router-experimental-experimental.mjs +920 -0
  4. package/dist/fesm2022/tanstack-angular-router-experimental.mjs +4131 -0
  5. package/dist/types/tanstack-angular-router-experimental-experimental.d.ts +110 -0
  6. package/dist/types/tanstack-angular-router-experimental.d.ts +733 -0
  7. package/experimental/injectRouteErrorHandler.ts +51 -0
  8. package/experimental/public_api.ts +8 -0
  9. package/package.json +98 -0
  10. package/src/DefaultNotFound.ts +9 -0
  11. package/src/Link.ts +352 -0
  12. package/src/Match.ts +338 -0
  13. package/src/Matches.ts +37 -0
  14. package/src/RouterProvider.ts +162 -0
  15. package/src/document/build-match-managed-document.ts +308 -0
  16. package/src/document/document-dehydration.ts +27 -0
  17. package/src/document/document-equality.ts +29 -0
  18. package/src/document/document-router-token.ts +6 -0
  19. package/src/document/index.ts +33 -0
  20. package/src/document/install-unified-document-sync.ts +108 -0
  21. package/src/document/managed-document-types.ts +36 -0
  22. package/src/document/managed-dom.ts +307 -0
  23. package/src/document/provide-tanstack-body-managed-tags.ts +78 -0
  24. package/src/document/provide-tanstack-document-title.ts +59 -0
  25. package/src/document/provide-tanstack-document.ts +62 -0
  26. package/src/document/provide-tanstack-head-managed-tags.ts +63 -0
  27. package/src/fileRoute.ts +232 -0
  28. package/src/index.ts +173 -0
  29. package/src/injectBlocker.ts +196 -0
  30. package/src/injectCanGoBack.ts +11 -0
  31. package/src/injectErrorState.ts +21 -0
  32. package/src/injectIntersectionObserver.ts +28 -0
  33. package/src/injectLoaderData.ts +49 -0
  34. package/src/injectLoaderDeps.ts +45 -0
  35. package/src/injectLocation.ts +38 -0
  36. package/src/injectMatch.ts +122 -0
  37. package/src/injectMatchRoute.ts +58 -0
  38. package/src/injectMatches.ts +79 -0
  39. package/src/injectNavigate.ts +24 -0
  40. package/src/injectParams.ts +71 -0
  41. package/src/injectRouteContext.ts +31 -0
  42. package/src/injectRouter.ts +17 -0
  43. package/src/injectRouterState.ts +53 -0
  44. package/src/injectSearch.ts +71 -0
  45. package/src/injectStore.ts +87 -0
  46. package/src/matchInjectorToken.ts +23 -0
  47. package/src/renderer/injectIsCatchingError.ts +40 -0
  48. package/src/renderer/injectRender.ts +69 -0
  49. package/src/route.ts +641 -0
  50. package/src/router.ts +141 -0
  51. package/src/routerInjectionToken.ts +24 -0
  52. package/src/routerStores.ts +107 -0
  53. package/src/ssr-scroll-restoration.ts +48 -0
  54. package/src/transitioner.ts +255 -0
@@ -0,0 +1,307 @@
1
+ import {
2
+
3
+
4
+
5
+
6
+
7
+ TSR_ID_ATTR,
8
+ TSR_MANAGED_ATTR
9
+ } from './managed-document-types'
10
+ import type {ManagedBucket, ManagedTag, ManagedTagCollection, ManagedTagId, ManagedTagRecord} from './managed-document-types';
11
+ import type * as Angular from '@angular/core'
12
+ import type { RouterManagedTag } from '@benjavicente/router-core'
13
+
14
+ export function normalizeManagedTag(
15
+ tag: RouterManagedTag | undefined,
16
+ ): Array<ManagedTag> {
17
+ if (!tag) {
18
+ return []
19
+ }
20
+
21
+ if (tag.tag === 'title') {
22
+ return []
23
+ }
24
+
25
+ if (tag.tag === 'script') {
26
+ const src = tag.attrs?.src
27
+ if (!src && typeof tag.children !== 'string') {
28
+ return []
29
+ }
30
+ }
31
+
32
+ const normalizedTag = {
33
+ ...tag,
34
+ attrs: normalizeAttrs(tag.attrs),
35
+ } as Exclude<RouterManagedTag, { tag: 'title' }>
36
+
37
+ return [
38
+ {
39
+ ...normalizedTag,
40
+ id: hashManagedTag(normalizedTag),
41
+ },
42
+ ]
43
+ }
44
+
45
+ function normalizeAttrs(attrs?: Record<string, unknown>) {
46
+ if (!attrs) {
47
+ return {}
48
+ }
49
+
50
+ const normalized: Record<string, unknown> = {}
51
+
52
+ for (const key of Object.keys(attrs).sort()) {
53
+ const value = attrs[key]
54
+
55
+ if (
56
+ value === undefined ||
57
+ value === null ||
58
+ value === false ||
59
+ key === 'suppressHydrationWarning'
60
+ ) {
61
+ continue
62
+ }
63
+
64
+ const attrName = key === 'className' ? 'class' : key
65
+ normalized[attrName] = value
66
+ }
67
+
68
+ return normalized
69
+ }
70
+
71
+ export function uniqManagedTags(tags: Array<ManagedTag>) {
72
+ const seen = new Set<ManagedTagId>()
73
+
74
+ return tags.filter((tag) => {
75
+ if (seen.has(tag.id)) {
76
+ return false
77
+ }
78
+
79
+ seen.add(tag.id)
80
+ return true
81
+ })
82
+ }
83
+
84
+ export function hashManagedTag(
85
+ tag: Exclude<RouterManagedTag, { tag: 'title' }>,
86
+ ): ManagedTagId {
87
+ const source = JSON.stringify(tag)
88
+ let hash = 2166136261
89
+
90
+ for (let i = 0; i < source.length; i++) {
91
+ hash ^= source.charCodeAt(i)
92
+ hash = Math.imul(hash, 16777619)
93
+ }
94
+
95
+ return `tsr-${(hash >>> 0).toString(36)}`
96
+ }
97
+
98
+ export function createManagedTagCollection({
99
+ root,
100
+ bucket,
101
+ renderer,
102
+ }: {
103
+ root: HTMLElement
104
+ bucket: ManagedBucket
105
+ renderer: Angular.Renderer2 | null
106
+ }): ManagedTagCollection {
107
+ const nodesById = new Map<ManagedTagId, HTMLElement>()
108
+ const itemsById = new Map<ManagedTagId, ManagedTagRecord>()
109
+ let orderedIds: Array<ManagedTagId> = []
110
+
111
+ return {
112
+ mount(initialNodes = adoptManagedNodes(root, bucket)) {
113
+ nodesById.clear()
114
+ itemsById.clear()
115
+ orderedIds = []
116
+
117
+ initialNodes.forEach((node) => {
118
+ const id = node.getAttribute(TSR_ID_ATTR)
119
+ if (!id) {
120
+ return
121
+ }
122
+
123
+ nodesById.set(id, node)
124
+ orderedIds.push(id)
125
+ })
126
+ },
127
+ sync(nextTags) {
128
+ const nextIds = nextTags.map((tag) => tag.id)
129
+
130
+ if (areOrderedIdsEqual(orderedIds, nextIds)) {
131
+ return
132
+ }
133
+
134
+ const nextItemsById = new Map<ManagedTagId, ManagedTagRecord>(
135
+ nextTags.map((tag) => [tag.id, { id: tag.id, tag }]),
136
+ )
137
+
138
+ for (const id of orderedIds) {
139
+ if (nextItemsById.has(id)) {
140
+ continue
141
+ }
142
+
143
+ const node = nodesById.get(id)
144
+ if (!node) {
145
+ continue
146
+ }
147
+
148
+ removeNode(node, root, renderer)
149
+ nodesById.delete(id)
150
+ itemsById.delete(id)
151
+ }
152
+
153
+ let cursor: ChildNode | null = root.firstChild
154
+
155
+ for (const tag of nextTags) {
156
+ let node = nodesById.get(tag.id)
157
+
158
+ if (!node) {
159
+ node = createManagedNode(tag, bucket, root.ownerDocument, renderer)
160
+ nodesById.set(tag.id, node)
161
+ }
162
+
163
+ if (node !== cursor) {
164
+ insertNodeBefore(root, node, cursor, renderer)
165
+ }
166
+
167
+ itemsById.set(tag.id, {
168
+ id: tag.id,
169
+ tag,
170
+ })
171
+ cursor = node.nextSibling
172
+ }
173
+
174
+ orderedIds = nextIds
175
+ },
176
+ destroy() {
177
+ nodesById.forEach((node) => {
178
+ node.parentNode?.removeChild(node)
179
+ })
180
+
181
+ nodesById.clear()
182
+ itemsById.clear()
183
+ orderedIds = []
184
+ },
185
+ }
186
+ }
187
+
188
+ function adoptManagedNodes(root: HTMLElement, bucket: ManagedBucket) {
189
+ return Array.from(
190
+ root.querySelectorAll<HTMLElement>(`[${TSR_MANAGED_ATTR}="${bucket}"]`),
191
+ )
192
+ }
193
+
194
+ function createManagedNode(
195
+ tag: ManagedTag,
196
+ bucket: ManagedBucket,
197
+ document: Document,
198
+ renderer: Angular.Renderer2 | null,
199
+ ) {
200
+ const node = createElement(document, tag.tag, renderer)
201
+ setAttribute(node, TSR_MANAGED_ATTR, bucket, renderer)
202
+ setAttribute(node, TSR_ID_ATTR, tag.id, renderer)
203
+
204
+ for (const [attrName, attrValue] of Object.entries(tag.attrs ?? {})) {
205
+ if (attrName === 'suppressHydrationWarning') continue
206
+
207
+ if (attrValue === undefined || attrValue === null || attrValue === false) {
208
+ continue
209
+ }
210
+
211
+ const domAttrName = attrName === 'className' ? 'class' : attrName
212
+ const domAttrValue =
213
+ typeof attrValue === 'boolean' ? '' : String(attrValue)
214
+ setAttribute(node, domAttrName, domAttrValue, renderer)
215
+ }
216
+
217
+ if ('children' in tag && typeof tag.children === 'string') {
218
+ setTextContent(node, tag.children, renderer)
219
+ }
220
+
221
+ return node
222
+ }
223
+
224
+ function createElement(
225
+ document: Document,
226
+ tagName: string,
227
+ renderer: Angular.Renderer2 | null,
228
+ ) {
229
+ return renderer
230
+ ? (renderer.createElement(tagName) as HTMLElement)
231
+ : document.createElement(tagName)
232
+ }
233
+
234
+ function setAttribute(
235
+ node: HTMLElement,
236
+ name: string,
237
+ value: string,
238
+ renderer: Angular.Renderer2 | null,
239
+ ) {
240
+ if (renderer) {
241
+ renderer.setAttribute(node, name, value)
242
+ return
243
+ }
244
+
245
+ node.setAttribute(name, value)
246
+ }
247
+
248
+ function setTextContent(
249
+ node: HTMLElement,
250
+ value: string,
251
+ renderer: Angular.Renderer2 | null,
252
+ ) {
253
+ if (renderer) {
254
+ renderer.setProperty(node, 'textContent', value)
255
+ return
256
+ }
257
+
258
+ node.textContent = value
259
+ }
260
+
261
+ function insertNodeBefore(
262
+ root: HTMLElement,
263
+ node: HTMLElement,
264
+ before: ChildNode | null,
265
+ renderer: Angular.Renderer2 | null,
266
+ ) {
267
+ if (node.parentNode === root && node.nextSibling === before) {
268
+ return
269
+ }
270
+
271
+ if (renderer) {
272
+ renderer.insertBefore(root, node, before)
273
+ return
274
+ }
275
+
276
+ root.insertBefore(node, before)
277
+ }
278
+
279
+ function removeNode(
280
+ node: HTMLElement,
281
+ root: HTMLElement,
282
+ renderer: Angular.Renderer2 | null,
283
+ ) {
284
+ if (renderer) {
285
+ renderer.removeChild(root, node)
286
+ return
287
+ }
288
+
289
+ root.removeChild(node)
290
+ }
291
+
292
+ function areOrderedIdsEqual(
293
+ prev: Array<ManagedTagId>,
294
+ next: Array<ManagedTagId>,
295
+ ) {
296
+ if (prev.length !== next.length) {
297
+ return false
298
+ }
299
+
300
+ for (let i = 0; i < prev.length; i++) {
301
+ if (prev[i] !== next[i]) {
302
+ return false
303
+ }
304
+ }
305
+
306
+ return true
307
+ }
@@ -0,0 +1,78 @@
1
+ import { DOCUMENT } from '@angular/common'
2
+ import * as Angular from '@angular/core'
3
+ import { injectStore } from '../injectStore'
4
+ import { buildMatchManagedDocumentContent } from './build-match-managed-document'
5
+ import { collectDehydrationScriptManagedTags } from './document-dehydration'
6
+ import { areManagedTagArraysEqual } from './document-equality'
7
+ import { TANSTACK_DOCUMENT_ROUTER } from './document-router-token'
8
+ import { createManagedTagCollection, uniqManagedTags } from './managed-dom'
9
+ import type { AnyRouter } from '@benjavicente/router-core'
10
+ import type { ManagedTag } from './managed-document-types'
11
+
12
+ /**
13
+ * Managed body scripts from matches, with a single `takeBufferedScripts()` capture
14
+ * on the server only (call from an injection context).
15
+ */
16
+ export function installTanstackBodyManagedTags(injectedRouter: AnyRouter) {
17
+ const document = Angular.inject(DOCUMENT)
18
+ const rendererFactory = Angular.inject(Angular.RendererFactory2, {
19
+ optional: true,
20
+ })
21
+ const destroyRef = Angular.inject(Angular.DestroyRef)
22
+ const activeMatches = injectStore(
23
+ injectedRouter.stores.activeMatchesSnapshot,
24
+ (matches) => matches,
25
+ )
26
+
27
+ /** One read per bootstrap when `router.serverSsr` exists (SSR); otherwise empty. */
28
+ const dehydrationBodyPrefix =
29
+ collectDehydrationScriptManagedTags(injectedRouter)
30
+
31
+ const composeBody = (): Array<ManagedTag> =>
32
+ dehydrationBodyPrefix.length === 0
33
+ ? buildMatchManagedDocumentContent(injectedRouter).body
34
+ : uniqManagedTags([
35
+ ...dehydrationBodyPrefix,
36
+ ...buildMatchManagedDocumentContent(injectedRouter).body,
37
+ ])
38
+
39
+ const renderer = rendererFactory?.createRenderer(null, null) ?? null
40
+ const bodyCollection = createManagedTagCollection({
41
+ root: document.body,
42
+ bucket: 'body',
43
+ renderer,
44
+ })
45
+
46
+ bodyCollection.mount()
47
+
48
+ let currentBody: Array<ManagedTag> = composeBody()
49
+ bodyCollection.sync(currentBody)
50
+
51
+ const syncEffect = Angular.effect(() => {
52
+ activeMatches()
53
+ const nextBody = composeBody()
54
+ if (areManagedTagArraysEqual(currentBody, nextBody)) {
55
+ return
56
+ }
57
+ currentBody = nextBody
58
+ bodyCollection.sync(nextBody)
59
+ })
60
+
61
+ destroyRef.onDestroy(() => {
62
+ syncEffect.destroy()
63
+ bodyCollection.destroy()
64
+ })
65
+ }
66
+
67
+ export function tanstackBodyManagedTagsInitializer() {
68
+ installTanstackBodyManagedTags(Angular.inject(TANSTACK_DOCUMENT_ROUTER))
69
+ }
70
+
71
+ export function provideTanstackBodyManagedTags(
72
+ router: AnyRouter,
73
+ ): Angular.EnvironmentProviders {
74
+ return Angular.makeEnvironmentProviders([
75
+ { provide: TANSTACK_DOCUMENT_ROUTER, useValue: router },
76
+ Angular.provideEnvironmentInitializer(tanstackBodyManagedTagsInitializer),
77
+ ])
78
+ }
@@ -0,0 +1,59 @@
1
+ import { DOCUMENT } from '@angular/common'
2
+ import * as Angular from '@angular/core'
3
+ import { injectStore } from '../injectStore'
4
+ import { buildMatchManagedDocumentContent } from './build-match-managed-document'
5
+ import { TANSTACK_DOCUMENT_ROUTER } from './document-router-token'
6
+ import type { AnyRouter } from '@benjavicente/router-core'
7
+
8
+ /** Sync `document.title` from the router (call from an injection context). */
9
+ export function installTanstackDocumentTitle(injectedRouter: AnyRouter) {
10
+ const document = Angular.inject(DOCUMENT)
11
+ const destroyRef = Angular.inject(Angular.DestroyRef)
12
+ const activeMatches = injectStore(
13
+ injectedRouter.stores.activeMatchesSnapshot,
14
+ (matches) => matches,
15
+ )
16
+
17
+ const initialTitle = document.title
18
+ let currentTitle: string | undefined = buildMatchManagedDocumentContent(
19
+ injectedRouter,
20
+ ).title
21
+
22
+ const applyTitle = (next?: string) => {
23
+ if (next !== undefined) {
24
+ document.title = next
25
+ } else {
26
+ document.title = initialTitle
27
+ }
28
+ }
29
+
30
+ applyTitle(currentTitle)
31
+
32
+ const syncEffect = Angular.effect(() => {
33
+ activeMatches()
34
+ const nextTitle = buildMatchManagedDocumentContent(injectedRouter).title
35
+ if (nextTitle === currentTitle) {
36
+ return
37
+ }
38
+ currentTitle = nextTitle
39
+ applyTitle(nextTitle)
40
+ })
41
+
42
+ destroyRef.onDestroy(() => {
43
+ syncEffect.destroy()
44
+ document.title = initialTitle
45
+ })
46
+ }
47
+
48
+ export function tanstackDocumentTitleInitializer() {
49
+ installTanstackDocumentTitle(Angular.inject(TANSTACK_DOCUMENT_ROUTER))
50
+ }
51
+
52
+ export function provideTanstackDocumentTitle(
53
+ router: AnyRouter,
54
+ ): Angular.EnvironmentProviders {
55
+ return Angular.makeEnvironmentProviders([
56
+ { provide: TANSTACK_DOCUMENT_ROUTER, useValue: router },
57
+ Angular.provideEnvironmentInitializer(tanstackDocumentTitleInitializer),
58
+ ])
59
+ }
@@ -0,0 +1,62 @@
1
+ import * as Angular from '@angular/core'
2
+ import { TANSTACK_DOCUMENT_ROUTER } from './document-router-token'
3
+ import { installUnifiedTanstackDocumentSync } from './install-unified-document-sync'
4
+ import { installTanstackBodyManagedTags } from './provide-tanstack-body-managed-tags'
5
+ import { installTanstackDocumentTitle } from './provide-tanstack-document-title'
6
+ import { installTanstackHeadManagedTags } from './provide-tanstack-head-managed-tags'
7
+ import type { AnyRouter } from '@benjavicente/router-core'
8
+
9
+ export type TanstackDocumentFeatures = {
10
+ /** Sync `document.title` from route meta (default: true). */
11
+ title?: boolean
12
+ /** Managed `<head>` tags from matches (default: true). */
13
+ headTags?: boolean
14
+ /** Managed `<body>` scripts: route/manifest scripts plus one SSR `takeBufferedScripts()` read (default: true). */
15
+ bodyScripts?: boolean
16
+ }
17
+
18
+ const defaultFeatures: Required<TanstackDocumentFeatures> = {
19
+ title: true,
20
+ headTags: true,
21
+ bodyScripts: true,
22
+ }
23
+
24
+ /**
25
+ * Composes document sync from route matches. When title, head, and body are all
26
+ * enabled, uses a single `activeMatches` effect (same churn as legacy
27
+ * `provideHeadContent`). Partial feature sets use separate installers each with
28
+ * their own effect.
29
+ */
30
+ export function provideTanstackDocument(
31
+ router: AnyRouter,
32
+ features: TanstackDocumentFeatures = {},
33
+ ): Angular.EnvironmentProviders {
34
+ const f = { ...defaultFeatures, ...features }
35
+ const allOn = f.title && f.headTags && f.bodyScripts
36
+
37
+ return Angular.makeEnvironmentProviders([
38
+ { provide: TANSTACK_DOCUMENT_ROUTER, useValue: router },
39
+ Angular.provideEnvironmentInitializer(() => {
40
+ if (allOn) {
41
+ installUnifiedTanstackDocumentSync(router)
42
+ return
43
+ }
44
+ if (f.title) {
45
+ installTanstackDocumentTitle(router)
46
+ }
47
+ if (f.headTags) {
48
+ installTanstackHeadManagedTags(router)
49
+ }
50
+ if (f.bodyScripts) {
51
+ installTanstackBodyManagedTags(router)
52
+ }
53
+ }),
54
+ ])
55
+ }
56
+
57
+ /** Same as `provideTanstackDocument(router)` (all features enabled). */
58
+ export function provideHeadContent(
59
+ router: AnyRouter,
60
+ ): Angular.EnvironmentProviders {
61
+ return provideTanstackDocument(router)
62
+ }
@@ -0,0 +1,63 @@
1
+ import { DOCUMENT } from '@angular/common'
2
+ import * as Angular from '@angular/core'
3
+ import { injectStore } from '../injectStore'
4
+ import { buildMatchManagedDocumentContent } from './build-match-managed-document'
5
+ import { areManagedTagArraysEqual } from './document-equality'
6
+ import { TANSTACK_DOCUMENT_ROUTER } from './document-router-token'
7
+ import { createManagedTagCollection } from './managed-dom'
8
+ import type { AnyRouter } from '@benjavicente/router-core'
9
+ import type { ManagedTag } from './managed-document-types'
10
+
11
+ /** Managed `<head>` tags from active matches (call from an injection context). */
12
+ export function installTanstackHeadManagedTags(injectedRouter: AnyRouter) {
13
+ const document = Angular.inject(DOCUMENT)
14
+ const rendererFactory = Angular.inject(Angular.RendererFactory2, {
15
+ optional: true,
16
+ })
17
+ const destroyRef = Angular.inject(Angular.DestroyRef)
18
+ const activeMatches = injectStore(
19
+ injectedRouter.stores.activeMatchesSnapshot,
20
+ (matches) => matches,
21
+ )
22
+
23
+ const renderer = rendererFactory?.createRenderer(null, null) ?? null
24
+ const headCollection = createManagedTagCollection({
25
+ root: document.head,
26
+ bucket: 'head',
27
+ renderer,
28
+ })
29
+
30
+ headCollection.mount()
31
+
32
+ let currentHead: Array<ManagedTag> =
33
+ buildMatchManagedDocumentContent(injectedRouter).head
34
+ headCollection.sync(currentHead)
35
+
36
+ const syncEffect = Angular.effect(() => {
37
+ activeMatches()
38
+ const nextHead = buildMatchManagedDocumentContent(injectedRouter).head
39
+ if (areManagedTagArraysEqual(currentHead, nextHead)) {
40
+ return
41
+ }
42
+ currentHead = nextHead
43
+ headCollection.sync(nextHead)
44
+ })
45
+
46
+ destroyRef.onDestroy(() => {
47
+ syncEffect.destroy()
48
+ headCollection.destroy()
49
+ })
50
+ }
51
+
52
+ export function tanstackHeadManagedTagsInitializer() {
53
+ installTanstackHeadManagedTags(Angular.inject(TANSTACK_DOCUMENT_ROUTER))
54
+ }
55
+
56
+ export function provideTanstackHeadManagedTags(
57
+ router: AnyRouter,
58
+ ): Angular.EnvironmentProviders {
59
+ return Angular.makeEnvironmentProviders([
60
+ { provide: TANSTACK_DOCUMENT_ROUTER, useValue: router },
61
+ Angular.provideEnvironmentInitializer(tanstackHeadManagedTagsInitializer),
62
+ ])
63
+ }