Skip to content

Commit

Permalink
feat: implement React Slots RFC (#10)
Browse files Browse the repository at this point in the history
* WIP: add rfc

* add tests

* Host -> HostSlots

* add createFill in test

* add isSlot util

* update after rebase

* reexport
  • Loading branch information
nihgwu authored Aug 16, 2022
1 parent 77c5cdf commit 19ddfe0
Show file tree
Hide file tree
Showing 22 changed files with 1,394 additions and 77 deletions.
117 changes: 69 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,39 +12,41 @@ Bring [Slots](https://github.com/reactjs/rfcs/pull/223) to React, with SSR suppo

## Usage

### Simple version (only one slot is used per slot type)

1. Create your component with slots

```tsx
import React, { useId } from 'react'
import createSlots from 'create-slots'
import { createHost, createSlot } from 'create-slots'

const { createHost, SlotComponents, useSlots } = createSlots({
Label: 'label',
Input: 'input',
Description: 'div',
})
const FieldLabel = createSlot('label')
const FieldInput = createSlot('input')
const FieldDescription = createSlot('div')

type FieldProps = React.ComponentPropsWithoutRef<'div'>

const FieldBase: React.FC<FieldProps> = (props) => {
const Slots = useSlots()
export const Field = (props: FieldProps) => {
const id = useId()
const inputId = Slots.getProps('Input')?.id || `${id}-label`
const descriptionId = Slots.has('Description') ? `${id}-desc` : undefined

return (
<div {...props}>
{Slots.render('Label', { htmlFor: inputId })}
{Slots.render('Input', {
id: inputId,
'aria-describedby': descriptionId,
})}
{Slots.render('Description', { id: descriptionId })}
</div>
)
return createHost(props.children, (Slots) => {
const labelProps = Slots.getProps(FieldLabel)
const inputProps = Slots.getProps(FieldInput)
const inputId = inputProps?.id || id

return (
<div {...props}>
{labelProps && <label {...labelProps} htmlFor={inputId} />}
<input id={id} {...inputProps} />
{Slots.get('Description')}
</div>
)
})
}

export const Field = Object.assign(createHost(FieldBase), SlotComponents)
Field.Label = FieldLabel
Field.Input = FieldInput
Field.Description = FieldDescription
```

2. Use it
Expand All @@ -57,43 +59,62 @@ export const Field = Object.assign(createHost(FieldBase), SlotComponents)
</Field>
```

### List slots
### List slots (fully implemented the [React Slots RFC](https://github.com/reactjs/rfcs/pull/223) with utils)

```tsx
import React, { useState } from 'react'
import createSlots from 'create-slots/list'

const { createHost, SlotComponents, useSlots } = createSlots({
Item: 'li',
Divider: 'hr',
})

const SelectBase: React.FC<React.ComponentPropsWithoutRef<'ul'>> = (props) => {
const [selected, setSelected] = useState<React.ReactNode>(null)
const slotItems = useSlots().renderItems(
({ name, props: itemProps, index }) => {
if (name === 'Item') {
return {
...itemProps,
'data-index': index,
'aria-selected': itemProps.children === selected,
onClick: () => {
setSelected(itemProps.value)
},
}
}
}
)
import { createHost, createSlot, getSlotProps, isSlot } from 'create-slots/list'

const SelectItem = createSlot('li')
const SelectDivider = createSlot('hr')

type SelectProps = React.FC<React.ComponentPropsWithoutRef<'ul'>>

const Select = (props: SelectProps) => {
const [selected, setSelected] = useState<string>()
const indexRef = React.useRef(0)

return (
<div>
<div>Selected: {selected}</div>
<ul {...props}>{slotItems}</ul>
<div>Selected: {selected ?? ''}</div>
{createHost(props.children, (slots) => {
indexRef.current = 0
return (
<ul {...props}>
{slots.map((slot) => {
if (isSlot(slot, SelectItem)) {
const slotProps = getSlotProps(slot)
return (
<li
{...slotProps}
data-index={indexRef.current++}
aria-selected={slotProps.value === selected}
onClick={() => setSelected(slotProps.value as string)}
/>
)
}

return slot
})}
</ul>
)
})}
</div>
)
}

export const Select = Object.assign(createHost(SelectBase), SlotComponents)
Select.Item = SelectItem
Select.Divider = SelectDivider
```

2. Use it

```tsx
<Select>
<Select.Item value="foo">Foo</Select.Item>
<Select.Divider />
<Select.Item value="bar">Bar</Select.Item>
</Select>
```

## License
Expand Down
38 changes: 38 additions & 0 deletions example/components/RFCItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { useId } from 'react'
import { createHost, createSlot, getSlot } from 'create-slots/list'

type ItemProps = Omit<React.ComponentPropsWithoutRef<'li'>, 'value'> & {
value: string
}

const ItemTitle = createSlot<'h4'>()
const ItemDescription = createSlot<'div'>()

export const Item = (props: ItemProps) => {
const id = useId()

return createHost(props.children, (slots) => {
const titleSlot = getSlot(slots, ItemTitle)
const descriptionSlot = getSlot(slots, ItemDescription)
const titleId = titleSlot ? `${id}-title` : undefined
const descId = descriptionSlot ? `${id}-desc` : undefined

return (
<li aria-describedby={descId} aria-label={titleId} {...props}>
{titleSlot && (
<h4 id={titleId} ref={titleSlot.ref} {...titleSlot.props} />
)}
{descriptionSlot && (
<div
id={descId}
ref={descriptionSlot.ref}
{...descriptionSlot.props}
/>
)}
</li>
)
})
}

Item.Title = ItemTitle
Item.Description = ItemDescription
55 changes: 55 additions & 0 deletions example/components/RFCSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { useRef, useState } from 'react'
import { createHost, createSlot, getSlotProps, isSlot } from 'create-slots/list'

import { Item } from './RFCItem'

const SelectItem = createSlot<typeof Item>()
const SelectDivider = createSlot('hr')

export const Select = (props: React.ComponentProps<'ul'>) => {
const [selected, setSelected] = useState<React.ReactNode>(null)
const indexRef = useRef(0)

return (
<div>
<div>Selected: {selected}</div>
{createHost(props.children, (slots) => {
indexRef.current = 0
return (
<ul {...props}>
{slots.map((slot) => {
if (isSlot(slot, SelectItem)) {
const itemProps = getSlotProps(slot)

return (
<Item
key={slot.key}
{...itemProps}
role="button"
tabIndex={0}
data-index={indexRef.current++}
aria-selected={itemProps.value === selected}
onClick={() => setSelected(itemProps.value)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
setSelected(itemProps.value)
}
}}
/>
)
}

return slot
})}
</ul>
)
})}
</div>
)
}

Select.Item = SelectItem
Select.Divider = SelectDivider

Select.Item.Title = Item.Title
Select.Item.Description = Item.Description
60 changes: 60 additions & 0 deletions example/components/SimpleField.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'

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

const FieldLabel = createSlot('label')
const FieldInput = createSlot('input')
const FieldDescription = createSlot(Description)

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

export const Field = (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(FieldLabel)
const inputProps = Slots.getProps(FieldInput)
const descriptionIdProps = Slots.getProps(FieldDescription)

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>
)
}

Field.Label = FieldLabel
Field.Input = FieldInput
Field.Description = FieldDescription
Field.StyledLabel = StyledLabel
Loading

1 comment on commit 19ddfe0

@vercel
Copy link

@vercel vercel bot commented on 19ddfe0 Aug 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

create-slots – ./

create-slots.vercel.app
create-slots-neonie.vercel.app
create-slots-git-master-neonie.vercel.app

Please sign in to comment.