插槽的整个想法是通过父组件管理它们 - 而不是通过子组件。所以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>