From da8e866a2b96d68c268e4c85979ce1830106eaac Mon Sep 17 00:00:00 2001 From: maofeng Date: Thu, 19 Jun 2025 22:40:14 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=96=B0=E8=AE=BE=E8=AE=A1=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E4=B8=8A=E4=B8=8B=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/record.tsx | 298 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 252 insertions(+), 46 deletions(-) diff --git a/src/core/record.tsx b/src/core/record.tsx index 91d4574..de25cbc 100644 --- a/src/core/record.tsx +++ b/src/core/record.tsx @@ -1,4 +1,5 @@ -import { createContext, useContext, useMemo } from "react"; +import { createContext, type ReactNode, useContext, useMemo } from "react"; +import { type Validator } from "./types"; /** * 定义上下文记录结构 @@ -12,19 +13,163 @@ export type RecordContextValue = { /** * 声明通用的记录上下文,配合下面 Component 和 hook 管理数据: * - * - RecordProvider 数据提供组件 - * - useRecord 根据位置获取最近的数据 - * - useNamedRecord 获取指定名称的数据 + * - RecordContextProvider 数据提供组件 + * - useRecordContext 根据位置获取最近的数据 + * + * @internal */ const RecordContext = createContext(null); -type RecordProviderProps = { - children: React.ReactNode; +/** + * 获取最近的一个记录值 + * + * @example + * + * 获取最近的记录值 + * + * ```jsx + * + * {useRecordContext().name} + * + * ``` + * + * @example + * + * 获取最近的记录值并验证 + * + * ```jsx + * + * + * {useRecordContext((value) => 'email' in value).phone} + * + * + * ``` + * + * @example + * + * 根据名称获取记录值 + * + * ```jsx + * + * {useRecordContext('user.2').name} + * + * ``` + */ +export function useRecordContext(): T | null; +export function useRecordContext(name: string): T | null; +export function useRecordContext(validator: Validator): T | null; +export function useRecordContext(name: string, validator: Validator): T | null; +export function useRecordContext(...args: unknown[]): T | null { + let name: string | undefined; + let validator: Validator | undefined; + + if (typeof args[0] === 'string') { + name = args[0]; + if (typeof args[1] === 'function') { + validator = args[1] as Validator; + } + } else if (typeof args[0] === 'function') { + validator = args[0] as Validator; + } + + let context = useContext(RecordContext); + + if (!context) { + throw new Error("useRecordContext must be used within RecordContextProvider"); + } + + if (validator == null && name == null) { + return context.value as T; + } + + return resolveRecord(context, c => { + return (name == null || c.name === name) + && (validator == null || validator(c.value)); + }) +} + +function resolveRecord( + context: RecordContextValue, + validate: (ctx: RecordContextValue) => boolean, +): T | null { + while (true) { + if (validate(context)) { + return context.value as T; + } + if (!context.parent) { + return null; + } + context = context.parent; + } +} + +/** + * 记录值提供者属性 + */ +export interface RecordProviderProps { + /** + * 子组件 + * + * 必须提供一个值,否则毫无意义。 + */ + children: ReactNode; + + /** + * 记录名称 + * + * 配合 useRecordContext 获取指定名称的数据。 + * + * @example + * + * ```jsx + * function UserName({ id }: { id: number }) { + * const user = useRecordContext(`user.${id}`); + * return
{user.name}
+ * } + * + * + * + *

+ * user name for id equal 2 is + * + *

+ *

+ * user name for id equal 1 is + * + *

+ *
+ *
+ * ``` + */ name: string; + + /** + * 记录值 + */ value: T; }; -export function RecordProvider({ +/** + * 通过上下文实现跨组件传递记录值 + * + * @example + * + * ```jsx + * + * + *

+ * user name for id equal 2 is + * + *

+ *

+ * user name for id equal 1 is + * + *

+ *
+ *
+ * ``` + */ +export function RecordContextProvider({ children, ...context }: RecordProviderProps) { @@ -36,48 +181,109 @@ export function RecordProvider({ Object.values(context), ) as RecordContextValue; - return {children}; + return ( + + {children} + + ); } -export function useRecord( - validate?: (value: unknown) => boolean, -): T | null { - let context = useContext(RecordContext); - if (!context) { - throw new Error("useRecord must be used within RecordProvider"); - } - if (validate == null) { - return context.value as T; - } - while (true) { - if (validate(context.value)) { - return context.value as T; - } - if (!context.parent) { - return null; - } - context = context.parent; +/** + * 可选记录提供器 + * + * 只有在定义了记录值的情况下,才用 RecordContextProvider 子元素, + * 即允许组件在记录上下文之外正常运行。 + * + * @example + * + * ```jsx + * const RecordTitle = ({ record }) => ( + * + * + * + * ); + * ``` + */ +export function OptionalRecordProvider({ + children, + name, + value, +}: Omit, 'value'> & { + value?: T | null | undefined; +}) { + if (value != null) { + return ( + + {children} + + ); } + + return children; } -export function useNamedRecord( - name: string, - validate?: (value: unknown) => boolean, -): T | null { - let context = useContext(RecordContext); - if (!context) { - throw new Error("useNamedRecord must be used within RecordProvider"); - } - while (true) { - if (context!.name === name) { - if (validate && !validate(context.value)) { - throw new Error("Invalid record value"); - } - return context!.value as T; - } - if (!context.parent) { - return null; - } - context = context!.parent; - } +export interface WithRecordProps { + /** + * 指定要查询的记录名称 + * + * @type {string} 可选 + */ + name?: string; + + /** + * 当记录存在时的渲染函数 + * + * @param {T} record 找到的记录值 + * @returns {ReactNode} + */ + render: (record: T) => ReactNode; + + /** + * 记录不存在或上下文不存在时渲染 + * + * @param {boolean} hasContext 是否存在记录上下文 + * @returns {ReactNode} + */ + fallback?: ReactNode | ((hasContext: boolean) => ReactNode); +} + +/** + * 查询并渲染记录 + * + * 内部自动使用 useRecordContext 的来完成记录查询, + * 实现快速渲染记录。 + * + * @example + * + * ```jsx + * const BookShow = () => ( + * + * + * {record.title}} /> + * + * + * ); + * ``` + */ +export function WithRecord({ + name, + render, + fallback, +}: WithRecordProps) { + const context = useContext(RecordContext); + const record = context != null + ? resolveRecord(context, c => name == null || c.name == name) + : null; + + if (record == null) { + if (typeof fallback === 'function') { + return fallback(context != null); + } + return fallback ?? null; + } + + return <>{render(record)} }