Skip to content

Commit ceec4f2

Browse files
feat: Add style to HeadContent (#4559)
Currently Route HeadContent don't have support to inline style tags. After a contribution proposition made by @schiller-manuel in the Discord Group I have decided to implement this. I also have used this in the basic example for react-start and solid-start and added style and style w/loader test cases to both modules. --------- Co-authored-by: Flo <fpellet@ensc.fr>
1 parent 3e1d2f4 commit ceec4f2

File tree

13 files changed

+276
-2
lines changed

13 files changed

+276
-2
lines changed

docs/router/framework/react/guide/document-head-management.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ For full-stack applications that use Start, and even for single-page application
1515
- Analytics
1616
- CSS and JS loading/unloading
1717

18-
To manage the document head, it's required that you render both the `<HeadContent />` and `<Scripts />` components and use the `routeOptions.head` property to manage the head of a route, which returns an object with `title`, `meta`, `links`, and `scripts` properties.
18+
To manage the document head, it's required that you render both the `<HeadContent />` and `<Scripts />` components and use the `routeOptions.head` property to manage the head of a route, which returns an object with `title`, `meta`, `links`, `styles`, and `scripts` properties.
1919

2020
## Managing the Document Head
2121

@@ -37,6 +37,15 @@ export const Route = createRootRoute({
3737
href: '/favicon.ico',
3838
},
3939
],
40+
styles: [
41+
{
42+
media: 'all and (max-width: 500px)',
43+
children: `p {
44+
color: blue;
45+
background-color: yellow;
46+
}`
47+
}
48+
]
4049
scripts: [
4150
{
4251
src: 'https://www.google-analytics.com/analytics.js',

e2e/react-start/basic/src/routes/__root.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ export const Route = createRootRoute({
5151
{ rel: 'manifest', href: '/site.webmanifest', color: '#fffff' },
5252
{ rel: 'icon', href: '/favicon.ico' },
5353
],
54+
styles: [
55+
{
56+
media: 'all and (min-width: 500px)',
57+
children: `
58+
.inline-div {
59+
color: white;
60+
background-color: gray;
61+
max-width: 250px;
62+
}`,
63+
},
64+
],
5465
}),
5566
errorComponent: (props) => {
5667
return (
@@ -158,6 +169,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
158169
</div>
159170
<hr />
160171
{children}
172+
<div className="inline-div">This is an inline styled div</div>
161173
<RouterDevtools position="bottom-right" />
162174
<Scripts />
163175
</body>

e2e/solid-start/basic/src/routes/__root.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ export const Route = createRootRoute({
4141
{ rel: 'manifest', href: '/site.webmanifest', color: '#fffff' },
4242
{ rel: 'icon', href: '/favicon.ico' },
4343
],
44+
styles: [
45+
{
46+
media: 'all and (min-width: 500px)',
47+
children: `
48+
.inline-div {
49+
color: white;
50+
background-color: gray;
51+
max-width: 250px;
52+
}`,
53+
},
54+
],
4455
}),
4556
errorComponent: (props) => <p>{props.error.stack}</p>,
4657
notFoundComponent: () => <NotFound />,
@@ -119,6 +130,7 @@ function RootComponent() {
119130
</Link>
120131
</div>
121132
<Outlet />
133+
<div class="inline-div">This is an inline styled div</div>
122134
<TanStackRouterDevtoolsInProd />
123135
</>
124136
)

packages/react-router/src/HeadContent.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,21 @@ export const useTags = () => {
120120
structuralSharing: true as any,
121121
})
122122

123+
const styles = useRouterState({
124+
select: (state) =>
125+
(
126+
state.matches
127+
.map((match) => match.styles!)
128+
.flat(1)
129+
.filter(Boolean) as Array<RouterManagedTag>
130+
).map(({ children, ...attrs }) => ({
131+
tag: 'style',
132+
attrs,
133+
children,
134+
})),
135+
structuralSharing: true as any,
136+
})
137+
123138
const headScripts = useRouterState({
124139
select: (state) =>
125140
(
@@ -142,6 +157,7 @@ export const useTags = () => {
142157
...meta,
143158
...preloadMeta,
144159
...links,
160+
...styles,
145161
...headScripts,
146162
] as Array<RouterManagedTag>,
147163
(d) => {

packages/react-router/src/Matches.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ declare module '@tanstack/router-core' {
3434
meta?: Array<React.JSX.IntrinsicElements['meta'] | undefined>
3535
links?: Array<React.JSX.IntrinsicElements['link'] | undefined>
3636
scripts?: Array<React.JSX.IntrinsicElements['script'] | undefined>
37+
styles?: Array<React.JSX.IntrinsicElements['style'] | undefined>
3738
headScripts?: Array<React.JSX.IntrinsicElements['script'] | undefined>
3839
}
3940
}

packages/react-router/tests/route.test.tsx

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,4 +475,101 @@ describe('route.head', () => {
475475
[{ href: 'index.css' }],
476476
])
477477
})
478+
479+
test('styles', async () => {
480+
const rootRoute = createRootRoute({
481+
head: () => ({
482+
styles: [
483+
{
484+
media: 'all and (min-width: 200px)',
485+
children: '.inline-div { color: blue; }',
486+
},
487+
],
488+
}),
489+
})
490+
const indexRoute = createRoute({
491+
getParentRoute: () => rootRoute,
492+
path: '/',
493+
head: () => ({
494+
styles: [
495+
{
496+
media: 'all and (min-width: 100px)',
497+
children: '.inline-div { background-color: yellow; }',
498+
},
499+
],
500+
}),
501+
component: () => <div className="inline-div">Index</div>,
502+
})
503+
const routeTree = rootRoute.addChildren([indexRoute])
504+
const router = createRouter({ routeTree, history })
505+
render(<RouterProvider router={router} />)
506+
const indexElem = await screen.findByText('Index')
507+
expect(indexElem).toBeInTheDocument()
508+
509+
const stylesState = router.state.matches.map((m) => m.styles)
510+
expect(stylesState).toEqual([
511+
[
512+
{
513+
media: 'all and (min-width: 200px)',
514+
children: '.inline-div { color: blue; }',
515+
},
516+
],
517+
[
518+
{
519+
media: 'all and (min-width: 100px)',
520+
children: '.inline-div { background-color: yellow; }',
521+
},
522+
],
523+
])
524+
})
525+
526+
test('styles w/loader', async () => {
527+
const rootRoute = createRootRoute({
528+
head: () => ({
529+
styles: [
530+
{
531+
media: 'all and (min-width: 200px)',
532+
children: '.inline-div { color: blue; }',
533+
},
534+
],
535+
}),
536+
})
537+
const indexRoute = createRoute({
538+
getParentRoute: () => rootRoute,
539+
path: '/',
540+
head: () => ({
541+
styles: [
542+
{
543+
media: 'all and (min-width: 100px)',
544+
children: '.inline-div { background-color: yellow; }',
545+
},
546+
],
547+
}),
548+
loader: async () => {
549+
await new Promise((resolve) => setTimeout(resolve, 200))
550+
},
551+
component: () => <div className="inline-div">Index</div>,
552+
})
553+
const routeTree = rootRoute.addChildren([indexRoute])
554+
const router = createRouter({ routeTree, history })
555+
render(<RouterProvider router={router} />)
556+
const indexElem = await screen.findByText('Index')
557+
expect(indexElem).toBeInTheDocument()
558+
559+
const stylesState = router.state.matches.map((m) => m.styles)
560+
expect(stylesState).toEqual([
561+
[
562+
{
563+
media: 'all and (min-width: 200px)',
564+
children: '.inline-div { color: blue; }',
565+
},
566+
],
567+
[
568+
{
569+
media: 'all and (min-width: 100px)',
570+
children: '.inline-div { background-color: yellow; }',
571+
},
572+
],
573+
])
574+
})
478575
})

packages/router-core/src/Matches.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export interface DefaultRouteMatchExtensions {
109109
links?: unknown
110110
headScripts?: unknown
111111
meta?: unknown
112+
styles?: unknown
112113
}
113114

114115
export interface RouteMatchExtensions extends DefaultRouteMatchExtensions {}

packages/router-core/src/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,6 +1124,7 @@ export interface UpdatableRouteOptions<
11241124
links?: AnyRouteMatch['links']
11251125
scripts?: AnyRouteMatch['headScripts']
11261126
meta?: AnyRouteMatch['meta']
1127+
styles?: AnyRouteMatch['styles']
11271128
}>
11281129
scripts?: (
11291130
ctx: AssetFnContextOptions<

packages/router-core/src/router.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2437,12 +2437,20 @@ export class RouterCore<
24372437
const meta = headFnContent?.meta
24382438
const links = headFnContent?.links
24392439
const headScripts = headFnContent?.scripts
2440+
const styles = headFnContent?.styles
24402441

24412442
const scripts =
24422443
await route.options.scripts?.(assetContext)
24432444
const headers =
24442445
await route.options.headers?.(assetContext)
2445-
return { meta, links, headScripts, headers, scripts }
2446+
return {
2447+
meta,
2448+
links,
2449+
headScripts,
2450+
headers,
2451+
scripts,
2452+
styles,
2453+
}
24462454
}
24472455

24482456
const runLoader = async () => {

packages/router-core/src/ssr/ssr-client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ export async function hydrate(router: AnyRouter): Promise<any> {
217217
match.meta = headFnContent?.meta
218218
match.links = headFnContent?.links
219219
match.headScripts = headFnContent?.scripts
220+
match.styles = headFnContent?.styles
220221
match.scripts = scripts
221222
}),
222223
)

0 commit comments

Comments
 (0)