I'm trying to validate input fields inside children components but using steps (each component it's a step), the action buttons (back and next) it's in parent component, i'm confused (logically) how's the best way to do that, i don't know if i use a library or the best way is validate inside each component
The active component is based in v-show="step" in each children component I'm using a watch for step to control all actions in the children components and trigger children functions using $refs if is necessary. Form.vue
<StepGuide class="mb-5" :step="step"></StepGuide>
<StepOne ref="step1" v-show="step == 1"></StepOne>
<StepTwo ref="step2" v-show="step == 2"></StepTwo>
<StepThree ref="step3" v-show="step == 3"></StepThree>
<StepFour ref="step4" v-show="step == 4"></StepFour>
<StepFive ref="step5" v-show="step == 5"></StepFive>
<div class="row justify-content-center">
<div class="col-sm-3 text-center mt-4 d-flex justify-content-around">
<button type="button" @click="step--" key="back" v-if="step > 1" class="px-4 py-2 rounded btn-add">Anterior</button>
<button type="button" @click="step++" key="next" v-if="step < 5" class="px-4 py-2 rounded btn-add">Próximo</button>
</div>
</div>
StepOne Component Input Example
<div class="col-sm-3">
<label for="customer">Nome do cliente *</label>
<input
name="customer_name"
class="form-control"
id="customerName"
type="text"
v-model="customer.name"
/>
</div>
If you take a look at the Vuetify source code (it's a UI library) - you will see how they use the provide + inject
pattern where the form (a parent component) provides a function which is injected by each form field component which is capable of validating its own content. The form fields call the provided (and injected) function with a boolean argument TRUE to notify the form about their existence (because they might not be a direct parent of the form) and with FALSE to unsubscribe from the form (in beforeUnmount
hook).
The form (through this notification function) collects a list of the form field components and when the form must be validated - it calls the validate()
method of each subscribed form field. The form fields on the other hand take care of the validation of their own content - and show appropriate error messages. The form fields are also expected to provide a method reset
to clear the validation errors.
You can start with the following example and extend it for your own use case.
<template>
<form v-bind="$attrs" v-on="listeners">
<slot />
</form>
</template>
<script>
export default
{
name: 'FormValidator',
provide()
{
return {
form: this
};
},
inheritAttrs: false,
data()
{
return {
fieldList: [],
};
},
computed:
{
listeners()
{
return {
...this.$listeners,
submit: (evt) =>
{
evt.preventDefault();
this.fieldList.forEach(item =>
{
item.setDirty(true);
});
this.$nextTick(() =>
{
this.$emit('submit', evt);
});
}
};
}
},
methods:
{
register(field)
{
this.fieldList.push(field);
},
unregister(field)
{
const idx = this.fieldList.indexOf(field);
if (idx !== -1) this.fieldList.splice(idx, 1);
},
valid()
{
const fields = this.fieldList;
return fields.every(item => item.valid);
},
reset()
{
this.fieldList.forEach(item => item.setDirty(false));
},
}
};
</script>
<template>
<div class="form_field">
<label v-if="label" style="text-align: initial;">{{ 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 || force)" class="field_error">{{ error }}</div>
</div>
</template>
<script>
export default
{
name: 'FormField',
inject:
{
form:
{
default: null
}
},
props:
{
label:
{
type: String,
default: ''
},
error:
{
// the actual validation happens in your own code - you just provide an error message here (if any)
type: String,
default: null
},
force:
{
// force real-time validation, no matter if the value is dirty or not
type: Boolean,
default: false
},
},
data()
{
return {
dirty: false,
};
},
computed:
{
valid() // eslint-disable-line vue/no-unused-properties
{
return !this.error;
}
},
created()
{
this.form && this.form.register(this);
},
beforeDestroy()
{
this.form && this.form.unregister(this);
},
methods:
{
setDirty(value)
{
this.dirty = value;
}
}
};
</script>
<style lang="scss">
$field_radius: 4px;
$input_disabled: grey;
.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: $input_prepend;
flex-grow: 1;
}
.form_group:focus-within
{
border-color: blue;
}
.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>