Problem
Let's say I have a vue component called:
Note: All vue components has been simplified to explain what I'm trying to do.
reusable-comp.vue
<template>
<div class="input-group input-group-sm">
<input type="text" :value.number="setValue" class="form-control" #input="$emit('update:setValue', $event.target.value)">
<span>
<button #click="incrementCounter()" :disabled="disabled" type="button" class="btn btn-outline-bordercolor btn-number" data-type="plus">
<i class="fa fa-plus gray7"></i>
</button>
</span>
</div>
</template>
<script>
import 'font-awesome/css/font-awesome.css';
export default {
props: {
setValue: {
type: Number,
required: false,
default: 0
}
},
data() {
return {
}
},
methods: {
incrementCounter: function () {
this.setValue += 1;
}
}
}
</script>
Then in a parent component I do something like this:
subform.vue
<div class="row mb-1">
<div class="col-md-6">
Increment Value of Num A
</div>
<div class="col-md-6">
<reuseable-comp :setValue.sync="numA"></reuseable-comp>
</div>
</div>
<script>
import reusableComp from '../reusable-comp'
export default {
components: {
reusableComp
},
props: {
numA: {
type: Number,
required: false,
default: 0
}
},
data() {
return {
}
}
</script>
then lastly
page_layout.vue
<template>
<div>
<subform :numA.sync="data1" />
</div>
</template>
<script>
import subform from '../subform.vue'
export default {
components: {
subform
},
data() {
return {
data1: 0
}
}
}
</script>
Question
So, how do I sync a value between reusable-comp.vue, subform.vue, and page_layout.vue
I'm using reuseable-comp.vue is many different places. I'm using subform.vue only a couple times in page_layout.vue
And I'm trying to use this pattern several times. But I can't seem to get this to work. The above gives me an error:
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: "numA"
Okay I found a solution that worked.
In subform.vue, we change:
data() {
return {
numA_data : this.numA
}
}
So we now have reactive data to work with. Then in the template, we refer to that reactive data instead of the prop:
<reuseable-comp :setValue.sync="numA_data"></reuseable-comp>
Then finally we add a watcher to check if the reactive data gets changed, and then emit to the parent:
watch: {
numA_data: function(val) {
this.$emit('update:numA', this.numA_data);
}
}
Now all values from grandchildren to parent are synced.
Update (4/13/2018)
I made new changes to the reusable-comp.vue:
I replaced where it says 'setValue' to 'value'
I replaced where it says 'update:value' to 'input'
Everything else says the same.
Then in subform.vue:
I replaced ':setValue.sync' to 'v-model'
v-model is two way binding, so I made use of that where it needed to be. The sync between the parent-child (not child to grandchild), is still using sync modifier, only because the parent has many props to pass. I could modify this where I could group up the props as a single object, and just pass that.
Related
I am building an MVP and this is the first time I do web development. I am using Vue2 and Firebase and so far, things go well.
However, I ran into a problem I cannot solve alone. I have an idea how it SHOULD work but cannot write it into code and hope you guys can help untangle my mind. By now I am incredibly confused and increasingly frustrated :D
So lets see what I got:
Child Component
I have built a child component which is a form with three text-areas. To keep it simple, only one is included it my code snippets.
<template>
<div class="wrap">
<form class="form">
<p class="label">Headline</p>
<textarea rows="2"
v-model="propHeadline"
:readonly="readonly">
</textarea>
// To switch between read and edit
<button
v-if="readonly"
#click.prevent="togglemode()">
edit
</button>
<button
v-else
type="submit"
#click.prevent="togglemode(), updatePost()"
>
save
</button>
</form>
</div>
</template>
<script>
export default {
name: 'PostComponent'
data() {
return {
readonly: true
}
},
props: {
propHeadline: {
type: String,
required: true
}
},
methods: {
togglemode() {
if (this.readonly) {
this.readonly = false
} else {
this.readonly = true
}
},
updatePost() {
// updates it to the API - that works
}
}
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
And my parent component:
<template>
<div class="wrap">
<PostComponent
v-for="post in posts"
:key="post.id"
:knugHeadline="post.headline"
/>
</div>
</template>
<script>
import PostComponent from '#/components/PostComponent.vue'
export default {
components: { PostComponent },
data() {
return {
posts: []
}
},
created() {
// Gets all posts from DB and pushes them in array "posts"
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
Current Status
So far, everything works. I can display all posts and when clicking on "edit" I can make changes and save them. Everything gets updated to Firebase - great!
Problem / Error Message
I get the following error message:
[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.
As the error says I should use a computed property based on the props value. But how can I achieve that?
Solution Approach
I believe I have to use a computed getter to return the prop value - how to do that?
And then I have to use the setter to emit an event to the parent to update the value so the prop passes it back down - how to do that?
I have found bits and pieces online but by now all I see is happy families passing around small packages of data...
Would be really thankful for a suggestion on how to solve this one! :)
Thanks a lot!
This error shows because of your v-model on texterea which mutate the prop, but in vue it is illegal to mutate props :
<textarea rows="2"
v-model="propHeadline"
:readonly="readonly">
</textarea>
So, what you could do is to use this created() lifecycle hook and set the propHeadline prop as data :
<script>
export default {
name: 'PostComponent'
data() {
return {
readonly: true,
headline: ""
}
},
props: {
propHeadline: {
type: String,
required: true
}
},
created() {
this.headline = this.propHeadline
}
}
</script>
An then, update the new variable on your textarea :
<textarea rows="2"
v-model="headline"
:readonly="readonly">
</textarea>
I have a question why not this component, not re-rendering after changing value so what I'm trying to do is a dynamic filter like amazon using the only checkboxes so let's see
I have 4 components [ App.vue, test-filter.vue, filtersInputs.vue, checkboxs.vue]
Here is code sandbox for my example please check the console you will see the value changing https://codesandbox.io/s/thirsty-varahamihira-nhgex?file=/src/test-filter/index.vue
the first component is App.vue;
<template>
<div id="app">
<h1>Filter</h1>
{{ test }}
<test-filter :filters="filters" :value="test"></test-filter>
</div>
</template>
<script>
import testFilter from "./test-filter";
import filters from "./filters";
export default {
name: "App",
components: {
testFilter,
},
data() {
return {
filters: filters,
test: {},
};
},
};
</script>
so App.vue that holds the filter component and the test value that I want to display and the filters data is dummy data that hold array of objects.
in my test-filter component, I loop throw the filters props and the filterInputs component output the input I want in this case the checkboxes.
test-filter.vue
<template>
<div class="test-filter">
<div
class="test-filter__filter-holder"
v-for="filter in filters"
:key="filter.id"
>
<p class="test-filter__title">
{{ filter.title }}
</p>
<filter-inputs
:value="value"
:filterType="filter.filter_type"
:options="filter.options"
#checkboxChanged="checkboxChanged"
></filter-inputs>
</div>
</div>
<template>
<script>
import filterInputs from "./filterInputs";
export default {
name: "test-filter",
components: {
filterInputs,
},
props:{
filters: {
type: Array,
default: () => [],
},
value: {
type: Array,
default: () => ({}),
},
},
methods:{
checkboxChanged(value){
// Check if there is a array in checkbox key if not asssign an new array.
if (!this.value.checkbox) {
this.value.checkbox = [];
}
this.value.checkbox.push(value)
}
};
</script>
so I need to understand why changing the props value also change to the parent component and in this case the App.vue and I tried to emit the value to the App.vue also the component didn't re-render but if I check the vue dev tool I see the value changed but not in the DOM in {{ test }}.
so I will not be boring you with more code the filterInputs.vue holds child component called checkboxes and from that, I emit the value of selected checkbox from the checkboxes.vue to the filterInputs.vue to the test-filter.vue and every component has the value as props and that it if you want to take a look the rest of components I will be glad if you Did.
filterInpust.vue
<template>
<div>
<check-box
v-if="filterType == checkboxName"
:options="options"
:value="value"
#checkboxChanged="checkboxChanged"
></check-box>
</div>
</template>
<script>
export default {
props: {
value: {
type: Object,
default: () => ({}),
},
options: {
type: Array,
default: () => [],
},
methods: {
checkboxChanged(value) {
this.$emit("checkboxChanged", value);
},
},
}
</script>
checkboxes.vue
<template>
<div>
<div
v-for="checkbox in options"
:key="checkbox.id"
>
<input
type="checkbox"
:id="`id_${_uid}${checkbox.id}`"
#change="checkboxChange"
:value="checkbox"
/>
<label
:for="`id_${_uid}${checkbox.id}`"
>
{{ checkbox.title }}
</label>
</div>
</div>
<template>
<script>
export default {
props: {
value: {
type: Object,
default: () => ({}),
},
options: {
type: Array,
default: () => [],
},
}
methods: {
checkboxChange(event) {
this.$emit("checkboxChanged", event.target.value);
},
},
};
</script>
I found the solution As I said in the comments the problem was that I'm not using v-model in my checkbox input Vue is a really great framework the problem wasn't in the depth, I test the v-model in my checkbox input and I found it re-render the component after I select any checkbox so I search more and found this article and inside of it explained how we can implement v-model in the custom component so that was the solution to my problem and also I update my codeSandbox Example if you want to check it out.
Big Thaks to all who supported me to found the solution: sarkiroka, Jakub A Suplick
My component template:
<template>
<section class="stage my-5">
<div class="stage-heading">
<h3 class="stage-number mb-4">Stage {{stage}}</h3>
<h6 class="stage-hrs">Total Hrs: {{totalHours}}</h6>
</div>
<div class="stage-courses card text-white bg-info mb-3" v-for="course in courses" :key="course.id">
<div class="card-header">Stage {{course.stage}}</div>
<div class="card-body">
<h5 class="card-title">{{course.title}}</h5>
<p class="card-text">{{course.creator}}</p>
<p class="card-text">{{course.hours}} Hours</p>
</div>
</div>
</section>
</template>
The state in my Vuex store:
const state = {
roadmapStage1: [],
roadmapStage2: [],
roadmapStage3: [],
};
I have getters in my Vuex store that look like:
getRoadmapStage1: state => state.roadmapStage1,
getRoadmapStage2: state => state.roadmapStage2,
getRoadmapStage3: state => state.roadmapStage3,
I'm trying to dynamically call one of these getters from a component, which one depends on a prop of the component:
export default {
name: "Stage",
data() {
return {
courses: []
}
},
props: ['stage'],
computed: mapGetters({courses: 'getRoadmapByStage'})
}
Is there any way to insert the prop into the 'getRoadmapByStage'? e.g. so it functions like
getRoadmapByStage${stage}?
Ultimately i'm trying to get the component to re-render anytime one the roadmapStage arrays are updated. Thanks!
I would suggest using a getter with a parameter for the stage id/number that returns the roadmap you want, like so:
// in getters.js
//gets the current roadmap based on the stage number
getRoadmapByStage: (state) => (stageNumber) => {
return state["roadmapStage" + stageNumber];
}
now in your component you can have:
computed: {
currentRoadmap() {
// now we'll pass in your 'stage' prop to get the appropriate map
// this will re-render the component as that prop changes
return this.$store.getters.getRoadmapByStage(this.stage);
}
}
You can declare your computed roadmap property as follows:
computed: {
roadmap() {
return this.stage ? this.$store.getters['getRoadmapByStage' + this.stage] : undefined
},
}
That way you are getting the roadmap by the value of the prop or undefined if the prop is not set to anything.
I am quite new with VueJS and I have been having trouble lately with some computed properties which do not update as I would like. I've done quite some research on Stack Overflow, Vue documentation and other ressources but i haven't found any solution yet.
The "app" is basic. I've got a parent component (Laundry) which has 3 child components (LaundryMachine). The idea is to have for each machine a button which displays its availability and updates the latter when clicked on.
In order to store the availability of all machines, I have a data in the parent component (availabilities) which is an array of booleans. Each element corresponds to a machine's availability.
When I click on the button, I know the array availibities updates correctly thanks to the console.log. However, for each machine, the computed property "available" does not update is I would want it to and I have no clue why.
Here is the code
Parent component:
<div id="machines">
<laundry-machine
name="AA"
v-bind:machineNum="0"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
<laundry-machine
name="BB"
v-bind:machineNum="1"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
<laundry-machine
name="CC"
v-bind:machineNum="2"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
</div>
</div>
</template>
<script>
import LaundryMachine from './LaundryMachine.vue';
export default {
name: 'Laundry',
components: {
'laundry-machine': LaundryMachine
},
data: function() {
return {
availabilities: [true, true, true]
};
},
methods: {
editAvailabilities(index) {
this.availabilities[index] = !this.availabilities[index];
console.log(this.availabilities);
}
}
};
</script>
Child component:
<template>
<div class="about">
<h2>{{ name }}</h2>
<img src="../assets/washing_machine.png" /><br />
<v-btn color="primary" v-on:click="changeAvailability">
{{ this.availability }}</v-btn>
</div>
</template>
<script>
export default {
name: 'LaundryMachine',
props: {
name: String,
machineNum: Number,
availableArray: Array
},
methods: {
changeAvailability: function(event) {
this.$emit('change-avlb', this.machineNum);
console.log(this.availableArray);
console.log('available' + this.available);
}
},
computed: {
available: function() {
return this.availableArray[this.machineNum];
},
availability: function() {
if (this.available) {
return 'disponible';
} else {
return 'indisponible';
}
}
}
};
</script>
Anyway, thanks in advance !
Your problem comes not from the computed properties in the children, rather from the editAvailabilities method in the parent.
The problem is this line in particular:
this.availabilities[index] = !this.availabilities[index];
As you can read here, Vue has problems tracking changes when you modify an array by index.
Instead, you should do:
this.$set(this.availabilities, index, !this.availabilities[index]);
To switch the value at that index and let Vue track that change.
Form (parent) component:
export default {
template: `
<form name="form" class="c form" v-on:change="onChange">
<slot></slot>
</form>`,
methods: {
onChange:function(){
console.log("something changed");
}
}
};
and a c-tool-bar component (child) that is (if existing) inside the c-form's slot
export default {
template: `
<div class="c tool bar m006">
<div v-if="changed">
{{ changedHint }}
</div>
<!-- some other irrelevant stuff -->
</div>`,
props: {
changed: {
type: Boolean,
default: false,
},
changedHint: String
}
};
What I want to achieve is, that when onChange of c-form gets fired the function
checks if the child c-tool-bar is present && it has a changedHint text declared (from backend) it should change the c-tool-bar's prop "changed" to true and the c-bar-tool updates itself so the v-if="changed" actually displays the changedHint. I read the vue.js docs but I don't really know where to start and what the right approach would be.
You can use the two components like this:
<parent-component></parent-component>
You will be passing in the :changed prop a variable defined in the parent component in data(). Note the :, we are using dynamic props, so once the prop value is changed in the parent it will update the child as well.
To change the display of the hint, change the value of the variable being passed in as prop to the child component:
export default {
template: `
<form name="form" class="c form" v-on:change="onChange">
<child-component :changed="shouldDisplayHint"></child-component>
</form>`,
data() {
return {
shouldDisplayHint: false,
};
},
methods: {
onChange:function(){
this.shouldDisplayHint = true; // change the value accordingly
console.log("something changed");
}
}
};
One simple way is to use $refs.
When you will create your child component inside your parent component, you should have something like: <c-tool-bar ref="child"> and then you can use it in your parent component code:
methods: {
onChange:function(){
this.$refs.child.(do whatever you want)
}
}
Hope it helps but in case of any more questions: https://v2.vuejs.org/v2/guide/components.html#Child-Component-Refs