@carefrees/table-async-validator
v0.0.12
Published
📢:在使用过程中,如果某个字段存储的是对象可能需要使用`ref`包裹后进行存储
Readme
使用 valtio 封装表格异步校验器
📢:在使用过程中,如果某个字段存储的是对象可能需要使用ref包裹后进行存储
import { ref } from 'valtio';
const value = ref({
name: '张三',
age: 18,
});
const value = ref([
{
name: '张三',
age: 18,
},
]);
const func = () => {
return {
name: '张三',
age: 18,
};
};
const value = ref(func);安装
npm install @carefrees/table-async-validator # yarn add @carefrees/table-async-validator # pnpm add @carefrees/table-async-validator方法
ProviderInstance父级实例useProviderInstance初始化父级实例ProviderInstanceContext获取父级实例useProviderInstanceContextState获取父级实例状态useProviderInstanceContext获取父级实例上下文useRegisterChildInstance注册子实例ChildInstance子实例useChildInstance初始化子实例ChildInstanceContext获取子实例上下文useChildInstanceContext获取子实例上下文useChildInstanceContextState获取子实例状态
类型参数
公共类型
import {
RuleItem,
ValidateFieldsError,
Values,
ValidateError,
} from 'async-validator';
export type MObject<T> = {
[K in keyof T]: T[K];
};
/**子实例验证返回 */
export interface ChildInstanceValidateAllResult<T extends MObject<T> = object> {
/**错误信息*/
errorInfo: Record<
PropertyKey,
{
errors: ValidateError[] | null;
fields: ValidateFieldsError | Values;
otherError?: any;
}
>;
/**成功数据列表*/
dataList: T[];
/**是否有错误*/
isErrorInfo: boolean;
/**是否存在正在操作的行*/
isHasOperationRow?: boolean;
}
/**映射类型,将每个子项的验证结果映射到父项验证结果中*/
export type ProviderInstanceValidateResultMappedType<T extends MObject<T>> = {
[K in keyof T]: ({
name: K;
} & ChildInstanceValidateAllResult<T[K]>)[];
}[keyof T];
export type MappedTypeSave<T extends MObject<T>> = {
[K in keyof T]: T[K][];
};
/**父项实例验证结果*/
export interface ProviderInstanceValidateResult<T extends MObject<T>> {
/** 没找到实例*/
nameToNotFound: {
name: keyof T;
message: string;
}[];
/**有错误实例*/
nameToErrorInfo: ProviderInstanceValidateResultMappedType<T>;
/**没有错误实例*/
nameToSuccessInfo: ProviderInstanceValidateResultMappedType<T>;
/**可以直接保存的数据*/
saveData: {
[K in keyof T]: T[K][];
};
/**是否存在正在操作的行*/
isHasOperationRow?: boolean;
}父级类型参数
import type { ProviderInstanceValidateResult, MObject } from './interface';
import { ChildInstance } from './child.instance';
/**父项实例*/
export declare class ProviderInstance<T extends MObject<T> = object> {
/*** 子实例 */
childInstanceState: { [K in keyof T]: ChildInstance<T[K]> };
/**
* 注册子实例
*/
register: <M extends keyof T>(
name: M,
childInstance: ChildInstance<T[M]>
) => void;
/**
* 注销子实例
*/
unRegister: (name: keyof T) => void;
/**判断子实例中是否存在正在操作的行*/
hasOperationRow: () => boolean;
/**调用子项验证
* @param options.names 子实例名称(可选)
* @param options.rowKey 行主键值数组(可选)
* @param options.fields 列字段数组(可选)
* @param options.isHasOperationRow 是否判断存在正在操作的行,如果存在则抛出错误(可选)
* @param options.isReject 是否使用 Promise.reject 抛出错误(可选)
* @returns 验证结果
*/
validate: (options?: {
names?: (keyof T)[];
rowKey?: PropertyKey[];
fields?: PropertyKey[];
isReject?: boolean;
isHasOperationRow?: boolean;
}) => Promise<ProviderInstanceValidateResult<T>>;
}
/**初始化实例*/
export declare function useProviderInstance<T extends MObject<T> = object>(
instance?: ProviderInstance<T>
): ProviderInstance<T>;
/**context*/
export declare const ProviderInstanceContext: import('react').Context<
ProviderInstance<any>
>;
/**获取状态+实例*/
export declare function useProviderInstanceContextState<
T extends MObject<T> = object
>(): [{ [K in keyof T]: ChildInstance<T[K]> }, ProviderInstance<T>];
/**仅获取实例*/
export declare function useProviderInstanceContext<
T extends MObject<T> = object
>(): ProviderInstance<T>;
/**注册子实例*/
export declare function useRegisterChildInstance<
T extends MObject<T> = object,
M extends keyof T = keyof T
>(
name: M
): {
childInstance: ChildInstance<T[M]>;
providerInstance: ProviderInstance<T>;
};子级类型参数
import { RuleItem, ValidateFieldsError, Values } from 'async-validator';
import type { ChildInstanceValidateAllResult, MObject } from './interface';
/**子项实例*/
export declare class ChildInstance<T extends MObject<T> = object> {
/**命名空间*/
namespace: PropertyKey;
/**行主键字段*/
rowKey: PropertyKey;
/**是否启用操作状态*/
enableOperationState: boolean;
/**
* 行数据的主键值,对应一行中字段的存储数据
*/
state: Record<PropertyKey, T>;
/**
* 行数据的主键值,对应一行中所有列的错误信息
*/
errorState: Record<PropertyKey, Record<PropertyKey, string[]>>;
/**操作状态 新增/编辑*/
operationState: Record<PropertyKey, 'add' | 'edit'>;
/**临时存储行数据(在编辑时使用,点击编辑之前存储数据,在取消编辑时可以根据存储的数据进行还原)*/
tempState: Record<PropertyKey, T>;
/**是否初始化*/
private isCtor;
/**原始数据列表(未初始化时存储数据)*/
_o_dataList: T[];
/**最新的列表渲染数据(每一次传入的数据)*/
_last_dataList: Record<PropertyKey, PropertyKey>[];
/**
* 初始化值(建议进行深度拷贝,避免直接引用导致数据存在问题)
*/
ctor: (data?: T[]) => Record<PropertyKey, PropertyKey>[];
// ===================================================挂载参数================================================================
/**
* 行数据删除时触发,由外部挂载事件
* @param rowKey 行主键值
*/
onDeleteRow: (rowKey: PropertyKey) => void;
/**新增一行数据时触发,由外部挂载事件
* @param list 新增数据列表
* @param rowKey 新增的行主键值
*/
onAddRows: (
list: Record<PropertyKey, PropertyKey>[],
rowKey: PropertyKey
) => void;
// ===================================================挂载参数================================================================
// ===================================================行数据处理================================================================
/**
* 更新行数据
* @param rowKey 行主键值
* @param objectData 更新数据对象
* @param isValidate 是否验证(可选)
*/
updatedRowData: (
rowKey: PropertyKey,
objectData: Partial<T>,
isValidate?: boolean
) => this;
/**
* 更新数据,并指定验证字段
* @param rowKey 行主键值
* @param objectData 更新数据对象
* @param fields 验证字段(需要把当前更新字段一起放入)
*/
updatedRowDataAndValidate: (
rowKey: PropertyKey,
objectData: Partial<T>,
fields: PropertyKey[]
) => this;
/**新增一行数据
* @param objectData 初始值
*/
addRowData: (objectData: Partial<T>) => {
rowId: string;
_item: T;
};
/**删除一行数据
* @param rowKey 行主键值
*/
deleteRowData: (rowKey: PropertyKey) => this;
/**
* 清理所有数据,并设置成未进行初始化
* @param isInitProxy 是否初始化为新的proxy对象(可选)
*/
clear: (isInitProxy?: boolean) => this;
// ===================================================行数据处理================================================================
// ===================================================错误信息处理================================================================
/**
* 更新行数据的错误信息
* @param rowKey 行主键值
* @param objectErrorInfo 行数据错误信息对象
*/
updatedErrorInfo: (
rowKey: PropertyKey,
objectErrorInfo: Record<PropertyKey, string[]>
) => this;
/**
* 清理错误信息
* @param rowKey 行主键值
* @param fields 列字段数组(可选)
*/
deleteErrorInfo: (
rowKey: PropertyKey,
fields?: PropertyKey | PropertyKey[]
) => this;
/**
* 清理所有错误信息
*/
clearErrorInfo: (isInitProxy?: boolean) => this;
// ===================================================错误信息处理================================================================
// ===================================================操作状态================================================================
/**更新操作状态*/
updatedOperationState: (
rowKey: PropertyKey | PropertyKey[],
operationState: 'add' | 'edit'
) => this;
/**移除操作状态*/
removeOperationState: (rowKey: PropertyKey | PropertyKey[]) => this;
/**判断是否存在操作状态数据*/
isExistOperationState: (rowKeys?: PropertyKey[]) => {
editKeys: any[];
addKeys: any[];
isExist: boolean;
};
// ===================================================操作状态================================================================
// ===================================================临时存储行数据================================================================
/**更新临时存储数据*/
updatedTempState: (rowKey: PropertyKey, objectData: Partial<T>) => this;
/**移除临时存储数据*/
removeTempState: (rowKey: PropertyKey) => this;
/**获取临时存储数据*/
getTempState: (rowKey: PropertyKey) => T;
// ===================================================临时存储行数据================================================================
// ========================================================操作方法===========================================================
/**点击取消 编辑/新增 状态 (编辑:会还原编辑之前的数据,新增:会删除当前这一条数据)
* @param rowKey 行主键值
*/
onCancelRowOperation: (rowKey: PropertyKey) => this;
/**点击编辑操作
* @param rowKey 行主键值
*/
onClickEditRowOperation: (rowKey: PropertyKey) => this;
/**点击新增操作
* @param initData 初始值
*/
onClickAddRowOperation: (initData: Partial<T>) => this;
/**点击保存操作
* @param rowKey 行主键值
* @param isThrowError 是否抛出错误(可选)
*/
onClickSaveRowOperation: (
rowKey: PropertyKey,
isThrowError?: boolean
) => Promise<boolean>;
// ========================================================操作方法===========================================================
// ===================================================规则处理================================================================
/**列规则 */
rules: Record<
PropertyKey,
| ((
rowData: T,
instance: ChildInstance<T>
) => RuleItem[] | Promise<RuleItem[]>)
| RuleItem[]
>;
/**规则验证
* @param rowData 行数据对象
* @param fields 列字段数组(可选)
* @param isReturn 是否返回验证结果(可选)
*/
validate: (
rowData: T,
fields?: PropertyKey[],
isReturn?: boolean
) => Promise<ValidateFieldsError | Values>;
/**验证所有数据
* @param options.rowKeys 行主键值数组(可选)
* @param options.fields 列字段数组(可选)
* @param options.isHasOperationRow 是否判断存在正在操作的行,如果存在则抛出错误(可选)
* @param options.isReject 存在错误时是否使用 Promise.reject 抛出错误(可选)
* @returns 验证结果
*/
validateAll: (options: {
rowKeys?: PropertyKey[];
fields?: PropertyKey[];
isReject?: boolean;
isHasOperationRow?: boolean;
}) => Promise<ChildInstanceValidateAllResult<T>>;
// ===================================================规则处理================================================================
// ===================================================数据转换================================================================
/**
* 把数组转换成 主键 => 数据 的对象
* @param array 数组
* @returns 转换后的对象
*/
convertArrayToObject: <K extends T = T>(
array: K[]
) => {
data: Record<PropertyKey, T>;
list: Record<PropertyKey, PropertyKey>[];
};
// ===================================================数据转换================================================================
}
/**初始化实例*/
export declare function useChildInstance<T extends MObject<T> = object>(
instance?: ChildInstance<T>
): ChildInstance<T>;
/**context*/
export declare const ChildInstanceContext: import('react').Context<
ChildInstance<any>
>;
/**仅获取实例*/
export declare function useChildInstanceContext<
T extends MObject<T> = object
>(): ChildInstance<T>;
/**获取状态+错误信息+实例*/
export declare function useChildInstanceContextState<
T extends MObject<T> = object
>(): [
Record<string, T>,
Record<string, Record<keyof T, string[]>>,
Record<string, 'add' | 'edit'>,
ChildInstance<T>
];基本使用
import {
ProviderInstanceContext,
useRegisterChildInstance,
ChildInstanceContext,
useProviderInstance,
useChildInstanceContextState,
useChildInstanceContext,
} from '@carefrees/table-async-validator';
import { ref } from 'valtio';
import { Table, Form, Button, Input, Tooltip, Popconfirm } from 'antd';
import type { TableProps } from 'antd';
import { useEffect, useMemo } from 'react';
interface TableNameStateRowType {
name: string;
age: number;
rowId: string;
file?: FileList | null;
}
interface TableNameState {
a: TableNameStateRowType;
b: TableNameStateRowType;
}
export const RenderCellDelete = (props: any) => {
const { rowData } = props;
const childInstance = useChildInstanceContext<TableNameStateRowType>();
return (
<Popconfirm
title='确认删除吗?'
description='删除后将无法恢复'
onConfirm={() => {
childInstance.onDeleteRow(rowData.rowId);
}}
okText='确认'
cancelText='取消'
>
<Button danger>Delete</Button>
</Popconfirm>
);
};
const RenderCellInputFile = (props: {
rowData: TableNameStateRowType;
field: 'file';
}) => {
const { rowData, field } = props;
const [state, errorState, operationState, childInstance] =
useChildInstanceContextState<TableNameStateRowType>();
// 获取当前行的主键值
const rowId = rowData.rowId;
// 获取当前行的列值
const value = state?.[rowId]?.[field];
// 获取当前行的列错误信息
const errorList = errorState?.[rowId]?.[field];
const errorTip = useMemo(() => {
if (Array.isArray(errorList) && errorList.length) {
return (
<div>
{errorList.map((item, index) => (
<div key={index} style={{ color: 'red' }}>
{item}
</div>
))}
</div>
);
}
return '';
}, [errorList]);
console.log(`${field} value: `, value);
return (
<Tooltip open={Boolean(errorTip)} title={errorTip} color='white'>
<div className={errorTip ? 'ant-form-item-has-error' : ''}>
<input
type='file'
multiple={true}
onChange={(e) => {
const _value = e.target.files;
childInstance.updatedRowData(rowId, {
[field]: _value ? ref(_value) : undefined,
});
}}
/>
</div>
</Tooltip>
);
};
const RenderCellInput = (props: {
rowData: TableNameStateRowType;
field: Exclude<keyof TableNameStateRowType, 'file'>;
}) => {
const { rowData, field } = props;
const [state, errorState, operationState, childInstance] =
useChildInstanceContextState<TableNameStateRowType>();
// 获取当前行的主键值
const rowId = rowData.rowId;
// 获取当前行的列值
const value = state?.[rowId]?.[field];
// 获取当前行的列错误信息
const errorList = errorState?.[rowId]?.[field];
const errorTip = useMemo(() => {
if (Array.isArray(errorList) && errorList.length) {
return (
<div>
{errorList.map((item, index) => (
<div key={index} style={{ color: 'red' }}>
{item}
</div>
))}
</div>
);
}
return '';
}, [errorList]);
return (
<Tooltip open={Boolean(errorTip)} title={errorTip} color='white'>
<div className={errorTip ? 'ant-form-item-has-error' : ''}>
<Input
className={errorTip ? 'ant-input-status-error' : ''}
placeholder={`请输入${field}`}
value={value}
onChange={(e) => {
childInstance.updatedRowData(rowId, { [field]: e.target.value });
}}
/>
</div>
</Tooltip>
);
};
const columns: TableProps['columns'] = [
{
title: '删除',
dataIndex: 'delete',
render: (_, rowData: any) => <RenderCellDelete rowData={rowData} />,
},
{
title: '姓名',
dataIndex: 'name',
render: (_, rowData: any) => (
<RenderCellInput rowData={rowData} field='name' />
),
},
{
title: '年龄',
dataIndex: 'age',
render: (_, rowData: any) => (
<RenderCellInput rowData={rowData} field='age' />
),
},
{
title: '文件',
dataIndex: 'file',
render: (_, rowData: any) => (
<RenderCellInputFile rowData={rowData} field='file' />
),
},
];
function ChildTable(props: {
name: keyof TableNameState;
onChange?: (value: Record<string, string | number>[]) => void;
value?: TableNameStateRowType[];
}) {
const { name, onChange, value } = props;
/**注册子实例*/
const { childInstance } = useRegisterChildInstance<TableNameState>(name);
/**设置行数据的主键值,对应一行中所有列的错误信息*/
childInstance.rowKey = 'rowId';
/**数据转换,如果未初始化会进行初始化,如果已经初始化完成,直接返回传入的数据列表*/
const _value = useMemo(
() => childInstance.ctor(value),
[childInstance, value]
);
useMemo(() => {
// 设置校验规则
childInstance.rules = {
name: [{ required: true, message: '请输入姓名' }],
age: [{ required: true, message: '请输入年龄' }],
};
}, [childInstance]);
const onDeleteRow = (rowKey: string) => {
childInstance.deleteRowData(rowKey);
if (onChange) {
onChange(_value?.filter((item) => item.rowId !== rowKey) || []);
}
};
/**挂载删除操作方法*/
childInstance.onDeleteRow = onDeleteRow;
useEffect(() => {
return () => {
childInstance.clear();
};
}, []);
return (
<ChildInstanceContext.Provider value={childInstance}>
<div>
<Table
size='small'
rowKey='rowId'
columns={columns}
dataSource={_value}
pagination={false}
/>
<Button
onClick={() => {
const result = childInstance.addRowData({ name: '', age: 0 });
if (onChange) {
onChange([...(_value || [])].concat([{ rowId: result.rowId }]));
}
}}
>
新增一行
</Button>
</div>
</ChildInstanceContext.Provider>
);
}
type PartialTableNameState = {
[K in keyof TableNameState]?: TableNameState[K][];
};
const formData: PartialTableNameState = {
a: [{ name: 'd', age: 0, rowId: 'a1' }],
};
const App = () => {
const providerInstance = useProviderInstance<TableNameState>();
const [form] = Form.useForm();
const onClickSubmit = async () => {
try {
const result = await form.validateFields();
console.log('result', result);
} catch (error) {
console.log('error', error);
}
try {
const result = await providerInstance.validate();
console.log('result', result);
} catch (error) {
console.log('error', error);
}
};
return (
<ProviderInstanceContext.Provider value={providerInstance}>
<Form layout='vertical' form={form} initialValues={formData}>
<Form.Item
name='1'
label='输入框1'
rules={[{ required: true, message: '请输入' }]}
>
<Input />
</Form.Item>
<Form.Item name='a' label='表格数据a'>
<ChildTable name='a' />
</Form.Item>
<Form.Item name='b' label='表格数据b'>
<ChildTable name='b' />
</Form.Item>
</Form>
<Button type='primary' onClick={onClickSubmit}>
提交
</Button>
</ProviderInstanceContext.Provider>
);
};
export default App;