-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add Fragment Portal page with dynamic rendering
- Loading branch information
Showing
4 changed files
with
222 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { | ||
Children, | ||
cloneElement, | ||
isValidElement, | ||
useEffect, | ||
useLayoutEffect, | ||
useRef, | ||
useState, | ||
type ElementType, | ||
type ReactNode, | ||
} from 'react' | ||
import { createPortal } from 'react-dom' | ||
|
||
const fragments = document.createDocumentFragment() | ||
const getProps = Symbol() | ||
const setProps = Symbol() | ||
const currentProps = Symbol() | ||
|
||
export type FragmentPortal = HTMLElement & { | ||
[getProps]?: () => any | ||
[setProps]?: (props: any) => void | ||
[currentProps]?: any | ||
|
||
} | ||
|
||
export function useFragment(type = 'div') { | ||
const [fragment] = useState(() => { | ||
const element = document.createElement(type) | ||
fragments.appendChild(element) | ||
return element as FragmentPortal | ||
}) | ||
|
||
useEffect( | ||
() => () => { | ||
fragment.parentElement?.removeChild(fragment) | ||
}, | ||
[fragment], | ||
) | ||
|
||
return fragment | ||
} | ||
|
||
export function Receptacle({ fragment, props, type = 'div', slot }: { | ||
fragment?: FragmentPortal | ||
props: any | ||
type?: ElementType | ||
slot?: React.MutableRefObject<HTMLElement | null> | ||
}) { | ||
const div = useRef<HTMLElement>() | ||
|
||
useLayoutEffect(() => { | ||
if (!fragment) return | ||
const receptacle = slot ? slot.current : div.current | ||
if (!receptacle) return | ||
receptacle.appendChild(fragment) | ||
return () => { | ||
fragments.appendChild(fragment) | ||
} | ||
}, [slot, fragment]) | ||
|
||
useLayoutEffect(() => { | ||
if (!fragment || fragment[currentProps]() === props) return | ||
fragment[setProps]?.(props) | ||
}, [fragment, props]) | ||
|
||
if (!fragment) return null | ||
|
||
fragment[getProps] = () => props | ||
|
||
if (slot) return null | ||
|
||
const Element = type | ||
return <Element ref={div} /> | ||
} | ||
|
||
export function Portal({ children, fragment }: { | ||
children: ReactNode | ||
fragment: FragmentPortal | ||
}) { | ||
const [extraProps, setExtraProps] = useState(fragment[getProps]?.() || {}) | ||
fragment[setProps] = setExtraProps | ||
fragment[currentProps] = () => extraProps | ||
return createPortal( | ||
Children.map(children, (child) => { | ||
if (!isValidElement(child)) return child | ||
return cloneElement(child, extraProps) | ||
}), | ||
fragment, | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { useEffect, useRef, useState } from "react" | ||
import { useFragment, Receptacle, Portal, type FragmentPortal } from './FragmentPortal' | ||
import styles from './styles.module.css' | ||
import { Link } from "~/Navigation" | ||
|
||
export const meta = { | ||
title: 'Fragment Portal' | ||
} | ||
|
||
export default function () { | ||
const fragment = useFragment('div') | ||
|
||
return ( | ||
<div className={styles.main}> | ||
<Link href="/">back</Link> | ||
<h1>{meta.title}</h1> | ||
<Slots fragment={fragment} /> | ||
<LiveComponents fragment={fragment} /> | ||
</div> | ||
) | ||
} | ||
|
||
function LiveComponents({ fragment }: { fragment: FragmentPortal }) { | ||
const [, rerender] = useState({}) | ||
const i = useRef(0).current++ | ||
return ( | ||
<Portal fragment={fragment}> | ||
<button onClick={() => rerender({})}>rerender: {i}</button> | ||
<Video /> | ||
<Timer /> | ||
</Portal> | ||
) | ||
} | ||
|
||
|
||
function Slots({ fragment }: { fragment: FragmentPortal }) { | ||
const [side, setSide] = useState([0, 0]) | ||
const onClick = () => { | ||
// pick a random receptacle to display the `fragment` in | ||
setSide(([_i, _j]) => { | ||
let i, j | ||
do { | ||
i = Math.floor(Math.random() * 2) | ||
j = Math.floor(Math.random() * 3) | ||
} while (i === _i && j === _j) | ||
return [i, j] | ||
}) | ||
} | ||
return ( | ||
<> | ||
<button onClick={onClick}> | ||
move somewhere else in DOM | ||
</button> | ||
<ul className={styles.grid}> | ||
{/* make many receptacles */} | ||
{[0, 1].map((i) => ( | ||
[0, 1, 2].map((j) => ( | ||
<li key={i + '-' + j}> | ||
<Receptacle | ||
fragment={(side[0] === i && side[1] === j) ? fragment : undefined} | ||
props={{ label: `${j + 1}x${i + 1}` }} | ||
/> | ||
</li> | ||
)) | ||
))} | ||
</ul> | ||
</> | ||
) | ||
} | ||
|
||
function Video({ label }: { label?: string }) { | ||
const i = useRef(0).current++ | ||
return ( | ||
<> | ||
<video width="320" height="240" controls loop muted autoPlay playsInline> | ||
<source | ||
src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4" | ||
type="video/mp4" | ||
/> | ||
</video> | ||
<div> | ||
renders: {i}, parent: {label} | ||
</div> | ||
</> | ||
) | ||
} | ||
|
||
function Timer({ label }: { label?: string }) { | ||
const [time, setTime] = useState(0) | ||
useEffect(() => { | ||
const id = setInterval(() => setTime((t) => t + 1), 1000) | ||
return () => clearInterval(id) | ||
}, []) | ||
const i = useRef(0).current++ | ||
return ( | ||
<p> | ||
{time}s since mounted, renders: {i}, parent: {label} | ||
</p> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
.main { | ||
font-family: sans-serif; | ||
|
||
ul { | ||
list-style: none; | ||
padding: 0; | ||
} | ||
|
||
video { | ||
display: block; | ||
margin: auto; | ||
} | ||
} | ||
|
||
.grid { | ||
text-align: center; | ||
|
||
display: grid; | ||
grid-template-columns: 1fr 1fr 1fr; | ||
grid-auto-rows: 1fr; | ||
|
||
>* { | ||
border: 1px solid red; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters