@leafygreen-ui/compound-component
v0.2.0
Published
LeafyGreen UI Kit Compound Component
Keywords
Readme
Compound Component
View on MongoDB.design
Utility functions for creating compound components in React. This package provides factory functions to create components with attached sub-components, following the compound component pattern.
Installation
PNPM
pnpm add @leafygreen-ui/compound-componentYarn
yarn add @leafygreen-ui/compound-componentNPM
npm install @leafygreen-ui/compound-componentUsage
Basic Compound Component
Use CompoundComponent to create a parent component with attached sub-components:
import {
CompoundComponent,
CompoundSubComponent,
} from '@leafygreen-ui/compound-component';
// Create a sub-component
const MySubComponent = CompoundSubComponent(
({ children }) => <div className="sub-component">{children}</div>,
{
displayName: 'MySubComponent',
key: 'isMySubComponent',
},
);
// Create the main compound component
const MyComponent = CompoundComponent(
({ children }) => <div className="main-component">{children}</div>,
{
displayName: 'MyComponent',
Sub: MySubComponent,
},
);
// Usage
function App() {
return (
<MyComponent>
Main content
<MyComponent.Sub>Sub content</MyComponent.Sub>
</MyComponent>
);
}Multiple Sub-Components
You can attach multiple sub-components to a compound component:
const Header = CompoundSubComponent(
({ children }) => <header>{children}</header>,
{
displayName: 'Header',
key: 'isHeader',
},
);
const Body = CompoundSubComponent(({ children }) => <main>{children}</main>, {
displayName: 'Body',
key: 'isBody',
});
const Footer = CompoundSubComponent(
({ children }) => <footer>{children}</footer>,
{
displayName: 'Footer',
key: 'isFooter',
},
);
const Card = CompoundComponent(
({ children }) => <div className="card">{children}</div>,
{
displayName: 'Card',
Header,
Body,
Footer,
},
);
// Usage
function App() {
return (
<Card>
<Card.Header>Card Title</Card.Header>
<Card.Body>Card content goes here</Card.Body>
<Card.Footer>Card actions</Card.Footer>
</Card>
);
}Finding Sub-Components in Parent Components
Use findChild and findChildren utilities to locate specific sub-components within a parent component's children:
import {
CompoundComponent,
CompoundSubComponent,
findChild,
findChildren,
} from '@leafygreen-ui/compound-component';
// Define property constants for type safety and consistency
const CardProperties = {
Header: 'isHeader',
Body: 'isBody',
Footer: 'isFooter',
} as const;
// Create sub-components with identifying properties
const Header = CompoundSubComponent(
({ children }) => <header>{children}</header>,
{
displayName: 'Header',
key: CardProperties.Header,
},
);
const Body = CompoundSubComponent(({ children }) => <main>{children}</main>, {
displayName: 'Body',
key: CardProperties.Body,
});
const Footer = CompoundSubComponent(
({ children }) => <footer>{children}</footer>,
{
displayName: 'Footer',
key: CardProperties.Footer,
},
);
// Parent component that uses findChild/findChildren
const Card = CompoundComponent(
({ children }) => {
// Find specific sub-components using property constants
const header = findChild(children, CardProperties.Header);
const body = findChild(children, CardProperties.Body);
const footer = findChild(children, CardProperties.Footer);
// Find all instances of a sub-component type
const allBodies = findChildren(children, CardProperties.Body);
return (
<div className="card">
{header && <div className="card-header">{header}</div>}
{body && <div className="card-body">{body}</div>}
{footer && <div className="card-footer">{footer}</div>}
{allBodies.length > 1 && (
<div className="warning">Multiple body sections found!</div>
)}
</div>
);
},
{
displayName: 'Card',
Header,
Body,
Footer,
},
);
// Usage - parent can control layout and add wrapper elements
function App() {
return (
<Card>
<Card.Header>Card Title</Card.Header>
<Card.Body>Main content here</Card.Body>
<Card.Footer>
<button>Action</button>
</Card.Footer>
</Card>
);
}Advanced Usage with Conditional Rendering
// Define property constants for the Modal component
const ModalProperties = {
Header: 'isModalHeader',
Body: 'isModalBody',
Footer: 'isModalFooter',
} as const;
const ModalHeader = CompoundSubComponent(
({ children }) => <div>{children}</div>,
{
displayName: 'ModalHeader',
key: ModalProperties.Header,
},
);
const ModalBody = CompoundSubComponent(
({ children }) => <div>{children}</div>,
{
displayName: 'ModalBody',
key: ModalProperties.Body,
},
);
const ModalFooter = CompoundSubComponent(
({ children }) => <div>{children}</div>,
{
displayName: 'ModalFooter',
key: ModalProperties.Footer,
},
);
const Modal = CompoundComponent(
({ children }) => {
// Use property constants instead of string literals
const header = findChild(children, ModalProperties.Header);
const body = findChild(children, ModalProperties.Body);
const footer = findChild(children, ModalProperties.Footer);
return (
<div className="modal">
{/* Only render header section if header component exists */}
{header && (
<div className="modal-header">
{header}
<button className="close-btn">×</button>
</div>
)}
{/* Body is required */}
<div className="modal-body">
{body || <div>No content provided</div>}
</div>
{/* Footer is optional */}
{footer && <div className="modal-footer">{footer}</div>}
</div>
);
},
{
displayName: 'Modal',
Header: ModalHeader,
Body: ModalBody,
Footer: ModalFooter,
},
);Hierarchical Compound Components (Nested SubComponents)
CompoundSubComponent can accept additional static properties to create hierarchical compound components without needing to nest CompoundComponent calls. This provides a cleaner DX:
import {
CompoundComponent,
CompoundSubComponent,
findChild,
} from '@leafygreen-ui/compound-component';
// Define property constants for each level
const ModalProperties = {
Header: 'isModalHeader',
Body: 'isModalBody',
Footer: 'isModalFooter',
} as const;
const FooterProperties = {
PrimaryAction: 'isPrimaryAction',
SecondaryAction: 'isSecondaryAction',
} as const;
// Create the deepest level sub-components
const PrimaryAction = CompoundSubComponent(
({ children, ...props }) => (
<button className="primary-action" {...props}>
{children}
</button>
),
{
displayName: 'PrimaryAction',
key: FooterProperties.PrimaryAction,
},
);
const SecondaryAction = CompoundSubComponent(
({ children, ...props }) => (
<button className="secondary-action" {...props}>
{children}
</button>
),
{
displayName: 'SecondaryAction',
key: FooterProperties.SecondaryAction,
},
);
// ModalFooter is BOTH a SubComponent AND has its own sub-components
// No need to wrap with CompoundComponent!
const ModalFooter = CompoundSubComponent(
({ children }) => {
// ModalFooter can use findChild for its own children
const primaryAction = findChild(children, FooterProperties.PrimaryAction);
const secondaryAction = findChild(
children,
FooterProperties.SecondaryAction,
);
return (
<div className="modal-footer">
{secondaryAction}
{primaryAction}
</div>
);
},
{
displayName: 'ModalFooter',
key: ModalProperties.Footer,
// Attach sub-components directly!
PrimaryAction,
SecondaryAction,
},
);
const ModalHeader = CompoundSubComponent(
({ children }) => <div className="modal-header">{children}</div>,
{
displayName: 'ModalHeader',
key: ModalProperties.Header,
},
);
const ModalBody = CompoundSubComponent(
({ children }) => <div className="modal-body">{children}</div>,
{
displayName: 'ModalBody',
key: ModalProperties.Body,
},
);
// Attach to parent Modal component
const Modal = CompoundComponent(
({ children }) => {
const header = findChild(children, ModalProperties.Header);
const body = findChild(children, ModalProperties.Body);
const footer = findChild(children, ModalProperties.Footer);
return (
<div className="modal">
{header}
{body}
{footer}
</div>
);
},
{
displayName: 'Modal',
Header: ModalHeader,
Body: ModalBody,
Footer: ModalFooter,
},
);
// Usage: Modal.Footer.PrimaryAction works without nesting CompoundComponent!
function App() {
return (
<Modal>
<Modal.Header>Confirm Action</Modal.Header>
<Modal.Body>Are you sure you want to proceed?</Modal.Body>
<Modal.Footer>
<Modal.Footer.SecondaryAction onClick={() => {}}>
Cancel
</Modal.Footer.SecondaryAction>
<Modal.Footer.PrimaryAction onClick={() => {}}>
Confirm
</Modal.Footer.PrimaryAction>
</Modal.Footer>
</Modal>
);
}API
CompoundComponent<Props, Properties>(componentRenderFn, properties)
Creates a compound component with attached sub-components.
Parameters:
componentRenderFn: The React component render functionproperties: Object containingdisplayNameand any sub-components to attach
Returns: A React component with the sub-components attached as static properties
CompoundSubComponent<Key, Props>(componentRenderFn, properties)
Creates a sub-component with a static key property for identification. Can optionally accept additional static properties (like nested sub-components) to create hierarchical compound components.
Parameters:
componentRenderFn: The React component render functionproperties: Object containing:displayName(required): The component display namekey(required): The static property name to identify this component- Additional properties (optional): Any nested sub-components or other static properties
Returns: A React component with the specified key property set to true and any additional properties attached
findChild(children, staticProperty)
Finds the first child component with a matching static property.
Parameters:
children: Any React children (ReactNode)staticProperty: The static property name to check for (string)
Returns: The first matching ReactElement or undefined if not found
Search Depth: Only searches direct children and children inside a single React Fragment level.
findChildren(children, staticProperty)
Finds all child components with a matching static property.
Parameters:
children: Any React children (ReactNode)staticProperty: The static property name to check for (string)
Returns: Array of matching ReactElements (empty array if none found)
Search Depth: Only searches direct children and children inside a single React Fragment level.
Key Property
Sub-components created with CompoundSubComponent have a static property (specified by the key parameter) set to true. This can be used for component identification:
const MySubComponent = CompoundSubComponent(() => <div />, {
displayName: 'MySubComponent',
key: 'isMySubComponent',
});
console.log(MySubComponent.isMySubComponent); // trueNote: The key property itself is not exposed on the component to avoid conflicts with React's built-in key prop.
