Skip to content

Commit 8d454f2

Browse files
feat: implement the compound components pattern
1 parent 1ea04fa commit 8d454f2

File tree

14 files changed

+546
-18
lines changed

14 files changed

+546
-18
lines changed

src/course/01-introduction/01-Welcome.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Each lesson is broken down in an exercise file and a final file. The exercise fi
4747
- 07 - [Higher order component](?path=/docs/lessons-07-higher-order-components-pattern-01-lesson--docs)
4848
- 08 - [The Provider pattern](?path=/docs/lessons-08-provider-pattern-01-lesson--docs)
4949
- 09 - [The State Reducer pattern](?path=/docs/lessons-09-state-reducer-pattern-01-lesson--docs)
50-
- 10 - [Compound components pattern](?path=/docs/lessons-10-compound-components-pattern-🚧-01-lesson--docs)
50+
- 10 - [Compound components pattern](?path=/docs/lessons-10-compound-components-pattern-01-lesson--docs)
5151
- 11 - [Slots pattern](?path=/docs/lessons-11-slots-01-lesson--docs)
5252
- 12 - [Portals pattern](?path=/docs/lessons-12-portals-01-lesson--docs)
5353

src/course/02- lessons/04-PresentationalAndContainer/exercise.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ interface IPaymentTemplate {
1919
// 1A 👨🏻‍💻 - Migrate the JSX from BrandPage One into the PaymentTemplate.
2020
// 🤔 Think about where that local state should live. It's not business logic so copy over the
2121
// showBillingAddress functionality as well.
22+
23+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
2224
// @ts-ignore
2325
const PaymentTemplate = ({}: IPaymentTemplate) => null;
2426

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import classNames from 'classnames';
2+
import styles from './Accoridon.module.css';
3+
import { ChevronDown } from './ChevronDown';
4+
5+
interface IAccordion {
6+
id: string;
7+
children:
8+
| React.ReactElement<IAccordionItem>
9+
| React.ReactElement<IAccordionItem>[];
10+
title: string;
11+
}
12+
13+
interface IAccordionItem {
14+
id: string;
15+
children: React.ReactNode | React.ReactNode[];
16+
title: string;
17+
isSelected?: boolean;
18+
onClick?: VoidFunction;
19+
onFocus?: VoidFunction;
20+
}
21+
22+
export const Accordion = ({ id, children, title }: IAccordion) => {
23+
// 👨🏻‍💻 1B - Paste that useState here
24+
25+
// 👨🏻‍💻 1C - Replacing {children}
26+
// We need to map the children and apply the props to the AccordionItem here so we can manage the state within the accordion. It looks like this the syntax:
27+
// Children.map(children, (child: React.ReactElement<IAccordionItem>, index) => cloneElement(child, { PROPS (look at the current props) }))
28+
29+
// 👨🏻‍💻 1D - Notice how there was an accordion-one in the id of the props on AccordionItem in exercise.tsx?
30+
// We need to use the index from the children map function as an identifier.
31+
/*
32+
isSelected: selectedAccordion === index,
33+
id: `${id}_${child.props.id}_${index}`,
34+
onClick: () => setSelectedAccordion(index),
35+
onFocus: () => setSelectedAccordion(index)
36+
*/
37+
38+
// Once this is completed return to the exercise.tsx file.
39+
40+
// 👨🏻‍💻 3B Now where you have the onClick which just does this - onClick: () => setSelectedAccordion(index) atm
41+
// Make it do this instead
42+
/*
43+
onClick: () => {
44+
if (child.props.onClick) {
45+
child.props.onClick();
46+
}
47+
48+
setSelectedAccordion(index)
49+
}
50+
*/
51+
// What is happening here now is that we are checking if the AccordionItem already has a onClick prop and firing that if it does exist as well as managing the local state of the accordion.
52+
return (
53+
<section id={id}>
54+
<h2 className="text-2xl font-bold mb-4">{title}</h2>
55+
{children}
56+
</section>
57+
);
58+
};
59+
60+
export const AccordionItem = ({
61+
id,
62+
title,
63+
children,
64+
isSelected,
65+
onClick,
66+
onFocus
67+
}: IAccordionItem) => (
68+
<div
69+
className={classNames(
70+
styles.accordionItem,
71+
'rounded-lg border-[1px] mb-4 overflow-hidden',
72+
isSelected ? 'border-blue-950' : 'border-blue-100'
73+
)}
74+
onFocus={onFocus}
75+
>
76+
<h3>
77+
<button
78+
type="button"
79+
aria-expanded={isSelected ? 'true' : 'false'}
80+
aria-controls={`accordion_panel_${id}`}
81+
id={`accordion_button_${id}`}
82+
onClick={onClick}
83+
className={classNames(
84+
'p-4 font-semibold text-lg flex justify-between items-center w-full transition-all duration-300',
85+
styles.accordionButton,
86+
{
87+
[styles.accordionButtonSelected]: isSelected
88+
}
89+
)}
90+
>
91+
{title}
92+
<span
93+
className={classNames(
94+
styles.accordionIcon,
95+
'w-[1.5em] h-[1.5em] transition-transform duration-300'
96+
)}
97+
>
98+
<ChevronDown />
99+
</span>
100+
</button>
101+
</h3>
102+
<div
103+
role="region"
104+
id={`accordion_panel_${id}`}
105+
aria-labelledby={`accordion_button_${id}`}
106+
className={classNames(styles.accordionPanel, {
107+
[styles.accordionPanelSelected]: isSelected
108+
})}
109+
>
110+
{children}
111+
</div>
112+
</div>
113+
);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
.accordionPanel {
2+
@apply py-0;
3+
@apply px-4;
4+
@apply h-0;
5+
@apply transition-all;
6+
@apply duration-300;
7+
}
8+
9+
.accordionItem:focus-within .accordionPanel,
10+
.accordionPanelSelected {
11+
@apply p-4;
12+
@apply h-auto;
13+
@apply max-h-[1000px];
14+
@apply transition-all;
15+
@apply duration-300;
16+
}
17+
18+
.accordionButton {
19+
@apply bg-blue-100;
20+
@apply hover:bg-blue-200;
21+
@apply text-blue-950;
22+
}
23+
24+
.accordionItem:focus-within .accordionButton,
25+
.accordionButtonSelected {
26+
@apply bg-blue-950;
27+
@apply hover:bg-blue-950;
28+
@apply text-white;
29+
}
30+
31+
.accordionIcon {
32+
@apply rotate-[-90deg];
33+
}
34+
35+
.accordionItem:focus-within .accordionIcon,
36+
.accordionButtonSelected .accordionIcon {
37+
@apply rotate-0;
38+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export const ChevronDown = () => (
2+
<svg viewBox="0 0 24 24">
3+
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
4+
<rect
5+
id="Rectangle"
6+
fill-rule="nonzero"
7+
x="0"
8+
y="0"
9+
width="24"
10+
height="24"
11+
></rect>
12+
<path
13+
d="M18,10 L12.3536,15.6464 C12.1583,15.8417 11.8417,15.8417 11.6464,15.6464 L6,10"
14+
id="Path"
15+
stroke="currentColor"
16+
strokeWidth="2"
17+
strokeLinecap="round"
18+
></path>
19+
</g>
20+
</svg>
21+
);
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import type { Meta, StoryObj } from "@storybook/react";
1+
import type { Meta, StoryObj } from '@storybook/react';
22

3-
import { Exercise } from "./exercise";
3+
import { Exercise } from './exercise';
44

55
const meta: Meta<typeof Exercise> = {
6-
title: "Lessons/10 - Compound Components Pattern 🚧/02-Exercise",
7-
component: Exercise,
6+
title: 'Lessons/10 - Compound Components Pattern/02-Exercise',
7+
component: Exercise
88
};
99

1010
export default meta;
@@ -16,5 +16,5 @@ type Story = StoryObj<typeof Exercise>;
1616
*/
1717
export const Default: Story = {
1818
play: async () => {},
19-
args: {},
19+
args: {}
2020
};
Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,108 @@
1+
import { useState } from 'react';
2+
import { Accordion, AccordionItem } from './components/Accordion';
3+
4+
/**
5+
* Exercise: Convert the current accordion implementation to use the compound pattern
6+
*
7+
* 🤔 Observations of this file
8+
* As you can see in this component we have some useState which is managing which accordion item is open at any given time. We need to move this logic into the Accordion component and pass down the props into the AccordionItem that way instead of managing it here in this file.
9+
*
10+
*/
11+
12+
// 👨🏻‍💻 1A Copy the useState on line 14 and go to ./components/Accordion.tsx
113
export const Exercise = () => {
2-
return null;
14+
// 💣 2A Remove the useState and the isSelected, id, onClick, onFocus props from all the AccordionItems
15+
16+
const [selectedAccordion, setSelectedAccordion] =
17+
useState<string>();
18+
19+
// 🤔 3A (Bonus round) - now the customer wants to add event tracking when you click ONLY the first accordion item. Since the props now live in the accordion for onClick, we need to persist that onClick to the accordionItem if we specify one at this level. Add an onClick on the first AccordionItem with a console.log('TRACK') and then move over to the Accordion.tsx.
20+
return (
21+
<Accordion id="my-accordion" title="FAQs">
22+
<AccordionItem
23+
title="Accordion Button One"
24+
isSelected={selectedAccordion === 'accordion-one'}
25+
id="accordion-one"
26+
onClick={() => setSelectedAccordion('accordion-one')}
27+
onFocus={() => setSelectedAccordion('accordion-one')}
28+
>
29+
<p>
30+
Per torquent, mus cursus hendrerit id aenean justo auctor
31+
donec. Turpis magna et, egestas dignissim nascetur. Sapien
32+
augue nisl varius diam aliquet. Litora velit, tortor at
33+
ante. Eros lacus faucibus consequat scelerisque proin
34+
volutpat. In pellentesque est curae; dapibus nisl risus
35+
sociosqu penatibus. Lobortis pulvinar scelerisque lacus.
36+
Elit vel eros facilisi dis mauris magna posuere? Cum class
37+
viverra bibendum rutrum odio scelerisque scelerisque libero,
38+
nisl est convallis non. Ac convallis odio suspendisse velit
39+
mollis libero. Morbi enim blandit venenatis{' '}
40+
<a href="#">lorem!</a>
41+
</p>
42+
</AccordionItem>
43+
<AccordionItem
44+
title="Accordion Button Two"
45+
isSelected={selectedAccordion === 'accordion-two'}
46+
id="accordion-two"
47+
onClick={() => setSelectedAccordion('accordion-two')}
48+
onFocus={() => setSelectedAccordion('accordion-two')}
49+
>
50+
<p>
51+
Per torquent, mus cursus hendrerit id aenean justo auctor
52+
donec. Turpis magna et, egestas dignissim nascetur. Sapien
53+
augue nisl varius diam aliquet. Litora velit, tortor at
54+
ante. Eros lacus faucibus consequat scelerisque proin
55+
volutpat. In pellentesque est curae; dapibus nisl risus
56+
sociosqu penatibus. Lobortis pulvinar scelerisque lacus.
57+
Elit vel eros facilisi dis mauris magna posuere? Cum class
58+
viverra bibendum rutrum odio scelerisque scelerisque libero,
59+
nisl est convallis non. Ac convallis odio suspendisse velit
60+
mollis libero. Morbi enim blandit venenatis{' '}
61+
<a href="#">lorem!</a>
62+
</p>
63+
</AccordionItem>
64+
<AccordionItem
65+
title="Accordion Button Three"
66+
isSelected={selectedAccordion === 'accordion-three'}
67+
id="accordion-three"
68+
onClick={() => setSelectedAccordion('accordion-three')}
69+
onFocus={() => setSelectedAccordion('accordion-three')}
70+
>
71+
<p>
72+
Per torquent, mus cursus hendrerit id aenean justo auctor
73+
donec. Turpis magna et, egestas dignissim nascetur. Sapien
74+
augue nisl varius diam aliquet. Litora velit, tortor at
75+
ante. Eros lacus faucibus consequat scelerisque proin
76+
volutpat. In pellentesque est curae; dapibus nisl risus
77+
sociosqu penatibus. Lobortis pulvinar scelerisque lacus.
78+
Elit vel eros facilisi dis mauris magna posuere? Cum class
79+
viverra bibendum rutrum odio scelerisque scelerisque libero,
80+
nisl est convallis non. Ac convallis odio suspendisse velit
81+
mollis libero. Morbi enim blandit venenatis{' '}
82+
<a href="#">lorem!</a>
83+
</p>
84+
</AccordionItem>
85+
<AccordionItem
86+
title="Accordion Button Four"
87+
isSelected={selectedAccordion === 'accordion-four'}
88+
id="accordion-four"
89+
onClick={() => setSelectedAccordion('accordion-four')}
90+
onFocus={() => setSelectedAccordion('accordion-four')}
91+
>
92+
<p>
93+
Per torquent, mus cursus hendrerit id aenean justo auctor
94+
donec. Turpis magna et, egestas dignissim nascetur. Sapien
95+
augue nisl varius diam aliquet. Litora velit, tortor at
96+
ante. Eros lacus faucibus consequat scelerisque proin
97+
volutpat. In pellentesque est curae; dapibus nisl risus
98+
sociosqu penatibus. Lobortis pulvinar scelerisque lacus.
99+
Elit vel eros facilisi dis mauris magna posuere? Cum class
100+
viverra bibendum rutrum odio scelerisque scelerisque libero,
101+
nisl est convallis non. Ac convallis odio suspendisse velit
102+
mollis libero. Morbi enim blandit venenatis{' '}
103+
<a href="#">lorem!</a>
104+
</p>
105+
</AccordionItem>
106+
</Accordion>
107+
);
3108
};

src/course/02- lessons/10-Compound/lesson.mdx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
11
import { Meta } from '@storybook/blocks';
22

3-
<Meta title="Lessons/10 - Compound Components Pattern 🚧/01-Lesson" />
3+
<Meta title="Lessons/10 - Compound Components Pattern/01-Lesson" />
44

5-
# Compound Components Pattern 🚧
5+
# Compound Components Pattern
6+
7+
Compound components is an advanced React container pattern that provides a simple and efficient way for multiple components to share states and handle logic — working together.
8+
9+
The compound components pattern provides an expressive and flexible API for communication between a parent component and its children. Also, the compound components pattern enables a parent component to interact and share state with its children implicitly, which makes it suitable for building declarative UI. A good example is the select html element:
10+
11+
```html
12+
<select>
13+
<option value="javaScript">JavaScript</option>
14+
<option value="python">Python</option>
15+
<option value="java">Java</option>
16+
</select>
17+
```
18+
19+
In the code above, the select element manages and shares its state implicitly with the options elements. Consequently, although there is no explicit state declaration, the select element knows what option the user selects.
20+
21+
The compound component pattern is useful in building complex React components such as a switch, tab switcher, accordion, dropdowns, tag list, and more. It can be implemented either by using the Context API or the React.cloneElement function.
622

723
## Exercise
824

25+
A requirement has come in to reuse the accordion in another location of our application. The current implementation of the accordion has it's state management implemented only on the page that this component is used on. We need to refactor the component to use the compound design pattern so that it can be re-used on both pages. Head over to the exercise.tsx to continue.
26+
927
## Feedback
1028

1129
Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.

0 commit comments

Comments
 (0)