So I've been investigating how mutating child props from the parent can be done.
I already encountered the pitfall with using v-model.
It throws this error:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "number1"
I learnt to deal with this problem by emitting events from the child and using .sync modifier in the parent.
However, I still feel like I didnt really understand whats going on under the hood in both cases.
As far as I understood, when mutating the childs properties from parent with v-model, the mutated data also mutates for any other parent importing the child.
To test this out, I built the following setup:
I have a child component with two inputs:
One using v-model, the other an emitted event to mutate the childs props.
We call it "compA":
<template>
<div>
<input
:value="number1"
#change="$emit('update:number1', $event.target.value)"
placeholder="number1emittedEvent"
/>
<input v-model="number1" placeholder="number1vmodel">
</div>
</template>
<script>
export default{
name: "compA",
props: {
number1: String
}
}
</script>
compA is being imported by compZ and compB. compZ also imports compB, and then compZ is finally being imported by myComplexView5.vue (my project uses routing).
compB:
<template>
<div>
<h1>compB containing compA</h1>
<compA/>
<p v-text="number1"></p>
</div>
</template>
<script>
import compA from "#/components/complexComponent5/compA.vue"
export default {
name: "compB",
components: {
compA
},
data(){
return {
number1:''
}
}
}
</script>
compZ:
<template>
<div>
<compA :number1.sync="number1"/>
<br>
<compB />
</div>
</template>
<script>
import compA from "#/components/complexComponent5/compA.vue"
import compB from "#/components/complexComponent5/compB.vue"
export default {
name: "compZ",
components: {
"compA" : compA,
"compB" : compB
},
data(){
return {
number1:''
}
}
}
</script>
myComplexView5.vue:
<template>
<div>
<h1>And More testing</h1>
<compZ />
</div>
</template>
<script>
import compZ from "#/components/complexComponent5/compZ.vue"
export default{
name: "myComplexView5",
components: {
compZ
}
}
</script>
Now, I expected the following behavior:
When inputting into the inputfield which has v-model, I'd expect the paragraph inside compB to display the changes, since its text value is bound to the number1 prop from child compA. Since I mutated the childs props directly, the changed value should show up in any of the childs parents (or grandparents and so on).
But it doesnt. And it gets even better: When I use the inputfield with the event emitting from child to parent, the paragraph inside compB receives the changes!
This is basically the opposite of what I've learned from the sync modifier docs:
In some cases, we may need “two-way binding” for a prop. Unfortunately, true two-way binding can create maintenance issues, because child components can mutate the parent without the source of that mutation being obvious in both the parent and the child.
That’s why instead, we recommend emitting events in the pattern of update:myPropName. For example, in a hypothetical component with a title prop, we could communicate the intent of assigning a new value with:
this.$emit('update:title', newTitle)
Then the parent can listen to that event and update a local data property, if it wants to
Maybe this is caused by the data() inside compB which sets number1 to EMPTYSTRING when the component rerenders? I don't know, I'm very new to vue and I don't really understand when components rerender. I also used this
data() {
return {
number1: ''
}
}
inside compB To prevent this error from occuring:
[Vue warn]: Property or method is not defined on the instance but referenced during render.
I don't really know how else to prevent this error from occuring, since it seems that even though compB imported the prop from compA, the prop still needs to be declared in compB Oo
EDIT:
I just found out that in the code I'm using, compZ still had this paragraph element:
<p v-text="number1"></p>
The string inputted into the inputfield appeared there, NOT in the paragraph element of compB. For some reason, even though I get no errors, neither through the emitted event from compA, nor through the v-model from compA does any change to the props inside compA show any impact inside compB... :(
I put your example code in a snippet at the bottom of the post so that we're on the same page about what code is actually being talked about (I removed the line with the <p v-text="number1"></p> from compB, because that component doesn't have a prop or data/computed property for number1).
Your compA component takes in a number1 prop, which it then uses for two inputs:
The first input uses the prop as its value and then emits an 'update:number1' event with the input's value in response to the input's change event. This allows you to specify the .sync modifier when the parent component binds a value to the compA component's number1 prop (as you do in compZ).
The second input directly binds the value of the number1 prop via v-model. This is not recommended (for reasons I'll explain), which is why you are seeing the "Avoid mutating a prop directly" warning. The effect of directly binding the number1 prop to this input is that whenever the value of this second input changes, the value of number1 will change, thus changing the value of the first input, and finally causing that first input to emit the update:number1 event. Technically Vue allows you to do this, but you can see how it can get confusing.
Your compB component simply renders some texts and a compA component without passing a value as a number1 prop to compA.
Your compZ component renders a compA component, binding its own number1 property value with the .sync modifier to the compA component. This compA component instance does not share any data with the compA component instance in the compB component, so we can't expect any changes to either component to affect the other.
Vue.component('compA', {
template: `
<div>
<input
:value="number1"
#change="$emit('update:number1', $event.target.value)"
placeholder="number1emittedEvent"
/>
<input v-model="number1" placeholder="number1vmodel">
</div>
`,
props: {
number1: String
}
})
Vue.component('compB', {
template: `
<div>
<h1>testString</h1>
<compA />
</div>
`
})
Vue.component('compZ', {
template: `
<div>
<compA :number1.sync="number1"/>
<br>
<compB />
</div>
`,
data() {
return {
number1: ''
}
}
})
new Vue({
el: '#app'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div>
<h1>And More testing</h1>
<comp-z/>
</div>
</div>
Related
Currently trying to use a method belonging to the parent
<p class="message-date text-center">
{{ $emit('format_date_day_month_year_time', message.date) }}
</p>
However I am getting the error.
Converting circular structure to JSON
--> starting at object with constructor 'Object'
How can I call a function inside a child component that does not rely on an event? I apologize for asking such a simple question but everything I was able to find on google is using $emit and using an event.
$emit was designed to only trigger an event on the current instance of vue. Therefore, it is not possible to receive data from another component this way.
For your case, I would suggest to use Mixins especially if you need to use certain functions among multiple vue components.
Alternately, let the child component call the the parent through $emit then receive the result from the parent through a prop.
Your code could be something as follows:
Child component
<template>
<p class="message-date text-center">
{{ date }}
</p>
</template>
<script>
export default {
name: 'Child',
props: {
date: String,
},
mounted() {
this.$emit("format-date", message.date);
},
}
</script>
Parent component
<template>
<Child :date="childDate" #format-date="formatChildDate" />
</template>
<script>
import Child from '#/components/Child';
export default {
components: {
Child,
},
data: () => ({
childDate: '',
}),
methods: {
formatChildDate(date) {
this.childDate = this.formatDateDayMonthYearTime(date)
},
formatDateDayMonthYearTime(date) {
//return the formatted date
},
},
}
</script>
with $emit you call a function where the Parent can listento.
where you are using it i would suggest a computed prop of the function.
But back to your Question here is a example of emiting and listen.
//Parent
<template>
<MyComponent #childFunction="doSomethingInParent($event)"/>
</template>
//Child
<template>
<button #click="emitStuff">
</template>
.....
methods:{
emitStuff(){
this.$emit(childFunction, somedata)
}
with the event you can give Data informations to a Parentcomponent.
Please see this minium example
1
This minimum template won't throw any error since the parent is not rendering anything.
<template>
<div v-if="false">
{{ i.dont.have.the.value }}
</div>
</template>
2
Now if I create a Modal wrapper with default slot, and still using v-if
Modal.vue
<template>
<div v-if="show">
<slot />
</div>
</template>
<script>
export default {
props: {
show: Boolean,
},
};
</script>
However, this will throw Error, Why?
App.vue
<template>
<Modal :show="false">
<div>{{ i.dont.have.the.value }}</div>
</Modal>
</template>
<script>
import Modal from "#/components/Modal.vue";
export default {
components: {
Modal,
},
};
</script>
[Vue warn]: Error in render: "TypeError: Cannot read property 'dont' of undefined"
Why is this happening?
How can I create a Modal with guarantee children won't be excuted if not show?
I can't directly use v-if in the Modal component because I need a transition.
You made a mistake about the slot in App.vue because VueJS will try to resolve your i.dont.have.the.value variable based on parent scope (your slot is not conditioned)
Remember this rule from official documentation
Everything in the parent template is compiled in parent scope; everything in the child template is compiled in the child scope.
https://v2.vuejs.org/v2/guide/components-slots.html#Compilation-Scope
Just define the variable in the right scope and you will be fine (if you define it in child component and want to expose it to parent, use a scoped slot then).
I have a component
In the component I have
export default {
name: "Modal",
props: ['showFooter'],
}
Then in the template i have
<template>
<div class='modal'>
<div class='footer' v-if='showFooter'>This is the footer</div>
</div>
</template>
THe footer doesnt display at all, if i pass the prop or not. It just says
[Vue warn]: Property or method "showFooter" is not defined on the instance but referenced during render.
Whats the issue here??
Ive also just tried
<template>
<div class='modal'>
<div class='footer' v-if='showFoot'>This is the footer</div>
</div>
</template>
export default {
name: "Modal",
props: ['showFooter'],
data(){
return {
showFoot:this.showFooter
}
}
}
Then I get told showFoot isnt defined. Which is even wierder because Im defining it right there in the data!??
Even if I just remove the property totally, and just define it in data
export default {
name: "Modal",
data(){
return {
showFoot:true
}
}
}
I still get
[Vue warn]: Property or method "showFoot" is not defined on the instance but referenced during render.
So wierd and I cant for the life of me figure it out
Its almost like the template has no access to anything I define in the data() or props of the componenet. Never seen this before
i think so you are not passing props from parent component. Ideally you can pass it from parent component and receive it in Children.
<childComponent :yourPropNameInChild="anyDataFromParentYouWantToPass" />
here this ChildComponent is rendered in your parent component.
to receive it in children you can do
export default {
name: "Modal",
props: ['yourPropNameInChild'],
}
as of for your example
<modal :showFooter="showFooter" />
in your parent you have to write above line of code
and in your child you have to receive it as you are doing
also in your example you were missing script tag
Component Code
<template lang="html">
<div class="chat-users">
<ul class="list-unstyled">
<li v-for="chatuser in chatusers" class="left clearfix">
{{ chatuser.UserName }}
</li>
</ul>
</div>
</template>
<script>
export default {
props: ["chatusers"],
created() {
axios.post("some url").then(response => {
this.chatusers = response.data.Data;
});
}
}
</script>
<style>
</style>
Everything works perfectly, but I am getting below warning.
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders. Instead, use a data or
computed property based on the prop's value. Prop being mutated:
"chatusers"
There is an explanation why prop should not be mutate in the vue documentation. If you need to mutate the state, perhaps you need a data instead.
Like:
export default {
data () { return { chatusers: null } },
created() {
axios.post("some url").then(response => {
this.chatusers = response.data.Data;
});
}
}
It is not the best way to mutate the props, since the parent is in control of this data and any changes it will overwrite child data, from the docs:
In addition, every time the parent component is updated, all props in
the child component will be refreshed with the latest value. This
means you should not attempt to mutate a prop inside a child
component. If you do, Vue will warn you in the console.
According to Vue.js data flow, the props received from parent should not be modified by child component. See the official documentation and this Stack Overflow answer.
As suggested by your warning: "use a data or computed property based on the prop's value", that in your case is the response received from axios call.
This is an easy question but I dont know the best way to do it correctly with Vue2:
Parent Component:
<template>
<div class="parent">
<child></child>
{{value}} //How to get it
</div>
</template>
Child Component:
<template>
<div class="child">
<p>This {{value}} is 123</p>
</div>
</template>
<script>
export default {
data: function () {
return { value: 123 }
}
}
</script>
Some ways you can achieve this include:
You can access the child VM directly via a ref, but this gets hairy because the ref won't be populated until the child component has mounted, and $refs isn't reactive, meaning you'd have to do something hacky like this:
<template>
<div class="parent">
<child ref="child"></child>
<template v-if="mounted">{{ $refs.child.value }}</template>
</div>
</template>
data() {
return {
mounted: false
}
},
mounted() {
this.mounted = true;
}
$emit the value from within the child component so that the parent can obtain the value when it is ready or whenever it changes.
Use Vuex (or similar principles) to share state across components.
Redesign the components so that the data you want is owned by the parent component and is passed down to the child via a prop binding.
(Advanced) Use portal-vue plugin when you want to have fragments of a component's template to exist elsewhere in the DOM.
Child components pass data back up the hierarchy by emitting events.
For example, in the parent, listen for an event and add the child value to a local one (an array for multiple child elements would be prudent), eg
<child #ready="childVal => { value.push(childVal) }"></child>
and in the child component, $emit the event on creation / mounting, eg
mounted () {
this.$emit('ready', this.value)
}
Demo ~ https://jsfiddle.net/p2jojsrn/