UNPKG

@meonode/ui

Version:

A structured approach to component composition, direct CSS-first prop styling, built-in theming, smart prop handling (including raw property pass-through), and dynamic children.

308 lines 17.7 kB
import React, { type ElementType, type ReactElement, type ReactNode } from 'react'; import type { Children, FinalNodeProps, HasRequiredProps, MergedProps, NodeElement, NodeInstance, NodeProps, PropsOf, RawNodeProps, Theme } from './node.type.js'; import { type Root as ReactDOMRoot } from 'react-dom/client'; /** * Represents a node in a React component tree with theme and styling capabilities. * This class wraps React elements and handles: * - Props processing and normalization * - Theme inheritance and resolution * - Child node processing and management * - Style processing with theme variables * @template E The type of React element or component this node represents */ export declare class BaseNode<E extends NodeElement> implements NodeInstance<E> { /** The underlying React element or component type that this node represents */ element: E; /** Original props passed during construction, preserved for cloning/recreation */ rawProps: RawNodeProps<E>; /** Flag to identify BaseNode instances */ readonly isBaseNode: boolean; /** Processed props after theme resolution, style processing, and child normalization */ private _props?; /** DOM element used for portal rendering */ private _portalDOMElement; /** React root instance for portal rendering */ private _portalReactRoot; /** Hash of the current children and theme to detect changes */ private _childrenHash?; /** Cache for normalized children */ private _normalizedChildren?; /** Indicates whether the code is running on the server (true) or client (false) */ private static _isServer; /** * WeakMap cache for processed children, keyed by object/array identity for GC friendliness. * Each entry stores the hash, processed children, and a server-side flag. */ private static _processedChildrenWeakCache; /** * Map cache for processed children, keyed by a stable string signature. * Used for non-object cases or as a fallback. Each entry stores the processed children and a server-side flag. */ private static _processedChildrenMapCache; /** Maximum number of entries in the Map cache to prevent unbounded growth */ private static readonly _MAX_PROCESSED_CHILDREN_CACHE = 1000; /** * Constructs a new BaseNode instance. * * This constructor initializes a node with a given React element or component type * and the raw props passed to it. The props are not processed until they are * accessed via the `props` getter, allowing for lazy evaluation. * @param element The React element or component type this node will represent. * @param rawProps The initial, unprocessed props for the element. */ constructor(element: E, rawProps?: RawNodeProps<E>); /** * Lazily processes and retrieves the final, normalized props for the node. * * The first time this getter is accessed, it triggers `_processProps` to resolve * themes, styles, and children. Subsequent accesses return the cached result * until the node is cloned or recreated. * @returns The fully processed and normalized `FinalNodeProps`. */ get props(): FinalNodeProps; /** * Performs the core logic of processing raw props into their final, normalized form. * * This method is called by the `props` getter on its first access. It handles: * 1. **Theme Resolution**: Selects the active theme from `theme` or `nodetheme` props. * 2. **Prop Resolution**: Resolves theme-aware values (functions) in `rawProps` and `nativeProps.style`. * 3. **Style Extraction**: Separates style-related props (`css`, `style`) from other DOM/component props. * 4. **Default Style Merging**: Combines default styles with resolved style props. * 5. **Child Processing**: Normalizes the `children` prop, propagating the theme. * @returns The processed `FinalNodeProps` object. * @private */ private _processProps; /** * Deeply clones processed children before returning them from cache so that each parent receives * independent `BaseNode` instances (prevents sharing cycles and mutation bugs). * * - If the input is an array, each child is cloned recursively. * - If the input is a `BaseNode`, a new instance is created with the same element and copied rawProps. * - For other objects/primitives, the value is returned as-is (they are immutable or safe to reuse). * * This ensures that cached children are never shared between different parents in the React tree. * @param processed The processed child or array of children to clone. * @returns A deep clone of the processed children, safe for use in multiple parents. * @private */ private static _cloneProcessedChildren; /** * Retrieves cached processed children for a given set of `children` and an optional `theme`. * * - Skips caching entirely when executed on the server (returns `null`). * - Uses a **WeakMap** for identity-based caching when `children` is an object or array, * ensuring garbage collection safety. * - Falls back to a **Map** keyed by a stable hash of `children` and `theme` * for value-based caching. * - Only returns cached entries that are **not server-side**. * @param children The child node(s) to resolve cached results for. * @param theme The theme context that may influence child processing. * @returns A cloned version of the cached processed children if available, otherwise `null`. * @private */ private _getCachedChildren; /** * Caches processed children for a given set of children and theme. * This method stores the processed NodeElement(s) in a Map keyed by a stable hash. * The cache is bounded to avoid unbounded memory growth. * No caching is performed on the server to avoid RSC issues. * @param children The original children to cache. * @param theme The theme associated with the children. * @param processed The processed NodeElement(s) to cache. * @private */ private _setCachedChildren; /** * Recursively processes raw children, converting them into `BaseNode` instances as needed * and propagating the provided theme. * * This method ensures consistent theme handling for all children and optimizes performance * using caching strategies: a Map for client-side and no caching for server-side. * * - If `children` is an array, each child is processed individually. * - If `children` is a single node, it is processed directly. * - The processed result is cached on the client to avoid redundant work. * @param children The raw child or array of children to process. * @param theme The theme to propagate to the children. * @returns The processed children, ready for normalization and rendering. * @private */ private _processChildren; /** * Renders a processed `NodeElement` into a `ReactNode`, applying a theme and key if necessary. * * This static method centralizes the logic for converting various types of processed elements * into renderable React nodes. It handles: * - `BaseNode` instances: Re-wraps them to apply a new key or theme. * - React class components: Wraps them in a new `BaseNode`. * - `NodeInstance` objects: Invokes their `render()` method. * - React component instances: Invokes their `render()` method. * - Functional components: Creates a React element from them. * - Other valid `ReactNode` types (strings, numbers, etc.): Returns them as-is. * @param processedElement The node element to render. * @param passedTheme The theme to propagate. * @param passedKey The React key to assign. * @returns A renderable `ReactNode`. * @private * @static */ static _renderProcessedNode(processedElement: NodeElement, passedTheme: Theme | undefined, passedKey?: string): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | ReactElement<unknown, string | React.JSXElementConstructor<any>> | React.ReactPortal | null | undefined> | ReactElement<any, string | React.JSXElementConstructor<any>> | null | undefined; /** * Renders the output of a function-as-a-child, ensuring theme propagation. * * This method is designed to handle "render prop" style children (`() => ReactNode`). * It invokes the function, processes its result, and ensures the parent's theme is * correctly passed down to any `BaseNode` instances returned by the function. * @param props The properties for the function renderer. * @param props.render The function to execute to get the child content. * @param props.passedTheme The theme to propagate to the rendered child. * @param props.processRawNode A reference to the `_processRawNode` method for recursive processing. * @returns The rendered `ReactNode`. * @private */ private _functionRenderer; /** * Generates a stable key for a node, especially for elements within an array. * * If an `existingKey` is provided, it is returned. Otherwise, a key is generated * based on the element's type name and its index within a list of siblings. * This helps prevent re-rendering issues in React when dealing with dynamic lists. * @param options The options for key generation. * @param options.nodeIndex The index of the node in an array of children. * @param options.element The element for which to generate a key. * @param options.existingKey An existing key, if one was already provided. * @param options.children The children of the node, used to add complexity to the key. * @returns A React key, or `undefined` if no key could be generated. * @private */ private _generateKey; /** * Processes a single raw node, recursively converting it into a `BaseNode` or other renderable type. * * This is a central method for normalizing children. It handles various types of input: * - **`BaseNode` instances**: Re-creates them to ensure the correct theme and key are applied. * - **Primitives**: Returns strings, numbers, booleans, null, and undefined as-is. * - **Functions (Render Props)**: Wraps them in a `BaseNode` that uses `_functionRenderer` to delay execution. * - **Valid React Elements**: Converts them into `BaseNode` instances, extracting props and propagating the theme. * - **React Component Types**: Wraps them in a `BaseNode` with the parent theme. * - **React Component Instances**: Renders them and processes the output recursively. * * It also generates a stable key for elements within an array if one is not provided. * @param rawNode The raw child node to process. * @param parentTheme The theme inherited from the parent. * @param nodeIndex The index of the child if it is in an array, used for key generation. * @returns A processed `NodeElement` (typically a `BaseNode` instance or a primitive). * @private */ private _processRawNode; /** * Normalizes a processed child node into a final, renderable `ReactNode`. * * This method is called during the `render` phase. It takes a child that has already * been processed by `_processChildren` and prepares it for `React.createElement`. * * - For `BaseNode` instances, it calls their `render()` method, ensuring the theme is consistent. * - It validates that other children are valid React element types. * - Primitives and other valid nodes are returned as-is. * @param child The processed child node to normalize. * @returns A renderable `ReactNode`. * @throws {Error} If the child is not a valid React element type. * @private */ private _normalizeChild; /** * Renders the `BaseNode` into a `ReactElement`. * * This method is the final step in the rendering pipeline. It constructs a React element * by: * 1. Validating that the node's `element` type is renderable. * 2. Normalizing processed children into `ReactNode`s using `_normalizeChild`. * 3. Caching normalized children to avoid re-processing on subsequent renders. * 4. Assembling the final props, including `key`, `style`, and other attributes. * 5. If the element has a `css` prop, it may be wrapped in a `StyledRenderer` to handle * CSS-in-JS styling. * 6. Finally, calling `React.createElement` with the element, props, and children. * @returns The rendered `ReactElement`. * @throws {Error} If the node's `element` is not a valid React element type. */ render(): ReactElement<FinalNodeProps>; /** * Ensures the necessary DOM elements for portal rendering are created and attached. * * On the client-side, this method checks for or creates a `div` element appended * to the `document.body` and initializes a React root on it. This setup is * required for the `toPortal` method to function. It is idempotent and safe * to call multiple times. * @returns `true` if the portal infrastructure is ready, `false` if on the server. * @private */ private _ensurePortalInfrastructure; /** * Renders the node into a React Portal. * * This method mounts the node's rendered content into a separate DOM tree * attached to the `document.body`. It's useful for rendering components like * modals, tooltips, or notifications that need to appear above other UI elements. * * The returned object includes an `unmount` function to clean up the portal. * @returns A `ReactDOMRoot` instance for managing the portal, or `null` if * called in a server-side environment. The returned instance is enhanced * with a custom `unmount` method that also cleans up the associated DOM element. */ toPortal(): ReactDOMRoot | null; } /** * Factory function to create a `BaseNode` instance. * @template AdditionalProps Additional props to merge with node props. * @template E The React element or component type. * @param element The React element or component type to wrap. * @param props The props for the node (optional). * @param additionalProps Additional props to merge into the node (optional). * @returns A new `BaseNode` instance as a `NodeInstance<E>`. */ export declare function Node<AdditionalProps extends Record<string, any>, E extends NodeElement>(element: E, props?: MergedProps<E, AdditionalProps>, additionalProps?: AdditionalProps): NodeInstance<E>; /** * Creates a curried node factory for a given React element or component type. * * Returns a function that, when called with props, produces a `NodeInstance<E>`. * Useful for creating reusable node factories for specific components or element types. * @template AdditionalInitialProps Additional initial props to merge with node props. * @template E The React element or component type. * @param element The React element or component type to wrap. * @param initialProps Initial props to apply to every node instance. * @returns A function that takes node props and returns a `NodeInstance<E>`. * @example * const ButtonNode = createNode('button', { type: 'button' }); * const myButton = ButtonNode({ children: 'Click me', style: { color: 'red' } }); */ export declare function createNode<AdditionalInitialProps extends Record<string, any>, E extends NodeElement>(element: E, initialProps?: MergedProps<E, AdditionalInitialProps>): HasRequiredProps<PropsOf<E>> extends true ? (<AdditionalProps extends Record<string, any> = Record<string, any>>(props: MergedProps<E, AdditionalProps>) => NodeInstance<E>) & { element: E; } : (<AdditionalProps extends Record<string, any> = Record<string, any>>(props?: MergedProps<E, AdditionalProps>) => NodeInstance<E>) & { element: E; }; /** * Creates a node factory function where the first argument is `children` and the second is `props`. * * Useful for ergonomic creation of nodes where children are the primary concern, * such as layout or container components. * * The returned function takes `children` as the first argument and `props` (excluding `children`) as the second. * It merges any `initialProps` provided at factory creation, then creates a `BaseNode` instance. * * Type parameters: * - `AdditionalInitialProps`: Extra props to merge with node props. * - `E`: The React element or component type. * @param element The React element or component type to wrap. * @param initialProps Initial props to apply to every node instance (excluding `children`). * @returns A function that takes `children` and `props`, returning a `NodeInstance<E>`. * @example * const Text = createChildrenFirstNode('p'); * const myDiv = Text('Hello', { className: 'text-lg' }); */ export declare function createChildrenFirstNode<AdditionalInitialProps extends Record<string, any>, E extends ElementType>(element: E, initialProps?: Omit<NodeProps<E>, keyof AdditionalInitialProps | 'children'> & AdditionalInitialProps): HasRequiredProps<PropsOf<E>> extends true ? (<AdditionalProps extends Record<string, any> = Record<string, any>>(children: Children, props: Omit<MergedProps<E, AdditionalProps>, 'children'>) => NodeInstance<E>) & { element: E; } : (<AdditionalProps extends Record<string, any> = Record<string, any>>(children?: Children, props?: Omit<MergedProps<E, AdditionalProps>, 'children'>) => NodeInstance<E>) & { element: E; }; //# sourceMappingURL=core.node.d.ts.map