@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,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
|
+
}
|