@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.
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/dist/fesm2022/tanstack-angular-router-experimental-experimental.mjs +920 -0
- package/dist/fesm2022/tanstack-angular-router-experimental.mjs +4131 -0
- package/dist/types/tanstack-angular-router-experimental-experimental.d.ts +110 -0
- package/dist/types/tanstack-angular-router-experimental.d.ts +733 -0
- package/experimental/injectRouteErrorHandler.ts +51 -0
- package/experimental/public_api.ts +8 -0
- package/package.json +98 -0
- package/src/DefaultNotFound.ts +9 -0
- package/src/Link.ts +352 -0
- package/src/Match.ts +338 -0
- package/src/Matches.ts +37 -0
- package/src/RouterProvider.ts +162 -0
- package/src/document/build-match-managed-document.ts +308 -0
- package/src/document/document-dehydration.ts +27 -0
- package/src/document/document-equality.ts +29 -0
- package/src/document/document-router-token.ts +6 -0
- package/src/document/index.ts +33 -0
- package/src/document/install-unified-document-sync.ts +108 -0
- package/src/document/managed-document-types.ts +36 -0
- package/src/document/managed-dom.ts +307 -0
- package/src/document/provide-tanstack-body-managed-tags.ts +78 -0
- package/src/document/provide-tanstack-document-title.ts +59 -0
- package/src/document/provide-tanstack-document.ts +62 -0
- package/src/document/provide-tanstack-head-managed-tags.ts +63 -0
- package/src/fileRoute.ts +232 -0
- package/src/index.ts +173 -0
- package/src/injectBlocker.ts +196 -0
- package/src/injectCanGoBack.ts +11 -0
- package/src/injectErrorState.ts +21 -0
- package/src/injectIntersectionObserver.ts +28 -0
- package/src/injectLoaderData.ts +49 -0
- package/src/injectLoaderDeps.ts +45 -0
- package/src/injectLocation.ts +38 -0
- package/src/injectMatch.ts +122 -0
- package/src/injectMatchRoute.ts +58 -0
- package/src/injectMatches.ts +79 -0
- package/src/injectNavigate.ts +24 -0
- package/src/injectParams.ts +71 -0
- package/src/injectRouteContext.ts +31 -0
- package/src/injectRouter.ts +17 -0
- package/src/injectRouterState.ts +53 -0
- package/src/injectSearch.ts +71 -0
- package/src/injectStore.ts +87 -0
- package/src/matchInjectorToken.ts +23 -0
- package/src/renderer/injectIsCatchingError.ts +40 -0
- package/src/renderer/injectRender.ts +69 -0
- package/src/route.ts +641 -0
- package/src/router.ts +141 -0
- package/src/routerInjectionToken.ts +24 -0
- package/src/routerStores.ts +107 -0
- package/src/ssr-scroll-restoration.ts +48 -0
- package/src/transitioner.ts +255 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { escapeHtml } from '@benjavicente/router-core'
|
|
2
|
+
import {
|
|
3
|
+
collectDehydrationScriptManagedTags,
|
|
4
|
+
mergeDehydrationPrefixIntoDocument,
|
|
5
|
+
} from './document-dehydration'
|
|
6
|
+
import { normalizeManagedTag, uniqManagedTags } from './managed-dom'
|
|
7
|
+
import type { AnyRouter, RouterManagedTag } from '@benjavicente/router-core'
|
|
8
|
+
import type { ManagedDocumentContent, RouteMetaEntry } from './managed-document-types'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Document content from the current route tree only (meta, links, route body scripts, etc.).
|
|
12
|
+
* Does not call `takeBufferedScripts` — mirrors the route-derived arrays passed into
|
|
13
|
+
* `renderScripts` before the server-buffered script is unshifted in React/Solid/Vue.
|
|
14
|
+
*/
|
|
15
|
+
export function buildMatchManagedDocumentContent(
|
|
16
|
+
router: AnyRouter,
|
|
17
|
+
): ManagedDocumentContent {
|
|
18
|
+
const nonce = router.options.ssr?.nonce
|
|
19
|
+
const matches = router.stores.activeMatchesSnapshot.state
|
|
20
|
+
const title = selectTitle(matches)
|
|
21
|
+
const metaTags = selectMetaTags(matches, nonce)
|
|
22
|
+
const links = selectConstructedLinks(matches, nonce)
|
|
23
|
+
const manifestLinks = selectManifestLinks(router, matches, nonce)
|
|
24
|
+
const preloadLinks = selectPreloadLinks(router, matches, nonce)
|
|
25
|
+
const styles = selectStyles(matches, nonce)
|
|
26
|
+
const headScripts = selectHeadScripts(matches, nonce)
|
|
27
|
+
const routeScripts = selectRouteScripts(matches, nonce)
|
|
28
|
+
const manifestScripts = selectManifestScripts(router, matches, nonce)
|
|
29
|
+
|
|
30
|
+
const head = uniqManagedTags(
|
|
31
|
+
[
|
|
32
|
+
...metaTags,
|
|
33
|
+
...preloadLinks,
|
|
34
|
+
...links,
|
|
35
|
+
...manifestLinks,
|
|
36
|
+
...styles,
|
|
37
|
+
...headScripts,
|
|
38
|
+
].flatMap((tag) => normalizeManagedTag(tag)),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const body = uniqManagedTags(
|
|
42
|
+
[...routeScripts, ...manifestScripts].flatMap((tag) =>
|
|
43
|
+
normalizeManagedTag(tag),
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
title,
|
|
49
|
+
head,
|
|
50
|
+
body,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Full managed document for a single snapshot: match-derived tags plus one
|
|
56
|
+
* `takeBufferedScripts()` read (for tests and tooling). Prefer composing
|
|
57
|
+
* `buildMatchManagedDocumentContent` + a one-time dehydration prefix in SSR, as
|
|
58
|
+
* the body managed-tags provider does.
|
|
59
|
+
*/
|
|
60
|
+
export function buildManagedDocumentContent(
|
|
61
|
+
router: AnyRouter,
|
|
62
|
+
): ManagedDocumentContent {
|
|
63
|
+
return mergeDehydrationPrefixIntoDocument(
|
|
64
|
+
buildMatchManagedDocumentContent(router),
|
|
65
|
+
collectDehydrationScriptManagedTags(router),
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function selectTitle(matches: Array<any>) {
|
|
70
|
+
const routeMeta = selectRouteMeta(matches)
|
|
71
|
+
|
|
72
|
+
for (let i = routeMeta.length - 1; i >= 0; i--) {
|
|
73
|
+
const metas = routeMeta[i]!
|
|
74
|
+
for (let j = metas.length - 1; j >= 0; j--) {
|
|
75
|
+
const meta = metas[j]
|
|
76
|
+
if (meta?.title) {
|
|
77
|
+
return meta.title
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return undefined
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function selectMetaTags(
|
|
86
|
+
matches: Array<any>,
|
|
87
|
+
nonce: string | undefined,
|
|
88
|
+
) {
|
|
89
|
+
const routeMeta = selectRouteMeta(matches)
|
|
90
|
+
const metaTags: Array<RouterManagedTag> = []
|
|
91
|
+
const metaByAttribute: Record<string, true> = {}
|
|
92
|
+
|
|
93
|
+
for (let i = routeMeta.length - 1; i >= 0; i--) {
|
|
94
|
+
const metas = routeMeta[i]!
|
|
95
|
+
for (let j = metas.length - 1; j >= 0; j--) {
|
|
96
|
+
const m = metas[j]
|
|
97
|
+
if (!m || m.title) continue
|
|
98
|
+
|
|
99
|
+
if ('script:ld+json' in m && m['script:ld+json'] !== undefined) {
|
|
100
|
+
try {
|
|
101
|
+
const json = JSON.stringify(m['script:ld+json'])
|
|
102
|
+
metaTags.push({
|
|
103
|
+
tag: 'script',
|
|
104
|
+
attrs: {
|
|
105
|
+
type: 'application/ld+json',
|
|
106
|
+
nonce,
|
|
107
|
+
},
|
|
108
|
+
children: escapeHtml(json),
|
|
109
|
+
})
|
|
110
|
+
} catch {
|
|
111
|
+
// Skip invalid JSON-LD objects
|
|
112
|
+
}
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const attribute = m.name ?? m.property
|
|
117
|
+
if (attribute) {
|
|
118
|
+
if (metaByAttribute[attribute]) {
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
metaByAttribute[attribute] = true
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
metaTags.push({
|
|
125
|
+
tag: 'meta',
|
|
126
|
+
attrs: {
|
|
127
|
+
...m,
|
|
128
|
+
nonce,
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (nonce) {
|
|
135
|
+
metaTags.push({
|
|
136
|
+
tag: 'meta',
|
|
137
|
+
attrs: {
|
|
138
|
+
property: 'csp-nonce',
|
|
139
|
+
content: nonce,
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
metaTags.reverse()
|
|
145
|
+
|
|
146
|
+
return metaTags
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function selectConstructedLinks(
|
|
150
|
+
matches: Array<any>,
|
|
151
|
+
nonce: string | undefined,
|
|
152
|
+
) {
|
|
153
|
+
return matches
|
|
154
|
+
.map((match) => match.links!)
|
|
155
|
+
.filter(Boolean)
|
|
156
|
+
.flat(1)
|
|
157
|
+
.map(
|
|
158
|
+
(link) =>
|
|
159
|
+
({
|
|
160
|
+
tag: 'link',
|
|
161
|
+
attrs: {
|
|
162
|
+
...link,
|
|
163
|
+
nonce,
|
|
164
|
+
},
|
|
165
|
+
}) satisfies RouterManagedTag,
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function selectManifestLinks(
|
|
170
|
+
router: AnyRouter,
|
|
171
|
+
matches: Array<any>,
|
|
172
|
+
nonce: string | undefined,
|
|
173
|
+
) {
|
|
174
|
+
return matches
|
|
175
|
+
.map((match) => router.ssr?.manifest?.routes[match.routeId]?.assets ?? [])
|
|
176
|
+
.filter(Boolean)
|
|
177
|
+
.flat(1)
|
|
178
|
+
.filter((asset) => asset.tag === 'link')
|
|
179
|
+
.map(
|
|
180
|
+
(asset) =>
|
|
181
|
+
({
|
|
182
|
+
tag: 'link',
|
|
183
|
+
attrs: {
|
|
184
|
+
...asset.attrs,
|
|
185
|
+
suppressHydrationWarning: true,
|
|
186
|
+
nonce,
|
|
187
|
+
},
|
|
188
|
+
}) satisfies RouterManagedTag,
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function selectPreloadLinks(
|
|
193
|
+
router: AnyRouter,
|
|
194
|
+
matches: Array<any>,
|
|
195
|
+
nonce: string | undefined,
|
|
196
|
+
) {
|
|
197
|
+
const preloadLinks: Array<RouterManagedTag> = []
|
|
198
|
+
|
|
199
|
+
matches
|
|
200
|
+
.map((match) => router.looseRoutesById[match.routeId]!)
|
|
201
|
+
.forEach((route) =>
|
|
202
|
+
router.ssr?.manifest?.routes[route.id]?.preloads
|
|
203
|
+
?.filter(Boolean)
|
|
204
|
+
.forEach((preload) => {
|
|
205
|
+
preloadLinks.push({
|
|
206
|
+
tag: 'link',
|
|
207
|
+
attrs: {
|
|
208
|
+
rel: 'modulepreload',
|
|
209
|
+
href: preload,
|
|
210
|
+
nonce,
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
}),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return preloadLinks
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function selectStyles(
|
|
220
|
+
matches: Array<any>,
|
|
221
|
+
nonce: string | undefined,
|
|
222
|
+
) {
|
|
223
|
+
return (
|
|
224
|
+
matches
|
|
225
|
+
.map((match) => match.styles!)
|
|
226
|
+
.flat(1)
|
|
227
|
+
.filter(Boolean) as Array<Record<string, unknown>>
|
|
228
|
+
).map(({ children, ...attrs }) => ({
|
|
229
|
+
tag: 'style',
|
|
230
|
+
attrs: {
|
|
231
|
+
...attrs,
|
|
232
|
+
nonce,
|
|
233
|
+
},
|
|
234
|
+
children: typeof children === 'string' ? children : undefined,
|
|
235
|
+
})) satisfies Array<RouterManagedTag>
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function selectHeadScripts(
|
|
239
|
+
matches: Array<any>,
|
|
240
|
+
nonce: string | undefined,
|
|
241
|
+
) {
|
|
242
|
+
return (
|
|
243
|
+
matches
|
|
244
|
+
.map((match) => match.headScripts!)
|
|
245
|
+
.flat(1)
|
|
246
|
+
.filter(Boolean) as Array<Record<string, unknown>>
|
|
247
|
+
).map(({ children, ...script }) => ({
|
|
248
|
+
tag: 'script',
|
|
249
|
+
attrs: {
|
|
250
|
+
...script,
|
|
251
|
+
nonce,
|
|
252
|
+
},
|
|
253
|
+
children: typeof children === 'string' ? children : undefined,
|
|
254
|
+
})) satisfies Array<RouterManagedTag>
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function selectRouteScripts(
|
|
258
|
+
matches: Array<any>,
|
|
259
|
+
nonce: string | undefined,
|
|
260
|
+
) {
|
|
261
|
+
return (
|
|
262
|
+
matches
|
|
263
|
+
.map((match) => match.scripts!)
|
|
264
|
+
.flat(1)
|
|
265
|
+
.filter(Boolean) as Array<Record<string, unknown>>
|
|
266
|
+
).map(({ children, ...script }) => ({
|
|
267
|
+
tag: 'script',
|
|
268
|
+
attrs: {
|
|
269
|
+
...script,
|
|
270
|
+
suppressHydrationWarning: true,
|
|
271
|
+
nonce,
|
|
272
|
+
},
|
|
273
|
+
children: typeof children === 'string' ? children : undefined,
|
|
274
|
+
})) satisfies Array<RouterManagedTag>
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function selectManifestScripts(
|
|
278
|
+
router: AnyRouter,
|
|
279
|
+
matches: Array<any>,
|
|
280
|
+
nonce: string | undefined,
|
|
281
|
+
) {
|
|
282
|
+
return matches
|
|
283
|
+
.map((match) => router.looseRoutesById[match.routeId]!)
|
|
284
|
+
.flatMap((route) =>
|
|
285
|
+
router.ssr?.manifest?.routes[route.id]?.assets
|
|
286
|
+
?.filter((asset) => asset.tag === 'script')
|
|
287
|
+
.map(
|
|
288
|
+
(asset) =>
|
|
289
|
+
({
|
|
290
|
+
tag: 'script',
|
|
291
|
+
attrs: {
|
|
292
|
+
...asset.attrs,
|
|
293
|
+
nonce,
|
|
294
|
+
},
|
|
295
|
+
children: asset.children,
|
|
296
|
+
}) satisfies RouterManagedTag,
|
|
297
|
+
) ?? [],
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function selectRouteMeta(matches: Array<any>) {
|
|
302
|
+
return matches.reduce<Array<Array<RouteMetaEntry>>>((acc, match) => {
|
|
303
|
+
if (match.meta) {
|
|
304
|
+
acc.push(match.meta as Array<RouteMetaEntry>)
|
|
305
|
+
}
|
|
306
|
+
return acc
|
|
307
|
+
}, [])
|
|
308
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { normalizeManagedTag, uniqManagedTags } from './managed-dom'
|
|
2
|
+
import type { AnyRouter } from '@benjavicente/router-core'
|
|
3
|
+
import type { ManagedDocumentContent, ManagedTag } from './managed-document-types'
|
|
4
|
+
|
|
5
|
+
export function collectDehydrationScriptManagedTags(
|
|
6
|
+
router: AnyRouter,
|
|
7
|
+
): Array<ManagedTag> {
|
|
8
|
+
return router.serverSsr
|
|
9
|
+
? uniqManagedTags(
|
|
10
|
+
normalizeManagedTag(router.serverSsr.takeBufferedScripts()),
|
|
11
|
+
)
|
|
12
|
+
: []
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function mergeDehydrationPrefixIntoDocument(
|
|
16
|
+
matchContent: ManagedDocumentContent,
|
|
17
|
+
dehydrationPrefix: Array<ManagedTag>,
|
|
18
|
+
): ManagedDocumentContent {
|
|
19
|
+
if (dehydrationPrefix.length === 0) {
|
|
20
|
+
return matchContent
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
...matchContent,
|
|
25
|
+
body: uniqManagedTags([...dehydrationPrefix, ...matchContent.body]),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ManagedDocumentContent, ManagedTag } from './managed-document-types'
|
|
2
|
+
|
|
3
|
+
export function areManagedDocumentContentsEqual(
|
|
4
|
+
prev: ManagedDocumentContent,
|
|
5
|
+
next: ManagedDocumentContent,
|
|
6
|
+
) {
|
|
7
|
+
return (
|
|
8
|
+
prev.title === next.title &&
|
|
9
|
+
areManagedTagArraysEqual(prev.head, next.head) &&
|
|
10
|
+
areManagedTagArraysEqual(prev.body, next.body)
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function areManagedTagArraysEqual(
|
|
15
|
+
prev: Array<ManagedTag>,
|
|
16
|
+
next: Array<ManagedTag>,
|
|
17
|
+
) {
|
|
18
|
+
if (prev.length !== next.length) {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < prev.length; i++) {
|
|
23
|
+
if (prev[i]?.id !== next[i]?.id) {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type { TanstackDocumentFeatures } from './provide-tanstack-document'
|
|
2
|
+
export {
|
|
3
|
+
provideHeadContent,
|
|
4
|
+
provideTanstackDocument,
|
|
5
|
+
} from './provide-tanstack-document'
|
|
6
|
+
export { provideTanstackDocumentTitle } from './provide-tanstack-document-title'
|
|
7
|
+
export { provideTanstackHeadManagedTags } from './provide-tanstack-head-managed-tags'
|
|
8
|
+
export { provideTanstackBodyManagedTags } from './provide-tanstack-body-managed-tags'
|
|
9
|
+
export {
|
|
10
|
+
buildManagedDocumentContent,
|
|
11
|
+
buildMatchManagedDocumentContent,
|
|
12
|
+
} from './build-match-managed-document'
|
|
13
|
+
export { TANSTACK_DOCUMENT_ROUTER } from './document-router-token'
|
|
14
|
+
export type {
|
|
15
|
+
ManagedBucket,
|
|
16
|
+
ManagedDocumentContent,
|
|
17
|
+
ManagedTag,
|
|
18
|
+
ManagedTagCollection,
|
|
19
|
+
ManagedTagId,
|
|
20
|
+
ManagedTagRecord,
|
|
21
|
+
RouteMetaEntry,
|
|
22
|
+
} from './managed-document-types'
|
|
23
|
+
export { TSR_ID_ATTR, TSR_MANAGED_ATTR } from './managed-document-types'
|
|
24
|
+
export {
|
|
25
|
+
createManagedTagCollection,
|
|
26
|
+
hashManagedTag,
|
|
27
|
+
normalizeManagedTag,
|
|
28
|
+
uniqManagedTags,
|
|
29
|
+
} from './managed-dom'
|
|
30
|
+
export {
|
|
31
|
+
collectDehydrationScriptManagedTags,
|
|
32
|
+
mergeDehydrationPrefixIntoDocument,
|
|
33
|
+
} from './document-dehydration'
|
|
@@ -0,0 +1,108 @@
|
|
|
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 {
|
|
6
|
+
collectDehydrationScriptManagedTags,
|
|
7
|
+
mergeDehydrationPrefixIntoDocument,
|
|
8
|
+
} from './document-dehydration'
|
|
9
|
+
import { areManagedDocumentContentsEqual } from './document-equality'
|
|
10
|
+
import { createManagedTagCollection } from './managed-dom'
|
|
11
|
+
import type { AnyRouter } from '@benjavicente/router-core'
|
|
12
|
+
import type { ManagedDocumentContent, ManagedTagCollection } from './managed-document-types'
|
|
13
|
+
|
|
14
|
+
function applyManagedDocumentContent({
|
|
15
|
+
document,
|
|
16
|
+
initialTitle,
|
|
17
|
+
headCollection,
|
|
18
|
+
bodyCollection,
|
|
19
|
+
nextContent,
|
|
20
|
+
}: {
|
|
21
|
+
document: Document
|
|
22
|
+
initialTitle: string
|
|
23
|
+
headCollection: ManagedTagCollection
|
|
24
|
+
bodyCollection: ManagedTagCollection
|
|
25
|
+
nextContent: ManagedDocumentContent
|
|
26
|
+
}) {
|
|
27
|
+
if (nextContent.title !== undefined) {
|
|
28
|
+
document.title = nextContent.title
|
|
29
|
+
} else {
|
|
30
|
+
document.title = initialTitle
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
headCollection.sync(nextContent.head)
|
|
34
|
+
bodyCollection.sync(nextContent.body)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One `activeMatches` subscription: title, head, and body stay in sync together.
|
|
39
|
+
* Used by `provideTanstackDocument` when all features are enabled.
|
|
40
|
+
*/
|
|
41
|
+
export function installUnifiedTanstackDocumentSync(injectedRouter: AnyRouter) {
|
|
42
|
+
const document = Angular.inject(DOCUMENT)
|
|
43
|
+
const rendererFactory = Angular.inject(Angular.RendererFactory2, {
|
|
44
|
+
optional: true,
|
|
45
|
+
})
|
|
46
|
+
const destroyRef = Angular.inject(Angular.DestroyRef)
|
|
47
|
+
const activeMatches = injectStore(
|
|
48
|
+
injectedRouter.stores.activeMatchesSnapshot,
|
|
49
|
+
(matches) => matches,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const renderer = rendererFactory?.createRenderer(null, null) ?? null
|
|
53
|
+
const initialTitle = document.title
|
|
54
|
+
const headCollection = createManagedTagCollection({
|
|
55
|
+
root: document.head,
|
|
56
|
+
bucket: 'head',
|
|
57
|
+
renderer,
|
|
58
|
+
})
|
|
59
|
+
const bodyCollection = createManagedTagCollection({
|
|
60
|
+
root: document.body,
|
|
61
|
+
bucket: 'body',
|
|
62
|
+
renderer,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const dehydrationBodyPrefix =
|
|
66
|
+
collectDehydrationScriptManagedTags(injectedRouter)
|
|
67
|
+
|
|
68
|
+
const composeDocumentContent = (): ManagedDocumentContent =>
|
|
69
|
+
mergeDehydrationPrefixIntoDocument(
|
|
70
|
+
buildMatchManagedDocumentContent(injectedRouter),
|
|
71
|
+
dehydrationBodyPrefix,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
headCollection.mount()
|
|
75
|
+
bodyCollection.mount()
|
|
76
|
+
|
|
77
|
+
let currentContent = composeDocumentContent()
|
|
78
|
+
applyManagedDocumentContent({
|
|
79
|
+
document,
|
|
80
|
+
initialTitle,
|
|
81
|
+
headCollection,
|
|
82
|
+
bodyCollection,
|
|
83
|
+
nextContent: currentContent,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const syncEffect = Angular.effect(() => {
|
|
87
|
+
activeMatches()
|
|
88
|
+
const nextContent = composeDocumentContent()
|
|
89
|
+
if (areManagedDocumentContentsEqual(currentContent, nextContent)) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
currentContent = nextContent
|
|
93
|
+
applyManagedDocumentContent({
|
|
94
|
+
document,
|
|
95
|
+
initialTitle,
|
|
96
|
+
headCollection,
|
|
97
|
+
bodyCollection,
|
|
98
|
+
nextContent,
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
destroyRef.onDestroy(() => {
|
|
103
|
+
syncEffect.destroy()
|
|
104
|
+
headCollection.destroy()
|
|
105
|
+
bodyCollection.destroy()
|
|
106
|
+
document.title = initialTitle
|
|
107
|
+
})
|
|
108
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RouterManagedTag } from '@benjavicente/router-core'
|
|
2
|
+
|
|
3
|
+
export const TSR_MANAGED_ATTR = 'data-tsr-managed'
|
|
4
|
+
export const TSR_ID_ATTR = 'data-tsr-id'
|
|
5
|
+
|
|
6
|
+
export type ManagedBucket = 'head' | 'body'
|
|
7
|
+
export type ManagedTagId = string
|
|
8
|
+
|
|
9
|
+
/** Route-driven tag with stable hash id for DOM reconciliation. */
|
|
10
|
+
export type ManagedTag = Exclude<RouterManagedTag, { tag: 'title' }> & {
|
|
11
|
+
id: ManagedTagId
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ManagedDocumentContent = {
|
|
15
|
+
title?: string
|
|
16
|
+
head: Array<ManagedTag>
|
|
17
|
+
body: Array<ManagedTag>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ManagedTagRecord = {
|
|
21
|
+
id: ManagedTagId
|
|
22
|
+
tag: ManagedTag
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ManagedTagCollection = {
|
|
26
|
+
mount: (initialNodes?: Array<HTMLElement>) => void
|
|
27
|
+
sync: (nextTags: Array<ManagedTag>) => void
|
|
28
|
+
destroy: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type RouteMetaEntry = {
|
|
32
|
+
title?: string
|
|
33
|
+
name?: string
|
|
34
|
+
property?: string
|
|
35
|
+
[key: string]: unknown
|
|
36
|
+
}
|