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

portable-form

v1.2.1

Published

A portable form library based on Vue 3

Downloads

485

Readme

环境依赖

必须安装以下插件:

  • unocss
  • dayjs

主题配置

使用 presetWind4,并导入 colors

uno.config.js

import {
    defineConfig,
    presetAttributify,
    presetIcons,
    presetTypography,
    presetWebFonts,
    presetWind4,
    transformerDirectives,
    transformerVariantGroup,
} from 'unocss'
import { colors } from "portable-form";

export default defineConfig({
    shortcuts: [
    ],
    theme: {
        colors: {
            ...colors,
        },
    },
    presets: [
        presetWind4(),
        presetAttributify(),
        presetIcons(),
        presetTypography(),
        presetWebFonts({
        }),
    ],
    transformers: [
        transformerDirectives(),
        transformerVariantGroup(),
    ],
    // 默认情况下,`.ts` 和 `.js` 文件不会被提取。
    // 如果你想提取它们,请使用以下配置。
    // 如果你使用 shadcn-vue 或 shadcn-svelte,则必须添加以下配置。
    content: {
        pipeline: {
            include: [
                // 默认的 /\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/
                // 包含 js/ts 文件
                "(components|src)/**/*.{js,ts,vue}",
            ],
        },
    },
})

outline: deep

Vben Form 表单

框架提供的表单组件,可适配 Element PlusAnt Design VueNaive UI 等框架。

如果文档内没有参数说明,可以尝试在在线示例内寻找

::: info 写在前面

如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,大可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。

:::

适配器

表单底层使用 vee-validate 进行表单验证,所以你可以使用 vee-validate 的所有功能。对于不同的 UI 框架,我们提供了适配器,以便更好的适配不同的 UI 框架。

适配器说明

每个应用都有不同的 UI 框架,所以在应用的 src/adapter/formsrc/adapter/component 内部,你可以根据自己的需求,进行组件适配。下面是 Ant Design Vue 的适配器示例代码,可根据注释查看说明:

::: details ant design vue 表单适配器


import { setupPortableForm, usePortableForm, z } from 'portable-form';

setupPortableForm({
  config: {
    // ant design vue组件库默认都是 v-model:value
    baseModelPropName: 'value',
    // 一些组件是 v-model:checked 或者 v-model:fileList
    modelPropNameMap: {
      Checkbox: 'checked',
      Radio: 'checked',
      Switch: 'checked',
      Upload: 'fileList',
    },
  },
  defineRules: {
    // 输入项目必填国际化适配
    required: (value, _params, ctx) => {
      if (value === undefined || value === null || value.length === 0) {
        return `请输入${ctx.label}`;
      }
      return true;
    },
    // 选择项目必填国际化适配
    selectRequired: (value, _params, ctx) => {
      if (value === undefined || value === null) {
        return `请选择${ctx.label}`;
      }
      return true;
    },
  },
});

:::

::: details ant design vue 组件适配器

/**
 * 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
 * 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
 */

import { h } from 'vue';

import { globalShareState } from 'portable-form';

const AutoComplete = defineAsyncComponent(
  () => import('ant-design-vue/es/auto-complete'),
);
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
const Checkbox = defineAsyncComponent(
  () => import('ant-design-vue/es/checkbox'),
);
const CheckboxGroup = defineAsyncComponent(() =>
  import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
);
const DatePicker = defineAsyncComponent(
  () => import('ant-design-vue/es/date-picker'),
);
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
const InputNumber = defineAsyncComponent(
  () => import('ant-design-vue/es/input-number'),
);
const InputPassword = defineAsyncComponent(() =>
  import('ant-design-vue/es/input').then((res) => res.InputPassword),
);
const Mentions = defineAsyncComponent(
  () => import('ant-design-vue/es/mentions'),
);
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
const RadioGroup = defineAsyncComponent(() =>
  import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
);
const RangePicker = defineAsyncComponent(() =>
  import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
);
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
const Textarea = defineAsyncComponent(() =>
  import('ant-design-vue/es/input').then((res) => res.Textarea),
);
const TimePicker = defineAsyncComponent(
  () => import('ant-design-vue/es/time-picker'),
);
const TreeSelect = defineAsyncComponent(
  () => import('ant-design-vue/es/tree-select'),
);
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));



const placeholderType = {
    "input": "请输入",
    "select": "请选择",
    "upload": "点击上传"
}
const withDefaultPlaceholder = (
    component,
    type,
    componentProps = {},
) => {
    return defineComponent({
        name: component.name,
        inheritAttrs: false,
        setup: (props, { attrs, expose, slots }) => {
            const placeholder =
                props?.placeholder ||
                attrs?.placeholder ||
                placeholderType[type];
            // 透传组件暴露的方法
            const innerRef = ref();
            expose(
                new Proxy(
                    {},
                    {
                        get: (_target, key) => innerRef.value?.[key],
                        has: (_target, key) => key in (innerRef.value || {}),
                    },
                ),
            );
            return () =>
                h(
                    component,
                    { ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
                    slots,
                );
        },
    });
};

async function initComponentAdapter() {
    const components = {
        // 如果你的组件体积比较大,可以使用异步加载
        // Button: () =>
        // import('xxx').then((res) => res.Button),

        ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', {
            component: Cascader,
            fieldNames: { label: 'label', value: 'value', children: 'children' },
            loadingSlot: 'suffixIcon',
            modelPropName: 'value',
            visibleEvent: 'onVisibleChange',
        }),
        ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
            component: Select,
            loadingSlot: 'suffixIcon',
            modelPropName: 'value',
            visibleEvent: 'onVisibleChange',
        }),
        ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
            component: TreeSelect,
            fieldNames: { label: 'label', value: 'value', children: 'children' },
            loadingSlot: 'suffixIcon',
            modelPropName: 'value',
            optionsPropName: 'treeData',
            visibleEvent: 'onVisibleChange',
        }),
        AutoComplete,
        Cascader,
        Checkbox,
        CheckboxGroup,
        DatePicker,
        // 自定义默认按钮
        DefaultButton: (props, { attrs, slots }) => {
            return h(Button, { ...props, attrs, type: 'default' }, slots);
        },
        Divider,
        Input: withDefaultPlaceholder(Input, 'input'),
        InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
        InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
        Mentions: withDefaultPlaceholder(Mentions, 'input'),
        // 自定义主要按钮
        PrimaryButton: (props, { attrs, slots }) => {
            return h(Button, { ...props, attrs, type: 'primary' }, slots);
        },
        Radio,
        RadioGroup,
        RangePicker,
        Rate,
        Select: withDefaultPlaceholder(Select, 'select'),
        Space,
        Switch,
        Textarea: withDefaultPlaceholder(Textarea, 'input'),
        TimePicker,
        TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
        // Upload: withPreviewUpload(),
    };

    // 将组件注册到全局共享状态中
    globalShareState.setComponents(components);

    // 定义全局共享状态中的消息提示
    globalShareState.defineMessage({
        // 复制成功消息提示
        copyPreferencesSuccess: (title, content) => {
            notification.success({
                description: content,
                message: title,
                placement: 'bottomRight',
            });
        },
    });
}

export { initComponentAdapter };

:::

API

usePortableForm 返回一个数组,第一个元素是表单组件,第二个元素是表单的方法。

<script setup>
import { usePortableForm } from '#/adapter/form';

// Form 为弹窗组件
// formApi 为弹窗的方法
const [Form, formApi] = usePortableForm({
  // 属性
  // 事件
});
</script>

<template>
  <Form />
</template>

FormApi

usePortableForm 返回的第二个参数,是一个对象,包含了一些表单的方法。

| 方法名 | 描述 | 类型 | 版本号 | | --- | --- | --- | --- | | submitForm | 提交表单 | (e:Event)=>Promise<Record<string,any>> | - | | validateAndSubmitForm | 提交并校验表单 | (e:Event)=>Promise<Record<string,any>> | - | | resetForm | 重置表单 | ()=>Promise<void> | - | | setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | (fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void> | - | | getValues | 获取表单值 | (fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void> | - | | validate | 表单校验 | ()=>Promise<void> | - | | validateField | 校验指定字段 | (fieldName: string)=>Promise<ValidationResult<unknown>> | - | | isFieldValid | 检查某个字段是否已通过校验 | (fieldName: string)=>Promise<boolean> | - | | resetValidate | 重置表单校验 | ()=>Promise<void> | - | | updateSchema | 更新formSchema | (schema:FormSchema[])=>void | - | | setFieldValue | 设置字段值 | (field: string, value: any, shouldValidate?: boolean)=>Promise<void> | - | | setState | 设置组件状态(props) | (stateOrFn:\| ((prev: VbenFormProps) => Partial<VbenFormProps>)\| Partial<VbenFormProps>)=>Promise<void> | - | | getState | 获取组件状态(props) | ()=>Promise<VbenFormProps> | - | | form | 表单对象实例,可以操作表单,见 useForm | - | - | | getFieldComponentRef | 获取指定字段的组件实例 | <T=unknown>(fieldName: string)=>T | >5.5.3 | | getFocusedField | 获取当前已获得焦点的字段 | ()=>string\|undefined | >5.5.3 |

Props

所有属性都可以传入 usePortableForm 的第一个参数中。

| 属性名 | 描述 | 类型 | 默认值 | | --- | --- | --- | --- | | layout | 表单项布局 | 'horizontal' \| 'vertical'\| 'inline' | horizontal | | showCollapseButton | 是否显示折叠按钮 | boolean | false | | wrapperClass | 表单的布局,基于tailwindcss | any | - | | actionWrapperClass | 表单操作区域class | any | - | | actionLayout | 表单操作按钮位置 | 'newLine' \| 'rowEnd' \| 'inline' | rowEnd | | actionPosition | 表单操作按钮对齐方式 | 'left' \| 'center' \| 'right' | right | | handleReset | 表单重置回调 | (values: Record<string, any>,) => Promise<void> \| void | - | | handleSubmit | 表单提交回调 | (values: Record<string, any>,) => Promise<void> \| void | - | | handleValuesChange | 表单值变化回调 | (values: Record<string, any>, fieldsChanged: string[]) => void | - | | handleCollapsedChange | 表单收起展开状态变化回调 | (collapsed: boolean) => void | - | | actionButtonsReverse | 调换操作按钮位置 | boolean | false | | resetButtonOptions | 重置按钮组件参数 | ActionButtonOptions | - | | submitButtonOptions | 提交按钮组件参数 | ActionButtonOptions | - | | showDefaultActions | 是否显示默认操作按钮 | boolean | true | | collapsed | 是否折叠,在showCollapseButtontrue时生效 | boolean | false | | collapseTriggerResize | 折叠时,触发resize事件 | boolean | false | | collapsedRows | 折叠时保持的行数 | number | 1 | | fieldMappingTime | 用于将表单内的数组值映射成 2 个字段 | [string, [string, string],Nullable<string>\|[string,string]\|((any,string)=>any)?][] | - | | commonConfig | 表单项的通用配置,每个配置都会传递到每个表单项,表单项可覆盖 | FormCommonConfig | - | | schema | 表单项的每一项配置 | FormSchema[] | - | | submitOnEnter | 按下回车健时提交表单 | boolean | false | | submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | boolean | false | | compact | 是否紧凑模式(忽略为校验信息所预留的空间) | boolean | false | | scrollToFirstError | 表单验证失败时是否自动滚动到第一个错误字段 | boolean | false |

::: tip handleValuesChange

handleValuesChange 回调函数的第一个参数values装载了表单改变后的当前值对象,第二个参数fieldsChanged是一个数组,包含了所有被改变的字段名。注意:第二个参数仅在v5.5.4(不含)以上版本可用,并且传递的是已在schema中定义的字段名。如果你使用了字段映射并且需要检查是哪些字段发生了变化的话,请注意该参数并不会包含映射后的字段名。

:::

::: tip fieldMappingTime

此属性用于将表单内的数组值映射成 2 个字段,它应当传入一个数组,数组的每一项是一个映射规则,规则的第一个成员是一个字符串,表示需要映射的字段名,第二个成员是一个数组,表示映射后的字段名,第三个成员是一个可选的格式掩码,用于格式化日期时间字段;也可以提供一个格式化函数(参数分别为当前值和当前字段名,返回格式化后的值)。如果明确地将格式掩码设为null,则原值映射而不进行格式化(适用于非日期时间字段)。例如:[['timeRange', ['startTime', 'endTime'], 'YYYY-MM-DD']]timeRange应当是一个至少具有2个成员的数组类型的值。Form会将timeRange的值前两个值分别按照格式掩码YYYY-MM-DD格式化后映射到startTimeendTime字段上。每一项的第三个参数是一个可选的格式掩码,

:::

TS 类型说明

::: details ActionButtonOptions

export interface ActionButtonOptions {
  /** 样式 */
  class?: ClassType;
  /** 是否禁用 */
  disabled?: boolean;
  /** 是否加载中 */
  loading?: boolean;
  /** 按钮大小 */
  size?: ButtonVariantSize;
  /** 按钮类型 */
  variant?: ButtonVariants;
  /** 是否显示 */
  show?: boolean;
  /** 按钮文本 */
  content?: string;
  /** 任意属性 */
  [key: string]: any;
}

:::

::: details FormCommonConfig

export interface FormCommonConfig {
  /**
   * 所有表单项的props
   */
  componentProps?: ComponentProps;
  /**
   * 所有表单项的控件样式
   */
  controlClass?: string;
  /**
   * 在表单项的Label后显示一个冒号
   */
  colon?: boolean;
  /**
   * 所有表单项的禁用状态
   * @default false
   */
  disabled?: boolean;
  /**
   * 所有表单项的控件样式
   * @default {}
   */
  formFieldProps?: Partial<typeof Field>;
  /**
   * 所有表单项的栅格布局
   * @default ""
   */
  formItemClass?: (() => string) | string;
  /**
   * 隐藏所有表单项label
   * @default false
   */
  hideLabel?: boolean;
  /**
   * 是否隐藏必填标记
   * @default false
   */
  hideRequiredMark?: boolean;
  /**
   * 所有表单项的label样式
   * @default ""
   */
  labelClass?: string;
  /**
   * 所有表单项的label宽度
   */
  labelWidth?: number;
  /**
   * 所有表单项的model属性名。使用自定义组件时可通过此配置指定组件的model属性名。已经在modelPropNameMap中注册的组件不受此配置影响
   * @default "modelValue"
   */
  modelPropName?: string;
  /**
   * 所有表单项的wrapper样式
   */
  wrapperClass?: string;
}

:::

::: details FormSchema

export interface FormSchema<
  T extends BaseFormComponentType = BaseFormComponentType,
> extends FormCommonConfig {
  /** 组件 */
  component: Component | T;
  /** 组件参数 */
  componentProps?: ComponentProps;
  /** 默认值 */
  defaultValue?: any;
  /** 依赖 */
  dependencies?: FormItemDependencies;
  /** 描述 */
  description?: string;
  /** 字段名,也作为自定义插槽的名称 */
  fieldName: string;
  /** 帮助信息 */
  help?: CustomRenderType;
  /** 是否隐藏表单项 */
  hide?: boolean;
  /** 表单的标签(如果是一个string,会用于默认必选规则的消息提示) */
  label?: CustomRenderType;
  /** 自定义组件内部渲染  */
  renderComponentContent?: RenderComponentContentType;
  /** 字段规则 */
  rules?: FormSchemaRuleType;
  /** 后缀 */
  suffix?: CustomRenderType;
}

:::

表单联动

表单联动需要通过 schema 内的 dependencies 属性进行联动,允许您添加字段之间的依赖项,以根据其他字段的值控制字段。

dependencies: {
  // 触发字段。只有这些字段值变动时,联动才会触发
  triggerFields: ['name'],
  // 动态判断当前字段是否需要显示,不显示则直接销毁
  if(values,formApi){},
  // 动态判断当前字段是否需要显示,不显示用css隐藏
  show(values,formApi){},
  // 动态判断当前字段是否需要禁用
  disabled(values,formApi){},
  // 字段变更时,都会触发该函数
  trigger(values,formApi){},
  // 动态rules
  rules(values,formApi){},
  // 动态必填
  required(values,formApi){},
  // 动态组件参数
  componentProps(values,formApi){},
}

表单校验

表单校验需要通过 schema 内的 rules 属性进行配置。

rules的值可以是字符串(预定义的校验规则名称),也可以是一个zod的schema。

预定义的校验规则

// 表示字段必填,默认会根据适配器的required进行国际化
{
  rules: 'required';
}

// 表示字段必填,默认会根据适配器的required进行国际化,用于下拉选择之类
{
  rules: 'selectRequired';
}

zod

rules也支持 zod 的 schema,可以进行更复杂的校验,zod 的使用请查看 zod文档

import { z } from '#/adapter/form';

// 基础类型
{
  rules: z.string().min(1, { message: '请输入字符串' });
}

// 可选(可以是undefined),并且携带默认值。注意zod的optional不包括空字符串''
{
  rules: z.string().default('默认值').optional();
}

// 可以是空字符串、undefined或者一个邮箱地址(两种不同的用法)
{
  rules: z.union([z.string().email().optional(), z.literal('')]);
}

{
  rules: z.string().email().or(z.literal('')).optional();
}

// 复杂校验
{
  z.string()
    .min(1, { message: '请输入' })
    .refine((value) => value === '123', {
      message: '值必须为123',
    });
}

Slots

可以使用以下插槽在表单中插入自定义的内容

| 插槽名 | 描述 | | ------------- | ------------------ | | reset-before | 重置按钮之前的位置 | | submit-before | 提交按钮之前的位置 | | expand-before | 展开按钮之前的位置 | | expand-after | 展开按钮之后的位置 |

::: tip 字段插槽

除了以上内置插槽之外,schema属性中每个字段的fieldName都可以作为插槽名称,这些字段插槽的优先级高于component定义的组件。也就是说,当提供了与fieldName同名的插槽时,这些插槽的内容将会作为这些字段的组件,此时component的值将会被忽略。

:::