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

@form-renderer/adapter-vue3

v1.0.0-alpha.12

Published

Vue3 adapter for FormEngine

Readme

FormAdapter

FormAdapter 是基于 FormEngine 的 Vue3 表单渲染适配器,提供了完整的表单渲染、交互和组件集成能力。它将 FormEngine 的声明式 Schema 转换为可视化的 Vue 组件,并提供响应式的数据绑定和事件处理。

项目结构

详见 项目结构文档

核心特性

1. 响应式引擎集成

  • Vue3 响应式系统深度集成
  • 基于 shallowRef 的性能优化
  • 不可变数据流,确保追踪性
  • 自动同步 FormEngine 和 Vue 状态

2. 组件注册系统

  • 灵活的组件定义机制
  • 支持单个注册、批量注册和预设注册
  • 内置值转换器和事件映射
  • 支持自定义渲染逻辑

3. 事件处理系统

  • 统一的事件处理接口
  • 可选的批量更新优化
  • 完整的事件生命周期管理
  • 错误处理和恢复机制

4. UI 框架集成

  • 支持多种 UI 框架(Element Plus、Ant Design Vue 等)
  • 自动转换校验规则(validators → rules)
  • FormItem 自动包裹
  • 主题和样式定制

5. 组合式 API

  • useFormAdapter - 编程式表单管理
  • useFieldComponent - 字段组件开发
  • 完整的 TypeScript 类型支持
  • 响应式状态和方法

6. 性能优化

  • 智能批量更新调度
  • 浅比较优化渲染
  • 按需加载组件
  • 结构共享机制

架构设计

FormAdapter 采用分层架构:

FormAdapter
├── FormAdapter.vue         # 主组件(入口)
├── SchemaRenderer.vue      # Schema 渲染器
├── Core 层                 # 核心模块
│   ├── ReactiveEngine      # 响应式引擎
│   ├── ComponentRegistry   # 组件注册表
│   ├── EventHandler        # 事件处理器
│   └── UpdateScheduler     # 更新调度器
├── Composables 层          # 组合式函数
│   ├── useFormAdapter      # 表单适配器
│   └── useFieldComponent   # 字段组件
├── Components 层           # 容器组件
│   ├── FormContainer       # 表单容器
│   ├── LayoutContainer     # 布局容器
│   └── ListContainer       # 列表容器
└── Presets 层             # 预设集成
    ├── ElementPlusPreset  # Element Plus 预设
    └── ExamplePreset      # 示例预设

设计原则

  1. 渐进增强 - 从基础功能到高级特性,按需使用
  2. 框架无关 - 核心逻辑与 UI 框架解耦
  3. 类型安全 - 完整的 TypeScript 类型定义
  4. 性能优先 - 批处理、浅比较、按需更新
  5. 开发友好 - 清晰的 API、完善的文档、丰富的示例
  6. 扩展性强 - 易于扩展新组件和预设

适用场景

  • 复杂动态表单渲染
  • 低代码/无代码平台
  • 配置化表单系统
  • 表单设计器/构建器
  • 多 UI 框架适配

快速开始

安装

npm install @form-renderer/adapter-vue3 @form-renderer/engine vue

基础使用

<template>
  <FormAdapter
    :schema="schema"
    v-model:model="formData"
    :components="ElementPlusPreset"
    @submit="handleSubmit"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { FormAdapter } from '@form-renderer/adapter-vue3'
import { ElementPlusPreset } from '@form-renderer/adapter-vue3/presets'

const schema = {
  type: 'form',
  properties: {
    name: {
      type: 'field',
      component: 'Input',
      required: true,
      formItemProps: {
        label: '姓名'
      }
    },
    age: {
      type: 'field',
      component: 'InputNumber',
      required: true,
      formItemProps: {
        label: '年龄'
      }
    }
  }
}

const formData = ref({
  name: '',
  age: undefined
})

const handleSubmit = async (data) => {
  console.log('提交数据:', data)
}
</script>

使用组合式 API

import { useFormAdapter } from '@form-renderer/adapter-vue3'
import { ElementPlusPreset } from '@form-renderer/adapter-vue3/presets'

const {
  renderSchema,
  model,
  updateValue,
  validate,
  submit,
  reset
} = useFormAdapter({
  schema: mySchema,
  model: { name: '', age: undefined },
  components: ElementPlusPreset,
  onSubmit: async (data) => {
    await api.submit(data)
  }
})

// 更新值
updateValue('name', 'John')

// 校验表单
const result = await validate()

// 提交表单
await submit()

// 重置表单
reset()

核心概念

1. ReactiveEngine - 响应式引擎

ReactiveEngine 是 FormAdapter 的核心,负责将 FormEngine 与 Vue3 响应式系统集成。

特性:

  • 基于 shallowRef 的浅响应式
  • 自动同步 FormEngine 的状态变化
  • 支持值变化和结构变化事件
  • 可选的更新调度优化

使用:

import { createReactiveEngine } from '@form-renderer/adapter-vue3'

const engine = createReactiveEngine({
  schema: mySchema,
  model: myModel,
  enableUpdateScheduler: true
})

// 获取响应式 renderSchema(只读)
const renderSchema = engine.getRenderSchema()

// 获取响应式 model(只读)
const model = engine.getModel()

// 更新值
engine.updateValue('name', 'John')

// 销毁引擎
engine.destroy()

2. ComponentRegistry - 组件注册表

ComponentRegistry 管理所有可用的组件定义。

组件定义结构:

interface ComponentDefinition {
  name: string                    // 组件名称
  component: Component            // Vue 组件
  type: ComponentType             // 组件类型
  defaultProps?: Record<string, any>
  valueTransformer?: ValueTransformer
  eventMapping?: EventMapping
  needFormItem?: boolean
  customRender?: (props) => VNode
}

使用:

import { createComponentRegistry } from '@form-renderer/adapter-vue3'

const registry = createComponentRegistry()

// 注册单个组件
registry.register({
  name: 'Input',
  component: ElInput,
  type: 'field',
  eventMapping: {
    onChange: 'update:modelValue'
  }
})

// 批量注册
registry.registerBatch([
  { name: 'Input', component: ElInput, type: 'field' },
  { name: 'Select', component: ElSelect, type: 'field' }
])

// 注册预设
registry.registerPreset(ElementPlusPreset)

// 获取组件
const inputDef = registry.get('Input')

3. EventHandler - 事件处理器

EventHandler 负责处理所有用户交互事件。

事件类型:

  • 字段值变化(change)
  • 字段聚焦/失焦(focus/blur)
  • 列表操作(add/remove/move)

使用:

import { createEventHandler } from '@form-renderer/adapter-vue3'

const handler = createEventHandler(engine, registry, {
  enableBatch: true,
  batchDelay: 16
})

// 处理字段变化
handler.handleFieldChange('name', 'John', 'Input')

// 处理列表添加
handler.handleListAdd('items', { name: '', price: 0 })

// 处理列表删除
handler.handleListRemove('items', 0)

// 立即刷新批量更新
handler.flush()

事件处理层次:

FormAdapter 支持两种层次的事件处理:

  1. 核心事件(由 EventHandler 处理):

    • onChange: 字段值变化,会更新 model
    • onInput: 输入事件,只更新显示值
    • onFocus: 字段聚焦
    • onBlur: 字段失焦
  2. 字段级自定义事件(直接绑定到组件):

    • onKeydownonKeyup: 键盘事件
    • onClickonMouseenter: 鼠标事件
    • 以及任何组件支持的原生事件

使用自定义事件:

const schema = {
  fields: [
    {
      path: 'username',
      component: 'input',
      componentProps: {
        // 在 componentProps 中定义事件处理器
        onKeydown: (e: KeyboardEvent) => {
          if (e.key === 'Enter') {
            // 处理回车
          }
        },
        onFocus: () => {
          console.log('字段获得焦点')
        }
      }
    }
  ]
}

详见:自定义事件处理指南

4. 组件预设(ComponentPreset)

组件预设是一组预定义的组件配置,方便快速集成 UI 框架。

预设结构:

interface ComponentPreset {
  name: string                    // 预设名称
  components: ComponentDefinition[] // 组件列表
  formItem?: Component            // FormItem 组件
  ruleConverter?: RuleConverter   // 校验规则转换器
  theme?: ThemeConfig             // 主题配置
  setup?: () => void              // 初始化函数
}

示例:

export const MyPreset: ComponentPreset = {
  name: 'my-preset',
  components: [
    {
      name: 'Input',
      component: MyInput,
      type: 'field',
      eventMapping: {
        onChange: 'update:modelValue'
      }
    }
  ],
  formItem: MyFormItem,
  ruleConverter: (node, computed, context) => {
    const rules = []
    if (computed.required) {
      rules.push({ required: true, message: '必填' })
    }
    return rules
  }
}

组件开发

字段组件(Field Component)

字段组件是最基础的组件类型,用于渲染表单字段。

接口规范:

interface FieldComponentProps {
  modelValue: any          // v-model 绑定值
  disabled?: boolean       // 禁用状态
  readonly?: boolean       // 只读状态
  placeholder?: string     // 占位符
  [key: string]: any       // 其他属性
}

interface FieldComponentEmits {
  'update:modelValue': (value: any) => void
  focus?: (event: FocusEvent) => void
  blur?: (event: FocusEvent) => void
  change?: (value: any) => void
}

示例:

<template>
  <input
    :value="modelValue"
    :disabled="disabled"
    :readonly="readonly"
    :placeholder="placeholder"
    @input="handleInput"
    @focus="handleFocus"
    @blur="handleBlur"
  />
</template>

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'

const props = defineProps<{
  modelValue: any
  disabled?: boolean
  readonly?: boolean
  placeholder?: string
}>()

const emit = defineEmits<{
  'update:modelValue': [value: any]
  focus: [event: FocusEvent]
  blur: [event: FocusEvent]
}>()

const handleInput = (e: Event) => {
  emit('update:modelValue', (e.target as HTMLInputElement).value)
}

const handleFocus = (e: FocusEvent) => {
  emit('focus', e)
}

const handleBlur = (e: FocusEvent) => {
  emit('blur', e)
}
</script>

布局组件(Layout Component)

布局组件用于组织和排列表单字段。

<template>
  <div class="layout-container">
    <h3 v-if="title">{{ title }}</h3>
    <slot />
  </div>
</template>

<script setup lang="ts">
defineProps<{
  title?: string
}>()
</script>

列表组件(List Component)

列表组件用于渲染动态列表。

<template>
  <div class="list-container">
    <div v-for="(row, index) in rows" :key="index" class="list-item">
      <slot :row="row" :index="index" />
      <button @click="emit('remove', index)">删除</button>
    </div>
    <button @click="emit('add')">添加</button>
  </div>
</template>

<script setup lang="ts">
defineProps<{
  rows: any[]
}>()

const emit = defineEmits<{
  add: []
  remove: [index: number]
}>()
</script>

值转换器(ValueTransformer)

值转换器用于在组件值和引擎值之间进行转换。

使用场景:

  • 日期组件:Datestring
  • 数字组件:numberstring
  • 多选组件:string[]string(逗号分隔)

示例:

const dateTransformer: ValueTransformer = {
  // 引擎值 → 组件值
  toComponent: (engineValue: string) => {
    return engineValue ? new Date(engineValue) : null
  },
  // 组件值 → 引擎值
  fromComponent: (componentValue: Date | null) => {
    return componentValue ? componentValue.toISOString() : ''
  }
}

// 注册时使用
registry.register({
  name: 'DatePicker',
  component: ElDatePicker,
  type: 'field',
  valueTransformer: dateTransformer
})

校验规则转换(RuleConverter)

RuleConverter 将 FormEngine 的 validators 转换为 UI 框架的 rules 格式。

示例(Element Plus):

const ruleConverter: RuleConverter = (node, computed, context) => {
  const rules = []

  // 必填校验
  if (computed.required) {
    rules.push({
      required: true,
      message: `${node.formItemProps?.label || '该字段'}为必填项`,
      trigger: 'blur'
    })
  }

  // 自定义校验器
  if (node.validators && Array.isArray(node.validators)) {
    node.validators.forEach((validator) => {
      rules.push({
        validator: async (rule, value, callback) => {
          try {
            const ctx = {
              path: node.path,
              getSchema: context.engine.getEngine().getSchema,
              getValue: context.engine.getEngine().getValue,
              getCurRowValue: () => ({}),
              getCurRowIndex: () => -1
            }
            const result = await validator(value, ctx)
            
            if (result === false || typeof result === 'string') {
              callback(new Error(typeof result === 'string' ? result : '校验失败'))
            } else {
              callback()
            }
          } catch (error) {
            callback(error)
          }
        },
        trigger: 'blur'
      })
    })
  }

  return rules
}

FormItem 集成

FormItem 组件用于包裹字段组件,提供标签、错误信息等。

要求:

  • 接收 labelrequirederror 等 props
  • 提供默认插槽用于渲染字段组件
  • 支持校验规则(如果使用 ruleConverter)

示例(Element Plus):

<template>
  <el-form-item
    :label="label"
    :required="required"
    :prop="prop"
    :rules="rules"
    :error="error"
  >
    <slot />
  </el-form-item>
</template>

更新调度(UpdateScheduler)

UpdateScheduler 用于优化批量更新性能。

特性:

  • 使用 requestAnimationFrame 调度
  • 合并多个连续更新
  • 避免不必要的渲染

使用:

// 创建时启用
const engine = createReactiveEngine({
  schema: mySchema,
  model: myModel,
  enableUpdateScheduler: true
})

// 多次更新会自动合并
engine.updateValue('name', 'John')
engine.updateValue('age', 25)
engine.updateValue('city', 'Beijing')
// ↑ 这三次更新会在下一帧合并为一次批量更新

// 立即刷新
engine.flush()

性能优化

1. 使用 shallowRef

FormAdapter 使用 shallowRef 而非 ref,避免深度响应式的性能开销。

// ✅ 好
const renderSchema = shallowRef(engine.getRenderSchema())

// ❌ 差
const renderSchema = ref(engine.getRenderSchema())

2. 启用更新调度

const engine = createReactiveEngine({
  schema,
  model,
  enableUpdateScheduler: true  // ✅ 启用批量更新
})

3. 启用事件批处理

const handler = createEventHandler(engine, registry, {
  enableBatch: true,  // ✅ 启用批处理
  batchDelay: 16
})

4. 避免不必要的计算属性

// ✅ 好:浅比较
const renderSchema = computed(() => engine.getRenderSchema().value)

// ❌ 差:深比较
const renderSchema = computed(() => JSON.parse(JSON.stringify(engine.getRenderSchema().value)))

5. 使用组件懒加载

const InputComponent = defineAsyncComponent(() => import('./MyInput.vue'))

registry.register({
  name: 'Input',
  component: InputComponent,
  type: 'field'
})

调试

启用日志

// 在 FormAdapter 组件中查看日志
console.log('RenderSchema:', renderSchema.value)
console.log('Model:', model.value)
console.log('Engine:', engine.value)

使用 Vue Devtools

安装 Vue Devtools 浏览器扩展,可以查看:

  • 组件树
  • 响应式数据
  • 事件触发

性能分析

// 使用 Vue 3 的性能 API
import { startMeasure, stopMeasure } from 'vue'

startMeasure('form-render')
// ... 渲染代码
stopMeasure('form-render')

API 文档

详见 API 文档

完整示例

动态表单

<template>
  <FormAdapter
    :schema="schema"
    v-model:model="formData"
    :components="ElementPlusPreset"
    @change="handleChange"
    @submit="handleSubmit"
    ref="formRef"
  />
  
  <div class="actions">
    <button @click="handleValidate">校验</button>
    <button @click="handleReset">重置</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { FormAdapter } from '@form-renderer/adapter-vue3'
import { ElementPlusPreset } from '@form-renderer/adapter-vue3/presets'

const formRef = ref()

const schema = {
  type: 'form',
  properties: {
    userType: {
      type: 'field',
      component: 'Select',
      formItemProps: { label: '用户类型' },
      componentProps: {
        options: [
          { label: '个人', value: 'personal' },
          { label: '企业', value: 'company' }
        ]
      }
    },
    personalInfo: {
      type: 'layout',
      ifShow: (ctx) => ctx.getValue('userType') === 'personal',
      properties: {
        name: {
          type: 'field',
          component: 'Input',
          required: true,
          formItemProps: { label: '姓名' }
        },
        idCard: {
          type: 'field',
          component: 'Input',
          required: true,
          formItemProps: { label: '身份证号' },
          validators: [
            (value) => {
              if (value && value.length !== 18) {
                return '身份证号必须是18位'
              }
            }
          ]
        }
      }
    },
    companyInfo: {
      type: 'layout',
      ifShow: (ctx) => ctx.getValue('userType') === 'company',
      properties: {
        companyName: {
          type: 'field',
          component: 'Input',
          required: true,
          formItemProps: { label: '企业名称' }
        },
        creditCode: {
          type: 'field',
          component: 'Input',
          required: true,
          formItemProps: { label: '统一社会信用代码' }
        }
      }
    },
    items: {
      type: 'list',
      items: {
        name: {
          type: 'field',
          component: 'Input',
          required: true,
          formItemProps: { label: '商品名称' }
        },
        price: {
          type: 'field',
          component: 'InputNumber',
          required: true,
          formItemProps: { label: '价格' },
          validators: [
            (value) => {
              if (value <= 0) return '价格必须大于0'
            }
          ]
        },
        quantity: {
          type: 'field',
          component: 'InputNumber',
          required: true,
          formItemProps: { label: '数量' }
        },
        total: {
          type: 'field',
          component: 'InputNumber',
          readonly: true,
          formItemProps: { label: '小计' },
          subscribes: {
            '.price': (ctx) => {
              const row = ctx.getCurRowValue()
              ctx.updateSelf((row.price || 0) * (row.quantity || 0))
            },
            '.quantity': (ctx) => {
              const row = ctx.getCurRowValue()
              ctx.updateSelf((row.price || 0) * (row.quantity || 0))
            }
          }
        }
      }
    }
  }
}

const formData = ref({
  userType: '',
  items: []
})

const handleChange = (event) => {
  console.log('值变化:', event)
}

const handleValidate = async () => {
  const result = await formRef.value.validate()
  console.log('校验结果:', result)
}

const handleSubmit = async (data) => {
  console.log('提交数据:', data)
  // 调用 API
  await api.submitForm(data)
}

const handleReset = () => {
  formRef.value.reset()
}
</script>

常见问题

1. 为什么 v-model 不更新?

确保 FormAdapter 使用了 v-model:model 而不是 :model

<!-- ✅ 正确 -->
<FormAdapter v-model:model="formData" :schema="schema" />

<!-- ❌ 错误 -->
<FormAdapter :model="formData" :schema="schema" />

2. 组件找不到?

确保组件已注册:

// 检查组件是否已注册
const registry = formRef.value.getRegistry()
console.log(registry.has('Input'))  // 应该返回 true

3. 校验不生效?

检查以下几点:

  • 是否正确配置了 requiredvalidators
  • 是否使用了 ruleConverter(UI 框架校验)
  • 字段是否被隐藏(ifShow: false)或禁用(disabled: true

4. 性能问题?

尝试以下优化:

  • 启用 enableUpdateScheduler
  • 启用 enableBatch
  • 使用组件懒加载
  • 减少深度响应式

5. 如何调试事件?

在 FormAdapter 组件上监听所有事件:

<FormAdapter
  @change="console.log('change', $event)"
  @field-blur="console.log('blur', $event)"
  @field-focus="console.log('focus', $event)"
  @list-change="console.log('list', $event)"
  @validate="console.log('validate', $event)"
/>

最佳实践

1. 组件注册

推荐:使用预设

// ✅ 好
import { ElementPlusPreset } from '@form-renderer/adapter-vue3/presets'

<FormAdapter :components="ElementPlusPreset" />

不推荐:手动注册每个组件

// ❌ 繁琐
const components = [
  { name: 'Input', component: ElInput, type: 'field' },
  { name: 'Select', component: ElSelect, type: 'field' },
  // ... 很多组件
]

2. 表单数据管理

推荐:使用 ref

// ✅ 好
const formData = ref({ name: '', age: 0 })

<FormAdapter v-model:model="formData" />

不推荐:使用 reactive

// ⚠️ 可能导致类型问题
const formData = reactive({ name: '', age: 0 })

3. 事件处理

推荐:使用具名回调

<template>
  <FormAdapter
    @change="handleChange"
    @submit="handleSubmit"
  />
</template>

<script setup>
const handleChange = (event) => {
  // 处理变化
}

const handleSubmit = async (data) => {
  // 提交逻辑
}
</script>

4. Schema 定义

推荐:提取为单独的文件

// schemas/user-form.ts
export const userFormSchema = {
  type: 'form',
  properties: {
    // ...
  }
}

// 使用
import { userFormSchema } from './schemas/user-form'

迁移指南

从原生表单迁移

<!-- 原生表单 -->
<form @submit="handleSubmit">
  <div>
    <label>姓名</label>
    <input v-model="formData.name" required />
  </div>
  <div>
    <label>年龄</label>
    <input v-model.number="formData.age" type="number" required />
  </div>
  <button type="submit">提交</button>
</form>

<!-- 迁移到 FormAdapter -->
<FormAdapter
  :schema="{
    type: 'form',
    properties: {
      name: {
        type: 'field',
        component: 'Input',
        required: true,
        formItemProps: { label: '姓名' }
      },
      age: {
        type: 'field',
        component: 'InputNumber',
        required: true,
        formItemProps: { label: '年龄' }
      }
    }
  }"
  v-model:model="formData"
  @submit="handleSubmit"
/>

扩展阅读

License

MIT