UNPKG

@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
/** * 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 {};