How to communicate/ validate Component inside form in vuejs? - vue.js

I have a form which contains name and address components. In the parent's page I have a submit button. I can send data from the parent to the children using props.
Now I am trying to get the children's values from the parent's form. And I want to validate child fields from the parent's form.
How to acheive this?
Here is my form structure.
parent.vue
<form #submit.prevent="handleSubmit">
<name-fields :user='user'></name-fields>
<address-fields :address='address'></address-fields>
<button>Register</button>
</form>
<script>
export default {
data () {
return {
user: {
firstName: 'Raja',
lastName: 'Roja',
},
address: {
doorNo: '11',
state: 'KL',
country: 'IN',
},
submitted: false
}
},
components: {
'name-fields': cNameFields,
'address-fields': cAddressFields,
},
}
</script>
cNameFields.vue
<template>
<div>
<div class="form-group">
<label for="firstName">First Name</label>
<input type="text" v-model="user.firstName" v-validate="'required'" name="firstName" class="form-control" :class="{ 'is-invalid': submitted && errors.has('firstName') }" />
<div v-if="submitted && errors.has('firstName')" class="invalid-feedback">{{ errors.first('firstName') }}</div>
</div>
<div class="form-group">
<label for="lastName">Last Name</label>
<input type="text" v-model="user.lastName" v-validate="'required'" name="lastName" class="form-control" :class="{ 'is-invalid': submitted && errors.has('lastName') }" />
<div v-if="submitted && errors.has('lastName')" class="invalid-feedback">{{ errors.first('lastName') }}</div>
</div>
</div>
</template>
<script>
export default {
name: 'name',
props: {
user: Object,
submitted : Boolean
},
</script>
Currently getting this output:
What I want to do:

Use this.$emit https://v2.vuejs.org/v2/api/#vm-emit
and also use watch https://v2.vuejs.org/v2/api/#vm-watch
So in child component you should watch for changes in user.firstName and user.lastName. Call emit in the watch and get the value in parent. Don't forget to also emit the this.errors bag which comes from vee-validate.
Hope this will help you :)

You are passing objects as props to your children, which are passed by reference in JavaScript. From the Vue docs ..
Note that objects and arrays in JavaScript are passed by reference, so if the prop is an array or object, mutating the object or array itself inside the child component will affect parent state.
This means that you're already getting the children's values in the parent and you can access them in the parent through this.user.firstName,this.user.lastName, this.address.doorNo, etc. If this isn't the intended behavior and you want to keep your parent's data isolated then you should look into deep cloning your objects.
If you want to expose your validation errors from your child components to the parent you can look into Scoped Slots. So you may do something like this ..
parent.vue
<form #submit.prevent="handleSubmit">
<name-fields :user='user'>
<span slot="firstName" slot-scope="{validationErrors}" style="color:red">
{{validationErrors.first('firstName')}}
</span>
<span slot="lastName" slot-scope="{validationErrors}" style="color:red">
{{validationErrors.first('lastName')}}
</span>
</name-fields>
<address-fields :address='address'>
<span slot="doorNo" slot-scope="{validationErrors}" style="color:red">
{{validationErrors.first('doorNo')}}
</span>
<span slot="state" slot-scope="{validationErrors}" style="color:red">
{{validationErrors.first('state')}}
</span>
<span slot="country" slot-scope="{validationErrors}" style="color:red">
{{validationErrors.first('country')}}
</span>
</address-fields>
<button>Register</button>
</form>
cNameFields.vue
<template>
<div>
<div class="form-group">
<label for="firstName">First Name</label>
<input type="text" v-model="user.firstName" v-validate="'required'" name="firstName" class="form-control" :class="{ 'is-invalid': submitted && errors.has('firstName') }" />
<slot name="firstName" :validationErrors="errors"></slot>
</div>
<div class="form-group">
<label for="lastName">Last Name</label>
<input type="text" v-model="user.lastName" v-validate="'required'" name="lastName" class="form-control" :class="{ 'is-invalid': submitted && errors.has('lastName') }" />
<slot name="lastName" :validationErrors="errors"></slot>
</div>
</div>
</template>
This is a great video that explains how scoped slots work.

Related

Vue 3 slot props not reactively changing data in parent

I have a slot containing radio buttons in a parent Vue3 component, I'm passing a v-model attribute to these radio buttons and the data model exists in the parent component. However when I change the selected radio button in the slot, the data in the parent component doesn't change.
parent template:
<template>
<div class="card">
<div class="card-body">
<slot
:type="type"
/>
</div>
</div>
</template>
parent vue:
<script>
export default {
data() {
return {
type: 'standard',
}
},
}
</script>
slot content:
<parent v-slot="slotProps">
<div class="row">
<label>
<span class="required">Type</span>
</label>
<label>
Standard Model
<input v-model="slotProps.type" type="radio" name="type" value="standard" required/>
</label>
<label>
Touch Model
<input v-model="slotProps.type" type="radio" name="type" value="touch" required/>
</label>
<label>
Display Model
<input v-model="slotProps.type" type="radio" name="type" value="display" required/>
</label>
</div>
</parent>
I do not think this is a good idea and this is also not recommended by the Vue team.
Anyway, if you really need to do it, as stated by posva from the Vue team, you can pass a method instead to change the value or you can pass an object and modify a property (keep in mind this is not recommended).Here is the object way to do it:
Parent:
<template>
<div class="card">
<div class="card-body">
<slot :myObject="myObject" />
</div>
</div>
</template>
<script>
export default {
data() {
return {
myObject: {
type: "standard",
},
};
},
};
</script>
Slot content:
<parent v-slot="slotProps">
<div class="row">
<label>
<span class="required">Type</span>
</label>
<label>
Standard Model
<input
v-model="slotProps.myObject.type"
type="radio"
name="type"
value="standard"
required
/>
</label>
<label>
Touch Model
<input
v-model="slotProps.myObject.type"
type="radio"
name="type"
value="touch"
required
/>
</label>
<label>
Display Model
<input
v-model="slotProps.myObject.type"
type="radio"
name="type"
value="display"
required
/>
</label>
</div>
</parent>
Here is the method way to do it:
Parent:
<template>
<div>
<div>
<slot :type="type" :onTypeChange="onTypeChange" />
</div>
</div>
</template>
<script>
export default {
data() {
return {
type: "touch",
};
},
methods: {
onTypeChange(event) {
this.type = event.target.value;
},
},
};
</script>
Slot content:
<parent v-slot="slotProps">
<div class="row">
<label>
<span class="required">Type</span>
</label>
<label>
Standard Model
<input
v-model="slotProps.type"
type="radio"
name="type"
value="standard"
required
#change="slotProps.onTypeChange"
/>
</label>
<label>
Touch Model
<input
v-model="slotProps.type"
type="radio"
name="type"
value="touch"
required
#change="slotProps.onTypeChange"
/>
</label>
<label>
Display Model
<input
v-model="slotProps.type"
type="radio"
name="type"
value="display"
required
#change="slotProps.onTypeChange"
/>
</label>
</div>
</parent>

Why my vuex store state chnage when i try to chnage the data using the method in my component?

I am making a small appliaction that allow the user to tell about their web application.
But In the Update component what I am doing is:
When a component is created.
Fetching the app from vue x store
Setting the values in the data property
And from data property taking it to the template
But the problem is when I try to update the stack of an app via methods addTechToStack by pushing the new value at the end of the app_stacks list it also changes the data of vuex state
Here is the code
<template>
<div>
<div class="section">
<div class="box">
<h1 class="title is-4 has-text-centered">Update App</h1>
<hr class="has-background-black" />
<form method="POST" #submit.prevent="">
<div class="field">
<label class="label">App Name:</label>
<div class="control">
<input
class="input"
type="text"
placeholder="Enter App Name"
v-model="updated.app_name"
/>
</div>
</div>
<div class="field">
<label class="label">App Subtitle:</label>
<div class="control">
<input
class="input"
type="text"
placeholder="Enter App Subtitle"
v-model="updated.app_subtitle"
/>
</div>
</div>
<div class="field">
<label class="label">App Url:</label>
<div class="control">
<input
class="input"
type="text"
placeholder="Enter App Url"
v-model="updated.app_url"
/>
</div>
</div>
<div class="field">
<label class="label">App Category:</label>
<div class="control">
<div class="select">
<select v-model="updated.app_category">
<option value="" disabled>Select Category</option>
<option
v-for="(category, index) in categoreis"
:key="index"
:value="category"
>{{ category }}</option
>
</select>
</div>
</div>
</div>
<div class="field">
<label class="label">App Description:</label>
<textarea
class="textarea"
placeholder="Description of your app"
v-model="updated.app_description"
></textarea>
</div>
<!-- IMP:Stack section -->
<div class="field">
<label class="label">App Stacks:</label>
<div class="field has-addons">
<div class="control">
<input
class="input"
type="text"
placeholder="Find Technology"
v-model="updated.stackTech"
id="stackTech"
/>
</div>
<div class="control">
<button class="button is-info" #click="addTechToStack">
Add
</button>
</div>
</div>
</div>
<div class="box">
<label class="label">Stacks:</label>
<span
class="tag is-medium mr-3"
v-for="(tech, index) in updated.app_stacks"
:key="index"
>{{ tech }}
<button
class="delete is-small"
#click="removeStack(index)"
></button>
</span>
</div>
<!-- Stack section ends -->
<div class="field is-grouped">
<p class="control">
<a class="button">
Cancel
</a>
</p>
<p class="control">
<button
type="submit"
class="button is-primary"
v-on:click="submitUpdate"
>
Update
</button>
</p>
</div>
</form>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Update",
data() {
return {
currentApp: null,
categoreis: ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"],
// Contains the updated value
updated: {
app_name: "",
app_subtitle: "",
app_description: "",
app_url: "",
app_category: "",
app_stacks: "",
stackTech: "",
},
};
},
methods: {
removeStack(index) {
console.log("Removed");
this.updated.app_stacks.splice(index, 1);
console.log(this.currentApp);
console.log(this.updated.app_stacks);
},
addTechToStack() {
this.updated.app_stacks.push(this.updated.stackTech);
console.log(this.currentApp.stacks);
console.log(this.updated.app_stacks);
},
submitUpdate() {
// Check if user has updated any content
if (
this.currentApp.app_name == this.updated.app_name &&
this.currentApp.subtitle == this.updated.app_subtitle &&
this.currentApp.app_url == this.updated.app_url &&
this.currentApp.category == this.updated.app_category &&
this.currentApp.description == this.updated.app_description &&
this.currentApp.stacks.toLocaleString()==this.updated.app_stacks.toLocaleString()
) {
console.log("Cannot Update Beasue every vlaue is the same");
console.log(this.currentApp.description);
console.log(this.updated.app_description);
console.log(this.currentApp.stacks);
console.log(this.updated.app_stacks);
} else {
console.log("Updated");
console.log(this.currentApp.description);
console.log(this.updated.app_description);
}
},
},
created() {
// const current = this.$store.state.your_apps[this.$route.params.index];
this.currentApp = this.$store.state.your_apps[this.$route.params.index];
this.updated.app_name = this.currentApp.app_name;
this.updated.app_subtitle = this.currentApp.subtitle;
this.updated.app_url = this.currentApp.app_url;
this.updated.app_category = this.currentApp.category;
this.updated.app_description = this.currentApp.description;
this.updated.app_stacks = this.currentApp.stacks;
console.log(this.currentApp);
}
};
</script>
The problem is you're assigning a reference the same object (array) to a different variable.
So this.updated.app_stacks === this.currentApp.stacks are both references to the same array. So if you mutate one, you mutate the other.
To avoid this, you'll have to create a copy of this array in order to create a new reference and detach both array.
If you array is hosting primitive values (i.e. not arrays or objects), you can make a shallow copy. There are a lot of ways to do so, here is one:
this.updated.app_stacks = [...this.currentApp.stacks];
Side note: in your data() initialisation, this.updated.app_stacks should be an empty array, not an empty string. It doesn't change anything since you overwrite it in created, but it's kind of confusing when reading the code.

Vue Conditional Classes from Prop Variable

So, currently, I have a set of radio buttons on my home.vue page and binding to a child component like so -
<div class="radio-toolbar prev-selector">
<input type="radio" id="one" value="Default" v-model="preview" />
<label for="one">Default</label>
<input type="radio" id="two" value="Padded" v-model="preview" />
<label for="two">Padded</label>
<input type="radio" id="three" value="Full" v-model="preview" />
<label for="three">Full</label>
<span>Picked: {{ preview }}</span>
</div>
<div class="media-wrapper">
<CardStyle
v-bind:card="this.preview"
/>
</div>
Which is then passing that "preview" data to the CardStyle Prop.
The prop is receiving it as ['card'] and I can use the data within my component.
However, am wondering how I can use the potential value of "this.preview" (which could be 'default', 'padded' or 'full') as a dynamic class to my child component without having to convert them to a true/false outcome and use -
:class="{ default: isDefault, padded: isPadded, full: isFull }"
(I have classes called .default, .padded and .full ready to use if it is as simple as passing the data in somehow.)
As long as this.preview has a value of the class name you want to use, just use :class="this.preview".
I built a couple of sample components that demonstrate how to solve your problem.
Parent.vue
<template>
<div class="parent">
<h5>Select Card Style</h5>
<div class="row">
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input" type="radio" id="one" value="default" v-model="preview" />
<label class="form-check-label" for="one">Default</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" id="two" value="padded" v-model="preview" />
<label class="form-check-label" for="two">Padded</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" id="three" value="full" v-model="preview" />
<label class="form-check-label" for="three">Full</label>
</div>
</div>
</div>
<card-style :preview="preview" />
</div>
</template>
<script>
import CardStyle from './CardStyle.vue'
export default {
components: {
CardStyle
},
data() {
return {
preview: 'default'
}
}
}
</script>
CardStyle.vue
<template>
<div class="card-style">
<hr>
<div class="row">
<div class="col-md-6">
<span :class="preview">{{ preview }} Card Style</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
preview: {
type: String,
required: true
}
},
}
</script>
<style scoped>
.default {
background-color: burlywood;
}
.padded {
background-color:lightskyblue
}
.full {
background-color:lightcoral
}
</style>
You can build a computed variable for your field class in your component. This will allow you to select or define what classes to use based on your conditions or case.
computed: {
myclasses() {
return [
'class1',
'class2',
[this.condition ? 'class3' : 'class4'],
{ class5: this.othercondition },
];
},
},
Then, just use it your template:
<div :class="myclasses"></div>

Submit values from multiple components

I am using vuejs-wizard to create registration page, I have each tab in component like this
<form-wizard color="#fcab1a" title="" subtitle="" finish-button-text="Register">
<tab-content title="Personal Info" icon="icon-location3 fa-2x">
<personal-info></personal-info>
</tab-content>
<tab-content title="Contact Info" icon="icon-box fa-2x">
<contact-info></contact-info>
</tab-content>
<tab-content title="Address" icon="icon-alarm fa-2x">
<address></address>
</tab-content>
</form-wizard>
personal info:
<template>
<div class="card">
<div class="card-header">
<h5 class="card-title">Personal Info</h5>
</div>
<div class="card-body">
<div class="form-group">
<div class="row">
<div class="col-md-6">
<label>Full Name <span class="text-danger">*</span></label>
<input type="text" value="" class="form-control" v-model="name" />
</div>
</div>
</div>
<div class="form-group">
<div class="row">
<div class="col-md-6">
<label>Age <span class="text-danger">*</span></label>
<input type="number" value="" class="form-control" v-model="age" />
</div>
</div>
</div>
</div>
</div>
</template>
contact info:
<template>
<div class="card">
<div class="card-header">
<h5 class="card-title">Contact Info</h5>
</div>
<div class="card-body">
<div class="form-group">
<div class="row">
<div class="col-md-6">
<label>Mobile <span class="text-danger">*</span></label>
<input type="text" value="" class="form-control" v-model="mobile" />
</div>
</div>
</div>
<div class="form-group">
<div class="row">
<div class="col-md-6">
<label>Email <span class="text-danger">*</span></label>
<input
type="number"
value=""
class="form-control"
v-model="email"
/>
</div>
</div>
</div>
</div>
</div>
</template>
so my question is, what is the best way to submit the form, do I need vuex to store the state or is there a better/easier way ?
Thanks
It depends...
The answer depends on various factors. For example, are you using vuex already, size of the app, test-ability of app, and even how are fields get validated (asynch/api validations?).
When it's a simple app, and I only have direct parent=>child relationships, I tend to skip adding the Vuex as a dependency. (but I mostly deal with SPAs, so I usually use it) YMMV.
In this case, that would require that the fields are defined in the parent. Then adding props and emitters for each value to the children, and a listener on the parent. As you start to add more fields though, you might find this tedious, and opt to pass fields in an object either in groups, or as whole (and only pick the ones you need in tab), and then you can implement your own v-model in the tab components which can make it pretty easy to pass the object around
If you're using a more recent Vue version (2.6+), you could use vue.observable to
share a store between multiple components without the bells/whistles of Vuex
There's a good article that shows how to build a vuex clone with it, but in reality it's much, much simpler than that to create a store that would suit your needs. Let me know in the comments if you're interested in how to implement it, and I can describe it.
Rolling with custom store
it's really as simple as this
Create a store.js file with...
import Vue from 'vue';
const store = Vue.observable({
name: null,
email: null,
age: null,
mobile: null,
});
export default store;
then in any component that you want to have access to it, add the store during create
import store from "../store";
export default {
name: "PersonalInfo",
created() {
this.$store = store;
}
};
now the all the store is available to you in the template through $store
<input type="text" value class="form-control" v-model="$store.name">
codesandbox example
You lose the benefits, such as "time-traveling" over mutations that Vuex offers. Because you're not dealing with the scale that the flux pattern (vuex) was meant for, I would use this solution in an app this size.
Ya you should just use vuex, it combines like data so that it isn't spread out across multiple files. If you are going to be sending this information back to your backend, it makes it easier to have most backend connections in one place. without the wizard thing I redid your code with a store like this. Note the computed property instead of using data. By doing it as a computed property you don't have to write any code to change the variables stored inside of the store.
Personal.vue
<template>
<div class="card">
<div class="card-header">
<h5 class="card-title">Personal Info</h5>
</div>
<div class="card-body">
<div class="form-group">
<div class="row">
<div class="col-md-6">
<label>Full Name <span class="text-danger">*</span></label>
<input
type="text"
value=""
class="form-control"
v-model="register.name"
/>
</div>
</div>
</div>
<div class="form-group">
<div class="row">
<div class="col-md-6">
<label>Age <span class="text-danger">*</span></label>
<input
type="number"
value=""
class="form-control"
v-model="register.age"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
computed: {
register() {
return this.$store.state.register;
},
},
};
</script>
Contact.vue
<template>
<div class="card">
<div class="card-header">
<h5 class="card-title">Contact Info</h5>
</div>
<div class="card-body">
<div class="form-group">
<div class="row">
<div class="col-md-6">
<label>Mobile <span class="text-danger">*</span></label>
<input
type="text"
value=""
class="form-control"
v-model="register.mobile"
/>
</div>
</div>
</div>
<div class="form-group">
<div class="row">
<div class="col-md-6">
<label>Email <span class="text-danger">*</span></label>
<input
type="number"
value=""
class="form-control"
v-model="register.email"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
computed: {
register() {
return this.$store.state.register;
},
},
methods: {
submit() {
this.$store.dispatch("register", {
person: this.register,
});
},
},
};
</script>
<style></style>
store/index.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
register: {},
},
actions: {
// register({commit}, data){
//put in some stuff here.
//},
},
});
If you decide to go the store route, all you have to do is this
1. npm install vuex
2. add a folder inside of your src folder called store
3. add a file named index.js
4. go to main.js and add this line"import store from "./store";"
5. where it says new "Vue({" add "store" this will register it.
Vuex is super easy and makes life way easier as your project gets bigger.
The .sync modifier provides two way binding pattern to props, you can read about it here https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier.
In the parent component you can use the .sync modifier this way:
<ChildComponent :name.sync="parentNameProperty" />
...
data: () => ({ parentNameProperty: '' }),
...
Then in the child component you receive name as prop and you can emit an event to update the value in the parent component, by using watch or a method:
...
props: {
name: {
type: String,
default: ''
}
}
...
this.$emit(update:parentNameProperty, newValue)
...
Vuex is a great way to handle state, but is fine to use the above pattern for small applications.

VueJS - v-model in for-loop

I'm trying to build something like the questions in OkayCupid, but all the questions - which are different forms - are located on the same component.
I use an object of questions and 3 possible answers for each question, and I use v-for to loop through the object and create cards with a question, 3 answers with radios, and a submit button.
The problem is that I want to get not only the answer the user chooses, but also the question it belongs to.
Here is my form in the template:
<div class="container">
<div class="row">
<div
class="col-lg-3 col-md-4 col-6"
v-for="(question,index) in questionCollection"
:key="index"
>
<form class="form">
<div class="img-fluid img-thumbnail shadow-lg p-3 mb-5 bg-white rounded">
<!-- <input type="text" :value="question.question" v-model="q" /> -->
<h3 class="d-block mb-4 h-100" alt data-holder-rendered="true">{{ question.question }}</h3>
<div class="card-body container">
<div class="card-text form-check">
<input
class="form-check-input"
type="radio"
name="gridRadios"
id="a1"
:value="question.answer1"
v-model="answer"
/>
<h4 class="font-weight-light" for="a1">{{ question.answer1 }}</h4>
</div>
<div class="card-text form-check">
<input
class="form-check-input"
type="radio"
name="gridRadios"
id="a2"
:value="question.answer2"
v-model="answer"
/>
<h4 class="font-weight-light" for="a2">{{ question.answer2 }}</h4>
</div>
<div class="card-text form-check">
<input
class="form-check-input"
type="radio"
name="gridRadios"
id="a3"
:value="question.answer3"
v-model="answer"
/>
<h4 class="font-weight-light" for="a3">{{ question.answer3 }}</h4>
</div>
</div>
<div class="card-text container">
<small class="text-muted">{{ question.user }}</small>
<button
href="#"
class="btn btn-primary my-3 mx-10 btn float-right shadow-sm rounded"
#click.prevent="answerQuestion"
>Save</button>
</div>
</div>
</form>
</div>
</div>
</div>
And the script:
export default {
name: "questions",
data() {
return {
q: null,
answer: null
};
},
}
As you can see, at the beginning of the form, I tried to get the question element using v-model in a "fake" input, but it gives me an error that it's conflicted with the v-bind of the value (the question) I want to grab. Of course, I can't use v-model on the headline itself because Vue allows to use it only on inputs.
I've tried to change the v-model into v-model="questionCollection[index].question, but then I have no idea how to get it in the script and, let's say, log it to the console with the corresponding answer.
One way to handle this is to submit the question and answer together in the Save button's click-handler. That is, change answerQuestion() to receive question (the iterator variable in v-for) and answer, and update the Save button's click handler in the template to pass those two variables:
// template
<div v-for="(question, index) in questionCollection">
...
<button #click.prevent="answerQuestion(question, answer)">Save</button>
</div>
// script
answerQuestion(question, answer) {
console.log({ question, answer })
}
demo