Skip to content

Commit

Permalink
slots rock
Browse files Browse the repository at this point in the history
  • Loading branch information
kentcdodds committed Feb 15, 2024
1 parent a16b0df commit 72dcd76
Show file tree
Hide file tree
Showing 60 changed files with 215 additions and 233 deletions.
4 changes: 1 addition & 3 deletions exercises/03.compound-components/01.problem/toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ export function ToggleOff({ children }: { children: React.ReactNode }) {
return <>{on ? null : children}</>
}

export function ToggleButton(
props: Omit<React.ComponentProps<typeof Switch>, 'on' | 'onClick'>,
) {
export function ToggleButton(props: React.ComponentProps<typeof Switch>) {
// 🐨 get `on` and `toggle` from the ToggleContext with `use`
const on = false
const toggle = () => {}
Expand Down
6 changes: 3 additions & 3 deletions exercises/03.compound-components/01.solution/toggle.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { use, useState } from 'react'
import { createContext, use, useState } from 'react'
import { Switch } from '#shared/switch.tsx'

type ToggleValue = { on: boolean; toggle: () => void }
const ToggleContext = React.createContext<ToggleValue | undefined>(undefined)
const ToggleContext = createContext<ToggleValue | undefined>(undefined)
ToggleContext.displayName = 'ToggleContext'

export function Toggle({ children }: { children: React.ReactNode }) {
Expand All @@ -28,7 +28,7 @@ export function ToggleOff({ children }: { children: React.ReactNode }) {

export function ToggleButton({
...props
}: Omit<React.ComponentProps<typeof Switch>, 'on' | 'onClick'>) {
}: Omit<React.ComponentProps<typeof Switch>, 'on'>) {
const { on, toggle } = use(ToggleContext)!
return <Switch {...props} on={on} onClick={toggle} />
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Toggle, ToggleOn, ToggleOff, ToggleButton } from './toggle.tsx'
import { Toggle, ToggleButton, ToggleOff, ToggleOn } from './toggle.tsx'

export function App() {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { use, useState } from 'react'
import { createContext, use, useState } from 'react'
import { Switch } from '#shared/switch.tsx'

type ToggleValue = { on: boolean; toggle: () => void }
const ToggleContext = React.createContext<ToggleValue | undefined>(undefined)
const ToggleContext = createContext<ToggleValue | undefined>(undefined)
ToggleContext.displayName = 'ToggleContext'

export function Toggle({ children }: { children: React.ReactNode }) {
Expand All @@ -28,7 +28,7 @@ export function ToggleOff({ children }: { children: React.ReactNode }) {

export function ToggleButton({
...props
}: Omit<React.ComponentProps<typeof Switch>, 'on' | 'onClick'>) {
}: Omit<React.ComponentProps<typeof Switch>, 'on'>) {
const { on, toggle } = use(ToggleContext)!
return <Switch {...props} on={on} onClick={toggle} />
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { use, useState } from 'react'
import { createContext, use, useState } from 'react'
import { Switch } from '#shared/switch.tsx'

type ToggleValue = { on: boolean; toggle: () => void }
const ToggleContext = React.createContext<ToggleValue | undefined>(undefined)
const ToggleContext = createContext<ToggleValue | undefined>(undefined)
ToggleContext.displayName = 'ToggleContext'

export function Toggle({ children }: { children: React.ReactNode }) {
Expand Down Expand Up @@ -38,7 +38,7 @@ export function ToggleOff({ children }: { children: React.ReactNode }) {

export function ToggleButton({
...props
}: Omit<React.ComponentProps<typeof Switch>, 'on' | 'onClick'>) {
}: Omit<React.ComponentProps<typeof Switch>, 'on'>) {
const { on, toggle } = useToggle()
return <Switch {...props} on={on} onClick={toggle} />
}
1 change: 1 addition & 0 deletions exercises/04.slots/01.problem.context/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Slot Context
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export function App() {
</div>
<hr />
<div>
{/* 🦉 feel free to test the id customization by passing an id here */}
<TextField>
{/* 🦉 feel free to test the prop merging by passing props here */}
<Label>Venue</Label>
<Input />
</TextField>
Expand Down
File renamed without changes.
File renamed without changes.
18 changes: 18 additions & 0 deletions exercises/04.slots/01.problem.context/slots.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 🦺 create a Slots type that's just an object of objects
// 🐨 create and export a SlotContext with that type and default it to an empty object

// 🐨 create a useSlotProps hook which:
// 1. accepts props (any type) and slot (string)
// 2. gets the slots from the SlotContext
// 3. gets the props from the slot by its name
// 4. returns the merged props with the slot and given props

export function Label(props: React.ComponentProps<'label'>) {
// 🐨 get the props from useSlotProps for a slot called "label" and apply those to the label
return <label {...props} />
}

export function Input(props: React.ComponentProps<'input'>) {
// 🐨 get the props from useSlotProps for a slot called "label" and apply those to the input
return <input {...props} />
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ export function TextField({
const generatedId = useId()
id = id ?? generatedId

// 🐨 use these for the slot you render
// const labelProps = { htmlFor: id }
// const inputProps = { id }

// 🐨 wrap this in a SlotContext.Provider with the value set to an object
// that has a label and input property with the values of labelProps and
// inputProps respectively.
return children
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { use, useState } from 'react'
import { createContext, use, useState } from 'react'
import { Switch } from '#shared/switch.tsx'

type ToggleValue = { on: boolean; toggle: () => void }
const ToggleContext = React.createContext<ToggleValue | undefined>(undefined)
const ToggleContext = createContext<ToggleValue | undefined>(undefined)
ToggleContext.displayName = 'ToggleContext'

export function Toggle({ children }: { children: React.ReactNode }) {
Expand Down Expand Up @@ -38,7 +38,7 @@ export function ToggleOff({ children }: { children: React.ReactNode }) {

export function ToggleButton({
...props
}: Omit<React.ComponentProps<typeof Switch>, 'on' | 'onClick'>) {
}: Omit<React.ComponentProps<typeof Switch>, 'on'>) {
const { on, toggle } = useToggle()
return <Switch {...props} on={on} onClick={toggle} />
}
7 changes: 0 additions & 7 deletions exercises/04.slots/01.problem/slots.tsx

This file was deleted.

1 change: 1 addition & 0 deletions exercises/04.slots/01.solution.context/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Slot Context
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
import { createContext, use } from 'react'

type Slots = Record<string, Record<string, unknown>>
const SlotContext = createContext<Slots | null>(null)
export function SlotProvider({
slots,
children,
}: {
slots: Slots
children: React.ReactNode
}) {
return <SlotContext.Provider value={slots}>{children}</SlotContext.Provider>
}
export const SlotContext = createContext<Slots>({})

export function useSlotProps<Props>(props: Props, slot: string): Props {
const slots = use(SlotContext) ?? ({} as Slots)
function useSlotProps<Props>(props: Props, slot: string): Props {
const slots = use(SlotContext)

// a more proper "mergeProps" function is in order here
// to handle things like merging event handlers better.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useId } from 'react'
import { SlotProvider } from './slots'
import { SlotContext } from './slots'

export function TextField({
id,
Expand All @@ -15,8 +15,8 @@ export function TextField({
const inputProps = { id }

return (
<SlotProvider slots={{ label: labelProps, input: inputProps }}>
<SlotContext.Provider value={{ label: labelProps, input: inputProps }}>
{children}
</SlotProvider>
</SlotContext.Provider>
)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { use, useState } from 'react'
import { createContext, use, useState } from 'react'
import { Switch } from '#shared/switch.tsx'

type ToggleValue = { on: boolean; toggle: () => void }
const ToggleContext = React.createContext<ToggleValue | undefined>(undefined)
const ToggleContext = createContext<ToggleValue | undefined>(undefined)
ToggleContext.displayName = 'ToggleContext'

export function Toggle({ children }: { children: React.ReactNode }) {
Expand Down Expand Up @@ -38,7 +38,7 @@ export function ToggleOff({ children }: { children: React.ReactNode }) {

export function ToggleButton({
...props
}: Omit<React.ComponentProps<typeof Switch>, 'on' | 'onClick'>) {
}: Omit<React.ComponentProps<typeof Switch>, 'on'>) {
const { on, toggle } = useToggle()
return <Switch {...props} on={on} onClick={toggle} />
}
1 change: 1 addition & 0 deletions exercises/04.slots/02.problem.generic/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Generic Slot Components
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { TextField } from './text-field.tsx'
import { Toggle, ToggleButton, ToggleOff, ToggleOn } from './toggle.tsx'

export function App() {
// 💣 delete this variable
const partyModeId = useId()
return (
<div>
<div>
<Toggle>
{/* 🐨 switch this label for the Label component from ./slots.tsx */}
<label htmlFor={partyModeId}>Party mode</label>
{/* 🐨 remove this id prop */}
<ToggleButton id={partyModeId} />
<ToggleOn>Let's party 🥳</ToggleOn>
<ToggleOff>Sad town 😭</ToggleOff>
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
import { createContext, use } from 'react'

type Slots = Record<string, Record<string, unknown>>
const SlotContext = createContext<Slots | null>(null)
export function SlotProvider({
slots,
children,
}: {
slots: Slots
children: React.ReactNode
}) {
return <SlotContext.Provider value={slots}>{children}</SlotContext.Provider>
}
export const SlotContext = createContext<Slots>({})

export function useSlotProps<Props>(props: Props, slot: string): Props {
const slots = use(SlotContext) ?? ({} as Slots)
function useSlotProps<Props>(props: Props, slot: string): Props {
const slots = use(SlotContext)

// a more proper "mergeProps" function is in order here
// to handle things like merging event handlers better.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useId } from 'react'
import { SlotProvider } from './slots'
import { SlotContext } from './slots'

export function TextField({
id,
Expand All @@ -15,8 +15,8 @@ export function TextField({
const inputProps = { id }

return (
<SlotProvider slots={{ label: labelProps, input: inputProps }}>
<SlotContext.Provider value={{ label: labelProps, input: inputProps }}>
{children}
</SlotProvider>
</SlotContext.Provider>
)
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { use, useState } from 'react'
import { createContext, use, useState } from 'react'
import { Switch } from '#shared/switch.tsx'

// 🐨 add an id string to the ToggleValue type
type ToggleValue = { on: boolean; toggle: () => void }
const ToggleContext = React.createContext<ToggleValue | undefined>(undefined)
const ToggleContext = createContext<ToggleValue | undefined>(undefined)
ToggleContext.displayName = 'ToggleContext'

// 🐨 update this to accept an optional id
export function Toggle({ children }: { children: React.ReactNode }) {
const [on, setOn] = useState(false)
// 🐨 generate an id using useId (💰 similar to in text-field.tsx)

const toggle = () => setOn(!on)

// 🐨 create labelProps that sets htmlFor to the id

// 🐨 wrap this in SlotContext.Provider and pass the labelProps in the label slot
// 🐨 add the id to the value in the ToggleContext.Provider
return (
<ToggleContext.Provider value={{ on, toggle }}>
{children}
Expand Down Expand Up @@ -38,7 +46,9 @@ export function ToggleOff({ children }: { children: React.ReactNode }) {

export function ToggleButton({
...props
}: Omit<React.ComponentProps<typeof Switch>, 'on' | 'onClick'>) {
}: Omit<React.ComponentProps<typeof Switch>, 'on'>) {
// 🐨 get the id out of useToggle
const { on, toggle } = useToggle()
// 🐨 pass the id for the ToggleButton here
return <Switch {...props} on={on} onClick={toggle} />
}
1 change: 0 additions & 1 deletion exercises/04.slots/02.problem/README.mdx

This file was deleted.

1 change: 1 addition & 0 deletions exercises/04.slots/02.solution.generic/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Generic Slot Components
File renamed without changes.
File renamed without changes.
File renamed without changes.
23 changes: 23 additions & 0 deletions exercises/04.slots/02.solution.generic/slots.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createContext, use } from 'react'

type Slots = Record<string, Record<string, unknown>>
export const SlotContext = createContext<Slots>({})

function useSlotProps<Props>(props: Props, slot: string): Props {
const slots = use(SlotContext)

// a more proper "mergeProps" function is in order here
// to handle things like merging event handlers better.
// we'll get to that a bit in a later exercise.
return { ...slots[slot], slot, ...props } as Props
}

export function Label(props: React.ComponentProps<'label'>) {
props = useSlotProps(props, 'label')
return <label {...props} />
}

export function Input(props: React.ComponentProps<'input'>) {
props = useSlotProps(props, 'input')
return <input {...props} />
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useId } from 'react'
import { SlotProvider } from './slots'
import { SlotContext } from './slots'

export function TextField({
id,
Expand All @@ -15,8 +15,8 @@ export function TextField({
const inputProps = { id }

return (
<SlotProvider slots={{ label: labelProps, input: inputProps }}>
<SlotContext.Provider value={{ label: labelProps, input: inputProps }}>
{children}
</SlotProvider>
</SlotContext.Provider>
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createContext, use, useId, useState } from 'react'
import { Switch } from '#shared/switch.tsx'
import { SlotProvider } from './slots'
import { SlotContext } from './slots'

type ToggleValue = { on: boolean; toggle: () => void; id: string }
const ToggleContext = createContext<ToggleValue | undefined>(undefined)
Expand All @@ -14,18 +14,19 @@ export function Toggle({
children: React.ReactNode
}) {
const [on, setOn] = useState(false)
const toggle = () => setOn(!on)
const generatedId = useId()
id = id ?? generatedId

const toggle = () => setOn(!on)

const labelProps = { htmlFor: id }

return (
<SlotProvider slots={{ label: labelProps }}>
<SlotContext.Provider value={{ label: labelProps }}>
<ToggleContext.Provider value={{ on, toggle, id }}>
{children}
</ToggleContext.Provider>
</SlotProvider>
</SlotContext.Provider>
)
}

Expand All @@ -51,7 +52,7 @@ export function ToggleOff({ children }: { children: React.ReactNode }) {

export function ToggleButton({
...props
}: Omit<React.ComponentProps<typeof Switch>, 'on' | 'onClick'>) {
}: Omit<React.ComponentProps<typeof Switch>, 'on'>) {
const { on, toggle, id } = useToggle()
return <Switch {...props} id={id} on={on} onClick={toggle} />
}
1 change: 0 additions & 1 deletion exercises/04.slots/02.solution/README.mdx

This file was deleted.

Loading

0 comments on commit 72dcd76

Please sign in to comment.