Compare commits
5 Commits
c33ccc6d5f
...
c336615c68
| Author | SHA1 | Date | |
|---|---|---|---|
| c336615c68 | |||
| 02b54e1cda | |||
| da8e866a2b | |||
| 889e759656 | |||
| 43b445c10b |
14
src/App.tsx
14
src/App.tsx
@ -1,8 +1,8 @@
|
|||||||
import "./App.css";
|
import "./App.css";
|
||||||
import { RecordProvider, useNamedRecord, useRecord } from "./core/record";
|
import { RecordContextProvider, useRecordContext } from "./core/record";
|
||||||
|
|
||||||
function Consumer({ name }: { name: string }) {
|
function Consumer({ name }: { name: string }) {
|
||||||
const record = useNamedRecord<{ text: string }>(name);
|
const record = useRecordContext<{ text: string }>(name);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -20,7 +20,7 @@ function Consumer({ name }: { name: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function RecordConsumer() {
|
function RecordConsumer() {
|
||||||
const record = useRecord<{ text: string }>();
|
const record = useRecordContext<{ text: string }>();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -42,16 +42,16 @@ const App = () => {
|
|||||||
<div className="content">
|
<div className="content">
|
||||||
<h1>Rsbuild with React</h1>
|
<h1>Rsbuild with React</h1>
|
||||||
<p>Start building amazing things with Rsbuild.</p>
|
<p>Start building amazing things with Rsbuild.</p>
|
||||||
<RecordProvider name="a" value={{ text: "aaa Rsbuild message" }}>
|
<RecordContextProvider name="a" value={{ text: "aaa Rsbuild message" }}>
|
||||||
<RecordProvider name="av" value={{ text: "vvv with Rsbuild" }}>
|
<RecordContextProvider name="av" value={{ text: "vvv with Rsbuild" }}>
|
||||||
<Consumer name="av" />
|
<Consumer name="av" />
|
||||||
<Consumer name="a" />
|
<Consumer name="a" />
|
||||||
<RecordConsumer />
|
<RecordConsumer />
|
||||||
</RecordProvider>
|
</RecordContextProvider>
|
||||||
<Consumer name="a" />
|
<Consumer name="a" />
|
||||||
<Consumer name="x" />
|
<Consumer name="x" />
|
||||||
<RecordConsumer />
|
<RecordConsumer />
|
||||||
</RecordProvider>
|
</RecordContextProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
2
src/core/data-provider/index.ts
Normal file
2
src/core/data-provider/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// https://tkdodo.eu/blog/react-query-meets-react-router
|
||||||
|
// https://remix.run/blog/remixing-react-router
|
||||||
0
src/core/data-provider/use-create.ts
Normal file
0
src/core/data-provider/use-create.ts
Normal file
0
src/core/data-provider/use-delete-many.ts
Normal file
0
src/core/data-provider/use-delete-many.ts
Normal file
0
src/core/data-provider/use-delete.ts
Normal file
0
src/core/data-provider/use-delete.ts
Normal file
0
src/core/data-provider/use-get-list.ts
Normal file
0
src/core/data-provider/use-get-list.ts
Normal file
0
src/core/data-provider/use-get-many-aggregate.ts
Normal file
0
src/core/data-provider/use-get-many-aggregate.ts
Normal file
0
src/core/data-provider/use-get-many-reference.ts
Normal file
0
src/core/data-provider/use-get-many-reference.ts
Normal file
0
src/core/data-provider/use-get-many.ts
Normal file
0
src/core/data-provider/use-get-many.ts
Normal file
0
src/core/data-provider/use-get-one.ts
Normal file
0
src/core/data-provider/use-get-one.ts
Normal file
0
src/core/data-provider/use-infinite-get-list.ts
Normal file
0
src/core/data-provider/use-infinite-get-list.ts
Normal file
0
src/core/data-provider/use-is-data-loaded.ts
Normal file
0
src/core/data-provider/use-is-data-loaded.ts
Normal file
0
src/core/data-provider/use-loading.ts
Normal file
0
src/core/data-provider/use-loading.ts
Normal file
0
src/core/data-provider/use-refresh.ts
Normal file
0
src/core/data-provider/use-refresh.ts
Normal file
0
src/core/data-provider/use-update-many.ts
Normal file
0
src/core/data-provider/use-update-many.ts
Normal file
0
src/core/data-provider/use-update.ts
Normal file
0
src/core/data-provider/use-update.ts
Normal file
@ -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<T extends object = object> = {
|
|||||||
/**
|
/**
|
||||||
* 声明通用的记录上下文,配合下面 Component 和 hook 管理数据:
|
* 声明通用的记录上下文,配合下面 Component 和 hook 管理数据:
|
||||||
*
|
*
|
||||||
* - RecordProvider 数据提供组件
|
* - RecordContextProvider 数据提供组件
|
||||||
* - useRecord 根据位置获取最近的数据
|
* - useRecordContext 根据位置获取最近的数据
|
||||||
* - useNamedRecord 获取指定名称的数据
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
const RecordContext = createContext<RecordContextValue | null>(null);
|
const RecordContext = createContext<RecordContextValue | null>(null);
|
||||||
|
|
||||||
type RecordProviderProps<T extends object> = {
|
/**
|
||||||
children: React.ReactNode;
|
* 获取最近的一个记录值
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* 获取最近的记录值
|
||||||
|
*
|
||||||
|
* ```jsx
|
||||||
|
* <RecordContextProvider name="..." value={{ name: 'John', age: 30 }}>
|
||||||
|
* {useRecordContext().name}
|
||||||
|
* </RecordContextProvider>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* 获取最近的记录值并验证
|
||||||
|
*
|
||||||
|
* ```jsx
|
||||||
|
* <RecordContextProvider name="..." value={{ name: 'John', age: 30 }}>
|
||||||
|
* <RecordContextProvider name="..." value={{ email: 'Jane@example.com', phone: '1234567890' }}>
|
||||||
|
* {useRecordContext((value) => 'email' in value).phone}
|
||||||
|
* </RecordContextProvider>
|
||||||
|
* </RecordContextProvider>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* 根据名称获取记录值
|
||||||
|
*
|
||||||
|
* ```jsx
|
||||||
|
* <RecordContextProvider name="user.2" value={{ id: 2, name: 'John', age: 30 }}>
|
||||||
|
* {useRecordContext('user.2').name}
|
||||||
|
* </RecordContextProvider>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function useRecordContext<T extends object = object>(): T | null;
|
||||||
|
export function useRecordContext<T extends object = object>(name: string): T | null;
|
||||||
|
export function useRecordContext<T extends object = object>(validator: Validator<object>): T | null;
|
||||||
|
export function useRecordContext<T extends object = object>(name: string, validator: Validator<object>): T | null;
|
||||||
|
export function useRecordContext<T extends object = object>(...args: unknown[]): T | null {
|
||||||
|
let name: string | undefined;
|
||||||
|
let validator: Validator<object> | undefined;
|
||||||
|
|
||||||
|
if (typeof args[0] === 'string') {
|
||||||
|
name = args[0];
|
||||||
|
if (typeof args[1] === 'function') {
|
||||||
|
validator = args[1] as Validator<object>;
|
||||||
|
}
|
||||||
|
} else if (typeof args[0] === 'function') {
|
||||||
|
validator = args[0] as Validator<object>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<T extends object = object>(
|
||||||
|
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<T extends object> {
|
||||||
|
/**
|
||||||
|
* 子组件
|
||||||
|
*
|
||||||
|
* 必须提供一个值,否则毫无意义。
|
||||||
|
*/
|
||||||
|
children: ReactNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录名称
|
||||||
|
*
|
||||||
|
* 配合 useRecordContext 获取指定名称的数据。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* ```jsx
|
||||||
|
* function UserName({ id }: { id: number }) {
|
||||||
|
* const user = useRecordContext<User>(`user.${id}`);
|
||||||
|
* return <div>{user.name}</div>
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* <RecordContextProvider name="user.1" value={{ id: 1, name: "John" }}>
|
||||||
|
* <RecordContextProvider name="user.2" value={{ id: 2, name: "Jane" }}>
|
||||||
|
* <p>
|
||||||
|
* user name for id equal 2 is
|
||||||
|
* <UserName id={2} />
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* user name for id equal 1 is
|
||||||
|
* <UserName id={1} />
|
||||||
|
* </p>
|
||||||
|
* </RecordContextProvider>
|
||||||
|
* </RecordContextProvider>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录值
|
||||||
|
*/
|
||||||
value: T;
|
value: T;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RecordProvider<T extends object>({
|
/**
|
||||||
|
* 通过上下文实现跨组件传递记录值
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* ```jsx
|
||||||
|
* <RecordContextProvider name="user.1" value={{ id: 1, name: "John" }}>
|
||||||
|
* <RecordContextProvider name="user.2" value={{ id: 2, name: "Jane" }}>
|
||||||
|
* <p>
|
||||||
|
* user name for id equal 2 is
|
||||||
|
* <UserName id={2} />
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* user name for id equal 1 is
|
||||||
|
* <UserName id={1} />
|
||||||
|
* </p>
|
||||||
|
* </RecordContextProvider>
|
||||||
|
* </RecordContextProvider>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function RecordContextProvider<T extends object>({
|
||||||
children,
|
children,
|
||||||
...context
|
...context
|
||||||
}: RecordProviderProps<T>) {
|
}: RecordProviderProps<T>) {
|
||||||
@ -36,48 +181,109 @@ export function RecordProvider<T extends object>({
|
|||||||
Object.values(context),
|
Object.values(context),
|
||||||
) as RecordContextValue;
|
) as RecordContextValue;
|
||||||
|
|
||||||
return <RecordContext value={value}>{children}</RecordContext>;
|
return (
|
||||||
|
<RecordContext value={value}>
|
||||||
|
{children}
|
||||||
|
</RecordContext>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRecord<T extends object = object>(
|
/**
|
||||||
validate?: (value: unknown) => boolean,
|
* 可选记录提供器
|
||||||
): T | null {
|
*
|
||||||
let context = useContext(RecordContext);
|
* 只有在定义了记录值的情况下,才用 RecordContextProvider 子元素,
|
||||||
if (!context) {
|
* 即允许组件在记录上下文之外正常运行。
|
||||||
throw new Error("useRecord must be used within RecordProvider");
|
*
|
||||||
}
|
* @example
|
||||||
if (validate == null) {
|
*
|
||||||
return context.value as T;
|
* ```jsx
|
||||||
}
|
* const RecordTitle = ({ record }) => (
|
||||||
while (true) {
|
* <OptionalRecordProvider name="post.1" value={record}>
|
||||||
if (validate(context.value)) {
|
* <TextField source="title" />
|
||||||
return context.value as T;
|
* </OptionalRecordProvider>
|
||||||
}
|
* );
|
||||||
if (!context.parent) {
|
* ```
|
||||||
return null;
|
*/
|
||||||
}
|
export function OptionalRecordProvider<T extends object>({
|
||||||
context = context.parent;
|
children,
|
||||||
}
|
name,
|
||||||
|
value,
|
||||||
|
}: Omit<RecordProviderProps<T>, 'value'> & {
|
||||||
|
value?: T | null | undefined;
|
||||||
|
}) {
|
||||||
|
if (value != null) {
|
||||||
|
return (
|
||||||
|
<RecordContextProvider
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</RecordContextProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useNamedRecord<T extends object = object>(
|
return children;
|
||||||
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) {
|
export interface WithRecordProps<T extends object = object> {
|
||||||
if (validate && !validate(context.value)) {
|
/**
|
||||||
throw new Error("Invalid record value");
|
* 指定要查询的记录名称
|
||||||
|
*
|
||||||
|
* @type {string} 可选
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当记录存在时的渲染函数
|
||||||
|
*
|
||||||
|
* @param {T} record 找到的记录值
|
||||||
|
* @returns {ReactNode}
|
||||||
|
*/
|
||||||
|
render: (record: T) => ReactNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录不存在或上下文不存在时渲染
|
||||||
|
*
|
||||||
|
* @param {boolean} hasContext 是否存在记录上下文
|
||||||
|
* @returns {ReactNode}
|
||||||
|
*/
|
||||||
|
fallback?: ReactNode | ((hasContext: boolean) => ReactNode);
|
||||||
}
|
}
|
||||||
return context!.value as T;
|
|
||||||
|
/**
|
||||||
|
* 查询并渲染记录
|
||||||
|
*
|
||||||
|
* 内部自动使用 useRecordContext 的来完成记录查询,
|
||||||
|
* 实现快速渲染记录。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* ```jsx
|
||||||
|
* const BookShow = () => (
|
||||||
|
* <Show>
|
||||||
|
* <SimpleShowLayout>
|
||||||
|
* <WithRecord render={record => <span>{record.title}</span>} />
|
||||||
|
* </SimpleShowLayout>
|
||||||
|
* </Show>
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function WithRecord<T extends object = object>({
|
||||||
|
name,
|
||||||
|
render,
|
||||||
|
fallback,
|
||||||
|
}: WithRecordProps<T>) {
|
||||||
|
const context = useContext(RecordContext);
|
||||||
|
const record = context != null
|
||||||
|
? resolveRecord<T>(context, c => name == null || c.name == name)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (record == null) {
|
||||||
|
if (typeof fallback === 'function') {
|
||||||
|
return fallback(context != null);
|
||||||
}
|
}
|
||||||
if (!context.parent) {
|
return fallback ?? null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
context = context!.parent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return <>{render(record)}</>
|
||||||
}
|
}
|
||||||
|
|||||||
64
src/core/source.tsx
Normal file
64
src/core/source.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { createContext, ProviderProps, useContext } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上下文中的数据源描述器
|
||||||
|
*/
|
||||||
|
export type SourceContextValue = {
|
||||||
|
/*
|
||||||
|
* 根据上下文返回要修改的字段或输入的源
|
||||||
|
*/
|
||||||
|
getSource: (source: string) => string;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 根据上下文返回要修改的字段或输入的标签,我们
|
||||||
|
* 可以配合 i18n/i10n 返回一个翻译键。
|
||||||
|
*/
|
||||||
|
getLabel: (source: string) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SourceContext = createContext<SourceContextValue | undefined>(undefined);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据源描述提供器
|
||||||
|
*
|
||||||
|
* 允许一些特殊的输入为它们的子源添加前缀或后缀。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* ```jsx
|
||||||
|
* const sourceContext = {
|
||||||
|
* getSource: source => `coordinates.${source}`,
|
||||||
|
* getLabel: source => `resources.posts.fields.${source}`,
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const CoordinatesInput = () => {
|
||||||
|
* return (
|
||||||
|
* <SourceContextProvider value={sourceContext}>
|
||||||
|
* <TextInput source="lat" />
|
||||||
|
* <TextInput source="lng" />
|
||||||
|
* </SourceContextProvider>
|
||||||
|
* );
|
||||||
|
* };
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function SourceContextProvider({ children, value }: ProviderProps<SourceContextValue>) {
|
||||||
|
return (
|
||||||
|
<SourceContext value={value}>
|
||||||
|
{children}
|
||||||
|
</SourceContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultContextValue: SourceContextValue = {
|
||||||
|
getSource: (source: string) => source,
|
||||||
|
getLabel: (source: string) => source,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useSourceContext(): SourceContextValue {
|
||||||
|
const context = useContext(SourceContext);
|
||||||
|
return context ?? defaultContextValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOptionalSourceContext(): SourceContextValue | undefined {
|
||||||
|
return useContext(SourceContext);
|
||||||
|
}
|
||||||
14
src/core/types.ts
Normal file
14
src/core/types.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
|
export type ValueGetter<T> = () => T;
|
||||||
|
export type ValueSetter<T> = (value: T) => void;
|
||||||
|
|
||||||
|
export type AsyncGetter<T> = () => Promise<T>;
|
||||||
|
export type AsyncSetter<T> = (value: T) => Promise<void>;
|
||||||
|
|
||||||
|
export type Validator<T> = (value: T) => boolean;
|
||||||
|
export type AsyncValidator<T> = (value: T) => Promise<boolean>;
|
||||||
|
|
||||||
|
export type Transformer<T, R> = (value: T) => R;
|
||||||
|
|
||||||
|
export type SetState<T> = Dispatch<SetStateAction<T>>;
|
||||||
Loading…
x
Reference in New Issue
Block a user