(VueJS) Update parent data from child component - javascript

I have a parent who's passing props to a child and the child emits events to the parent. However this is not fully working and I am not sure why. Any suggestions?
Parent:
<template>
<div class="natural-language">
<div class="natural-language-content">
<p class="natural-language-results">
<msm-select :options="payments" :model="isShowingUpdate" />
</p>
</div>
</div>
</template>
<script>
import msmSelect from '../form/dropdown.vue'
export default {
components: {
'msm-select': msmSelect
},
data() {
return {
isShowingUpdate: true,
payments: [
{'value': 'paying anuualy', 'text': 'paying anuualy'},
{'value': 'paying monthly', 'text': 'paying monthly'}
],
}
}
}
</script>
Child:
<template>
<div class="form-pseudo-select">
<select :model="flagValue" #change="onChange($event.target.value)">
<option disabled value='Please select'>Please select</option>
<option v-for="(option, index) in options" :value="option.value">{{ option.text }}</option>
</select>
</div>
</template>
<script>
export default {
props: {
options: {
elType: Array
},
isShowingUpdate: {
type: Boolean
}
},
data() {
return {
selected: '',
flagValue: false
}
},
methods: {
onChange(value) {
if (this.selected !== value) {
this.flagValue = true;
} else {
this.flagValue = false;
}
}
},
watch: {
'flagValue': function() {
console.log('it changed');
this.$emit('select', this.flagValue);
}
},
created() {
console.log(this.flagValue);
this.flagValue = this.isShowingUpdate;
}
}
</script>
Basically, when the option in the select box changes, the boolean flag should be updated. However, in my child I am getting undefined for isShowingUpdate. What am I missing?

There isn't the relation that you said between the two components.
The component that you called parent is in reality the child... and the child is parent.
The parent component is always the one that calls the other, in your case:
//Parent component
<template>
...
<msm-select :options="policies" :model="isShowingUpdate" /> << the child
...
</template>
You should change the props/events between the two components.
Edit:
You can edit the:
onChange(value) {
if (this.selected !== value) {
this.flagValue = true;
} else {
this.flagValue = false;
}
}
To a new one like the following:
On the children:
onChange(value) {
if (this.selected !== value) {
this.flagValue = true;
} else {
this.flagValue = false;
}
this.$emit('flagChanged', this.flagValue)
}
On the parent use the emit event to capture and call some other method:
//HTML part:
<msm-select :options="payments" :model="isShowingUpdate" v-on:flagChanged="actionFlagChanged" />
//JS part:
methods: {
actionFlagChanged () {
//what you want
}
}
Can I give you some tips?
It's not a (very) good name of a function the one called: onChange
inside a onChange event... try something like: updateFlag (more
semantic).
I think that you can delete the watch part and do it in the onChange
event
Try to find a good documentation/tutorial (i.e the official documentation) to learn more about parent/child communication.
Remember to add the event-bus import:
import { EventBus } from './event-bus'
Hope it helps!

Related

Issue with multiple custom events firing, second event not working in Vue

I am emitting two different custom events from child, and for some reason the second event doesn't work.
I have checked on the Vue console and both events are being fired, however, nothing happens for the second event. When I comment out the first one, the second works correctly, as if the first one was blocking the second one.
<child-component
#upload="handleUpload"
#uploading="mediaUploading"
/>
methods: {
handleUpload(response) { //doesn't work
this.mediaLoading = false;
console.log(this.mediaLoading);
},
mediaUploading() { //works
this.mediaLoading = true;
},
}
In child component
this.$emit('uploading');
try {
data = await this.$api.get('/photo');
} catch (e) {
console.log(e)
}
this.$emit('upload', data);
i have pasted the code on my machine and both are working fine
parent
<template>
<child-component #upload="handleUpload" #uploading="mediaUploading" />
</template>
<script>
import childComponent from "./page4child.vue";
export default {
components: {
childComponent,
},
data() {
return {
mediaLoading: true,
};
},
methods: {
handleUpload(response) {
//works
this.mediaLoading = false;
console.log(this.mediaLoading);
},
mediaUploading() {
//works
this.mediaLoading = true;
console.log(this.mediaLoading);
},
},
};
</script>
Child
<template>
<div>
<button #click="trigger('item')">run</button>
</div>
</template>
<script>
export default {
methods: {
async trigger(uploadedItem) {
this.$emit("uploading");
await new Promise((res) => {
console.log("duck");
res();
});
this.$emit("upload", uploadedItem);
},
},
};
</script>

How to pass initial form values to child component in Vue.js?

I'm using Vue.js. From my template I include the child component (componentB) which includes several input elements. I want to initialize those input elements from my parent template. I found a way to do this (see code below). However, I'm wondering if this is a correct way, as the articles I have read so far use different approaches (e.g. with $emit):
https://simonkollross.de/posts/vuejs-using-v-model-with-objects-for-custom-components
https://zaengle.com/blog/using-v-model-on-nested-vue-components
https://alligator.io/vuejs/add-v-model-support/
Can you confirm that my code below matches the Vue.js design concepts or are there flaws?
<template>
<div>
<div class="md-layout">
<div class="md-layout-item md-size-100">
<ComponentB ref="componentB" v-model="componentB"></ComponentB>
</div>
</div>
</div>
</template>
<script>
import { ComponentB } from "#/components";
export default {
components: {
ComponentB
},
data() {
return {
componentB: {
textInputField: "my-initial-value"
}
};
},
methods: {
validate() {
return this.$refs.componentB.validate().then(res => {
this.$emit("on-validated", res);
return res;
});
}
}
};
</script>
<style></style>
Form componentB
<template>
<div>
<md-field
:class="[
{ 'md-valid': !errors.has('textInputField') && touched.textInputField },
{ 'md-form-group': true },
{ 'md-error': errors.has('textInputField') }
]"
>
<md-icon>label_important</md-icon>
<label>My text input</label>
<md-input
v-model="textInputField"
data-vv-name="textInputField"
type="text"
name="textInputField"
required
v-validate="modelValidations.textInputField"
>
</md-input>
<slide-y-down-transition>
<md-icon class="error" v-show="errors.has('textInputField')"
>close</md-icon
>
</slide-y-down-transition>
<slide-y-down-transition>
<md-icon
class="success"
v-show="!errors.has('textInputField') && touched.textInputField"
>done</md-icon
>
</slide-y-down-transition>
</md-field>
</div>
</template>
<script>
import { SlideYDownTransition } from "vue2-transitions";
export default {
name: "componentB",
props: ['value'],
components: {
SlideYDownTransition
},
computed: {
textInputField: {
get() {return this.value.textInputField},
set(textInputField) { this.$emit('input', { ...this.value, ['textInputField']: textInputField })}
}
},
data() {
return {
touched: {
textInputField: false
},
modelValidations: {
textInputField: {
required: true,
min: 5
}
}
};
},
methods: {
getError(fieldName) {
return this.errors.first(fieldName);
},
validate() {
return this.$validator.validateAll().then(res => {
return res;
});
}
},
watch: {
textInputField() {
this.touched.runnerName = true;
}
}
};
</script>
<style></style>
The simplest way to pass data to child component is to use props, which are then available in the child component and can pass the values back up to the parent.
https://v2.vuejs.org/v2/guide/components-props.html
// PARENT COMPONENT
<ComponentB :textInputField="textInputField" ...></ComponentB>
// CHILD COMPONENT
// TEMPLATE SECTION
<md-input
v-model="textInputField"
value="textInputField"
...
>
// SCRIPT SECTION
export default {
props: {
textInputField: String
}
}

Nested components not re-rendering properly: VueJs

I'm new to Vue and I'm building this forum kind of thing which can add nested comments in it. In here there are two components. PostForum and Comment. PostForum contains an input box and parent Comments. And inside each comment, I added child comments recursively.
When I adding comments, It works fine. But when deleting, it sends the ajax req but there's no re-rendering.
So this is how I designed it. When deleting a comment, I emit a global event and in PostForum component I listen to that event and deleting that comment from its data. So isn't that supposed to re-render all the comments accordingly? Can anyone tell me what am I doing wrong here?
PostForum.vue
<template>
<!-- comment box here -->
<comment
v-for="(comment, index) in comments"
v-if="!comment.parent_id"
:reply="true"
:initialChildren="getChildren(comment.id)"
:key="index"
:comment="comment">
</comment>
</template>
<script>
export default {
data () {
return {
comments: [], // all comments
comment: { // new comment [at comment box]
body: '',
parent_id: 0,
},
}
},
methods: {
deleteComment (node) {
axios.delete(`/comments/${node.id}`)
.then(res => {
this.comments.splice(node.key, 1)
})
.catch(err => {
console.log(err)
})
},
getChildren: function (parent_id) {
return this.comments.filter(child => parent_id == child.parent_id)
},
},
mounted: function () {
window.Event.$on('comment-deleted', (node) => this.deleteComment(node))
}
}
</script>
Comment.vue
<template>
<button #click="deleteComment">X</button>
<!-- comment body goes here -->
<comment v-for="(child, i) in children" :key="i" :reply="false" :comment="child"></comment>
<!-- reply form here -->
</template>
<script>
export default {
props: ['initialChildren']
data: function () {
return {
newComment: {
body: '',
parent_id: this.comment.id,
},
children: this.initialChildren,
}
},
methods: {
deleteComment () {
window.Event.$emit('comment-deleted', {key: this.$vnode.key, id: this.comment.id})
},
}
}
</script>
I've tried this:
This code is just an example that may help you. In my case, child component is comment component in your case, and each child component has its own #action listener for his child component. So, he can use that to modify his own childrens.
Here is an example on codesandbox: https://codesandbox.io/s/qzrp4p3qw9
ParentComponent
<template>
<div>
<Child v-for="(children,index) in childrens" :child="children" :key="index" :parent="0" :pos="index"></Child>
</div>
</template>
import Child from './child';
export default {
data() {
return {
childrens:[
{
name:"a",
childrens:[
{
name:'aa',
},
{
name:'ba',
childrens:[
{
name:'baa',
childrens:[
{
name:'baaa',
},
{
name:'baab',
}
]
}
]
}
]
},
{
name:"a",
childrens:[
{
name:'aa',
},
{
name:'ab',
childrens:[
{
name:'aba',
childrens:[
{
name:'abaa',
childrens:[
{
name:'baa',
childrens:[
{
name:'baaa',
},
{
name:'baa',
}
]
}
]
},
{
name:'abab',
}
]
}
]
}
]
}
]
}
},
components:{
Child
}
}
ChildComponent
<template>
<div>
<div style="padding:5px">
{{ child.name }}
<button #click="deleteComment(child)">x</button>
</div>
<child #delete="deleteSubComment" style="padding-left:15px" v-if="typeof child.childrens !== 'undefined'" v-for="(children,index) in child.childrens" :child="children" :pos="index" :key="index" :parent="children.parent"></child>
</div>
</template>
export default {
name:"child",
props:['child','parent',"pos"],
methods:{
deleteComment(child) {
this.$emit('delete',child);
},
deleteSubComment(obj) {
this.child.childrens.splice(this.child.childrens.indexOf(obj),1);
}
}
}

Vue2: handling multi-child prop synchronization with models

I'm new to Vue (about a, and while reading the docs is very helpful it is often the case where I can not derive how I am supposed to achieve the desired behavior.
I have made several small components to get the feel for passing props and handling events, so now I am trying to makes something a bit larger but am facing some difficulty.
This difficulty stems from the following:
I would like to have a custom select component that are initialized via a v-for loop. All the while I would like to have access to these components selected option. I can bind the select data with v-model in the select component, but I am struggling to get that information out to the wrapper, yet alone the container looping over the wrappers.
Note: I am using Rollup and single file components
top-level
<template >
<div>
<select-container
v-for="(select, index) in selects"
:index="index"
:key="select.id"
:select.sync="select"
/>
</div>
</template>
<script>
import selectContainer from './select-container.vue';
export default {
components: { selectContainer },
props: {
records: {
default: function(){return{}},
type: Object
}
},
data: function() {
return {
selects: [{}, {}]
}
},
computed: {
},
methods: {
}
}
</script>
<style scoped>
</style>
select-container
<template>
<div>
<my-select
v-model.sync="select"
/>
</div>
</template>
<script>
import mySelect from './my-select.vue';
export default {
components: { mySelect },
props: {
index: { type: Number },
select: {
type: Object,
default: function(){return{}}
}
},
data : function(){
return {
}
},
methods: {
},
computed: {
}
}
</script>
<style scoped>
</style>
my-select
<template>
<select v-model="selected">
<option
v-for="(attr, index) in attributes"
:value="attr"
:selected="attr == selected"
>
{{attr}}
</option>
</select>
</template>
<script>
export default {
props: {
attributes: {
type: Array,
default: function() {
return []
}
}
},
data: function() {
return {
selected: ""
}
}
}
</script>
<style scoped>
</style>
There's still a lot of code in the post that I'm not following, but I think the gist of your question is how to reflect the selected property from the <my-select> component through to the <select-container> component. If that's the case, then the most straightforward approach is probably just to add a value and emit input events.
In the template, add an event handler for the native <select>
<select v-model="selected" #input="onInput">
Then, in the code, reflect that event up to the parent. Also be sure to accept a value property from that parent.
export default {
props: {
value: null,
},
data: function() {
return {
selected: this.value
}
},
methods: {
onInput() {
this.$emit("input", this.selected)
}
},
watch: {
value(newValue) {
this.selected = newValue;
}
}
}
And then the parent can simply use the conventional v-model binding.
<my-select v-model="select"/>

Vue.js Component rendering after prop update

In Vue.js i have a component (Answer Component) like this:
<template>
<a class="quiz-input-choice" :class="{'quiz-input-choice--selected': answer.selected}"
#click="toggleSelect()" :selected="answer.selected">
<img :src="answer.image_path"/>
<p class="quiz-input-choice__description">{{answer.title}}</p>
</a>
</template>
<script>
export default {
props: ['answer'],
methods: {
toggleSelect() {
this.$parent.$emit('answer-selected', this.answer.id);
}
}
}
</script>
If in the parent (Question Component) I update the "selected" attribute of the element, this component will not be rerendered.
export default {
props: ['question'],
components: {QuizAnswer},
created: function () {
let _self = this;
this.$on('answer-selected', id => {
let i = _self.question.answers.map(item => item.id).indexOf(id);
let answer = _self.question.answers[i];
answer.selected = !answer.selected;
});
}
}
In Vue Developer Console, i checked that Answer component data are updated, so the answer is marked as selected. Anyway, is not rendered with the "quiz-input-choice--selected" class.
If, strangely, I update from the parent other attribute of the prop (for example (answer.title), then the child component is rendered correctly with also the class "quiz-input-choice--selected".
So i guess it's a problem of detecting changes from the child.
Thank you everybody for the answers.
I discovered the problem. The "selected" attribute of the answer was not present in the initial object, so Vue cannot make reactive that attribute.
https://v2.vuejs.org/v2/guide/reactivity.html
I solved making reactive that property in the parent component.
created() {
let self = this;
this.question.answers.forEach(function (answer) {
self.$set(answer, 'selected', false);
});
},
I think you have a structural issue here. You shouldn't submit an event to your parent, since the component is supposed to be self-contained.
What you can do however is emitting an event in the child component (Answer) that will be catch in the parent (Question).
Answer.vue
<template>
<a class="quiz-input-choice" :class="{'quiz-input-choice--selected': answer.selected}"
#click="toggleSelect()" :selected="answer.selected">
<img :src="answer.image_path"/>
<p class="quiz-input-choice__description">{{answer.title}}</p>
</a>
</template>
<script>
export default {
props: ['answer'],
methods: {
toggleSelect() {
this.$emit('answer-selected');
}
}
}
</script>
Question.vue
Your template will have be catching the event like this (I don't know where your answers are so I assume that you have a answers array) :
<answer
v-for="(answer, index) in answers"
:answer="answer"
#answer-selected="answerSelected(index)"
></answer>
And your script will look like this :
export default {
props: ['question'],
components: {QuizAnswer},
data() {
return {
answers: [],
selectedAnswer: -1,
};
},
watch: {
selectedAnswer(newIndex, oldIndex) {
if (oldIndex > -1 && this.answers.length > oldIndex) {
// Reset old value
this.answers[oldIndex].selected = false;
}
if (newIndex > -1 && this.answers.length > newIndex) {
// Set new value
this.answers[newIndex].selected = true;
}
},
},
methods: {
answerSelected(index) {
this.selectedAnswer = index;
},
},
};

Categories

Resources