npm.io
0.1.1 • Published 2 weeks ago

@leafygreen-ui/compound-component

Licence
Apache-2.0
Version
0.1.1
Deps
0
Vulns
0
Weekly
0
Stars
274

Compound Component

npm (scoped)

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-component
Yarn
yarn add @leafygreen-ui/compound-component
NPM
npm install @leafygreen-ui/compound-component

Usage

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

API

CompoundComponent<Props, Properties>(componentRenderFn, properties)

Creates a compound component with attached sub-components.

Parameters:

  • componentRenderFn: The React component render function
  • properties: Object containing displayName and 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.

Parameters:

  • componentRenderFn: The React component render function
  • properties: Object containing displayName and key

Returns: A React component with the specified key property set to true

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); // true

Note: The key property itself is not exposed on the component to avoid conflicts with React's built-in key prop.