【问题标题】:Vue: can I bind v-model name from slot to an array in child element?Vue:我可以将插槽中的 v-model 名称绑定到子元素中的数组吗?
【发布时间】:2021-10-02 22:18:14
【问题描述】:

今天我被要求使用插槽构建一个动态表单组件。表单组件是包含一个动态的输入列表,用户可以在包含 v-model 属性的自己的输入字段中放入。填写完最后一行后,应在其下方出现一个新的空白行。我从这个开始:

Parent.vue(注意:无关代码跳过):

<template>
  <div>
    <FormComponent><input type="text" v-model="something"/></FormComponent>
  <div>
</template>

<script>
import FormComponent from '../components/FormComponent.vue';

export default {
  components: { FormComponent },
  data() {
    return {
      something: []  // could be a list, or a string, number, Object, etc...
    };
  }
}
</script>

FormComponent.vue:

<template>
  <div>
    <div v-for="(item, index) in items" :key="index">
      {{index+1}}<slot v-bind="items"></slot>
    </div>
  </div>
</template>

<script>
export default {
  name: "FormComponent",
  data() {
    return {
      items: []
    }
  },
  props: {
    value: String,
  },
  created() {
    console.log("create newform");
  },
  watch: {
    items(oldValue, newValue) {
      if (oldValue[oldValue.length] == "" && newValue[newValue.length] != "") {
        newValue.push("");
      }
    }
  },

现在我想将 parent 中定义的 v-model 绑定到一个数组以自动生成新行,但似乎我无法从插槽中提取 v-model 值。如何将输入中的 v-model 定义的值链接到数组?

另外,我怎样才能允许动态更新列表?

【问题讨论】:

    标签: javascript vue.js vuejs2


    【解决方案1】:

    插槽的整个想法是通过父组件管理它们 - 而不是通过子组件。所以v-for 应该在父组件内部而不是FormComponent。并且父级还负责在v-models 的数组中添加一个新的空字段。 您可能有一个 FormComponent 并在其中放入几个 FormField 组件,每个 FormField 可能有不同类型的 INPUT。

    FormComponent.vue

    <template>
      <form v-bind="$attrs" v-on="listeners">
        <slot/>
      </form>
    </template>
    
    <script>
    export default
    {
      name: 'FormValidator',
      data ()
      {
        return {
          dirty: false,
        }
      },
      computed:
        {
          listeners ()
          {
            return {
              ...this.$listeners,
              submit: (evt) =>
              {
                evt.preventDefault();
                this.fields().forEach(item =>
                {
                  item.setDirty(true);
                });
                this.$emit('submit', evt);
              }
            };
          }
        },
      methods:
        {
          fields ()
          {
            // we can not use computed property because $children is not reactive
            const child = this.$children.slice();
            const fields = [];
            let cur;
            while (child.length > 0)
            {
              cur = child.shift();
              if (cur.$options.name === 'FormField') fields.push(cur);
              else
              {
                cur = cur.$children.slice();
                for (let i = 0; i < cur.length; i++) child.push(cur[i]);
              }
            }
            return fields;
          },
          valid ()
          {
            const fields = this.fields();
            return !fields.some(item => item.$options.propsData.error && item.dirty);
          },
          reset ()
          {
            this.fields().forEach(item => item.setDirty(false));
          }
        }
    }
    </script>
    

    FormField.vue

    <template>
      <div class="form_field">
        <label v-if="label">{{ label }}</label>
        <div class="form_group" @input="setDirty" @change="setDirty">
          <div v-if="$slots.prepend" class="form_input_prepend">
            <slot name="prepend"/>
          </div>
          <slot/>
          <div v-if="$slots.append" class="form_input_append">
            <slot name="append"/>
          </div>
        </div>
        <div v-if="error && dirty" class="field_error">{{ error }}</div>
      </div>
    </template>
    
    <script>
    export default
    {
      name: 'FormField',
      props:
        {
          label:
            {
              type: String,
              default: ''
            },
          error:
            {
              type: String,
              default: null
            },
        },
      data ()
      {
        return {
          dirty: false,
        }
      },
      computed:
        {
          valid ()
          {
            return !this.error;
          }
        },
      methods:
        {
          setDirty (value)
          {
            this.dirty = value;
          }
        }
    }
    </script>
    
    <style lang="scss">
      $field_radius: $corner-2;
    
      .form_field
      {
        display: flex;
        flex-direction: column;
      }
    
      .form_field + .form_field
      {
        margin-top: 8px;
      }
    
      .form_field > label
      {
        padding-bottom: 4px;
      }
    
      .form_group
      {
        border: 1px solid $dialog_border;
        border-radius: $field_radius;
        display: flex;
        align-items: center;
        background-color: $bg_color;
        flex-grow: 1;
      }
    
      .form_group:focus-within
      {
        border-color: $input_border;
      }
    
      .form_group input,
      .form_group select,
      .form_group textarea
      {
        border: none;
        flex: 1 1 auto;
        padding: 0 6px;
        margin: 0;
        min-height: calc(1.5rem + 8px);
        min-width: 48px;
        font-size: 1rem;
        line-height: 1.5;
        border-radius: $field_radius; /* probably should be conditional - based on the absence of prepend/append slot contents */
      }
    
      .form_group select
      {
        background: white;
      }
    
      .form_group textarea
      {
        height: auto;
      }
    
      .form_group input[type="file"]
      {
        padding: 0;
        min-height: 0;
      }
    
      .form_group input:focus,
      .form_group select:focus,
      .form_group textarea:focus
      {
        outline: none;
      }
    
      .form_group input:disabled,
      .form_group select:disabled,
      .form_group textarea:disabled
      {
        background-color: $input_disabled;
      }
    
      .form_group > *:not(.form_input_prepend):not(.form_input_append)
      {
        align-self: stretch;
      }
    
      .form_input_prepend,
      .form_input_append
      {
        background-color: $input_prepend;
        display: flex;
        justify-content: center;
        align-items: center;
      }
    
      .form_input_prepend
      {
        border-top-left-radius: $field_radius;
        border-bottom-left-radius: $field_radius;
      }
    
      .form_input_append
      {
        border-top-right-radius: $field_radius;
        border-bottom-right-radius: $field_radius;
      }
    
      .field_error
      {
        font-family: 'Segoe UI', Tahoma, sans-serif;
        font-size: 85%;
        color: red;
        margin: 3px 0;
      }
    </style>
    

    然后你可以像这样编写:

        <FormComponent ref="frm" @submit.prevent="submitForm">
          <FormField v-for="(field,index) in fields" :key="index" label="Password">
            <input v-model.trim="field.value">
          </FormField>
        </FormComponent>
    
    <script>
    export default
    {
      name: 'MyComp',
      data()
      {
        return {
          fields: [
            {
              type: 'text',
              value: '',
            },
          ];
        };
      },
      computed:
      {
        allFieldsNotEmpty()
        {
          return this.fields.filter(item => !item.value).length > 0;
        }
      },
      watch:
      {
        allFieldsNotEmpty(newValue, oldValue)
        {
          if (newValue && !oldValue) this.fields.push({type: 'text', value: ''});
        }
      },
      methods:
      {
        submitForm()
        {
          if (this.$refs.frm.valid()) // check if no FormField has a non-empty "error" prop
          {
            // make your AJAX request here
          }
        }
      }
    }
    </script>
    

    【讨论】:

      【解决方案2】:

      我猜你需要这样的东西。 (是 vue 3。只有 v-mode 系统有点不同)

      表单组件:

      <template>
        <div>
          <div v-for="(item, index) in items" :key="item.id">
            {{ index + 1 }}
            <slot :item="item" :input="onInput" />
          </div>
        </div>
      </template>
      
      <script>
      export default {
        name: "FormComponent",
      
        props: {
          value: String,
          items: Array,
        },
        created() {
          console.log("create newform");
        },
        methods: {
          onInput(id, event) {
            console.log("id", id);
            console.log("event", event);
            // HERE you cant handle changing  the item and pass it up
            // this.$emit('update:items', NEW_ARRAY)
          },
        },
        watch: {
          items(oldValue, newValue) {
            if (oldValue[oldValue.length] == "" && newValue[newValue.length] != "") {
              newValue.push("");
            }
          },
        },
      };
      </script>
      

      父.vue

      <template>
        <div>
          <FormComponent v-model:items="something">
            <template #default="{ item, input }">
              <input type="text" :value="item.text" @input="input(item.id, $event)" />
            </template>
          </FormComponent>
        </div>
      </template>
      
      
      <script>
      import FormComponent from "./FormComponent.vue";
      
      export default {
        components: { FormComponent },
        data() {
          return {
            something: [
              {
                id: 1,
                text: "Good",
              },
              {
                id: 2,
                text: "Good",
              },
            ], // could be a list, or a string, number, Object, etc...
          };
        },
      };
      </script>
      

      【讨论】:

        【解决方案3】:

        我想要实现的更像是这样的:

        
            <!-- What I wanted to achieve -->
            <FormComponent>
              <component />
            </FormComponent>
        
            <!-- Not what I wanted to achieve! Too much complexity for the end user.  -->
            <FormComponent>
              <FormField>
                <component />
              </FormField>
            </FormComponent>
        
        

        【讨论】:

          猜你喜欢
          • 2018-03-07
          • 1970-01-01
          • 1970-01-01
          • 2018-11-20
          • 1970-01-01
          • 2019-06-16
          • 2021-07-01
          • 1970-01-01
          • 2019-02-14
          相关资源
          最近更新 更多