Skip to content

Commit

Permalink
feat: handle mount/unmount effects
Browse files Browse the repository at this point in the history
  • Loading branch information
Valerioageno committed Oct 20, 2024
1 parent e764322 commit 00bfcc4
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 209 deletions.
72 changes: 36 additions & 36 deletions examples/tutorial/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,48 @@ import { Head, type TuonoProps } from 'tuono'
import PokemonLink from '../components/PokemonLink'

interface Pokemon {
name: string
name: string
}

interface IndexProps {
results: Pokemon[]
results: Pokemon[]
}

export default function IndexPage({
data,
data,
}: TuonoProps<IndexProps>): JSX.Element {
if (!data?.results) {
return <></>
}
if (!data?.results) {
return <></>
}

return (
<>
<Head>
<title>Tuono tutorial</title>
</Head>
<header className="header">
<a href="https://crates.io/crates/tuono" target="_blank">
Crates
</a>
<a href="https://www.npmjs.com/package/tuono" target="_blank">
Npm
</a>
</header>
<div className="title-wrap">
<h1 className="title">
TU<span>O</span>NO
</h1>
<div className="logo">
<img src="rust.svg" className="rust" />
<img src="react.svg" className="react" />
</div>
</div>
<ul style={{ flexWrap: 'wrap', display: 'flex', gap: 10 }}>
<PokemonLink pokemon={{ name: 'GOAT' }} id={0} />
{data.results.map((pokemon, i) => {
return <PokemonLink pokemon={pokemon} id={i + 1} key={i} />
})}
</ul>
</>
)
return (
<>
<Head>
<title>Tuono tutorial</title>
</Head>
<header className="header">
<a href="https://crates.io/crates/tuono" target="_blank">
Crates
</a>
<a href="https://www.npmjs.com/package/tuono" target="_blank">
Npm
</a>
</header>
<div className="title-wrap">
<h1 className="title">
TU<span>O</span>NO
</h1>
<div className="logo">
<img src="rust.svg" className="rust" />
<img src="react.svg" className="react" />
</div>
</div>
<ul style={{ flexWrap: 'wrap', display: 'flex', gap: 10 }}>
<PokemonLink pokemon={{ name: 'GOAT' }} id={0} />
{data.results.map((pokemon, i) => {
return <PokemonLink pokemon={pokemon} id={i + 1} key={i} />
})}
</ul>
</>
)
}
1 change: 1 addition & 0 deletions packages/html-tags/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@tanstack/config": "^0.7.11",
"@testing-library/jest-dom": "^6.6.0",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"vitest": "^2.0.0"
Expand Down
30 changes: 15 additions & 15 deletions packages/html-tags/src/meta-tags-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,30 @@ import type { ReactNode } from 'react'
type ExtractFn = (elements: ReactNode) => void

interface MetaTagsContextValues {
extract?: ExtractFn
extract?: ExtractFn
}

const MetaContext = createContext<MetaTagsContextValues>({})

interface MetaTagsContextProps {
children: ReactNode
extract?: ExtractFn
children: ReactNode
extract?: ExtractFn
}

export default function MetaContextProvider({
extract,
children,
extract,
children,
}: MetaTagsContextProps): ReactNode {
return (
<MetaContext.Provider
value={{
extract,
}}
>
{children}
</MetaContext.Provider>
)
return (
<MetaContext.Provider
value={{
extract,
}}
>
{children}
</MetaContext.Provider>
)
}

export const useMetaTagsContext = (): MetaTagsContextValues =>
useContext(MetaContext)
useContext(MetaContext)
72 changes: 62 additions & 10 deletions packages/html-tags/src/meta-tags.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,77 @@
import * as React from 'react'
import { afterEach, describe, expect, test } from 'vitest'
import { cleanup, render } from '@testing-library/react'
import { cleanup, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

import '@testing-library/jest-dom'
import MetaContextProvider from './meta-tags-context'
import MetaTags from './meta-tags'

const MockRouter = (): React.ReactNode => {
const [showRoute, setShowRoute] = React.useState(false)

return (
<MetaContextProvider>
<button onClick={() => setShowRoute((st) => !st)}>Toggle route</button>
{showRoute ? (
<MetaTags>
<title>Updated title</title>
</MetaTags>
) : (
<MetaTags>
<title>Tuono</title>
</MetaTags>
)}
</MetaContextProvider>
)
}

describe('Should correctly render the head tags', () => {
afterEach(() => {
cleanup()
})

test('It should correctly render the head element', async () => {
await React.act(async () => {
render(
<MetaContextProvider>
<MetaTags>
<title>Tuono</title>
</MetaTags>
</MetaContextProvider>,
)
})
render(
<MetaContextProvider>
<MetaTags>
<title>Tuono</title>
</MetaTags>
</MetaContextProvider>,
)
expect(document.title).toEqual('Tuono')
})

test('It should remove the properties when unmount', async () => {
const { unmount } = render(
<MetaContextProvider>
<MetaTags>
<title>Tuono</title>
</MetaTags>
</MetaContextProvider>,
)
expect(document.title).toEqual('Tuono')

unmount()

expect(document.title).toEqual('')
})

test('It should update the existing values with the newly mounted', async () => {
const user = userEvent.setup()
render(<MockRouter />)

expect(document.title).toEqual('Tuono')

await user.click(screen.getByText('Toggle route'))
await waitFor(() => {
expect(document.title).toEqual('Updated title')
})

await user.click(screen.getByText('Toggle route'))

await waitFor(() => {
expect(document.title).toEqual('Tuono')
})
})
})
41 changes: 39 additions & 2 deletions packages/html-tags/src/meta-tags.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import React, { useEffect, useRef, useState } from 'react'
import type { ReactNode } from 'react'
import { useMetaTagsContext } from './meta-tags-context'
import { appendChild } from './utils'
import {
appendChild,
getDuplicateTitle,
removeChild,
getDuplicateMeta,
getDuplicateCanonical,
getDuplicateElementById,
} from './utils'

interface MetaTagsProps {
children: ReactNode
Expand All @@ -19,7 +26,37 @@ export default function MetaTags({ children }: MetaTagsProps): ReactNode {

setLastChildren(children)

const childNodes = Array.prototype.slice.call(elementRef.current?.children)
let childNodes = Array.prototype.slice.call(elementRef.current?.children)

const head = document.head
const headHtml = head.innerHTML

//filter children remove if children has not been changed
childNodes = childNodes.filter((child) => {
return headHtml.indexOf(child.outerHTML) === -1
})

//create clone of childNodes
childNodes = childNodes.map((child) => child.cloneNode(true))

//remove duplicate title and meta from head
childNodes.forEach((child) => {
const tag = child.tagName.toLowerCase()
if (tag === 'title') {
const title = getDuplicateTitle()
if (title.length > 0) removeChild(head, title)
} else if (child.id) {
// if the element has id defined remove the existing element with that id
const elm = getDuplicateElementById(child)
if (elm.length > 0) removeChild(head, elm)
} else if (tag === 'meta') {
const meta = getDuplicateMeta(child)
if (meta.length > 0) removeChild(head, meta)
} else if (tag === 'link' && child.rel === 'canonical') {
const link = getDuplicateCanonical()
if (link.length > 0) removeChild(head, link)
}
})

appendChild(document.head, childNodes)
}
Expand Down
22 changes: 11 additions & 11 deletions packages/html-tags/src/server/meta-tags-server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { ReactNode } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'

interface MetaServerTagsOut {
extract: (el: ReactNode) => void
renderToString: () => string
extract: (el: ReactNode) => void
renderToString: () => string
}

/*
Expand All @@ -13,14 +13,14 @@ interface MetaServerTagsOut {
*
*/
export default function MetaTagsServer(): MetaServerTagsOut {
let headElms: ReactNode
let headElms: ReactNode

return {
extract(elms: ReactNode): void {
headElms = elms
},
renderToString(): string {
return renderToStaticMarkup(headElms)
},
}
return {
extract(elms: ReactNode): void {
headElms = elms
},
renderToString(): string {
return renderToStaticMarkup(headElms)
},
}
}
Loading

0 comments on commit 00bfcc4

Please sign in to comment.