@aiera-inc/react-slots
Version:
A simple utility for adding named slots to React components, inspired by the slot pattern in Vue.
151 lines (150 loc) • 7.26 kB
TypeScript
/**
* A simple library to add named slots to React components. Wrapping your
* component export in the `withSlots()` method will automatically add
* (and effectively namespace) the slot components to the parent.
*
* _e.g._ export default withSlots(MemberNav, {
* ErrorMsg: MemberNavErrorMsg,
* SuccessMsg: MemberNavSuccessMsg,
* EditButton: MemberNavEditButton
* });
*
* Any component importing the MemberNav can then pass the slots using
* <MemberNav>
* <MemberNav.ErrorMsg></MemberNav.ErrorMsg>
* <MemberNav.SuccessMsg></MemberNav.SuccessMsg>
* <MemberNav.EditButton></MemberNav.EditButton>
* </MemberNav>
*
* Named slotted children can be retrieved by calling `getSlots()` in the
* parent component and passing in the children prop and the same schema used
* in the export statement.
*
* The returned object will map the slot name to the JSX Element. From the
* above example slots.ErrorMsg would link to the MemberNav.ErrorMsg child.
*
* You can allow for repeat slots by wrapping the component type in your schema,
* in square brackets.
*
* _e.g._ export default withSlots(MemberNav, {
* ...
* SuccessMsg: [MemberNavSuccessMsg],
* });
*
* This indicates that multiple Success message components can be slotted and
* returned.
*
* Namespaced components are also supported. These are intended to be used when you
* want to add a child component to the parent's namespace but don't particularly
* care about where it is surfaced in the Parent JSX. Namespaced components will
* still be passed to the `getSlots()` method but will not be included in the
* returned slots object.
*
* _e.g._ export default withSlots(MemberNav, {
* ...
* SuccessMsg: {MemberNavSuccessMsg},
* });
*
* __Warning__: This does not support nested fragments. Wrapping a slotted child
* two levels deep in a fragment will cause it to be treated as a generic child.
*/
import React from 'react';
/** A narrowed version of the React.JSXElementConstructor. */
export type SlotConstructor<P = any> = (props: P) => JSX.Element | null;
/** A dictionary defining the types of slots on an element. Each key should
* point to a React component or a tuple containing a React component. The
* tuple indicates that multiple instances of the component are allowed.
* The component type should be a function that returns a valid React element. */
export type SlotDictionary<P = any> = {
[x: string]: React.JSXElementConstructor<P> | readonly [React.JSXElementConstructor<P>] | {
[x: string]: React.JSXElementConstructor<P>;
};
};
/** This looks deceivingly complex but it's realy just because of the nested
* ternaries that TypeScript syntax requires.
*
* For every key in the SlotDictionary, we check if the value is a tuple
* containing a SlotConstructor. If it is, we return the return type of the
* first element of the tuple. If it's not a tuple, we check if it's an object
* containing SlotConstructors. If it is, we return the union of all the
* SlotConstructors. If it's not an object, we check if it's a single
* SlotConstructor and return it's return type.
*/
type Slots<T extends SlotDictionary> = {
[Property in keyof T]: T[Property] extends [SlotConstructor] ? ReturnType<T[Property]['0']> | ReturnType<T[Property]['0']>[] : T[Property] extends {
[x: string]: SlotConstructor;
} ? T[Property][keyof T[Property]] : T[Property] extends SlotConstructor ? ReturnType<T[Property]> | ReturnType<T[Property]>[] : any;
};
/** A utility type that will merge the slot definitions from
* your Slot Schema into the component props. */
export type WithSlotProps<P, T extends SlotDictionary> = React.PropsWithChildren<P & {
slots: {
[K in keyof T as T[K] extends {
readonly [x: string]: SlotConstructor;
} ? never : K]: T[K] extends readonly [React.JSXElementConstructor<any>] ? JSX.Element[] : JSX.Element;
};
}>;
/** Alternative utility type if you prefer to declare your component props
* using the `interface` keyword. */
export interface SlotProviderInterface<T extends SlotDictionary> {
children?: React.ReactNode | undefined;
slots: {
[K in keyof T as T[K] extends {
readonly [x: string]: SlotConstructor;
} ? never : K]: T[K] extends readonly [React.JSXElementConstructor<any>] ? JSX.Element[] : JSX.Element;
};
}
/**
* Creates a record containing the JSX elements
* passed as children on the component
* @param children
* @param schema
* @returns
*/
export declare function getSlots<D extends SlotDictionary>(children: undefined | React.ReactNode | React.ReactNode[], schema: D): {
slots: Slots<D>;
children: (undefined | React.ReactNode | React.ReactNode[])[];
};
/**
* Automatically applies a Slot Signature to the
* parent component
* @param parent
* @param schema
* @returns
*/
export declare function withSlots<C extends (props: WithSlotProps<any, T>) => JSX.Element, T extends SlotDictionary, P extends React.PropsWithChildren<Parameters<C>[0]> = React.PropsWithChildren<Parameters<C>[0]>, _P = {
[K in keyof P as K extends 'slots' ? never : K]: P[K];
}, _S = {
readonly [Property in keyof T]: T[Property] extends readonly [SlotConstructor] ? T[Property]['0'] : T[Property] extends {
[x: string]: React.JSXElementConstructor<infer P>;
} ? SlotConstructor<P> : T[Property];
}>(Parent: C, schema: T): React.FC<_P> & _S;
/**
* A higher order component that wraps a parent component
* and automatically applies the slot schema to the parent
* component. This allows for a more declarative way of
* defining slots in your components.
*
* @param schema - The slot schema to apply to the parent component.
* @returns A higher order component that wraps the parent component.
*/
export declare function SlottedComponent<T extends SlotDictionary>(schema: T): <P>(Parent: (props: WithSlotProps<P, T>) => JSX.Element) => React.FC<React.PropsWithChildren<WithSlotProps<P, T>> extends infer T_1 extends React.PropsWithChildren<Parameters<typeof Parent>[0]> ? { [K in keyof T_1 as K extends "slots" ? never : K]: React.PropsWithChildren<WithSlotProps<P, T>>[K]; } : never> & { readonly [Property in keyof T]: T[Property] extends readonly [SlotConstructor<any>] ? T[Property]["0"] : T[Property] extends {
[x: string]: React.JSXElementConstructor<infer P_1>;
} ? SlotConstructor<P_1> : T[Property]; };
/**
* A custom React hook that extracts and organizes slot components from the given children
* based on a provided slot schema. Returns an object containing the extracted slots and
* the remaining children that do not match any slot in the schema.
*
* @experimental
* @param children - The React children elements to be processed for slot extraction.
* @param schema - An object defining the expected slots and their types.
* @returns An object with two properties:
* - slots: An object mapping slot names to their corresponding React elements.
* - children: The remaining React children that were not matched to any slot.
*/
export declare function useSlots<T extends SlotDictionary>(children: React.ReactNode, schema: T): {
slots: Slots<T>;
children: (React.ReactNode | React.ReactNode[])[];
};
export {};