log
Swift Code Chronicles

[Vue3] Dynamic Form Design: Implementation and Optimization with Linked List Structure

Published on January 29, 2025
Updated on January 30, 2025
45 min read
Vue

In complex business scenarios, form items are often not static but need to dynamically show or hide based on conditions. For example, in a form, OtherFavorite needs to be displayed only when the value of Favorite includes “Other.” A common approach is to control visibility using if conditions, but as the number of dynamic form items increases, code complexity grows significantly, making it harder to maintain.

This article introduces a more flexible design using a linked list structure to address the challenges of dynamic form rendering and validation.


Problem Background

Below is an example of a simple form requirement:

Order Field Type Value Display Condition
1 Name text string Always visible
2 Address text string Always visible
3 Favorite checkbox [“Apple”, “Banana”, “Cherry”, “Other”] Always visible
4 OtherFavorite text string Displayed when Favorite includes “Other”
5 Comment textarea string Always visible

A typical implementation might look like this, using v-if to dynamically display OtherFavorite and watch to adjust validation rules dynamically:

<template>
  <el-form
    ref="ruleFormRef"
    style="max-width: 600px"
    :model="ruleForm"
    :rules="rules"
    label-width="auto"
    :validate-on-rule-change="false"
    status-icon
  >
    <el-form-item label="Name" prop="name">
      <el-input v-model="ruleForm.name" />
    </el-form-item>
    <el-form-item label="Address" prop="address">
      <el-input v-model="ruleForm.address" />
    </el-form-item>
    <el-form-item label="Favorite" prop="favorite">
      <el-checkbox-group v-model="ruleForm.favorite">
        <el-checkbox label="Apple" value="Apple" />
        <el-checkbox label="Banana" value="Banana" />
        <el-checkbox label="Cherry" value="Cherry" />
        <el-checkbox label="Other" value="Other" />
      </el-checkbox-group>
    </el-form-item>
    <!-- Dynamically show OtherFavorite based on Favorite's value -->
    <el-form-item v-if="ruleForm.favorite.includes('Other')" label="OtherFavorite" prop="other">
      <el-input v-model="ruleForm.other" />
    </el-form-item>
    <el-form-item label="Comment" prop="comment">
      <el-input v-model="ruleForm.comment" />
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="submitForm(ruleFormRef)"> Create </el-button>
      <el-button @click="resetForm(ruleFormRef)">Reset</el-button>
    </el-form-item>
  </el-form>
</template>

<script lang="ts" setup>
import { ref, watch } from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import type { FormInputs } from '@/models/form.model';

const ruleFormRef = ref<FormInstance>();

const ruleForm = ref<FormInputs>({
  name: '',
  address: '',
  favorite: [],
  other: '',
  comment: '',
});

const rules = ref<FormRules<FormInputs>>({
  name: [{ required: true, message: 'Please input name', trigger: 'blur' }],
  address: [{ required: true, message: 'Please input address', trigger: 'blur' }],
  favorite: [{ required: true, message: 'Please select favorite', trigger: 'change' }],
  comment: [{ required: true, message: 'Please input comment', trigger: 'blur' }],
});

// Watch Favorite's value to dynamically set validation rules for Other
watch(
  () => ruleForm.value.favorite,
  (value) => {
    if (value.includes('Other')) {
      rules.value.other = [
        { required: true, message: 'Please input other favorite', trigger: 'blur' },
      ];
    } else {
      rules.value.other = [];
    }
  },
);

const submitForm = async (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  await formEl.validate((valid, fields) => {
    if (valid) {
      console.log('submit!');
    } else {
      console.log('error submit!', fields);
    }
  });
};

const resetForm = (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  formEl.resetFields();
};
</script>

While this method works for simple cases, as the number of dynamic fields and conditions increases, the code becomes harder to maintain.


Solution: Linked List Structure

We can abstract each form item as a linked list node, where each node contains the following information:

  • Basic form item details (e.g., id, label, type, etc.).
  • Logic for dynamic visibility conditions.
  • Pointer to the next node(s).

Data Structure Design

Define metadata types for form items:

import type { FormItemRule } from 'element-plus';

/**
 * Form input types
 */
export type FormInputs = {
  name: string;
  address: string;
  favorite: string[];
  other?: string;
  comment: string;
};

/**
 * Metadata for form items
 */
export type FormItemMeta = {
  /** Form item ID, must match a key from FormInputs */
  id: keyof FormInputs;
  /** Form item label */
  label: string;
  /** Form item type */
  type: 'text' | 'checkbox' | 'textarea';
  /** Validation rules for the form item */
  rules: Array<FormItemRule> | undefined;
  /** Options for checkboxes */
  options?: Array<{ label: string; value: string }>;
  /** Next form item(s) */
  next?: FormItemMeta | Array<{ condition: string; item: FormItemMeta } | FormItemMeta>;
};

Using the structure above, we can organize form items as a linked list:

export const formItemMeta: FormItemMeta = {
  id: 'name',
  label: 'Name',
  type: 'text',
  rules: [{ required: true, message: 'Please input name', trigger: 'blur' }],
  // The next form item after "name" is "address"
  next: {
    id: 'address',
    label: 'Address',
    type: 'text',
    rules: [{ required: true, message: 'Please input address', trigger: 'blur' }],
    // The next form item after "address" is "favorite"
    next: {
      id: 'favorite',
      label: 'Favorite',
      type: 'checkbox',
      rules: [{ required: true, message: 'Please select favorite', trigger: 'change' }],
      options: [
        { label: 'Apple', value: 'apple' },
        { label: 'Banana', value: 'banana' },
        { label: 'Cherry', value: 'cherry' },
        { label: 'Other', value: 'other' },
      ],
      // The next form items after "favorite" are "other" and "comment"
      next: [
        // "OtherFavorite" is displayed only when Favorite includes "Other"
        {
          condition: 'other',
          item: {
            id: 'other',
            label: 'Other',
            type: 'text',
            rules: [{ required: true, message: 'Please input other', trigger: 'blur' }],
          },
        },
        // "Comment" is always displayed
        {
          id: 'comment',
          label: 'Comment',
          type: 'textarea',
          rules: [{ required: true, message: 'Please input comment', trigger: 'blur' }],
        },
      ],
    },
  },
};

Rendering Logic: Recursive Implementation

Define a recursive component FormItemRenderer.vue to dynamically render form items based on metadata.

<template>
  <el-form
    ref="ruleFormRef"
    style="max-width: 600px"
    :model="form"
    :rules="rules"
    label-width="auto"
    :validate-on-rule-change="false"
    status-icon
  >
    <FormItemRenderer
      :meta="formItemMeta"
      :values="form"
      v-on:value-changed="onValueChanged"
      v-on:rule-changed="onRuleChanged"
    />
    <el-form-item>
      <el-button type="primary" @click="submitForm(ruleFormRef)"> Create </el-button>
      <el-button @click="resetForm(ruleFormRef)">Reset</el-button>
    </el-form-item>
  </el-form>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import type { FormInstance, FormItemRule, FormRules } from 'element-plus';
import type { FormInputs } from '@/models/form.model';
import FormItemRenderer from './FormItemRenderer.vue';
import { formItemMeta } from '@/models/form.model';

const ruleFormRef = ref<FormInstance>();

const form = ref<FormInputs>({
  name: '',
  address: '',
  favorite: [],
  other: '',
  comment: '',
});

const rules = ref<FormRules<FormInputs>>({});

const onValueChanged = (id: string, value: string | string[] | undefined) => {
  form.value = {
    ...form.value,
    [id]: value,
  };
};

const onRuleChanged = (id: string, itemRules: Array<FormItemRule> | undefined) => {
  rules.value = {
    ...rules.value,
    [id]: itemRules,
  };
};

const submitForm = async (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  await formEl.validate((valid, fields) => {
    if (valid) {
      console.log('submit!');
    } else {
      console.log('error submit!', fields);
    }
  });
};

const resetForm = (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  formEl.resetFields();
};
</script>

Summary

Using a linked list structure for dynamic forms simplifies code logic and improves scalability and maintainability. Complex visibility and validation rules can be easily defined and updated through metadata.

About

A personal blog sharing technical insights, experiences and thoughts

Quick Links

Contact

  • Email: hushukang_blog@proton.me
  • GitHub

© 2025 Swift Code Chronicles. All rights reserved