UNPKG

react-cmp-selector

Version:

A powerful and extensible utility for filtering and selecting React components based on specified attributes and values.

354 lines (267 loc) 8.41 kB
# react-cmp-selector A powerful and extensible utility for selecting and manipulating React components based on attributes. Ideal for dynamic layouts, slotted rendering, and component injection patterns. --- ## Table of Contents - [react-cmp-selector](#react-cmp-selector) - [Table of Contents](#table-of-contents) - [Installation](#installation) - [Motivation](#motivation) - [Table of Proposals](#table-of-proposals) - [Usage](#usage) - [1. Basic Slot Matching](#1-basic-slot-matching) - [2. Matching Multiple Components](#2-matching-multiple-components) - [3. Combining Event Handlers](#3-combining-event-handlers) - [4. Declarative API with `<Slot>`](#4-declarative-api-with-slot) - [5. Fallback Slot Content](#5-fallback-slot-content) - [6. Slot Markers](#6-slot-markers) - [7. Slot Validation](#7-slot-validation) - [Use Cases](#use-cases) - [API Reference](#api-reference) - [`getCmpByAttr<P>()`](#getcmpbyattrp) - [`ComponentFinderProps`](#componentfinderprops) - [`Slot`](#slot) - [`SlotUtils`](#slotutils) - [Caveats](#caveats) - [License](#license) --- ## Installation ```bash npm install react-cmp-selector ``` or using yarn: ```bash yarn add react-cmp-selector ``` --- ## Motivation React does not support named slots or dynamic selection of children out of the box. This utility solves that by providing: - A **hook** to search through children by attribute - A **component-based API** (`<Slot>`) for declarative slot usage - Tools to **inject props**, **merge handlers**, and **validate layout contracts** --- ## Table of Proposals | Feature | Prop / Option | Type | Description | | ----------------------------- | -------------------------- | --------------------------------- | ------------------------------------------------------------- | | **Attribute Matching** | `attribute` | `string` | Attribute name to search for (default: `'data-slot'`) | | **Value Matching** | `value` / `name` | `string` | The value to match against the selected attribute | | **Prop Merging** | `props` | `Partial<P>` | Injected props to merge into the matched component(s) | | **Function Merging Strategy** | `functionPropMerge` | `'combine' \| 'override'` | Defines how function props like `onClick` should be merged | | **Debug Mode** | `debug` | `boolean` | Enables logging of matching and merging behavior | | **Match All** | `findAll` | `boolean` | If true, returns all matching components instead of the first | | **Hook Interface** | `getCmpByAttr()` | | Programmatic interface to extract and modify children | | **Declarative API** | `<Slot>` | | React component alternative to the hook | | **Slot Markers** | `SlotUtils.createMarker()` | | Creates a named slot wrapper component | | **Slot Validation** | `SlotUtils.validate()` | | Dev-only validation for required slot presence | | **Fallback Rendering** | `fallback` | `ReactNode` | Rendered if no matching slot is found | | **onFound Callback** | `onFound` | `(element: ReactElement) => void` | Runs when a match is found (e.g. for side effects) | --- ## Usage ### 1. Basic Slot Matching **Children** ```tsx export function ChildComponents() { return ( <> <div data-slot="header">Header</div> <div data-slot="body">Body</div> <div data-slot="footer">Footer</div> </> ); } ``` **Parent** ```tsx const header = getCmpByAttr({ children: <ChildComponents />, value: "header", props: { className: "highlighted" }, }); ``` **Output** ```html <div data-slot="header" class="highlighted">Header</div> ``` **How It Works** - `getCmpByAttr()` searches children for `data-slot="header"`. - The match is cloned with the `className` prop added. --- ### 2. Matching Multiple Components **Children** ```tsx function Buttons() { return ( <> <button data-role="action-button">Save</button> <button data-role="action-button">Cancel</button> </> ); } ``` **Parent** ```tsx const buttons = getCmpByAttr({ children: <Buttons />, attribute: "data-role", value: "action-button", findAll: true, props: { "data-tracked": true }, }); ``` **Output** ```html <button data-role="action-button" data-tracked="true">Save</button> <button data-role="action-button" data-tracked="true">Cancel</button> ``` --- ### 3. Combining Event Handlers **Children** ```tsx function CTA() { return ( <button data-slot="cta" onClick={() => console.log("child")}> Click Me </button> ); } ``` **Parent** ```tsx const cta = getCmpByAttr({ children: <CTA />, value: "cta", props: { onClick: () => console.log("parent"), }, functionPropMerge: "combine", }); ``` **Behavior** Console logs: ``` child parent ``` --- ### 4. Declarative API with `<Slot>` ```tsx export function Layout({ children }: { children: React.ReactNode }) { return ( <div> <Slot name="header">{children}</Slot> <main> <Slot name="content">{children}</Slot> </main> </div> ); } ``` ```tsx function PageContent() { return ( <> <div data-slot="header">Welcome</div> <div data-slot="content">Hello, world!</div> </> ); } <Layout> <PageContent /> </Layout>; ``` **Output** ```html <div> <div data-slot="header">Welcome</div> <main> <div data-slot="content">Hello, world!</div> </main> </div> ``` --- ### 5. Fallback Slot Content ```tsx <Slot name="hero" fallback={<div>Default Hero</div>}> {children} </Slot> ``` If no `data-slot="hero"` is found, it renders: ```html <div>Default Hero</div> ``` --- ### 6. Slot Markers **Marker Declaration** ```tsx const HeroSlot = SlotUtils.createMarker("hero"); function Page() { return ( <HeroSlot> <div className="hero-banner">Custom Hero</div> </HeroSlot> ); } ``` **Parent** ```tsx <Slot name="hero" fallback={<div>Default Hero</div>}> <Page /> </Slot> ``` --- ### 7. Slot Validation ```tsx SlotUtils.validate(children, ["header", "footer"]); ``` - Dev-only. - Warns if `data-slot="header"` or `footer` is missing in children. --- ## Use Cases - **Composable Layouts**: Dynamically slot content into shared layouts. - **Design Systems**: Enable flexible API layers with predictable slot names. - **Multi-brand / White-label UIs**: Inject branding-specific content without hardcoding. - **Next.js Layouts**: Use context + slots to bridge `app/layout.tsx` and pages. - **Dynamic Prop Injection**: Apply analytics, A/B testing, or class injection to specific slots. --- ## API Reference ### `getCmpByAttr<P>()` ```ts function getCmpByAttr<P>( options: ComponentFinderProps<P> ): ReactNode | ReactNode[] | null; ``` ### `ComponentFinderProps` ```ts interface ComponentFinderProps<P = unknown> { children: ReactNode; attribute?: string; value?: string; props?: Partial<P>; debug?: boolean; findAll?: boolean; onFound?: (component: ReactElement) => void; functionPropMerge?: "combine" | "override"; } ``` ### `Slot` ```tsx <Slot name="footer" props={{ className: "sticky" }} fallback={<DefaultFooter />} > {children} </Slot> ``` ### `SlotUtils` ```ts SlotUtils.createMarker(name: string, attribute?: string): Component SlotUtils.validate(children: ReactNode, requiredSlots: string[], attribute?: string): void ``` --- ## Caveats - **Next.js layouts** require a shared context if crossing page boundaries. - `getCmpByAttr` only works on elements rendered within the same render cycle. - This is **not a DOM query tool** it’s entirely based on **React element trees**. --- ## License MIT License