diff --git a/exercises/04.slots/01.problem.context/slots.tsx b/exercises/04.slots/01.problem.context/slots.tsx index 20ea7c3c..3f1a8ea9 100644 --- a/exercises/04.slots/01.problem.context/slots.tsx +++ b/exercises/04.slots/01.problem.context/slots.tsx @@ -1,4 +1,5 @@ // 🦺 create a Slots type that's just an object of objects +// 💰 type Slots = Record> // 🐨 create and export a SlotContext with that type and default it to an empty object // 🐨 create a useSlotProps hook which: diff --git a/exercises/04.slots/01.problem.context/text-field.tsx b/exercises/04.slots/01.problem.context/text-field.tsx index aaa37af3..498bc017 100644 --- a/exercises/04.slots/01.problem.context/text-field.tsx +++ b/exercises/04.slots/01.problem.context/text-field.tsx @@ -8,14 +8,11 @@ export function TextField({ children: React.ReactNode }) { const generatedId = useId() - id = id ?? generatedId + id ??= generatedId - // 🐨 use these for the slot you render - // const labelProps = { htmlFor: id } - // const inputProps = { id } + // 🐨 create a slots object that has props for both label and input slots + // 💰 the label should provide an htmlFor prop and the input should provide an 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. + // 🐨 wrap this in a SlotContext.Provider with the value set to the slots object return children } diff --git a/exercises/04.slots/01.solution.context/text-field.tsx b/exercises/04.slots/01.solution.context/text-field.tsx index c3107e2f..31023d0b 100644 --- a/exercises/04.slots/01.solution.context/text-field.tsx +++ b/exercises/04.slots/01.solution.context/text-field.tsx @@ -9,14 +9,12 @@ export function TextField({ children: React.ReactNode }) { const generatedId = useId() - id = id ?? generatedId + id ??= generatedId - const labelProps = { htmlFor: id } - const inputProps = { id } + const slots = { + label: { htmlFor: id }, + input: { id }, + } - return ( - - {children} - - ) + return {children} } diff --git a/exercises/04.slots/02.problem.generic/text-field.tsx b/exercises/04.slots/02.problem.generic/text-field.tsx index c3107e2f..31023d0b 100644 --- a/exercises/04.slots/02.problem.generic/text-field.tsx +++ b/exercises/04.slots/02.problem.generic/text-field.tsx @@ -9,14 +9,12 @@ export function TextField({ children: React.ReactNode }) { const generatedId = useId() - id = id ?? generatedId + id ??= generatedId - const labelProps = { htmlFor: id } - const inputProps = { id } + const slots = { + label: { htmlFor: id }, + input: { id }, + } - return ( - - {children} - - ) + return {children} } diff --git a/exercises/04.slots/02.problem.generic/toggle.tsx b/exercises/04.slots/02.problem.generic/toggle.tsx index 7490ff53..a1cb6fa9 100644 --- a/exercises/04.slots/02.problem.generic/toggle.tsx +++ b/exercises/04.slots/02.problem.generic/toggle.tsx @@ -12,7 +12,8 @@ export function Toggle({ children }: { children: React.ReactNode }) { const toggle = () => setOn(!on) - // 🐨 create labelProps that sets htmlFor to the id + // 🐨 create a slots object that has props for a slot called + // "label" with an htmlFor prop // 🐨 wrap this in SlotContext.Provider and pass the labelProps in the label slot // 🐨 add the id to the value in the ToggleContext.Provider diff --git a/exercises/04.slots/02.solution.generic/text-field.tsx b/exercises/04.slots/02.solution.generic/text-field.tsx index c3107e2f..31023d0b 100644 --- a/exercises/04.slots/02.solution.generic/text-field.tsx +++ b/exercises/04.slots/02.solution.generic/text-field.tsx @@ -9,14 +9,12 @@ export function TextField({ children: React.ReactNode }) { const generatedId = useId() - id = id ?? generatedId + id ??= generatedId - const labelProps = { htmlFor: id } - const inputProps = { id } + const slots = { + label: { htmlFor: id }, + input: { id }, + } - return ( - - {children} - - ) + return {children} } diff --git a/exercises/04.slots/02.solution.generic/toggle.tsx b/exercises/04.slots/02.solution.generic/toggle.tsx index e6154602..b4497fc0 100644 --- a/exercises/04.slots/02.solution.generic/toggle.tsx +++ b/exercises/04.slots/02.solution.generic/toggle.tsx @@ -14,14 +14,14 @@ export function Toggle({ }) { const [on, setOn] = useState(false) const generatedId = useId() - id = id ?? generatedId + id ??= generatedId const toggle = () => setOn(!on) - const labelProps = { htmlFor: id } + const slots = { label: { htmlFor: id } } return ( - + {children} diff --git a/exercises/04.slots/03.problem.prop/slots.tsx b/exercises/04.slots/03.problem.prop/slots.tsx index 24bf627c..02db9eca 100644 --- a/exercises/04.slots/03.problem.prop/slots.tsx +++ b/exercises/04.slots/03.problem.prop/slots.tsx @@ -38,9 +38,8 @@ export function Text(props: React.ComponentProps<'span'>) { } // 🐨 add an optional slot to the props type here -export function Switch( - props: Omit, 'on'>, -) { +type SwitchProps = Omit, 'on'> +export function Switch(props: SwitchProps) { return ( - {children} - - ) + return {children} } diff --git a/exercises/04.slots/03.problem.prop/toggle.tsx b/exercises/04.slots/03.problem.prop/toggle.tsx index 7601cb3f..06ecda1c 100644 --- a/exercises/04.slots/03.problem.prop/toggle.tsx +++ b/exercises/04.slots/03.problem.prop/toggle.tsx @@ -15,20 +15,18 @@ export function Toggle({ }) { const [on, setOn] = useState(false) const generatedId = useId() - id = id ?? generatedId + id ??= generatedId const toggle = () => setOn(!on) - const labelProps = { htmlFor: id } - // 🐨 add props objects for onText, offText, and switch + const slots = { + label: { htmlFor: id }, + // 🐨 add slots for onText (hidden prop), offText (hidden prop), + // and switch (id, on, onClick props) + } return ( - + {/* 🐨 get rid of the ToggleContext here */} {children} diff --git a/exercises/04.slots/03.solution.prop/slots.tsx b/exercises/04.slots/03.solution.prop/slots.tsx index e74f9358..0a4acc75 100644 --- a/exercises/04.slots/03.solution.prop/slots.tsx +++ b/exercises/04.slots/03.solution.prop/slots.tsx @@ -38,11 +38,10 @@ export function Text(props: React.ComponentProps<'span'> & { slot?: string }) { return } -export function Switch( - props: Omit, 'on'> & { - slot?: string - }, -) { +type SwitchProps = Omit, 'on'> & { + slot?: string +} +export function Switch(props: SwitchProps) { return ( - {children} - - ) + return {children} } diff --git a/exercises/04.slots/03.solution.prop/toggle.tsx b/exercises/04.slots/03.solution.prop/toggle.tsx index c79261ef..99af9c5f 100644 --- a/exercises/04.slots/03.solution.prop/toggle.tsx +++ b/exercises/04.slots/03.solution.prop/toggle.tsx @@ -10,7 +10,7 @@ export function Toggle({ }) { const [on, setOn] = useState(false) const generatedId = useId() - id = id ?? generatedId + id ??= generatedId const toggle = () => setOn(!on) diff --git a/exercises/04.slots/README.mdx b/exercises/04.slots/README.mdx index 74336e96..3623ebcd 100644 --- a/exercises/04.slots/README.mdx +++ b/exercises/04.slots/README.mdx @@ -5,6 +5,11 @@ particular role in the overall collection of components. +This pattern is particularly useful for situations where you're building a UI +library with a lot of components that need to work together. It's a way to +provide a flexible API for your components that allows them to be used in a +variety of contexts. + If you're building a component library, you have to deal with two competing interests: @@ -71,3 +76,39 @@ This can be used to apply appropriate `aria-` attributes as well as `id`s and event handlers. You might think about it as a way to implement compound components in a way that doesn't require an individual component for every single use case. + +## Implementation + +Folks tend to struggle with this one a bit more than the rest, but it's simpler +than it seems. + +The basic concept is your root component creates collections of props like so: + +```tsx +function NumberField({ children }: { children: React.ReactNode }) { + // setup state/events/etc + + const slots = { + label: { htmlFor: inputId }, + decrement: { onClick: decrement }, + increment: { onClick: increment }, + description: { id: descriptionId }, + input: { id: inputId, 'aria-describedby': descriptionId }, + } + return {children} +} +``` + +Then the consuming components use the `use(SlotContext)` to get access to the +`slots` object and pluck off the props they need to do their job: + +```tsx +function Input(props) { + props = useSlotProps(props, 'input') + + return +} +``` + +The `useSlotProps` hook is responsible for taking the props that have been +specified and combining it with those from the `SlotContext` for the named slot.