npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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;