This is my first Vue app so go easy on the code :P
I have a list of users on one side and a component to edit those user details on the other. When selecting a user you see their details and can then click 'edit details'. I want to hide the edit details and show the user details when a new user is selected. Inside the component, I have a ShowEdit variable that is true or false and will either show or hide the edit area. I am sending a prop from the parent into this component when a new user is selected to hide the edit if it is open. I feel I am close as it is currently working perfect but I would like to get rid of the error
"Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders...."
Here are the important bits:
Home
<transition name="component-fade" mode="out-in">
<sidebar-client-content :theClient="selectedUser" :showEditHP="showEditHP"></sidebar-client-content>
</transition>
activate: function(el) {
this.showEditHP = true // Hide edit if new user selected
},
Component
<div id="sidebar-client-content">
<div class="staff_header">
<a v-if="showEdit" v-on:click="showEdit = !showEdit"><i class="fal fa-edit"></i>Edit</a>
<a v-if="!showEdit" v-on:click="showEdit = !showEdit"><i class="far fa-times"></i>Close</a>
</div>
<transition-group name="fadeHeight" mode="out-in">
<div class="client_information" v-if="showEdit">
<!-- The Client Details -->
</div>
<div class="client_information" v-if="!showEdit">
<!-- Client Edit Form -->
</div>
</transition-group>
</div>
export default {
props: [
'showEditHP', // Close edit if new user selected
],
computed: {
showEdit: {
get: function() {
return this.showEditHP
},
set: function (newValue) {
this.showEditHP = newValue
}
}
},
I understand the this.showEditHP = newValue line is where I make the edit but I can't seem to get it to work any other way. I want the parent to be able to overwrite it as the error says. Is there a way to achieve this and have the error removed?
Thanks.
As Nguyun said you can use $emit to send the value back to your parent to keep the values in sync. You can only change your parent data with emit otherwise it will just remain either true or false while the child continues to make it's changes. Keep the values in sync with emit and then use watch to check if the parent makes it's change.
<transition name="component-fade" mode="out-in">
<sidebar-client-content #clicked="onClickChild" :theClient="selectedUser" :showEditHP="showEditHP"></sidebar-client-content>
</transition>
onClickChild (value) {
// I am the child value inside the parent!
},
Then in the child
<div class="staff_header">
<a v-if="showEdit" v-on:click="showEdit = !showEdit; $emit('clicked', showEdit)"><i class="fal fa-edit"></i>Edit</a>
<a v-if="!showEdit" v-on:click="showEdit = !showEdit; $emit('clicked', showEdit)"><i class="far fa-times"></i>Close</a></i>Close</a>
</div>
The warning is really clear
In your child component, you're trying to mutate the value of showEditHP:
set: function (newValue) {
this.showEditHP = newValue
}
However, this kind of mutating is discouraged. Instead, we should emit an event from the child component and then let the parent component listen to that event and mutate the property itself.
In other words, the property showEditHP belongs to the parent component, well then the parent component should be the one which mutates it.
Related
I have build a really simple app of notes, just for practice, i have a two components in one view, one is for form, and the other is for list of notes. The notes listed have two actions buttons, one for edit, and one for delete. When i click in edit button, it trigger an event to a parent component with a note selected and then it fill the data variable 'noteEdit' and pass this data in prop to a form component to will update.
When i update the note selected, and i try to click again in edit button from the same note, the event click is trigger but the form is not recived the prop anymore, i must to click in edit button from another note listed, and then its works normaly. Is like cant trigger the event in the same element consecutively.
in vuex index.js
async editar(context,payload){
try {
let response = await axios.put(`/nota/${payload._id}`,payload)
console.log(response.data.nota)
context.commit('actualizar',response.data.nota)
return [true,'Eliminado']
} catch (error) {
return [false,error]
}
}
in view Notes
<div class="row">
<formulario :notaEdit="notaEdit" class="col-md-4"></formulario>
<lista #nota-edit="notaEdit = $event" class="col-md-8"></lista>
</div>
...
data() {
return {
notaEdit: []
};
},
in list component
<div class="w-50 pl-3">
<h4>Acciones</h4>
<b-button block href="#" variant="primary" #click="$emit('nota-edit',nota)">Editar</b-button>
<b-button block href="#" variant="outline-danger" #click="remove(nota._id)">Eliminar</b-button>
</div>
view of the app
View with 2 components
List component
Form component
Evening. I've created a button which adds a component that has an input field inside. I might need to press that button few times so there would be 2-3 input fields that appear. Whenever I type the text I would like to send a request from the parent component but I don't know how to retrieve the data from every child component that has been created. Is this the time to start using vuex (never used it)?
ParentComponent.vue
<template>
<div>
<button class="btn btn-success" #click="addStep">Add step</button>
<div v-for="i in count">
<recipe-step v-bind:step-number="i"></recipe-step>
</div>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
addStep() {
this.count += 1;
}
}
}
</script>
StepComponent.vue
<template>
<div>
<div class="from-group">
<label for="step-input"></label>
<input id="step-input" v-model="text" type="text">
</div>
</div>
</template>
<script>
export default {
props: {
stepNumber: {
type: Number,
required: true
}
},
data() {
return {
step: this.stepNumber,
text: ""
}
}
}
</script>
No, you really don't need Vuex yet. As long as you are still dealing with parent-child-component communication, you should be fine. Vuex comes into play when components, spread across the hole component hierarchy, need to exchange information.
Now, you should do something like this:
Don't store the text in the child component. When the input changes, send a Custom Event right to the parent component. Note that
<input v-model="text">
is only syntax sugar for
<input :value="text" #input="text = $event">
Both have the same effect. That's way you can send the input event up to the parent, like this:
<input #input="$emit('input', $event)">
Add another prop to your child component called value which should replace text.
And now you can use v-model in the parent component:
<recipe-step v-model="text">
To store multiple values, just use an array in your data properties.
<recipe-step v-model="textArray[i]">
Vuex can help you on that, however if all you want is to get the input text value back to the parent with the minimum effort you can create a prop called value in the children and then pass it as v-model in the parent.
Since you have a v-for you could make it iterate over a list instead a counter and then pass some prop inside each item as v-model
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/
I have a parent component that contains an array and an object:
data() {
return {
products: [],
attributes: {},
}
},
When my parent component is loaded, it gets some AJAX data and populates the variables:
mounted() {
axios.get('/requests/getProducts/' + this.currentCategory).then(response => {
this.products = response.data;
this.createAttributes(); // runs some methods to populate the attributes object
});
},
I then have a child component that I will use to display the attributes. Code from parent template:
<search-attributes :attributes="attributes"></search-attributes>
I have the props declared in my child component:
props: ['attributes'],
So far so good, I can console log the data from parent and child...
Problem is when I try to v-for the attributes in the child component, it simply returns nothing:
/////////////// this does not render anything ///////////////
<template>
<div>
<div v-for="(value, key) in attributes">
{{ key }}
</div>
</div>
</template>
However, if I pass both the products and attributes variables to the child component, and render products in the child component, attributes will start working!
/////////////// this works /////////////
<template>
<div>
<div v-for="(value, key) in attributes">
{{ key }}
</div>
{{ products }}
</div>
</template>
What the heck is going on?
Do you need to see my parent methods?
I expect you are running into a change detection caveat. Vue cannot detect when you add properties dynamically to an object. In this case, you begin with an empty attributes object. If you add properties to it without using $set, then Vue does not understand a change has occurred and will not update the DOM.
Update the createAttributes to use $set.
The reason it works when you pass products is Vue can detect the change you make (this.products = response.data), so it renders the DOM, which shows the latest attributes as a by product.
I have this fiddle where if you use the inputSpinner and then use filter at the top the value of the input spiner stays the same
JsFiddle
My problem is like this
Code
<script type="text/x-template" id="grid-template">
<div class="container" style="margin-top:10px;">
<ul class="list-group">
<li v-for="entry in filteredData" class="list-group-item" >{{entry.name}} <div><numberinputspinner
:min="0"
:max="2"
:step="0.0001"
:card="entry"
#newNumber="updateTable"
/></div></li>
</ul>
</div>
</script> ....
When you filter the data, Vue destroys/recreate the unused components (for example if looking for Bruce Lee, would destroy Chuck Norris), as components get destroyed you will lose the data, as is not persistent.
You need to keep that data sync with the parent so when recreated again it would reassign its previous value.
Here's the updated jsFiddle: https://jsfiddle.net/myeu0sL3/9/
What I just did was pass the data to the parent in the newNumber event, and assign it to the collection, like this:
updateTable:function(card, data){
card.value = data;
}
and then in the component just assign it whenever is passed:
data: function () {
const vm = this;
return {
numericValue: vm.card.value,
};
},
and emit the card in the newNumber event:
this.$emit('newNumber', this.card, parseFloat(val).toFixed(4), parseFloat(oldVal).toFixed(4) );
Oh and finally there was a mistake in your props declaration, you had it twice, so I merged to include the cards:
card: {
type: Object,
required: true
},
That's it :)