Frontend Development 11 min read

Design and Implementation of a Configurable Dynamic Form in Vue

This article walks through the design and implementation of a configurable dynamic form in Vue, covering user‑side API design, type definitions, enhanced next‑function logic, recursive component rendering, and a complete example with code snippets to illustrate building and using the form component.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Design and Implementation of a Configurable Dynamic Form in Vue

This article was written at the inception of the "Dynamic Form" project and provides a step‑by‑step guide to building a B‑side linked form component, reproducing the author's design thinking and implementation techniques for a configurable form tool.

By following the tutorial, readers will obtain an elegant linked‑form solution, insights into the design of configurable utilities, and practical experience with recursive components.

The final goal is to achieve a declarative configuration that renders a dynamic form, as illustrated by the accompanying GIF.

From the user’s perspective, the first step is to define how users will consume the API. A function (e.g., Fn ) receives user options and returns a form‑item configuration object. The typical usage looks like Fn(some options) , where the function might be named createFormItem . The configuration needs several properties:

A type field indicating the component to render, such as "input", "select" or "checkbox".

A payload object containing fields like value and options that map to the target component’s value and option .

A next function that decides which form item appears next based on the current item and its ancestors ( current and acients ), enabling logic where the next item depends on the state of all previous items.

The type definition for createFormItem is shown below:

import { reactive } from "vue";

export type TFormItemType = 'input' | 'select' | 'checkbox'

export interface IFormItem  {
  type: TFormItemType;
  payload: any;
  next: (current: IFormItem, acients: IFormItem[]) => IFormItem | null
  parent: IFormItem | null; // added to facilitate constructing the acients array later
}

export function createFormItem(
  type: IFormItem['type'],
  payload: IFormItem['payload'],
  next: IFormItem['next']
): IFormItem {
  // ...
  // return like { type, payload, next, parent };
}

An example of creating a form item with conditional logic:

const formItem1 = createFormItem(
  'input',
  {
    label: '值为show-select则展示下拉框',
    value: 'show-select'
  },
  (current, acients) => {
    // When the current form's value is 'show-select', render formItem2 next
    if (current.payload.value === 'show-select') {
      return formItem2
    }
    return null;
  }
)

Before runtime, the raw configuration must be "enhanced". The next function is wrapped so that when it returns the next item, the returned item receives a parent reference to the current item, allowing the runtime to construct the acients array.

export function createFormItem(
  type: IFormItem['type'],
  payload: IFormItem['payload'],
  next: IFormItem['next']
): IFormItem {
  // Enhance the next method; core logic still calls next and returns its result
  const nextFunc: IFormItem['next'] = (current, acients) => {
    const nextItem = next(current, acients);
    // Enhancement: set parent of the next item to the current item
    if (!nextItem) {
      return null;
    }
    nextItem.parent = current;
    return nextItem;
  }
  // Return a reactive object so state changes trigger view updates
  return reactive({
    type,
    payload,
    next: nextFunc,
    parent: null,
  })
}

The runtime component ( FormItem.vue ) receives a form‑state prop, conditionally renders the appropriate Element‑Plus component based on type , and recursively renders the next form item using a getNext helper that builds the acients array from the linked parent references.

<template>
  <template v-if="props.formState">
    <el-form-item :label="props.formState.payload.label">
<el-input v-if="props.formState.type==='input'" v-model="props.formState.payload.value" />
      <el-select v-if="props.formState.type==='select'" v-model="props.formState.payload.value">
        <el-option
          v-for="item in props.formState.payload.options"
          :key="item.value"
          :label="item.label"
          :value="item.value"
        />
      </el-select>
      <el-checkbox-group v-if="props.formState.type==='checkbox'" v-model="props.formState.payload.value">
        <el-checkbox
          v-for="item in props.formState.payload.options"
          :key="item.value"
          :label="item.label"
          :value="item.value"
        />
      </el-checkbox-group>
    </el-form-item>
<form-item :form-state="getNext()"></form-item>
  </template>
</template>

<script setup lang="ts">
import { IFormItem } from './FormItem';
import { ElFormItem, ElSelect, ElInput, ElCheckboxGroup, ElCheckbox } from 'element-plus';
const props = defineProps<{ formState: IFormItem | null }>();

// getNext is called at runtime; it provides current and ancestors
const getNext = () => {
  const current = props.formState;
  if (!current) return null;
  // Build ancestors array
  let ptr = current;
  const acients: IFormItem[] = [];
  while (ptr && ptr.parent) {
    ptr = ptr.parent;
    acients.unshift(ptr);
  }
  return props.formState?.next(current, acients);
}
</script>

Finally, App.vue demonstrates how to mount the dynamic form by importing the generated configuration object ( dynamicFormItem ) and the FormItem component inside an el-form container.

<script setup lang="ts">
import dynamicFormItem from './components/dynamic-form/DynamicForm';
import FormItem from './components/dynamic-form/FormItem.vue';
import { ElForm } from 'element-plus';
</script>

<template>
  <el-form>
    <form-item :form-state="dynamicFormItem"></form-item>
  </el-form>
</template>

<style scoped>
</style>

The source code is available at https://gitee.com/jin-rongda/dynamic-forms . The version shown corresponds to the first commit, which is the most minimal implementation but has known limitations such as inconvenient ancestor access; later versions address these issues.

typescriptFrontend DevelopmentVueComponent DesignRecursive ComponentDynamic Form
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.