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

dynamicformdjx

v0.5.0

Published

Dynamic form for Vue3

Readme

dynamicformdjx

基于 Vue 3 的动态表单及录入。

Document

Vue2 版本

React 版本

概述

新增综合CRUD模板

DynamicForm 一个灵活且动态的表单组件,使用数组,简化模版操作,提供多种hook快速操作表单等。

  • 简化template代码,快速处理表单
  • 提供render2函数渲染表单项,使用函数渲染值或自定义h函数
  • 提供多种hooks函数,快速处理数据值

DynamicInput 组件是一个灵活且动态的表单输入组件,允许用户添加、修改和删除键值对。它提供了多种自定义选项,如按钮文本、表单布局和输入过滤

  • 支持通过 v-model 双向绑定任意对象,(包含受控和非受控) 可动态增删字段
  • 支持将值解析为:字符串 / 数字 / 数组(字符串数组、数字数组)
  • 文案、样式、数组分隔符等均可配置

安装

# 任意一种
npm install dynamicformdjx
# or
yarn add dynamicformdjx
# or
pnpm add dynamicformdjx

综合CRUD template

(依赖于naive ui或element plus组件库,请配合一起使用)

与Naive ui配合


<script setup lang="ts">
  import {h, nextTick, onMounted, ref} from "vue";
  import {type DataTableColumnKey, type DataTableColumns, NButton, NDataTable, NSpace, useMessage} from "naive-ui";
  import {
    NaiPopupModal,
    useDecorateForm,
    NaiZealCard,
    NaiDynamicForm,
    NaiZealTableSearch,
    NaiZealTablePaginationControl,
    renderInput,
    renderInputNumber,
  } from "dynamicformdjx/naiveUi";
  import type {
    naiPopupModalRef,
    naiDynamicFormRef,
    naiZealTableSearchRef
  } from "dynamicformdjx/naiveUi"
  import {useDyForm, useReactiveForm, usePagination} from "dynamicformdjx";

  interface SongType {
    no: number | string
    title: string
    length: string
  }

  const zealData = ref<SongType[]>([
    {no: 3, title: 'Wonderwall', length: '4:18'},
    {no: 4, title: 'Don\'t Look Back in Anger', length: '4:48'},
    {no: 12, title: 'Champagne Supernova', length: '7:27'},
    ...Array.from({length: 50}).map((_, i) => ({no: i + 13, title: `test Data ${i + 1}`, length: `${i * i}`}))
  ])
  const message = useMessage()
  const referId = ref<string | number>('-1')
  const tableData = ref<SongType[]>([])
  const handleDynamicFormRef = ref<naiDynamicFormRef | null>(null)
  const naiZealTableSearchRef = ref<naiZealTableSearchRef | null>(null)
  const naiPopupModalRef = ref<naiPopupModalRef | null>(null)
  const tableLoading = ref<boolean>(false)
  const selectOpts = ref<(number | string)[]>([])
  // search form
  const searchFormItems = useDecorateForm([
    {
      key: "no",
      label: "No",
      renderType: 'renderInputNumber',
    },
    {
      key: "title",
      label: "Title",
    },
    {
      key: "length",
      label: "Length",
    },
  ].map(it => ({
    value: null,
    clearable: true,
    renderType: 'renderInput',
    span: 8,
    ...it,
  })) as any[])
  // table column
  const columns: DataTableColumns<SongType> = [
    {
      type: 'selection'
    },
    {
      title: 'No',
      key: 'no'
    },
    {
      title: 'Title',
      key: 'title'
    },
    {
      title: 'Length',
      key: 'length'
    },
    {
      title: 'Action',
      key: 'actions',
      fixed: 'right',
      render(row) {
        return h(
            NSpace, {}, [
              h(NButton,
                  {
                    size: 'small',
                    onClick: () => upItem(row)
                  },
                  {default: () => 'update'}),
              h(NButton,
                  {
                    size: 'small',
                    type: 'error',
                    onClick: () => delItem(row)
                  },
                  {default: () => 'delete'})
            ]
        )
      }
    }
  ]
  const pagination = usePagination(fetchData)
  const updateFormItems = useReactiveForm<SongType>([
    {
      key: "no",
      label: "No",
      clearable: true,
      value: null,
      render2: (f) => renderInputNumber(f.value, {}, f)
    },
    {
      key: "title",
      label: "Title",
      value: null,
      clearable: true,
      render2: (f) => renderInput(f.value, {}, f),
    },
    {
      key: "length",
      label: "Length",
      value: null,
      clearable: true,
      render2: (f) => renderInput(f.value, {}, f),
    },
  ])
  const useForm = useDyForm(updateFormItems)
  const doSearch = () => {
    fetchData()
    pagination.pageNo = 1
  }
  const doReset = () => {
    fetchData()
    pagination.pageNo = 1
  }

  function rowKey(row: SongType) {
    return row.no
  }

  // mock http request
  async function fetchData() {
    tableLoading.value = true
    const {pageNo, pageSize} = pagination
    const params = naiZealTableSearchRef.value?.getParams<SongType>?.()
    const r = await new Promise<{ data: SongType[], total: number }>((resolve, reject) => {
      setTimeout(() => {
        const start = (pageNo - 1) * pageSize
        const {length, no, title} = params!
        const data = zealData.value.filter(it => (!length || it.length.includes(length)) && (!title || it.title.includes(title)) && (!no || it.no === parseInt(no as string)))
        resolve({
          data: data.slice(start, start + pageSize),
          total: data.length
        })
      }, 1500)
    })
    tableData.value = r.data
    pagination.setTotalSize(r.total)
    tableLoading.value = false
  }

  const newItem = () => {
    referId.value = '-1'
    useForm.onReset()
    nextTick(() => {
      naiPopupModalRef.value?.toggle?.(true)
    })
  }

  function upItem(r: SongType) {
    referId.value = r.no
    useForm.setValues(r)
    nextTick(() => {
      naiPopupModalRef.value?.toggle?.(true)
    })
  }

  function delItem(r: SongType) {
    zealData.value = zealData.value.filter(it2 => it2.no !== r.no)
    message.success('delete successful')
    fetchData()
  }

  const deleteAll = () => {
    zealData.value = zealData.value.filter(it2 => !selectOpts.value.includes(it2.no))
    message.success('delete all successful')
    fetchData()
  }
  const onSubmit = async () => {
    handleDynamicFormRef.value?.validator().then((v: any) => {
      if (referId.value === '-1') {
        zealData.value.unshift({...v, key: Date.now()})
        message.success('Add successful')
      } else {
        zealData.value = zealData.value.map(it => {
          if (referId.value === it.no) return v as SongType
          return it
        })
        message.success('Update successful')
      }
      nextTick(() => {
        naiPopupModalRef.value?.toggle?.(false)
        fetchData()
      })
    })
  }
  const handleSelectionChange = (v: DataTableColumnKey[]) => {
    selectOpts.value = v
  }
  onMounted(() => {
    fetchData()
  })
</script>

<template>
  <NaiZealCard>
    <template #header="{isMobile}">
      <NaiZealTableSearch :isMobile="isMobile" :search-items="searchFormItems" ref="naiZealTableSearchRef"
                          :mobile-drawer="true"
                          title="zeal test" @onReset="doReset"
                          @onSearch="doSearch"/>
    </template>
    <template #controlBtn>
      <n-button type="success" size="small" @click="newItem">Add</n-button>&nbsp;
      <n-button type="error" size="small" @click="deleteAll" :disabled="!selectOpts.length">Del Selected</n-button>
    </template>
    <template #toolBtn>
      <n-button type="default" size="small" @click="()=>{}">
        Tool...
      </n-button>
    </template>
    <template #default="{tableHeight}">
      <n-data-table
          :row-key="rowKey"
          :loading="tableLoading"
          :columns="columns"
          :data="tableData"
          :bordered="false"
          :style="{ height: tableHeight+'px'}"
          :flex-height="true"
          :scroll-x="600"
          @update:checked-row-keys="handleSelectionChange"
      />
    </template>
    <template #footer="{isMobile}">
      <NaiZealTablePaginationControl :is-mobile="isMobile" :pagination="pagination">
        <template #prefix="{ itemCount }">
          Total {{ itemCount }}
        </template>
      </NaiZealTablePaginationControl>
    </template>
    <template #rest>
      <NaiPopupModal :title="referId==='-1'?'add Test':'update Test'" ref="naiPopupModalRef" :on-submit="onSubmit">
        <NaiDynamicForm :items="updateFormItems" ref="handleDynamicFormRef"/>
      </NaiPopupModal>
    </template>
  </NaiZealCard>
</template>

<style scoped>

</style>

与Element-plus配合


<script setup lang="ts">
  import {h, nextTick, onMounted, ref} from "vue";
  import {ElMessage, ElButton, ElSpace} from "element-plus";
  import {
    ElePopupModal,
    useDecorateForm,
    EleZealCard,
    EleDynamicForm,
    EleZealTableSearch,
    EleZealTablePaginationControl,
    EleZealTable,
    renderInput,
    renderInputNumber,
  } from "dynamicformdjx/elementPlus";
  import type {
    elePopupModalRef,
    eleDynamicFormRef,
    eleZealTableSearchRef
  } from "dynamicformdjx/elementPlus"
  import {useDyForm, useReactiveForm, usePagination} from "dynamicformdjx";
  import type {ZealColumn} from "dynamicformdjx/types/form";

  interface SongType {
    no: number | string
    title: string
    length: string
  }

  const zealData = ref<SongType[]>([
    {no: 3, title: 'Wonderwall', length: '4:18'},
    {no: 4, title: 'Don\'t Look Back in Anger', length: '4:48'},
    {no: 12, title: 'Champagne Supernova', length: '7:27'},
    ...Array.from({length: 50}).map((_, i) => ({no: i + 13, title: `test Data ${i + 1}`, length: `${i * i}`}))
  ])
  const referId = ref<string | number>('-1')
  const tableData = ref<SongType[]>([])
  const handleDynamicFormRef = ref<eleDynamicFormRef | null>(null)
  const naiZealTableSearchRef = ref<eleZealTableSearchRef | null>(null)
  const naiPopupModalRef = ref<elePopupModalRef | null>(null)
  const tableLoading = ref<boolean>(false)
  const selectOpts = ref<(number | string)[]>([])
  // search form
  const searchFormItems = useDecorateForm([
    {
      key: "no",
      label: "No",
      renderType: 'renderInputNumber',
    },
    {
      key: "title",
      label: "Title",
    },
    {
      key: "length",
      label: "Length",
    },
  ].map(it => ({
    value: null,
    clearable: true,
    renderType: 'renderInput',
    span: 8,
    ...it,
  })) as any[])
  // table column
  const columns: ZealColumn<SongType>[] = [
    {type: 'selection', width: 55},
    {type: 'expand', render2: row => h('div', {}, JSON.stringify(row, null, 2))},
    {label: "No", prop: "no", width: 80},
    {label: "Title", prop: "title", slot: "title"},
    {label: "Length", prop: "length"},
    {
      label: "Actions", fixed: 'right', width: 160, render2: (row) => h(
          ElSpace, {}, [
            h(ElButton,
                {
                  size: 'small',
                  onClick: () => upItem(row)
                },
                'update'),
            h(ElButton,
                {
                  size: 'small',
                  type: 'danger',
                  onClick: () => delItem(row)
                },
                'delete')
          ]
      )
    },
  ];

  const pagination = usePagination(fetchData)
  const updateFormItems = useReactiveForm<SongType>([
    {
      key: "no",
      label: "No",
      clearable: true,
      value: null,
      render2: (f) => renderInputNumber(f.value, {}, f)
    },
    {
      key: "title",
      label: "Title",
      value: null,
      clearable: true,
      render2: (f) => renderInput(f.value, {}, f),
    },
    {
      key: "length",
      label: "Length",
      value: null,
      clearable: true,
      render2: (f) => renderInput(f.value, {}, f),
    },
  ])
  const useForm = useDyForm(updateFormItems)
  const doSearch = () => {
    fetchData()
    pagination.pageNo = 1
  }
  const doReset = () => {
    fetchData()
    pagination.pageNo = 1
  }

  // mock http request
  async function fetchData() {
    tableLoading.value = true
    const {pageNo, pageSize} = pagination
    const params = naiZealTableSearchRef.value?.getParams<SongType>?.()
    const r = await new Promise<{ data: SongType[], total: number }>((resolve, reject) => {
      setTimeout(() => {
        const start = (pageNo - 1) * pageSize
        const {length, no, title} = params!
        const data = zealData.value.filter(it => (!length || it.length.includes(length)) && (!title || it.title.includes(title)) && (!no || it.no === parseInt(no as string)))
        resolve({
          data: data.slice(start, start + pageSize),
          total: data.length
        })
      }, 1500)
    })
    tableData.value = r.data
    pagination.setTotalSize(r.total)
    tableLoading.value = false
  }

  const newItem = () => {
    referId.value = '-1'
    useForm.onReset()
    nextTick(() => {
      naiPopupModalRef.value?.toggle?.(true)
    })
  }

  function upItem(r: SongType) {
    referId.value = r.no
    useForm.setValues(r)
    nextTick(() => {
      naiPopupModalRef.value?.toggle?.(true)
    })
  }

  function delItem(r: SongType) {
    zealData.value = zealData.value.filter(it2 => it2.no !== r.no)
    ElMessage.success('delete successful')
    fetchData()
  }

  const deleteAll = () => {
    zealData.value = zealData.value.filter(it2 => !selectOpts.value.includes(it2.no))
    ElMessage.success('delete all successful')
    fetchData()
  }
  const onSubmit = async () => {
    handleDynamicFormRef.value?.validator().then((v: any) => {
      if (referId.value === '-1') {
        zealData.value.unshift({...v, key: Date.now()})
        ElMessage.success('Add successful')
      } else {
        zealData.value = zealData.value.map(it => {
          if (referId.value === it.no) return v as SongType
          return it
        })
        ElMessage.success('Update successful')
      }
      nextTick(() => {
        naiPopupModalRef.value?.toggle?.(false)
        fetchData()
      })
    })
  }
  const handleSelectionChange = (v: SongType[]) => {
    selectOpts.value = v.map(it => it.no)
  }
  onMounted(() => {
    fetchData()
  })
</script>

<template>
  <EleZealCard>
    <template #header="{isMobile}">
      <EleZealTableSearch :is-mobile="isMobile" :search-items="searchFormItems" ref="naiZealTableSearchRef"
                          :mobile-drawer="true"
                          title="zeal test" @onReset="doReset"
                          @onSearch="doSearch">
        <template #drawerBtn="{openDrawer}">
          <el-button @click="openDrawer">+</el-button>
        </template>
      </EleZealTableSearch>
    </template>
    <template #controlBtn>
      <el-button type="success" size="small" @click="newItem">Add</el-button>
      <el-button type="danger" size="small" @click="deleteAll" :disabled="!selectOpts.length">Del Selected</el-button>
    </template>
    <template #toolBtn>
      <el-button type="default" size="small" @click="()=>{}">
        Tool...
      </el-button>
    </template>
    <template #default="{tableHeight}">
      <EleZealTable :data="tableData" :columns="columns" :max-height="tableHeight" :loading="tableLoading"
                    @selection-change="handleSelectionChange">
        <template #title="{ row }">
          <el-tag>{{ row.title }}</el-tag>
        </template>
        <template #empty>
          <p> no data</p>
        </template>
      </EleZealTable>
    </template>
    <template #footer="{isMobile}">
      <EleZealTablePaginationControl :is-mobile="isMobile" :pagination="pagination"/>
    </template>
    <template #rest>
      <ElePopupModal :title="referId==='-1'?'add Test':'update Test'" ref="naiPopupModalRef" :on-submit="onSubmit">
        <EleDynamicForm :items="updateFormItems" ref="handleDynamicFormRef"/>
      </ElePopupModal>
    </template>
  </EleZealCard>
</template>

<style scoped>

</style>

动态表单

与Naive ui配合

简单表单

还有自定义表单,装饰表单请参考文档


<script setup lang="ts">
  import {ref} from "vue";
  import {NButton} from "naive-ui";
  import {useDyForm, useReactiveForm} from "dynamicformdjx";
  import {type naiDynamicFormRef, NaiDynamicForm, renderInput, renderRadioGroup} from "dynamicformdjx/naiveUi";
  import type {PresetType} from "dynamicformdjx/types/index";

  type FormRow = {
    username: string
    password: string
    preset: PresetType
  }
  const naiDynamicFormRef = ref<naiDynamicFormRef | null>(null)
  const presetType = ref<PresetType>('fullRow')
  const formItems = useReactiveForm<FormRow>([
    {
      key: "username",
      label: "姓名",
      value: ref<string | null>(null),
      clearable: true,
      placeholder: '请输入姓名',
      required: true, // 是否必填 (简化rules规则)
      render2: f => renderInput(f.value, {}, f),
      span: 6
    },
    {
      key: "password",
      label: "密码",
      value: ref<string | null>(null),
      clearable: true,
      type: 'password',
      required: true,
      placeholder: '请输入密码',
      render2: f => renderInput(f.value, {showPasswordOn: 'click'}, f),
      span: 8,
      offset: 2,
      requiredHint: l => `${l} is not empty`
    },
    {
      key: "preset",
      label: "表格预设",
      value: ref<PresetType | null>(presetType.value),
      render2: f => renderRadioGroup(f.value, [
        {label: '整行', value: 'fullRow'},
        {label: '表格', value: 'grid'},
      ], {name: 'preset'}, f),
      onChange: (v) => {
        presetType.value = v
      }
    },
  ])
  const useForm = useDyForm<FormRow>(formItems)
  const getData = () => {
    // const res=useForm.getValues() // 或
    const res = naiDynamicFormRef.value?.getResult?.()
    console.log(res)
  }
  const resetData = () => {
    // useForm.onReset() // 或
    naiDynamicFormRef.value?.reset?.()
  }
  const setData = () => {
    // 隐藏username
    // useForm.setHidden(true, ['username'])
    // 设置username 为不可输入
    // useForm.setDisabled(true, ['username'])
    //  直接修改
    useForm.setValues({
      username: 'naive-ui',
      password: '520'
    })
  }
  const validatorData = () => {
    // 校验
    naiDynamicFormRef.value?.validator().then(data => {
      console.log(data)
    }).catch(err => {
      console.log(err)
    })
  }
</script>

<template>
  <NaiDynamicForm :items="formItems" ref="naiDynamicFormRef" :preset="presetType">
    <template #header>
      <h3>与Naive ui结合简单表单</h3>
    </template>
    <template #footer>
      <div class="control">
        <n-button @click="getData" type="success" size="small">get Data</n-button>
        <n-button @click="setData" type="warning" size="small">set Data</n-button>
        <n-button @click="validatorData" type="default" size="small">validate Data</n-button>
        <n-button @click="resetData" type="error" size="small">reset Data</n-button>
      </div>
    </template>
  </NaiDynamicForm>
</template>

<style scoped>
  h3 {
    text-align: center;
    margin: 0 0 10px 0;
  }

  .control {
    display: flex;
    gap: 5px;
  }
</style>

与Element-plus配合

(只是导入从"dynamicformdjx/elementPlus"中,类型方法与上方naive ui一致)

简单表单

<script setup lang="ts">
  import {ref} from "vue";
  import {ElButton} from "element-plus";
  import {useDyForm, useReactiveForm} from "dynamicformdjx";
  import {type eleDynamicFormRef, renderInput, renderRadioGroup, EleDynamicForm} from "dynamicformdjx/elementPlus";
  import type {PresetType} from "dynamicformdjx/types/index";

  type FormRow = {
    username: string
    password: string
    preset: PresetType
  }
  const eleDynamicFormRef = ref<eleDynamicFormRef | null>(null)
  const presetType = ref<PresetType>('fullRow')
  const formItems = useReactiveForm<FormRow>([
    {
      key: "username",
      label: "姓名",
      value: ref<string | null>(null),
      clearable: true,
      placeholder: '请输入姓名',
      required: true, // 是否必填 (简化rules规则)
      render2: f => renderInput(f.value, {}, f),
      span: 8
    },
    {
      key: "password",
      label: "密码",
      value: ref<string | null>(null),
      clearable: true,
      type: 'password',
      required: true,
      placeholder: '请输入密码',
      render2: f => renderInput(f.value, {showPassword: true}, f),
      span: 8,
      offset: 2,
      requiredHint: l => `${l} is not empty`
    },
    {
      key: "preset",
      label: "表格预设",
      value: ref<PresetType | null>(presetType.value),
      render2: f => renderRadioGroup(f.value, [
        {label: '整行', value: 'fullRow'},
        {label: '表格', value: 'grid'},
      ], {name: 'preset'}, f),
      onChange: (v) => {
        presetType.value = v
      }
    },
  ])
  const useForm = useDyForm<FormRow>(formItems)
  const getData = () => {
    const res = eleDynamicFormRef.value?.getResult?.()
    console.log(res)
  }
  const resetData = () => eleDynamicFormRef.value?.reset?.()
  const setData = () => useForm.setValues({
    username: 'element-plus',
    password: '520'
  })
  const validatorData = () => {
    // 校验
    eleDynamicFormRef.value?.validator().then(data => {
      console.log(data)
    }).catch(err => {
      console.log(err)
    })
  }
</script>

<template>
  <EleDynamicForm :items="formItems" ref="eleDynamicFormRef" :preset="presetType">
    <template #header>
      <h3>与Element plus结合简单表单</h3>
    </template>
    <template #footer>
      <div class="control">
        <el-button @click="getData" type="success" size="small">get Data</el-button>
        <el-button @click="setData" type="warning" size="small">set Data</el-button>
        <el-button @click="validatorData" type="default" size="small">validate Data</el-button>
        <el-button @click="resetData" type="danger" size="small">reset Data</el-button>
      </div>
    </template>
  </EleDynamicForm>
</template>

<style scoped>
  h3 {
    text-align: center;
    margin: 0 0 10px 0;
  }

  .control {
    display: flex;
    gap: 5px;
  }
</style>

动态录入

此录入无需组件库依赖

1.单组件


<script setup lang="ts">
  import {ref} from "vue";
  import {DynamicInput, type dynamicInputRef} from "dynamicformdjx";

  const test = ref<{ a: string, b: number, c: number[] }>({
    a: 'Hello world',
    b: 1314,
    c: [5, 2, 0]
  })
  const dyRef = ref<dynamicInputRef>()
  const setData = () => {
    dyRef.value?.onSet?.({test: "helloWorld"})
  }
</script>

<template>
  <p>Input</p>
  <DynamicInput v-model="test" is-controller ref="dyRef"/>
  <p>Result</p>
  <pre>{{ test }}</pre>
  <div>
    <button @click="setData">setData helloWorld</button>
  </div>
</template>

2.级联基本使用


<script setup lang="ts">
  import {ref} from "vue";
  import {dynamicCascadeInputRef, DynamicCascadeInput} from "dynamicformdjx";

  const dyCascadeRef = ref<dynamicCascadeInputRef | null>(null)
  const test2 = ref({
    a: {
      b: {
        c: {
          d: {
            e: "hello world"
          }
        }
      }
    },
    aa: [5, 2, 0],
    aaa: 1314
  })
  const setData = () => {
    dyCascadeRef.value?.onSet?.({a: 8888})
  }
</script>

<template>
  <p>Cascade dynamicInput</p>
  <dynamic-cascade-input v-model="test2" :depth="5" ref="dyCascadeRef" is-controller/>
  <pre>{{ test2 }}</pre>
  <p>Result</p>
  <button @click="setData">setData 8888</button>
</template>