2 Way Databind components within Components - vue.js

I am struggling to reuse my components.
I want to pass the data passed to my component as a prop to another component.
If I do that vue complains about a mutation of the prop.
Example:
I have contacts that I want to show on multiple location of my app.
For that I created a contact component to reuse it:
<template>
<div>
<input :value="contact.firstName" #input="$emit('update:contact', {...contact, firstName: $event.target.value})">
<Mother v-model:mother="contact.mother"/>
</div>
</template>
<script>
import Mother from '#/components/Mother'
export default {
name: 'Contact',
components: {
Mother
},
props: {
contact: Object,
},
emit: ['update:contact'],
methods: {
}
}
</script>
Every contact has a mother, mother are shown in other places not only in the contact component.
That is why I created a mother component, that is used by the contact.
<template>
<div>
<input :value="mother.lastName" #input="$emit('update:mother', {...mother, lastName: $event.target.value})">
</div>
</template>
<script>
export default {
name: 'Mother',
props: {
mother: Object,
},
emit: ['update:mother'],
methods: {
}
}
</script>
Now I want to be able to mutate the contact an the mother as well, and I want to be able to use two contact components on the same site.
If I use it the way explained I get this error:
ERROR Failed to compile with 1 error 09:17:25
error in ./src/components/Contact.vue
Module Error (from ./node_modules/eslint-loader/index.js):
/tmp/vue-example/src/components/Contact.vue
4:27 error Unexpected mutation of "contact" prop vue/no-mutating-props
✖ 1 problem (1 error, 0 warnings)
I have an example project showing my problem:
https://gitlab.com/FirstWithThisName/vue-example.git
Thanks for your help

First I need to assume a few points.
You wanted to use v-model.
You wanted the component to be chained.
Working Example here on Vue SFC Playground.
*Note that the import path is different on the example site.
App.vue
<template>
<Contact v-model="contact" />
{{ contact }}
</template>
... remaining code omitted
Contact.vue
<template>
<div>
<input v-model="localValue"/>
<Mother v-model="childValue" />
</div>
</template>
<script>
import Mother from "./Mother.vue"
export default {
name: "Contact",
components: {
Mother
},
props: {
modelValue: Object,
},
mounted(){
this.childValue = this.modelValue.mother
},
data: () => ({
localValue: "",
childValue: null
}),
watch:{
updatedData(){
this.$emit('update:modelValue', this.updatedData)
}
},
computed: {
updatedData() {
return { firstName: this.localValue, mother: this.childValue };
},
},
};
</script>
Mother.vue
<template>
<div>
<input
v-model="localValue"
#input="$emit('update:modelValue', updatedData)"
/>
</div>
</template>
<script>
export default {
name: "Mother",
props: {
modelValue: Object,
},
data: () => ({
localValue: "",
}),
computed: {
updatedData() {
return { ...this.modelValue, lastName: this.localValue };
},
},
};
</script>
As you might know, props cannot be mutated, so you will need to "make a copy" of the value on each component to process locally.
If Mother component are never going to be used separately, v-model can be split into v-on and v-bind instead.
Lastly, as for recommendation, chaining like this can become very messy if the data starts to grow or the depth level increases. You could just make another Wrapper component that contains Contact and Mother component that scales horizontally instead.

Depends on how complex your application will get.
One option is two-way data-binding as explained here:
https://v3.vuejs.org/guide/component-basics.html#using-v-model-on-components
So you basically emit the changes to the parent.
For more complex applications I wouldn't pass data that are used in multiple components as props, but use a store. Either a simple reactive object; with provide/inject or use something like Vuex.

Related

Parent component updates a child component v-for list, the new list is not rendered in the viewport (vue.js)

My app structure is as follows. The Parent app has an editable form, with a child component list placed at the side. The child component is a list of students in a table.
I'm trying to update a child component list. The child component uses a 'v-for', the list is generated through a web service call using Axios.
In my parent component, I am editing a students name, but the students new name is not reflected in the List that I have on screen.
Example:
Notice on the left the parent form has the updated name now stored in the DB. However, the list (child component) remains unchanged.
I have tried a few things such as using props, ref etc. I am starting to think that my app architecture may be incorrect.
Does anyone know how I might go about solving this issue.
Sections of the code below. You may understand that I am a novice at Vue.
Assistance much appreciated.
// Child component
<component>
..
<tr v-for="student in Students.slice().reverse()" :key="student._id">
..
</component>
export default {
env: '',
// list: this.Students,
props: {
inputData: Boolean,
},
data() {
return {
Students: [],
};
},
created() {
// AXIOS web call...
},
};
// Parent component
import List from "./components/students/listTerms";
export default {
name: "App",
components: {
Header,
Footer,
List,
},
};
// Implementation
<List />
I think that it is better to use vuex for this case and make changes with mutations. Because when you change an object in the data array, it is not overwritten. reactivity doesn't work that way read more about it here
If your list component doesn't make a fresh API call each time the form is submitted, the data won't reflect the changes. However, making a separate request each time doesn't make much sense when the component is a child of the form component.
To utilise Vue's reactivity and prevent overhead, it would be best to use props.
As a simplified example:
// Child component
<template>
...
<tr v-for="student in [...students].reverse()" :key="student._id">
...
</template>
<script>
export default {
props: {
students: Array,
},
};
</script>
// Parent component
<template>
<div>
<form #submit.prevent="submitForm">
<input v-model="studentData.name" />
<input type="submit" value="SUBMIT" />
</form>
<List :students="students" />
</div>
</template>
<script>
import List from "./components/students/listTerms";
export default {
name: "App",
components: {
List,
},
data() {
return {
students: [],
studentData: {
name: ''
}
}
},
methods: {
submitForm() {
this.$axios.post('/endpoint', this.studentData).then(() => {
this.students.push({ ...this.studentData });
}).catch(err => {
console.error(err)
})
}
}
};
</script>
Working example.
This ensures data that isn't stored successfully won't be displayed and data that is stored successfully reflects in the child component.

Vue.js: too many $emit from children to parent

I have the main component that containing other components which containing anothers components.
So, I have the click events in these components, but to handle it in my parent component, I need to make $emit call in each nested component.
How to make this process more simple, for example like in React, where I need just pass the function handler into component.
In vue 2.2.3+ you can use provide and inject to pass function from ancestor component to child, like great grandparent to child.
please refer following code
// app.vue
<template>
<div id="app">
<HelloWorld msg="button1" />
<HelloWorld msg="button2" />
<HelloWorld msg="button3" />
count: {{ count }}
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld";
export default {
name: "App",
provide() {
return {
clickHandler: this.clickHandler,
};
},
data() {
return {
count: 0,
};
},
components: {
HelloWorld,
},
methods: {
clickHandler() {
this.count += 1;
console.log("click received");
},
},
};
</script>
// HelloWorld.vue
<template>
<button #click="clickHandler">{{ msg }}</button>
</template>
<script>
export default {
name: "HelloWorld",
inject: ["clickHandler"],
props: {
msg: String,
},
};
</script>
you can see the same clickHandler function from parent is executed with modifying parents count prop on click of each children.
this clickHandler can be injected directly to any descendent at any level therefore application like
parent > child.1 > child.1.1 > child.1.1.1 > child.1.1.1.1(click)
the child.1.1.1.1 can be injected with clickHandler form parent.
try the code at codesandbox
also refer provide/inject
if you need the same value up in the hierarchy or anywhere in the current module, you should try to use the Vuex(State Management) library.

How to bind a local component's data object to an external component

how do you use a local component's data attriutes to bind an external component's v-model
for example i have this component
<publish-blog>
<VueTrix v-model="form.editorContent">
</publish-blog>
so the form.editorContent there refers to the publish-blog component's form.editorContent inside data, how do I do that ?
You can pass a prop to the publish-blog component.
This would be what ever page or component you are using the publish blog on, though to be honest I'm not sure why you would not just put the VueTrix component inside of the publish-blog component.
This would be on what ever page/component you are wanting it on.
<template>
<PublishBlog :trix="trix">
<VueTrix v-model="trix" />
</PublishBlog>
</template>
<script>
import PublishBlog from './PublishBlog.vue';
export default {
components: {
PublishBlog,
},
data() {
return {
trix: '',
};
},
};
</script>
and inside of the publish blog component make the form.editorContent the prop passed or a default value.
But without a global store/state you are stuck with props.
UPDATE: Showing what a publish blog component might look like
PublishBlog.vue
<template>
<section>
what ever goes here.
<slot />
</section>
</template>
<script>
export default {
name: 'PublishBlog',
props: {
trix: {
type: String,
default: '',
},
},
data() {
return {
form: {
editorContent: this.trix
},
};
},
};
</script>

How to pass mixins as props and use them in child components using Vue.js

I have a parent component which contains two child components called add and edit, these components have some common properties and i want to use mixins, for that i add an object called mix to the parent's data object and i pass it as props to child components as follow
the parent component :
<template>
<div id="app">
<add :mixin="mix" operation="add"></add>
...
<edit :mixin="mix" operation="edit"></edit>
</div>
</template>
<script>
export default {
name: "App",
data(){
return{
/****/
mix:{
data() {
return {
user: { name: "", email: "" },
users: []
};
},
methods: {
add() {
this.users.push(this.user);
},
}
}
}
/*****/
};
},
components: {
add,edit
}
};
</script>
I could receive that object (mix) in my child component, but how could i assign it to the mixins property?
A low hanging fruit kind of way to solve this would be to just refactor your code and write the mixin in a separate file. You can then import the mixin object in both of your components and assign it to the mixins property.

VueJS Passing computed data as a prop returns undefined

I've tried and tried, but i can't figure it out the problem. From what I could read elsewhere, the variable passed to the child component gets sent as undefined before the data is available in the parent.
Please see here for reference:
the code in codesandbox
<template>
<div id="app">
<child :parentData="data.message"/>
</div>
</template>
<script>
import Child from "./components/Child";
export default {
name: "App",
components: {
Child
},
computed: {
quote() { return 'Better late than never' }
},
data() {
return {
data: { message: this.quote } ,
thisWorks: { message: "You can see this message if you replace what is passed to the child" }
};
}
};
</script>
Then in the child:
<template>
<div>
<h1>I am the Child Component</h1>
<h2> {{ parentData }}</h2>
</div>
</template>
<script>
export default {
name: "Child",
props: {
parentData: { type: String, default: "I don't have parent data" },
},
};
</script>
The answer is, you cannot access the value of this.quote because at the moment the data objectis creating, the computed object actually does not exist.
This is an alternative, we will use the created() lifecycle hook to update the value of data object:
created(){
this.data = {
message: this.quote
}
},
You don't need to change any things, just adding those line of codes is enough.
I've already tested those codes in your CodeSandbox project and it works like a charm.
Hopefully it helps!