ant+vue:表单动态增删与赋值

565 阅读1分钟

动态表单的原理就是,需要每个表单都需要唯一的标志。不可直接使用index作为唯一标志。因为在增删表单的时候,index重新赋值,会造成错乱。

效果预览

示例1 示例1.gif

示例2

示例2.gif

1 方法一

每个表单项的name值中使用 【guid唯一标志 + 表单name】结合,进行增删,好处就是代码量少,赋值删除与校验都方便。不好的地方就是,它不像组件式表单一样,方便管理。所有的逻辑都混合在一个页面。而且拿表单值的时候,拿到的key不是单纯的name形式,而是name + guid形式。


<template>
<div>
  <a-form
    :form="form"
    :label-col="formItemLayout.labelCol"
    :wrapper-col="formItemLayout.wrapperCol"
    @submit="handleSubmit"
  >
    <a-row v-for="(item, index) in data" :key="item.guid">
      <a-col :span="9">
        <a-form-item label="名称">
          <a-input
            v-decorator="[`name_${item.guid}`, { rules: [{ required: true, message: '名称不能为空'}] }]"
            @change="(e)=> {item.name = e.target.value}"
          />
          </a-form-item>
      </a-col>
      <a-col :span="9">
        <a-form-item label="年龄">
          <a-input
            v-decorator="[`age_${item.guid}`, {initialValue: 'initialValue默认值'}, { rules: [{ required: true, message: '年龄不能为空'}] }]"
            @change="(e)=> item.age = e.target.value"
          />
          </a-form-item>
      </a-col>

      <!-- 以下为操作按钮 -->
      <a-col :span="2">
        <a-form-item >
          <a-button @click="setChildFormData(item)">默认赋值</a-button>
        </a-form-item>
      </a-col>
      <a-col :span="2">
        <a-form-item >
          <a-icon type="plus" title="添加" style="font-size: 20px" @click="add(index)"/>
        </a-form-item>
      </a-col>
      <a-col :span="2">
        <a-form-item>
          <a-icon v-if="index >= 1" type="minus" title="删除" style="font-size: 20px" @click="del(index)"/>
        </a-form-item>
      </a-col>
    </a-row>

    <a-form-item :wrapper-col="{ span: 12, offset: 5 }">
      <a-button type="primary" html-type="submit">
        Submit
      </a-button>
    </a-form-item>
  </a-form>
</div>
</template>

<script>
export default {
  name: 'formAddDel',
  data () {
    return {
      formLayout: 'horizontal',
      form: this.$form.createForm(this),
      data: [] // 表单数据
    }
  },
  created () {
    this.add()
  },
  computed: {
    formItemLayout () {
      const { formLayout } = this
      return formLayout === 'horizontal'
        ? {
          labelCol: { span: 4 },
          wrapperCol: { span: 20 }
        }
        : {}
    }
  },
  methods: {
    /**
     * 表单新增条目
     * @method add
     */
    add (index = 0) {
      this.data.splice(index + 1, 0, {
        guid: this.guid(),
        name: undefined,
        age: undefined
      })
    },

    /**
     * 表单删除条目
     * @method add
     */
    del (index) {
      this.data.splice(index, 1)
    },
    /**
     * 设置表单值
     * @method setChildFormData
     */
    setChildFormData (item) {
      this.form.setFieldsValue({
        [`age_${item.guid}`]: '设置的age~~',
        [`name_${item.guid}`]: '设置的name~~'
      })
    },

    /**
     * 唯一值guid计算
     * @method guid
     */
    guid () {
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
        var r = Math.random() * 16 || 0
        let v = c === 'x' ? r : (r & 0x3 || 0x8)
        return v.toString(16)
      })
    },
    /**
     * 表单submit
     * @method handleSubmit
     */
    handleSubmit (e) {
      e.preventDefault()
      this.form.validateFields((err) => {
        console.log('示例1', this.data) // 打印值
        if (!err) {
          // 执行操作
        }
      })
    }
  }
}
</script>

2方法2

使用组件形式引用,结合guid + index,好处就是便于管理,不好的地方就是,拿值与赋值和表单校验都要稍微麻烦一点。但是好处就是,便于管理与复用。子表单项的逻辑与父表单的逻辑分开,拿取的子表单的值都是单纯的name。复杂逻辑的表单更加适用于这个方法。

2.1

子组件 formAddDel2_Child.vue

<template>
<div>
  <a-form
    :form="form"
    :label-col="formItemLayout.labelCol"
    :wrapper-col="formItemLayout.wrapperCol"
  >
    <a-row>
      <a-col :span="12">
        <a-form-item label="名称">
          <a-input
            v-decorator="[`name`, { rules: [{ required: true, message: '名称不能为空'}] }]"
            @change="(e)=> {data.name = e.target.value}"
          />
          </a-form-item>
      </a-col>
      <a-col :span="12">
        <a-form-item label="年龄">
          <a-input
            v-decorator="[`age`, { rules: [{ required: true, message: '年龄不能为空'}] }]"
            @change="(e)=> data.age = e.target.value"
          />
          </a-form-item>
      </a-col>
    </a-row>
  </a-form>
</div>
</template>

<script>
export default {
  name: 'formAddDel',
  props: {
    data: {
      type: Object,
      default: () => {}
    },
    index: {
      type: [Number, String],
      default: () => '1'
    }
  },
  data () {
    return {
      formLayout: 'horizontal',
      form: this.$form.createForm(this)
    }
  },
  watch: {
    // // 监听数据并直接赋值
    // data (newV, oldV) {
    //   this.form.setFieldsValue({
    //     ...newV
    //   })
    // }
  },
  computed: {
    formItemLayout () {
      const { formLayout } = this
      return formLayout === 'horizontal'
        ? {
          labelCol: { span: 4 },
          wrapperCol: { span: 20 }
        }
        : {}
    }
  },
  methods: {
    /**
     * 唯一值guid计算
     * @method guid
     */
    guid () {
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
        var r = Math.random() * 16 || 0
        let v = c === 'x' ? r : (r & 0x3 || 0x8)
        return v.toString(16)
      })
    },

    /**
     * 提供给父组件校验表单
     * @method validate
     * @returns {Boolean}
     */
    validate (e) {
      this.form.validateFields((err, values) => {
        if (err) {
          return false
        } else {
          return true
        }
      })
    },

    /**
     * 提供给父组件拿取表单值
     * @method getData
     */
    getData () {
      return this.form.getFieldsValue()
      // 如果你需要基于这个index来标记哪一个数据
      // const result = {}
      // const formData = this.form.getFieldsValue() || []
      // Object.keys(formData).forEach(item => {
      //   result[`${item}${this.index}`] = formData[item]
      // })
      // return result
      // return this.form.getFieldsValue()
    },

    /**
     * 提供给父组件设置表单值
     * @method setFormData
     */
    setFormData (data) {
      if (data && typeof data === 'object' && data instanceof Object) {
        this.form.setFieldsValue(data)

        // 如果你想更谨慎些,你可以这样
        // const fields = Object.keys(this.form.getFieldsValue())
        // const formData = {}
        // Object.keys(data).forEach(it => {
        //   if (fields.includes(it)) {
        //     formData[it] = data[it]
        //   }
        // })
        // this.form.setFieldsValue(formData)
      }
    }
  }
}
</script>

父组件

<template>
<div>
  <a-form
    :form="form"
    :label-col="formItemLayout.labelCol"
    :wrapper-col="formItemLayout.wrapperCol"
    @submit="handleSubmit"
  >
    <a-row v-for="(item, index) in data" :key="item.guid">
      <a-col :span="18">
        <formAddDelChild :data="item" :ref="item.guid" :index="index"></formAddDelChild>
      </a-col>
      <a-col :span="2">
        <a-form-item >
          <a-button @click="setChildFormData(item)">默认赋值</a-button>
        </a-form-item>
      </a-col>
      <a-col :span="2">
        <a-form-item >
          <a-icon type="plus" title="添加" style="font-size: 20px" @click="add(index)"/>
        </a-form-item>
      </a-col>
      <a-col :span="2">
        <a-form-item>
          <a-icon v-if="index >= 1" type="minus" title="删除" style="font-size: 20px" @click="del(index)"/>
        </a-form-item>
      </a-col>
    </a-row>
    <a-form-item :wrapper-col="{ span: 12, offset: 5 }">
      <a-button type="primary" html-type="submit">
        Submit
      </a-button>
    </a-form-item>
  </a-form>
</div>
</template>

<script>
import formAddDelChild from './formAddDel2_Child'
export default {
  name: 'formAddDel',
  components: {formAddDelChild},
  data () {
    return {
      formLayout: 'horizontal',
      form: this.$form.createForm(this),
      data: []
    }
  },
  created () {
    this.add()
  },
  computed: {
    formItemLayout () {
      const { formLayout } = this
      return formLayout === 'horizontal'
        ? {
          labelCol: { span: 4 },
          wrapperCol: { span: 20 }
        }
        : {}
    }
  },
  methods: {
    /**
     * 表单新增条目
     * @method add
     */
    add (index = 0) {
      this.data.splice(index + 1, 0, {
        guid: this.guid(),
        name: undefined,
        age: undefined
      })
    },
    /**
     * 表单删除条目
     * @method add
     */
    del (index) {
      this.data.splice(index, 1)
    },
    /**
     * 唯一值guid计算
     * @method guid
     */
    guid () {
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
        var r = Math.random() * 16 || 0
        let v = c === 'x' ? r : (r & 0x3 || 0x8)
        return v.toString(16)
      })
    },
    /**
     * 设置组件的表单值
     * @method setChildFormData
     */
    setChildFormData (item) {
      // this?.$refs?.[item.guid]?.[0]?.setFormData 这种写法我的破浏览器不支持,哎~
      if (this.$refs[item.guid] && this.$refs[item.guid][0] && this.$refs[item.guid][0].setFormData) {
        const forData = {
          name: '默认名称999',
          age: '默认age~~~'
        }
        this.$refs[item.guid][0].setFormData(forData)
      }
      // 你也可以直接拿取子组件的form进行赋值操作,但是不建议,如下
      // if (this.$refs[item.guid] && this.$refs[item.guid][0] && this.$refs[item.guid][0].form) {
      //   const forData = {
      //     name: '默认名称999',
      //     age: '默认age~~~'
      //   }
      //   this.$refs[item.guid][0].form.setFieldsValues(forData)
      // }
    },

    /**
     * 表单submit
     * @method handleSubmit
     *
     */
    handleSubmit (e) {
      e.preventDefault()
      let flag = false

      // 通过子组件提供的getData,可以直接循环表单项拿值
      const result = []
      this.data.forEach(item => {
        if (item.guid && this.$refs[item.guid] && this.$refs[item.guid][0]) {
          if (this.$refs[item.guid][0].validate()) {
            flag = true
          }
          result.push(this.$refs[item.guid][0].getData())
        }
      })

      // 校验父组件的表单
      this.form.validateFields((err, values) => {
        if (!err && !flag) {
          flag = true
        }
      })

      console.log('示例2', result)
      if (flag) {
        // 执行操作
      }
    }
  }
}
</script>