I have a vue, which uses an accordion table that displays the data when a particular row is selected. There is a button "Edit" which hides the data and shows a form.
The form is in another vue (to separate them out out..) The form is showing on clicking the button, however, inside the form I have another button "Save" which calls an ajax request, then hides the form and shows the data.
The problem I'm having is that I cannot seem to figure out how I can update the variable inside the first vue from the second vue. I could use the store but this is not an option as it would update for everyone, whereas it should only update for the particular user.
Enquiries vue: (HTML)
<tr v-if="row.id in expanded">
<td :colspan="9" style="background-color: #F0FFFF;">
<div class="accordian-body">
<div v-if="editing != false">
<enquiries-view-edit></enquiries-view-edit>
</div>
<div v-else>
<div class="container">
<div class="pull-right">
<button type="button" class="btn btn-primary btn-md" #click="editing = !editing">Edit</button>
</div>
</div>
</div>
</div>
</td>
</tr>
Javascript:
export default {
components: {
Multiselect
},
data() {
return {
msg: 'This is just an estimation!',
tooltip: {
actual_price: 'Click on the price to edit it.'
},
expanded: {},
replacedCounter: 0,
theModel: [],
enquiry: [],
center: {lat: 53.068165, lng: -4.076803},
markers: [],
needsAlerting: false,
editing: false
}
},
}
Inside EnquiriesVue I have:
export default {
props: ['editing'],
computed: {
editing: function {
console.log("Computed the value");
}
}
}
I have tried to compute the value, but this is not doing anything inside the console.
EDIT:
Basically, inside enquiries-view-edit I want a button where you click on it, and it updates the variable editing inside the Enquiries vue so that the form hides and the data vue is then shown.
A child component can communicate with its parent by emitting events. Like this:
export default {
props: ['editing'],
methods: {
onClick: function {
console.log("Clicked on child!");
this.$emit('stop-editing');
}
}
}
This assumes you have something like this in your child component's template:
<button #click="onClick">Stop editing</button>
You could then "listen" to that event from the parent component as you would any other native event, by registering a handler when including the child component.
<enquiries-view-edit #stop-editing="editing = !editing"></enquiries-view-edit>
Obviously this is a very simple example. You can also pass data along with your emitted event for more complex scenarios.
Related
I am using Vue Formulate and would like to customise the 'add more' button for group repeatable fields. I'm using a custom slot component which is working fine, however I can't figure out what the click event I need to use is so that when my button is clicked it actually adds another field. The same applies to a custom remove button component. I couldn't see anywhere in the docs of how to set this. So far I have this:
<template>
<a :for="context.id" #click="context.addMore()">
{{ context.addLabel }}
</a>
</template>
<script>
export default {
props: {
context: {
type: Object,
required: true
},
}
}
</script>
context.addMore() does not work
You have to add addMore to props, it's not inside the context object.
<template>
<a :for="context.id" #click="addMore">
{{ context.addLabel }}
</a>
</template>
<script>
export default {
props: {
context: {
type: Object,
required: true
},
addMore: {
type: Function
},
}
}
</script>
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>
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.
I'm used to approaching this problem the React way, with 1-way data binding and state, so I'm having trouble thinking about it with Vue.
I have a map that renders points based on the lat/lng of news stories. When a user changes the value of a select, the points on the map update. When the user clicks on a point, a popup opens with a link to the story. However, I cannot get these two functionalities to work together.
Here's my Vue template:
<div class="row">
<div id="map" class="map"></div>
</div>
<select v-model="selectedMonth">
<option disabled value="">Please select one</option>
<option>January 2018</option>
<option>December 2017</option>
<option>November 2017</option>
<option>October 2017</option>
</select>
<button v-on:click="reset">Reset all</button>
<div class="row">
<div class="col col-xs-12 col-sm-6 col-md-3 col-lg-3"
v-for="(story, index) in displayedStories">
<img v-bind:src="story.img_src" />
<br />
<a v-bind:href="story.url" target="_blank">{{ story.card_title }}</a>
<p>{{ story.published }}</p>
</div>
</div>
and the JS:
export default {
name: 'app',
data() {
return {
leafleftMap: null,
tileLayer: null,
markers: [],
allStories: [],
selectedMonth: null,
}
},
mounted() {
this.getNodes()
this.initMap()
},
computed: {
displayedStories() {
const displayedStories = this.selectedMonth
? dateFilter(this.selectedMonth, this.allStories)
: this.allStories
if (this.leafleftMap) {
/* remove old markers layer */
this.leafleftMap.removeLayer(this.markers)
/* create a new batch of markers */
const markers = displayedStories.map(story => L.marker(story.coords)
.bindPopup(mapLink(story))
)
const storyMarkers = L.layerGroup(markers)
/* add current markers to app state and add to map */
this.markers = storyMarkers
this.leafleftMap.addLayer(storyMarkers)
this.changedMonth = this.selectedMonth
}
return displayedStories
},
},
methods: {
getNodes() { /* make api request */ }
initMap () { /* initialize map with */ }
},
}
The problem is with the line this.leafleftMap.removeLayer(this.markers). When it's there, the markers render and change with the select button, but the popup doesn't work. When I remove that line, the popup works, but the map loses its ability to update when the select changes.
I tried adding a custom directive to the select:
<select v-model="selectedMonth" v-updateMonth>
in hopes to focus when the JavaScript is enacted:
directives: {
updateMonth: {
update: function(el, binding, vnode) {
console.log('select updated')
vnode.context.leafleftMap.removeLayer(vnode.context.markers)
}
}
},
but the directive is called whenever anything on the page changed, not just when I update the select.
I'm trying to call a function (to remove the markers) only when the select is changed, but can't seem to get that to work in Vue. It wants to call every function with every update.
I was able to solve this issue with an #change and I think I have a better understanding of when to use computed in Vue.
First, I moved a lot of the update logic out of computed:
computed: {
displayedStories() {
return this.selectedMonth
? dateFilter(this.selectedMonth, this.allStories)
: this.allStories
},
},
so that it's just returning an array. Then I added a listener to the select:
<select v-model="selectedMonth" #change="updateStories()">
and then created a new method for handling that change:
methods {
updateStories() {
const markers = displayedStories.map(story => L.marker(story.coords)
.bindPopup(mapLink(story)))
const storyMarkers = L.layerGroup(markers)
this.markers = storyMarkers
this.leafleftMap.addLayer(storyMarkers)
this.changedMonth = this.selectedMonth
},
},
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