Validating Fields of an Object in a Form - Vue 3 + Vee-validate + Yup - vue.js

I have a form where certain fields are inside an object:
<script setup lang="ts">
...
const schema: yup.SchemaOf<any> =
yup.object({
group: yup.string().nullable().required(),
description: yup.string().nullable().required(),
user: yup
.object({
first_name: yup.string().required(),
last_name: yup.string().required(),
})
.required(),
})
const { errors, handleSubmit, isSubmitting } = useForm({
validationSchema: schema,
initialValues: {
group: '',
description: '',
user: {}
},
});
const { value: group } = useField<string>('group');
const { value: description } = useField<string>('description');
const { value: user } = useField<any>('user');
const isValid = useIsFormValid();
...
</script>
<template>
<div>
<label for="group">Group:</label
><input id="group" v-model="group" type="text" />
</div>
<div>
<label for="description">Description:</label
><input id="description" v-model="description" type="text" />
</div>
<div>
<label for="first-name">First name</label
><input id="first-name" v-model="user.first_name" type="text" />
</div>
<div>
<label for="last-name">Last name</label
><input id="last-name" v-model="user.last_name" type="text" />
</div>
<button :disabled="!isValid">Save</button>
...
</template>
But the data validation of this object is only done after changing a field outside of it, i.e. fill in group, description, first_name, last_name (in this same order) and the form will NOT be considered valid, only if you edit group or description again.
How can I make the validation be done when I change the field myself?
Here is the link to the complete code.
I am using the following versions:
"vue":"^3.2.37",
"vee-validate": "^4.5.11",
"yup": "^0.32.11"

When you use useField() with an Object, the nested properties lose their reactivity connection. So here are two options to resolve this: wrap useField with reactive() or use useField() for each nested property separately.
option 1
const { value: user } =reactive(useField('user'));
option 2
const { value: first_name } = useField('user.first_name');
const { value: last_name } = useField('user.last_name');
here is a working example here

Related

I'm emiting a value from the watcher but this value is not received in the parent component Vue 3 Select component

I have a Custom select component which emits a value in the watch method but when I have to get that value, is not shown in the parent component. (VUE 3)
this is my select component:
<template>
<div class="input-group">
<label class="label">{{ label }}</label>
<select v-model="select">
<option v-for="option in options" :value="option.id" :key="option.id">
{{ option.name }}
</option>
<option :value="select" disabled hidden>Select...</option>
</select>
</div>
</template>
<script lang="ts">
import { ref, watch, getCurrentInstance } from "vue";
export default {
name: "Select",
props: {
options: {
type: Array,
required: true,
},
select: {
type: String,
},
label: {
type: String,
},
},
setup(props) {
const { emit } = getCurrentInstance();
const select = ref(props.value);
watch(select, (value) => {
emit("input", value);
});
return {
select,
};
},
};
</script>
This is the implementation in the parent component
<template>
<Select
label="Group Name"
:options="questions_groups"
v-model="groupName"
/>
<template>
<script>
......
.....
const groupName = ref(null);
<script/>
I need to know why I cannot get the value od the v-model variable in the implementation
for vue3 (because it supports multiple models the event name changed) you need to use update:modelValue
watch(select, (value) => {
emit("update:modelValue", value);
})
for clarity...
modelValue is the default model's name, so
<input v-model="searchText" /> is equivalent to <input v-model:modelValue="searchText" />
having multiple models allows use such as:
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
more info at https://vuejs.org/guide/components/v-model.html#component-v-model

V-Model is not Reactive (Vue3)

I have an string input Component, which passes the inputted data back to the parent component, and stores it in a Form Object.
This was working fine, until I began to use a Validation Library (Vee-Validate), I'm trying to retrace my changes, but cannot seem to solve this issue without breaking the validator. I've copied the relevant code below.
Parent Component:
<template>
<form class="form" #submit.prevent="submitFormData">
<BaseInput
v-model="form.salutation.firstName"
type="text"
class="text-class"
label="case_file.table.first_name"
:required="true"
/>
</form>
</template>
const form = reactive({
salutation: {
firstName: ""
},
});
Child Component:
<template>
<div class="form-control">
<label class="label">
<span class="label-text">{{ $t(`${labelName}`) }}</span>
</label>
<input
type="text"
class="input input-bordered"
:value="value"
#input="$emit('update:modelValue', $event.target.value)"
v-on="validationListeners"
/>
<span>{{ errorMessage }}</span>
</div>
</template>
<script setup>
import { useField } from "vee-validate";
import { reactive, computed } from "vue";
import { toRefs } from "#vue/reactivity";
import { ref, watch } from "vue";
import { useParticipantStore } from "#/store/participant";
const participantStore = useParticipantStore();
const props = defineProps({
label: {
type: [String, Boolean],
default: false,
},
modelValue: {
type: String,
default: "",
},
required: {
type: Boolean,
default: false,
},
});
const labelName = ref(props.label);
const localInputValue = ref(props.modelValue);
function validateField(value) {
if (!value && props.required) {
return "This is required";
}
return true;
}
const { errorMessage, value, handleChange } = useField(
"fieldName",
validateField,
{
validateOnValueUpdate: false,
}
);
const validationListeners = computed(() => {
// If the field is valid or have not been validated yet
// lazy
if (!errorMessage.value) {
return {
blur: handleChange,
change: handleChange,
input: (e) => handleChange(e, false),
};
}
// Aggressive
return {
blur: handleChange,
change: handleChange,
input: handleChange, // only switched this
};
});
</script>
With this TextInput component:
value does not need to be a prop
Just emit the update:modelValue event to the parent (this should
be enough)
For the required validation, the browser will help us if we manage the form submit and input attributes properly.
IMHO, other validations should go in the form component rather than the input component. the input should only open the gate for errorMessages to render.
Also, if we have the error messages object (reactive) in the form component, we can determine the submit button state easily.
This does not include vee-validate code but I think you can embed it easily. I feel vee validation should go inside the form component.
<template>
<div class="form-control">
<label class="label">
<span class="label-text">{{ labelName }}</span>
</label>
<input
type="text"
class="input input-bordered"
:value="tv"
:required="required"
#input="updateModelValue"
#change="$emit('change', $event.target.value)"
#focus="$emit('focus', $event.target.value)"
#blur="$emit('blur', $event.target.value)"
#keyup="$emit('keyup', $event.target.value)"
>
<span>{{ errorMessage }}</span>
</div>
</template>
<script setup>
import {ref, defineProps, defineEmits} from "vue"
const emit = defineEmits([
"update:modelValue",
"change",
"blur",
"focus",
"keyup",
])
const tv = ref("")
defineProps({
labelName: {
type: String,
default: ""
},
errorMessage: {
type: String,
default: ""
},
required: {
type: Boolean,
default: false
}
})
const updateModelValue = ($event) => {
tv.value = $event.target.value
emit("update:modelValue", $event.target.value)
}
</script>
And this is in the Parent component:
<template>
<form #submit.prevent="submitForm">
<TextInput
v-model="form.salutation.firstName"
label-name="First Name"
:error-message="formErrors.salutation.firstName"
required
#keyup="validateFirstName"
/>
<button
type="submit"
:disabled="formErrors.salutation.firstName"
>
Submit
</button>
</form>
</template>
<script setup>
import {reactive, watch} from "vue"
const form = reactive({
salutation: {
firstName: "",
}
})
const formErrors = reactive({
salutation: {
firstName: null,
}
})
const validateFirstName = () => {
if (form.salutation.firstName && form.salutation.firstName.length < 3) {
formErrors.salutation.firstName = "First name must be at least 3 characters"
return false
} else {
formErrors.salutation.firstName = null
return true
}
}
const submitForm = () => {
console.log(form.salutation.firstName)
}
</script>
This should make your form:
not submittable when empty (required validation)
show|hide error messages on keydown

Vuex-ORM two-way-data binding cannot watch a nested object

this question is related to Two way data binding with Vuex-ORM
i tried using a watch with deep to handle a user form like this.
<template>
<div id="app">
<div style="display: inline-grid">
<label for="text-1">Text-1: </label>
<input name="text-1" type="text" v-model="user.name" />
<label for="text-2">Text-2: </label>
<input name="text-2" type="text" v-model="user.lastName" />
<label for="text-3">Text-3: </label>
<input name="text-3" type="text" v-model="user.birth" />
<label for="text-4">Text-4: </label>
<input name="text-4" type="text" v-model="user.hobby" />
</div>
<div>
<h5>Result</h5>
{{ userFromStore }}
</div>
</div>
</template>
<script>
import { mapGetters, mapMutations, mapActions } from "vuex";
export default {
name: "App",
computed: {
...mapGetters({
userFromStore: "getUserFromStore",
messageFromStore: "getMessage",
}),
user: function () {
return this.userFromStore ?? {}; // basically "User.find(this.userId)" inside store getters
},
},
watch: {
user: {
handler(value) {
console.log('called')
// this.updateUser(value);
},
deep: true,
},
},
methods: {
...mapActions({
fetchUser: "fetchUser",
}),
...mapMutations({
updateUser: "updateUser",
}),
},
created() {
this.fetchUser();
},
};
</script>
problem is my watcher is not watching, no matter what i try. as soon as the data came from Vuex-ORM my component is not able to watch on the getters user
Anyone idea why?
User.find(...) returns a model. The properties of that model are not reactive i.e. you cannot perform two-way data binding on items that are not being tracked. Hence your watcher will not trigger.
My advice would be to push your user data as props to a component that can handle the data programmatically.
Or, by way of example, you can simply handle two-way binding manually:
Vue.use(Vuex)
class User extends VuexORM.Model {
static entity = 'users'
static fields() {
return {
id: this.number(null),
name: this.string(''),
lastName: this.string(''),
birth: this.string(''),
hobby: this.string('')
}
}
}
const db = new VuexORM.Database()
db.register(User)
const store = new Vuex.Store({
plugins: [VuexORM.install(db)]
})
User.insert({
data: {
id: 1,
name: 'John',
lastName: 'Doe',
birth: '12/12/2012',
hobby: 'This, that, the other'
}
})
Vue.component('user-input', {
props: {
value: { type: String, required: true }
},
template: `<input type="text" :value="value" #input="$emit('input', $event.target.value)" placeholder="Enter text here...">`
})
new Vue({
el: '#app',
computed: {
user() {
return User.find(1)
}
},
methods: {
update(prop, value) {
this.user.$update({
[prop]: value
})
}
}
})
<script src="https://unpkg.com/vue#2.6.12/dist/vue.min.js"></script>
<script src="https://unpkg.com/vuex#3.6.2/dist/vuex.min.js"></script>
<script src="https://unpkg.com/#vuex-orm/core#0.36.4/dist/vuex-orm.global.prod.js"></script>
<div id="app">
<div v-if="user" style="display: inline-grid">
<label for="text-1">Name: </label>
<user-input
id="text-1"
:value="user.name"
#input="update('name', $event)"
></user-input>
<label for="text-2">Last name: </label>
<user-input
id="text-2"
:value="user.lastName"
#input="update('lastName', $event)"
></user-input>
<label for="text-3">D.O.B: </label>
<user-input
id="text-3"
:value="user.birth"
#input="update('birth', $event)"
></user-input>
<label for="text-4">Hobby: </label>
<user-input
id="text-4"
:value="user.hobby"
#input="update('hobby', $event)"
></user-input>
</div>
<pre>User in store: {{ user }}</pre>
</div>

problems with Vuex data rendering

I'm studying reactivity of vuex using nuxt and module mode of store. The problem is, that despite all data in store is changed by actions => mutations successfully, they do not appear on the page, and shows only empty new element of store array. here are my files:
store>contacts>index.js:
let initialData = [
{
id: 1,
name: 'Michael',
email: 'michael.s#mail.com',
message: 'message from Michael'
},
{
id: 2,
name: 'Mark',
email: 'mark.sh#email.com',
message: 'message from Mark'
},
{
id: 3,
name: 'Valery',
email: 'valery.sh#mail.com',
message: 'message from Valery'
}
]
const state = () =>{
return {
contacts: []
}
}
const getters = {
allContacts (state) {
return state.contacts
}
}
const actions = {
async initializeData({ commit }) {
commit('setData', initialData)
},
addNewContact({ commit, state }, newContact) {
commit('addContact', newContact)
}
}
const mutations = {
setData: (state, contacts) => (state.contacts = contacts),
addContact: (state, newContact) => state.contacts.push(newContact)
}
export default { state, getters, mutations, actions}
component itself:
<template>
<div class="contact-form">
<div class="links">
<nuxt-link to="/">home</nuxt-link>
<nuxt-link to="/contact-form">contact form</nuxt-link>
</div>
<h1>leave your contacts and message here:</h1>
<div class="input-wrapper">
<form class="feedback-form" action="">
<div class="name">
<label for="recipient-name" class="col-form-label">Ваше имя:</label>
<input type="text" id="recipient-name" v-model="obj.userName" name="name" class="form-control" placeholder="Представьтесь, пожалуйста">
</div>
<div class="form-group">
<label for="recipient-mail" class="col-form-label">Ваш email:</label>
<input type="email" v-model="obj.userEmail" name="email" id="recipient-mail" class="form-control" placeholder="example#mail.ru">
</div>
<div class="form-group">
<label for="message-text" class="col-form-label">Сообщение:</label>
<textarea name="message" v-model="obj.userMessage" id="message-text" class="form-control"></textarea>
</div>
<button #click.prevent="addToStore()" type="submit">submit</button>
</form>
</div>
<h3>list of contacts</h3>
<div class="contacts-list">
<div class="list-element" v-for="contact in allContacts" :key="contact.id">
id: {{contact.id}} <br> name: {{contact.name}}<br/> email: {{contact.email}}<br/> message: {{contact.message}}
</div>
</div>
</div>
</template>
<script>
import { mapMutations, mapGetters, mapActions } from 'vuex'
export default {
data() {
return {
obj: {
userName: '',
userEmail: '',
userMessage: ''
}
}
},
mounted() {
console.log(this.showGetters)
},
created() {
this.initializeData()
},
methods: {
...mapActions({
initializeData: 'contacts/initializeData',
addNewContact: 'contacts/addNewContact'
}),
addToStore() {
this.addNewContact(this.obj)
},
},
computed: {
...mapGetters({
allContacts: 'contacts/allContacts',
}),
showGetters () {
return this.allContacts
}
},
}
</script>
so, could anybody help to understand, what is wrong?
You've got mismatched field names.
Inside obj you've called them userName, userEmail and userMessage. For all the other contacts you've called them name, email and message.
You can use different names if you want but somewhere you're going to have to map one onto the other so that they're all the same within the array.
You should be able to confirm this via the Vue Devtools. The first 3 contacts will have different fields from the newly added contact.

How to pass form data from parent component to child?

I am trying to learn some vuejs and am struggling to understand how to pass data from a parent component to its child component. I know more is required but i'm not sure which way to go. How do pass the name in an input field in the parent component when the submit button is pressed to display in the child component?
I have tried using v-model because from what i have read and understand it is supposed to do what i need but it updates it without me even needing to press the button.
//Parent component
<template>
<div id="app">
<form #submit.prevent="handleSubmit">
<input type="text" name="fname" id="fname" placeholder="First Name" v-model="fname">
<input type="text" name="lname" id="lname" placeholder="Last Name" v-model="lname">
<input type="submit" value="Submit Name">
</form>
<Name lname="lname" fname="fname"></Name>
</div>
</template>
<script>
import Name from './components/fullName.vue'
export default {
name: 'app',
data () {
return {
fname: '',
lname: '',
submittedFname: '',
submittedLname: ''
}
},
components: {
Name
},
methods: {
handleSubmit() {
submittedFname = fname,
submittedLname = lname
}
}
}
</script>
//child component
<template>
<div id="my-name">
<label>Your name is:</label>
{{ submittedFname }} {{ submittedLname }}
</div>
</template>
<script>
export default {
name: 'my-name',
data () {
return {
}
},
props: {
submittedFname: String,
submittedLname: String
}
}
</script>
I am expecting to display the full name on the child component when the button is pressed but instead it is displayed as i am typing it.
//Parent component
<template>
<div id="app">
<form>
<input type="text" name="fname" id="fname" placeholder="First Name" v-model="fname">
<input type="text" name="lname" id="lname" placeholder="Last Name" v-model="lname">
</form>
<button #click="handleSubmit(fname,lname)">submit</button>
<Name :submittedFname="submittedFname" :submittedLname="submittedLname" ></Name>
</div>
</template>
<script>
import Name from './components/fullName.vue'
export default {
name: 'app',
data () {
return {
fname: '',
lname: '',
submittedFname: '',
submittedLname: ''
}
},
components: {
Name
},
methods: {
handleSubmit(fname,lname) {
this.submittedFname = fname,
this.submittedLname = lname
}
}
}
</script>
//child component
<template>
<div id="my-name">
<label>Your name is:</label>
{{ submittedFname }} {{ submittedLname }}
</div>
</template>
<script>
export default {
name: 'my-name',
data () {
return {
}
},
props: {
submittedFname: String,
submittedLname: String
}
}
</script>
in case I forgot some things here are screenshots:
Parent component
child component
v-model means that the fname and lname instance data properties are updated each time the value of their respective input elements changes (it uses the input event behind the scenes). You then pass fname and lname directly as props to the child component. These props are reactive so it behaves as you see and the name is updated as you type.
To only change the name when submit is pressed, you can do this:
Add 2 more data properties in the parent component (e.g. submittedfname and submittedlname)
Add an #submit event listener on the form that copies the values from fname and lname to submittedfname and submittedlname
Use submittedfname and submittedlname as props for the child component.
Working code:
//Parent component
Vue.component('app', {
template: `
<div>
<form #submit.prevent="handleSubmit">
<input type="text" name="fname" id="fname" placeholder="First Name" v-model="fname">
<input type="text" name="lname" id="lname" placeholder="Last Name" v-model="lname">
<input type="submit" value="Submit Name">
</form>
<name-comp :submittedFname="submittedFname" :submittedLname="submittedLname"></Name>
</div>`,
data () {
return {
fname: '',
lname: '',
submittedFname: '',
submittedLname: ''
}
},
methods: {
handleSubmit() {
this.submittedFname = this.fname;
this.submittedLname = this.lname;
}
}
});
//child component
Vue.component('name-comp', {
template: `
<div>
<label>Your name is:</label>
{{ submittedFname }} {{ submittedLname }}
</div>`,
props: {
submittedFname: String,
submittedLname: String
}
});
var vapp = new Vue({
el: '#app',
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<app />
</div>
You were missing ":" in front of your props given to the Name component. Also you didn't use this like in this.lname.