分类
为方便开发而开发的组件

动态增减表单项组件

开发背景

在某次的开发中遇到这样一个需求,表单中有一个具体的属性,该属性对应一个数组对象。

qi ye wei xin jie tu 15939419538073 - 动态增减表单项组件
在ant-design-react中看到了比较好的实现。

但是,项目使用的框架是ant-design-vue。该框架的实现方法是定义一个keys,当keys为3时相当于数组的长度为3。提交时获取的值是firstName1,firstName2,firstName3, lastName1, lastName2,lastName3。这就相当不友好了,每次向后台传值都要进行一番转义。如果需求就这一个那是可以接受的。后来这样的需求变成了9个…。

代码

首先要解决的问题当然是与ant-design-vue的form表单进行联动,可以通过ant-design-vue的form文档。了解如何自定义表单控件。为了在下面解释这个组件的内部逻辑,先把代码贴出来。

<template>
  <div :class="[drag?'drag-wrap':'']">
    <div :class="['group-wrap', bordered? 'border-wrap':'']" v-for="key in keys" :key="key">
      <div class="item" v-if="param1&&!getUnVisible(1)">
        <slot name="param1" :index="key - 1" :innerValue="paramsList[key - 1][param1]" :paramName="param1">
          <a-input
            :disabled="disabled"
            :value="paramsList[key - 1][param1]"
            :placeholder="placeholder1"
            @change="handleChange(key - 1, $event, param1)"
          ></a-input>
        </slot>
      </div>
      <div class="item" v-if="param2&&!getUnVisible(2)">
        <slot name="param2" :index="key - 1" :innerValue="paramsList[key - 1][param2]" :paramName="param2">
          <a-input
            class="item"
            :disabled="disabled"
            :placeholder="placeholder2"
            @change="handleChange(key - 1, $event, param2)"
            :value="paramsList[key - 1][param2]"
          ></a-input>
        </slot>
      </div>
      <div class="item" v-if="param3&&!getUnVisible(3)">
        <slot name="param3" :index="key - 1" :innerValue="paramsList[key - 1][param3]" :paramName="param3">
          <a-input
            :disabled="disabled"
            class="item"
            :placeholder="placeholder3"
            @change="handleChange(key - 1, $event, param3)"
            :value="paramsList[key - 1][param3]"
          ></a-input>
        </slot>
      </div>
      <div class="item" v-if="param4&&!getUnVisible(4)">
        <slot name="param4" :index="key - 1" :innerValue="paramsList[key - 1][param4]" :paramName="param4">
          <a-input
            :disabled="disabled"
            class="item"
            :placeholder="placeholder4"
            @change="handleChange(key - 1, $event, param4)"
            :value="paramsList[key - 1][param4]"
          ></a-input>
        </slot>
      </div>
      <div class="item" v-if="param5&&!getUnVisible(5)">
        <slot name="param5" :index="key - 1" :innerValue="paramsList[key - 1][param5]" :paramName="param5">
          <a-input
            :disabled="disabled"
            class="item"
            :placeholder="placeholder5"
            @change="handleChange(key - 1, $event, param5)"
            :value="paramsList[key - 1][param5]"
          ></a-input>
        </slot>
      </div>
      <!--      <div v-if="param3" class="gap-line">&#45;&#45;</div>-->

      <div v-if="!aline" class="remove-item" @click="onRemoveItem(key - 1)"><icon-font type="icontubiao_shanchu"/></div>
      <div v-if="!aline&&drag" class="drag-thumb" draggable="true"><icon-font type="icontubiao_tuozhuai"/></div>
      <div class="extra-line" v-if="dpsparam"><slot name="dpsparam" :index="key-1" :innerValue="paramsList[key -1][dpsparam]" :paramName="dpsparam"></slot></div>
    </div>
    <a-button type="dashed" v-if="!aline" block @click="onCreateItem">
      <a-icon type="plus" /> <slot name="btnName">添加</slot>
    </a-button>
  </div>
</template>
<script>
let target;
export default {
  props: [
    'value',
    'only', // 是否只显示一行,且没有添加
    'drag', // 是否支持拖拽排序
    'param1',
    'param2',
    'param3',
    'param4',
    'param5',
    'dpsparam',
    'disabled',
    'bordered',
    'placeholder1',
    'placeholder2',
    'placeholder3',
    'placeholder4',
    'placeholder5',
    'UnVisibleArr', // 动态不显示[1,2,3,4,5]
  ],
  data() {
    const value = this.value || [];
    return {
      keys: value.length,
      paramsList: value || [],
      // 是否只显示一行
      aline: this.only !== undefined,
      // param1: "attributeName",
      // param2: "attributeValue"
    };
  },
  name: 'ParamsInput',
  computed: {
    getUnVisible() {
      return (i) => {
        if (this.UnVisibleArr) {
          return this.UnVisibleArr.includes(i);
        }
        return false;
      };
    },
  },
  watch: {
    value(val = []) {
      this.paramsList = val || [];
      this.keys = this.paramsList.length;
    },
    disabled(val = false) {
      if (Object.prototype.toString.call(val).includes('Boolean')) {
        this.disabled = val;
      } else {
        this.disabled = false;
      }
    },
  },
  mounted() {
    if (this.drag) {
      this.onDragMethod();
    }
  },
  methods: {
    onDragMethod() {
      const dropbox = [].slice.call(document.querySelectorAll('.drag-wrap'));
      dropbox.forEach((item) => {
        item.addEventListener('dragstart', (event) => {
          target = event.target.parentNode;
        });
        item.addEventListener('dragover', (event) => {
          event.preventDefault();
        });
        item.addEventListener('drop', (event) => {
          event.stopPropagation();
          event.preventDefault();
          let currentTarget;
          [].slice.call(event.currentTarget.querySelectorAll('.group-wrap')).forEach((res) => {
            const rectRange = res.getBoundingClientRect();
            if (event.y >= rectRange.y && event.y <= rectRange.y + rectRange.height) {
              currentTarget = res;
            }
          });
          if (currentTarget) {
            const rect = currentTarget.getBoundingClientRect();
            const y = rect.y + rect.height / 2;
            switch (target.compareDocumentPosition(currentTarget)) {
              case 2:
                if (y > event.y) {
                  currentTarget.before(target);
                } else {
                  currentTarget.after(target);
                }
                break;
              case 4:
                if (y > event.y) {
                  currentTarget.before(target);
                } else {
                  currentTarget.after(target);
                }
                break;
              default:
                break;
            }
          }
        });
      });
    },
    onCreateItem() {
      this.keys = this.keys + 1;
      const obj = {};
      this.paramsList.push(obj);
    },
    onRemoveItem(k) {
      const paramsList = [...this.paramsList];
      paramsList.splice(k, 1);
      this.triggerChange(paramsList);
      this.keys = this.keys - 1;
    },
    handleChange(key, event, paramName) {
      const paramsList = [...this.paramsList];
      paramsList[key][paramName] = event.target.value;
      this.triggerChange(paramsList);
    },
    // 外部使用
    handleValueChange(key, value, paramName) {
      const paramsList = [...this.paramsList];
      paramsList[key][paramName] = value;
      this.triggerChange(paramsList);
    },
    // 外部使用-列改变
    handleColumnValueChange(key, value, paramName, otherValue = undefined) {
      const paramsList = [...this.paramsList];
      paramsList.forEach((item) => {
        item[paramName] = otherValue;
      });
      paramsList[key][paramName] = value.target.checked;
      this.triggerChange(paramsList);
    },
    triggerChange(changedValue) {
      this.$emit('change', changedValue);
    },
  },
};
</script>
<style scoped>
.group-wrap {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
}
.item {
  flex: 1;
}
.item + .item {
  margin-left: 45px;
}
.gap-line {
  flex: none;
  width: 50px;
  display: flex;
  justify-content: center;
  align-items: center;
}
.item + .item::before {
  content: "--";
  position: absolute;
  width: 40px;
  font-size: 14px;
  margin-left: -28px;
}
.remove-item {
  flex: none;
  width: 35px;
  text-align: center;
  cursor: pointer;
}
.drag-thumb{
  flex: none;
  width: 35px;
  text-align: center;
  cursor: pointer;
}
  .extra-line{
    width: 100%;
  }
  .border-wrap{
    border-width: 1px;
    border-style: solid;
    border-radius: 5px;
    border-color: #e8e8e8;
    padding: 10px;
    margin: 0 0 16px;
  }
</style>

组件功能

参数说明类型默认值
btnName按钮名称,在组件内加入添加配送方式template‘添加’
dpsparam自定义组件内加入一个自定义组件template
unVisibleArr动态不显示的组件,用于被关联时某列不显示。Array[]
only显示一行内容,隐藏添加按钮Booleanfalse
param<index>index范围是1-5,例如param1=”attributeList”,若需自定义组件,则在组件内内容。index为当前行,innerValue为当前值,paramName为该列属性名。String
placeholder<index>index范围是1-5,例如placeholder1=”属性名”,定义了param若需要加上placeholder则添加,若param运用了自定义组件,则不需要。String
disabled是否不可输入,若定义了自定义组件,则不需要。Booleanfalse
bordered加入边框Booleanfalse
drag是否支持拖拽排序Booleanfalse
功能点1:作为一个form控件
  • props接收value参数,通过value知道数组列表的长度keys,数组列表的内容paramsList
  • 为input组件绑定值,我们可知该input为第几行的数据,并知道是哪个字段,因此可以通过paramsList[第几行][哪个字段]来绑定。
  • 给input设置change,每个input的change改变都会影响整体的值,因此我们会使用change函数来改变整体的值。input使用的change函数是handleChange,通过它改变整体的值,并且调用了triggerChange,通知包含该组件的form知道里面有值改变了。
功能点2:需要能够自定义子组件,例如选择等
  • 通过slot作用域插槽,将index(是第几行),paramName(该列的名称),innerValue(该列的值)传递到外部。
  • 外部使用时在该组件上定义ref例如ref=”list”,方便调用该组件内部的函数
  • 外部使用该组件可使用的插槽名为param1,param2,…,param5。
<template v-slot:param1={ index, innerValue, paramName}>
<a-select
  @change="refChange(index, $event, paramName)"
  placeholder="选择承运商"
  :value="form.getFieldValue('baseWhCarriers')[index][paramName]"
>
  <a-select-option v-for="(item ,index) in  autoDict.CARRIER" :value="item.id.toString()" :key="index">{{item.carrierName}}</a-select-option>
</a-select>
</template>
  • 通过ref使用改变组件内的值的函数,函数有两个可供调用handleValueChange(key,value,paramName),该函数接收当前在第几行key,变更的值value,该列名称paramName,用于改变某行某列的值。还有handleColumnValueChange(key,value,paramName,otherValue=undefined),该函数接收的值与上面的函数类似,多了一个otherValue,用于改变某行某列的值,并将该列的其他值都变为otherValue。
  • 绑定value

功能点3:动态增减表单组件递归

qi ye wei xin jie tu 15940035951799 - 动态增减表单项组件
<a-form-item
            :colon="false"
            label="配送方式"
            class="span2"
            v-bind="modalLayout"
          >
            <params-input
              ref="list"
              param1="methodName"
              param2="distMethod"
              dpsparam="baseCarrierDists"
              placeholder1="名称"
              placeholder2="承运商code-请输入编码"
              :bordered="true"
              v-decorator="['otherWays',{
                initialValue: [{}],
                rules: [{
                  required: true,
                  message: '请填写配送方式',
                }]
              }]"
            >
              <template v-slot:btnName>
                添加配送方式
              </template>
              <template v-slot:dpsparam="{ index, innerValue, paramName}">
                <params-input
                  :value="dockForm.getFieldValue('otherWays')[index][paramName]"
                  param1="paramCode"
                  param2="paramName"
                  param3="paramValue"
                  placeholder1="参数名"
                  placeholder2="参数code"
                  placeholder3="参数值"
                  @change="refChange(index, $event, paramName)"
                >
                  <template v-slot:btnName>
                    添加参数
                  </template>
                </params-input>
              </template>
            </params-input>
          </a-form-item>
  • 在组件中调动该组件,使用的插槽是dpsParam,实现逻辑与自定义子组件并无多大区别,只是对dpsParam插槽的位置做了ui处理。
  • 加上:bordered=”true”用于区分内容块
  • 使用插槽btnName修改添加一行的名字

如何使用npm引入

https://github.com/zhuishao/ec-zs-components中的README有引入说明

使用文档

文档