Skip to content

Commit

Permalink
WIP: add rfc
Browse files Browse the repository at this point in the history
  • Loading branch information
nihgwu committed Aug 11, 2022
1 parent 5597108 commit 800b41e
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 2 deletions.
60 changes: 60 additions & 0 deletions example/components/TextField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { useId, useState } from 'react'
import { createHost, createSlot } from 'create-slots/rfc'

const Description = (props: React.ComponentPropsWithoutRef<'div'>) => (
<div
{...props}
style={{ borderLeft: '4px solid lightgray', paddingLeft: 4 }}
/>
)

const TextFieldLabel = createSlot('label')
const TextFieldInput = createSlot('input')
const TextFieldDescription = createSlot(Description)

const StyledLabel = (props: React.ComponentPropsWithoutRef<'label'>) => (
<TextFieldLabel {...props} style={{ color: 'red' }} />
)

export const TextField = (props: React.ComponentPropsWithoutRef<'div'>) => {
const id = useId()
const [value, setValue] = useState('')

if (value === 'a') return null

return (
<div {...props}>
{createHost(props.children, (Slots) => {
const labelProps = Slots.getProps(TextFieldLabel)
const inputProps = Slots.getProps(TextFieldInput)
const descriptionIdProps = Slots.getProps(TextFieldDescription)

const inputId = inputProps?.id || `${id}-label`
const descriptionId = descriptionIdProps ? `${id}-desc` : undefined

return (
<>
{labelProps && <label {...labelProps} htmlFor={inputId} />}
{inputProps && (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
id={inputId}
aria-describedby={descriptionId}
{...inputProps}
/>
)}
{descriptionIdProps && (
<Description id={descriptionId} {...descriptionIdProps} />
)}
</>
)
})}
</div>
)
}

TextField.Label = TextFieldLabel
TextField.Input = TextFieldInput
TextField.Description = TextFieldDescription
TextField.StyledLabel = StyledLabel
23 changes: 23 additions & 0 deletions example/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import styles from '../styles/Home.module.css'
import { Field } from '../components/Field'
import { StaticField } from '../components/StaticField'
import { Select } from '../components/Select'
import { TextField } from '../components/TextField'

const Home: NextPage = () => {
const [count, setCount] = React.useState(0)
Expand All @@ -27,6 +28,28 @@ const Home: NextPage = () => {
</div>

<div className={styles.grid}>
<div className={styles.card}>
<h2>RFC</h2>
<div>
<TextField>
<TextField.Input />
<TextField.Label>Label</TextField.Label>
{count % 3 !== 0 && (
<TextField.Description>
Description {count}
<TextField>
<TextField.Input />
<TextField.Label>Label</TextField.Label>
<TextField.Description>
Nested TextField {count}
</TextField.Description>
</TextField>
</TextField.Description>
)}
</TextField>
</div>
</div>

<div className={styles.card}>
<h2>Dynamic</h2>
<div>
Expand Down
13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@
"require": "./dist/index.js"
}
},
"./rfc": {
"development": {
"import": "./dev/rfc/index.mjs",
"require": "./dev/rfc/index.js"
},
"default": {
"import": "./dist/rfc/index.mjs",
"require": "./dist/rfc/index.js"
}
},
"./list": {
"development": {
"import": "./dev/list/index.mjs",
Expand Down Expand Up @@ -60,7 +70,8 @@
"entry": [
"src/index.tsx",
"src/list/index.tsx",
"src/static/index.tsx"
"src/static/index.tsx",
"src/rfc/index.tsx"
],
"format": [
"esm",
Expand Down
15 changes: 15 additions & 0 deletions rfc/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"main": "../dist/rfc/index.js",
"module": "../dist/rfc/index.mjs",
"types": "../dist/rfc/index.d.ts",
"exports": {
"development": {
"import": "./../dev/rfc/index.mjs",
"require": "./../dev/rfc/index.js"
},
"default": {
"import": "./../dist/rfc/index.mjs",
"require": "./../dist/rfc/index.js"
}
}
}
31 changes: 31 additions & 0 deletions src/rfc/SlotsManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react'

type Slot = React.ElementType

export const createSlotsManager = (onChange: (slot: Slot) => void) => {
const elementMap = new Map<Slot, React.ReactElement>()
return {
register(slot: Slot, element: React.ReactElement) {
elementMap.set(slot, element)
},
update(slot: Slot, element: React.ReactElement) {
elementMap.set(slot, element)
onChange(slot)
},
unmount(slot: Slot) {
elementMap.delete(slot)
onChange(slot)
},
get<T extends Slot>(slot: T) {
return elementMap.get(slot) as
| React.ReactElement<React.ComponentProps<T>, T>
| undefined
},
getProps<T extends Slot>(slot: T) {
const element = elementMap.get(slot)
if (!element) return undefined
const { ref, props } = element as any
return (ref ? { ...props, ref } : props) as React.ComponentProps<T>
},
}
}
55 changes: 55 additions & 0 deletions src/rfc/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as React from 'react'

import { createSlotsContext, getComponentName } from '../utils'
import { createSlotsManager } from './SlotsManager'

type Slots = ReturnType<typeof createSlotsManager>
type Callback = (Slots: Slots) => JSX.Element | null

const SlotsContext = createSlotsContext<Slots | undefined>(undefined)

export const SlotsHost = ({
children,
callback,
}: {
children: React.ReactNode
callback: Callback
}) => {
const forceUpdate = React.useReducer(() => [], [])[1]
const Slots = React.useMemo(
() => createSlotsManager(forceUpdate),
[forceUpdate]
)

return (
<>
<SlotsContext.Provider value={Slots}>{children}</SlotsContext.Provider>
{callback(Slots)}
</>
)
}

export const createHost = (children: React.ReactNode, callback: Callback) => {
return <SlotsHost children={children} callback={callback} />
}

export const createSlot = <T extends React.ElementType>(
Target: T,
fallback?: boolean
) => {
const ForwardRef = (props: any, ref: any) => {
const Slots = React.useContext(SlotsContext)
if (!Slots) return fallback ? <Target ref={ref} {...props} /> : null

const element = <Target ref={ref} {...props} />
React.useState(() => Slots.register(Slot, element))
React.useEffect(() => Slots.update(Slot, element))
React.useEffect(() => () => Slots.unmount(Slot), [Slots])

return null
}
ForwardRef.displayName = `Slot[${getComponentName(Target)}]`
const Slot = React.forwardRef(ForwardRef) as unknown as T

return Slot
}
3 changes: 2 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export const createSlotsContext = <T>(defaultValue: T) => {
return context
}

export const getComponentName = (Component: React.ComponentType) => {
export const getComponentName = (Component: React.ElementType) => {
if (typeof Component === 'string') return Component
// istanbul ignore next
return Component.displayName || Component.name || 'Component'
}
Expand Down

0 comments on commit 800b41e

Please sign in to comment.