Vue.js : Method not firing in parent component after emit - javascript

A child component emits an event on input change with a payload:
Vue.component('gum-option', {
inheritAttrs: false,
props: ['label'],
template: `
<label class="gum-option">
<input
type="radio"
v-bind="$attrs"
v-on:change="$emit('update', this.label)">
<span>{{ label }}</span>
</label>
`
});
Its parent component listens to this event in order to fire a method that throws an alert:
Vue.component('gum-select', {
template: `
<div v-on:update="updateValue">
<label>{{label}}</label>
<div class="selected">
{{selectedOption}}
</div>
<slot v-bind="options"></slot>
</div>
`,
data: function () {
return {
selectedOption: ''
}
},
props:['options', 'name', 'label'],
methods: {
updateValue: function(event) { // method invoked by the "update" event
alert(event); // Not firing ! Why
}
}
});
You can find the bug on this pen:
https://codepen.io/bourpie/pen/JjXdjzg?editors=1011

The listener should be put on the gum-option component in App, as well as the method updateValue. There were some other mistakes so this is the fixed demo:
<div id="myApp">
<gum-select name="options" label="Options" v-bind:options="options" >
<gum-option
v-for="option in options"
v-bind:key="option.value"
v-bind:value="option.value"
v-bind:label="option.texte"
name="province"
v-on:update="updateValue">
</gum-option>
</gum-select>
</div>
Vue.component('gum-option', {
inheritAttrs: false,
props: ['label'],
template: `
<label class="gum-option">
<input
type="radio"
v-bind="$attrs"
v-on:change="$emit('update', label)">
<span>{{ label }}</span>
</label>
`
});
Vue.component('gum-select', {
template: `
<div>
<label>{{label}}</label>
<div class="selected">
{{selectedOption}}
</div>
<slot v-bind="options"></slot>
</div>
`,
data: function () {
return {
selectedOption: ''
}
},
props:['options', 'name', 'label'],
});
new Vue({
el: '#myApp',
data: {
options: [
{value: '1', texte: 'Option 1'},
{value: '2', texte: 'Option 2'},
{value: '3', texte: 'Option 3'}
]
},
methods: {
updateValue: function(label) {
console.log(label);
}
}
});

Related

How to bind input of type checkbox to v-model of child component in `vuejs` to a parent component?

How can input of type checkbox present in child component be bonded to v-model on the parent component?
Required Component Way -
Parent-
<button v-if="checkedNames">Confirm</button> //show this button if 1 checkbox is selected
<div v-model="checkedNames" >
<child-component v-for="person in collection" :key="person.id"
:person="person"><child-component>
</div>
new Vue({
el: '...',
data: {
checkedNames: [],
collection: [
{id: 1, name: 'Jack', items:[{id:100,name:"Pen",quantity:5}]},
{id: 2, name: 'John', items:[{id:200,name:"Pen",quantity:10},
{id:201,name:"Paper",quantity:100,}]},
{id: 3, name: 'Mike', items:[{id:300,name:"Pen",quantity:20}]},
]
}
})
Child-
<div v-for="(item, i) in person.items" :key="i">
<input type="checkbox" :id="item.id" :value="item.name" >
<label :for="item.id">{{item.name}}</label>
<input type="number" :id="i" :value="item.quantity" >
<label :for="i">{{item.quantity}}</label>
</div>
new Vue({
el: '...',
props:{person:Object}
})
How to send the item id, quantity and main id if the checkbox is selected to the parent?
Using v-model on Components
new Vue() is to use only to create top-level Vue instance (usually called vm or app). Components used as a child elements must be registered instead - Component Registration
Always use :key with v-for - Maintaining State
Vue.component('checkboxes', {
props: ['value', 'options'],
template: `
<div>
<template v-for="item in optionsWithKey">
<input type="checkbox" :id="item.key" :value="item.name" v-model="model" :key="item.key">
<label :for="item.key">{{item.name}}</label>
</template>
</div>
`,
computed: {
model: {
get() { return this.value },
set(newValue) { this.$emit('input', newValue) }
},
optionsWithKey() {
return this.options.map(item => ({
...item,
key: "checkbox_"+ item.id
}))
}
}
})
new Vue({
el: '#app',
data: {
checkedNames: [],
options:[
{id: 1, name: 'Jack'},
{id: 2, name: 'John'},
{id: 3, name: 'Mike'},
]
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.14/vue.js"></script>
<div id='app'>
<checkboxes v-model="checkedNames" :options="options">
</checkboxes>
<pre>{{ checkedNames }}</pre>
</div>
I'm not sure but try it...
child:
<input type="checkbox" :id="item.id" :value="item.name" :change="$emit('valueChecked',item.name)">
And in parent:
<child-component #valuChecked="getValue"><child-component>
methods:{
getValue(value){
this.checkedNames.push(value);
}
}
See if this helps
// parent template
<child-component #update:checked-names="updateCheckedNames">
<child-component>
// parent
data() {
return {
checkedNames: []
};
},
methods: {
updateCheckedNames(name) {
if (this.checkedNames.includes(name)) {
const index = this.checkedNames.indexOf(name);
this.checkedNames.splice(index, 1);
} else {
this.checkedNames.push(name);
}
}
}
// child template
<div v-for="item in collection">
<input type="checkbox"
:id="item.id"
:value="item.name"
#click="$emit('update:checked-names', item.name)"> // emit here
<label :for="item.id">
{{item.name}}</label>
</div>

Vue Events - Can't listen to $emit event from child component

I'm having a simple issue, that I just can't figure out why it isn't working.
I have a child component "app-buttons", where i have an input field, i want to listen to, so i can filter a list based on the input value.
If i put the input in the root component where i have the list, all works good. But i want to split it up, and $emit the search input value, to the parent en then use it.
// THIS IS THE COMPONENT WHERE I WAN'T TO LISTEN TO THE SEARCH INPUT
import Buttons from './app-buttons.js';
import Event from './vue-event.js';
export default Vue.component('post-item', {
template: `
<section class="posts flex" v-if="posts">
<app-buttons :posts="posts"></app-buttons>
<transition-group name="posts" tag="section" class="posts flex">
<article class="postitem" :class="{ 'danger': !post.published }" v-for="post in posts" :key="post.id">
<p v-if="post.published">Post is published</p>
<p v-else>Post is <em><strong>not</strong></em> published</p>
<h4>{{ post.title }}</h4>
<button type="submit" #click="post.published = !post.published">Change status</button>
</article>
</transition-group>
</section>
`,
data() {
return {
};
},
components: {
Buttons,
},
props: {
posts: Array,
filterdList: [],
},
created() {
console.log('%c Post items', 'font-weight: bold;color:blue;', 'created');
},
computed: {
//filteredList() {
// return this.posts.filter(post => {
// return post.title.toLowerCase().includes(this.search.toLowerCase());
// });
//},
},
methods: {
update() {
console.log('test');
}
}
});
// THIS IS THE COMPONENT IM TRIGGERING THE $EVENT FROM
import Event from './vue-event.js';
export default Vue.component('app-buttons', {
template: `
<div class="postswrapper flex flex--center flex--column">
<section :class="className" class="flex flex--center">
<button type="button" #click="showUnpublished">Unpublish</button>
<button type="button" #click="shufflePosts">Shuffle</button>
</section>
<section :class="className">
<input type="text" v-model="search" v-on:input="updateValue" placeholder="Search..." />
</section>
</div>
`,
data() {
return {
className: 'buttons',
search: '',
}
},
props: {
posts: Array,
},
created() {
//console.log(this);
console.log('%c Buttons', 'font-weight: bold;color:blue;', 'created');
},
methods: {
updateValue() {
//console.log(this.search);
this.$emit('searchquery', this.search);
},
showUnpublished() {
this.posts.forEach(item => {
item.published = true;
})
},
shufflePosts() {
this.$emit('postshuffled', 'Fisher-Yates to the rescue');
for (var i = this.posts.length - 1; i >= 0; i--) {
let random = Math.floor(Math.random() * i);
let temp = this.posts[i];
Vue.set(this.posts, i, this.posts[random]);
Vue.set(this.posts, random, temp);
}
},
}
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue JS</title>
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.6/dist/vue.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div id="app">
<app-header :logo="logo" :name="name"></app-header>
<post-item :posts="posts" v-on:searchquery="update" v-on:sq="update" v-on:postshuffled="update"></post-item>
</div>
<script type="module">
import AppHeader from './components/app-header.js';
import PostItem from './components/post-item.js';
const app = new Vue({
el: '#app',
data: {
name: 'Vue App',
logo: {
class: 'vue-logo',
src: 'https://vuejs.org/images/logo.png',
},
components: {
AppHeader,
PostItem,
},
posts: [
{id: 1, title: 'Test', published: true},
{id: 2, title: 'New post', published: true},
{id: 3, title: 'Added', published: true},
{id: 4, title: 'Another post', published: true},
{id: 5, title: 'In the future', published: false},
{id: 6, title: 'Last post', published: true},
],
},
created() {
console.log('Created');
},
mounted() {
console.log('Mounted');
},
methods: {
update() {
console.log('updated');
}
},
});
</script>
</body>
</html>
You say:
I have a child component "app-buttons", where i have an input field, i
want to listen to, so i can filter a list based on the input value.
You have:
<div id="app">
<app-header :logo="logo" :name="name"></app-header>
<post-item :posts="posts" v-on:searchquery="update" v-on:sq="update" v-on:postshuffled="update"></post-item>
</div>
That says you expect post-item to emit a searchquery event. It does not.
Within post-item, you have:
<app-buttons :posts="posts"></app-buttons>
So you expect app-buttons to emit an event and post-item to implicitly bubble it up, but Vue events do not bubble. If you want that behavior, you will need to have post-item handle the event:
<app-buttons :posts="posts" v-on:searchquery="$emit('searchquery', $event)"></app-buttons>
Okay so I've changed the markup, and it works. But would this be the best way to do it? :)
And thanks to Roy J for helping out.
import Event from './vue-event.js';
import Buttons from './app-buttons.js';
export default Vue.component('post-item', {
template: `
<section class="posts flex" v-if="posts">
<app-buttons :posts="posts" v-on:searchquery="update($event)"></app-buttons>
<transition-group name="posts" tag="section" class="posts flex">
<article class="postitem" :class="{ 'danger': !post.published }" v-for="post in filteredItems" :key="post.id">
<p v-if="post.published">Post is published</p>
<p v-else>Post is <em><strong>not</strong></em> published</p>
<h4>{{ post.title }}</h4>
<button type="submit" #click="post.published = !post.published">Change status</button>
</article>
</transition-group>
</section>
`,
data() {
return {
search: '',
};
},
components: {
Buttons,
},
props: {
posts: Array,
},
created() {
console.log('%c Post items', 'font-weight: bold;color:blue;', 'created');
},
computed: {
filteredItems(search) {
return this.posts.filter(post => {
return post.title.toLowerCase().indexOf(this.search.toLowerCase()) > -1
});
}
},
methods: {
update(event) {
this.search = event;
}
}
});
// Child component
import Event from './vue-event.js';
export default Vue.component('app-buttons', {
template: `
<div class="postswrapper flex flex--center flex--column">
<section :class="className" class="flex flex--center">
<button type="button" #click="showUnpublished">Unpublish</button>
<button type="button" #click="shufflePosts">Shuffle</button>
</section>
<section :class="className">
<input type="text" v-model="search" v-on:input="updateValue" placeholder="Search..." />
</section>
</div>
`,
data() {
return {
className: 'buttons',
search: '',
}
},
props: {
posts: Array,
},
created() {
console.log('%c Buttons', 'font-weight: bold;color:blue;', 'created');
},
methods: {
updateValue() {
this.$emit('searchquery', this.search);
},
showUnpublished() {
this.posts.forEach(item => {
item.published = true;
})
},
shufflePosts() {
for (var i = this.posts.length - 1; i >= 0; i--) {
let random = Math.floor(Math.random() * i);
let temp = this.posts[i];
Vue.set(this.posts, i, this.posts[random]);
Vue.set(this.posts, random, temp);
}
},
}
});
Index same as before, just without alle the events on the components.

Vue component $emit firing twice

I'm recently trying to reutilize my Vue components in some real-world application to remove unnecessary duplicates and clutter with <divs>.
But I'm having trouble in doing so. After hours I managed to "accomplish" it, but now the event fires twice and I don't know exactly why.
I've made a basic setup to show the problem:
Vue.component("bs-select",{
template:
`<div class="form-group">
<label class="control-label">{{ label }}</label>
<select2
ref="select2control"
:options="options"
:value="value"
#input="chosenOption"
></select2>
</div>`,
props: ["value", "label", "options"],
methods: {
chosenOption(val) {
this.$emit("input", val);
}
}
});
Vue.component("select2",{
template:
`<select :value="value" style="width: 100%">
<option value="" selected disabled>Choose...</option>
<option v-if="!options">{{ value }}</option>
<option v-for="option in options" :value="option.value">{{ option.text }}</option>
</select>`,
props: ["value", "options"],
mounted: function() {
const vm = this;
$(vm.$el)
.select2()
.on("change", function() {
console.log("CHANGE", vm.$el.value);
vm.$emit("input", vm.$el.value);
});
},
watch: {
value: function(val) {
$(this.$el)
.val(val)
.trigger("change");
}
}
});
new Vue({
el: "#app",
data: {
test: "bug",
options: [
{
value: "hello",
text: "Hello"
},
{
value: "bug",
text: "Bug"
}
]
}
})
* {
font-family: Arial;
font-size: 10px;
}
div {
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/js/select2.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/css/select2.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<bs-select v-model="test" :options="options"></bs-select>
<br><br>
<button #click="test = 'bug'">
Set 'test' variable to 'bug' (Two-way check)
</button>
{{ test }}
</div>
<div>
Event is firing twice in console...
</div>
I also Googled a lot and came to no conclusion on why this happens and/or how to fix this issue.
Any help is greatly appreciated.
After asking some of my friends, one figured it out that the "change" trigger must be in beforeUpdate.
So, the solved code looks like this:
Vue.component("bs-select",{
template:
`<div class="form-group">
<label class="control-label">{{ label }}</label>
<select2
ref="select2control"
:options="options"
:value="value"
#input="chosenOption"
></select2>
</div>`,
props: ["value", "label", "options"],
methods: {
chosenOption(val) {
this.$emit("input", val);
}
}
});
Vue.component("select2",{
template:
`<select :value="value" style="width: 100%">
<option value="" selected disabled>Choose...</option>
<option v-if="!options">{{ value }}</option>
<option v-for="option in options" :value="option.value">{{ option.text }}</option>
</select>`,
props: ["value", "options"],
mounted: function() {
const vm = this;
$(vm.$el)
.select2()
.on("change", function() {
console.log("CHANGE", vm.$el.value);
vm.$emit("input", vm.$el.value);
});
},
beforeUpdate() {
$(this.$el).val(this.value).trigger('change.select2')
}
});
new Vue({
el: "#app",
data: {
test: "hello",
options: [
{
value: "hello",
text: "Hello"
},
{
value: "solved",
text: "Solved"
}
]
}
})
* {
font-family: Arial;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/js/select2.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/css/select2.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script></script>
<div id="app">
<bs-select v-model="test" :options="options"></bs-select>
<br><br>
{{ test }}
<br><br>
<button #click="test = 'solved'">
Set 'test' variable to 'solved'
</button>
</div>
It works quite nice, but he also suggested me to use this approach, which is a lot cleaner. I am currently using that now, but I leave the original answer to the question too, in case someone needs it.
I'm not sure what exactly you are trying to do here, but my guess is you were firing the onchange event twice, once when it actually changed and once in the watcher. Anyways you don't really need to use listeners like that when there are vue solutions available like this:
<div id="app">
<bs-select :value="test" v-on:change-value="test = $event" :options="options"></bs-select>
<br><br>
{{ test }}
<br><br>
<button #click="test = 'bug'">
Set 'test' variable to 'bug'
</button>
</div>
<div>
Event is firing twice in console...
</div>
component:
Vue.component("bs-select",{
template:
`<select :value="value" v-on:change="changeVal" style="width: 100%">
<option value="" selected disabled>Choose...</option>
<option v-if="!options">{{ value }}</option>
<option v-for="option in options" :value="option.value">{{ option.text }}</option>
</select>`,
props: ["value", "options"],
methods: {
changeVal: function(event) {
this.$emit('change-value', event.target.value)
}
}
});
new Vue({
el: "#app",
data: {
test: "bug",
options: [
{
value: "hello",
text: "Hello"
},
{
value: "bug",
text: "Bug"
}
]
}
})
https://jsfiddle.net/kv3bq1dw/
For anyone coming across this now, this is the proper way to get it to fire only once.
<template>
<select><slot></slot></select>
</template>
<script>
module.exports = {
props: ['options', 'value'],
mounted: function () {
console.log(this.options);
var vm = this;
$(this.$el)
// init select2
.select2({
data: this.options,
theme: 'bootstrap',
width: '100%',
placeholder: 'Select',
allowClear: true
})
.val(this.value)
.trigger('change')
// emit event on change.
.on('select2:select', function () { // bind to `select2:select` event
vm.$emit('input', this.value)
});
},
watch: {
value: function (value) {
// update value
$(this.$el)
.val(value)
.trigger('change');
},
options: function (options) {
// update options
$(this.$el).empty().select2({ data: options })
}
},
destroyed: function () {
$(this.$el).off().select2('destroy')
}
}
</script>

Vue js v-if condicional rendering on a shared toggle button

//PARENT COMPONENT
<template>
....
<div class="input-wrapper">//Toggle button container
<label class="input-label">
SELECT YES OR NOT
</label>
<toggle //child component, toggle button
:options="shipping"
/>
</div>
<div
v-if="destiny[0].value"
class="input-wrapper">
<label class="input-label">
IF YES THIS CONTAINER WILL BE DISPLAYED
</label>
<toggle
:options="Options"
/>
</div>
.....
</template>
<script>
import Toggle from "....";
export default {
components: {
Toggle,
},
data: function () {
return {
destiny: [{
label: 'Yes',
value: true
},
{
label: 'No',
value: false
}
],
Options: [{
label: 'A',
value: 'a'
},
{
label: 'B',
value: 'b'
},
{
label: 'C',
value: 'c'
}]
}
}
}
</script>
///CHILD COMPONENT
<template>
<div class="toggle">
<button
v-for="option in options"
:key="option.value"
:class="{
active: option.value === value
}"
class="btn"
#click="() => toggleHandler(option.value)">{{ option.label }} .
</button>
</div>
</template>
<script>
export default {
props: {
options: {
type: Array,
required: true
}
},
data: function () {
return {
value: this.options[0].value
}
},
methods: {
toggleHandler (value) {
this.$emit('input', value)
this.value = value
}
}
}
</script>
There is toggle with to options YES or NOT, if yes is selected the child component will be rendered otherwise will keep hide.
I'm trying to use a conditional in order to display a child component into a parent component using directives v-if or v-show, but I could not find the way to send the boolean value from the child component to the parent component.
Hope this helps!!
// CHILD
Vue.component('child', {
template: '<div>TOGGLE:- <input type="checkbox" #click="emit"/></div>',
data() {
return {
checked: false
};
},
methods: {
emit: function() {
this.checked = !this.checked;
this.$emit('event_child', this.checked);
}
}
});
// PARENT
var vm = new Vue({
el: '#app',
data: function() {
return {
toggleStatus: false
}
},
methods: {
eventChild: function(checked) {
this.toggleStatus = checked;
},
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<div id="app">
<child v-on:event_child="eventChild"></child>
<div id="toggle">TOGGLE STATUS => {{toggleStatus}}</div>
</div>

What's the best way to pass and retrieve data from a child form component?

This is my current method:
Parent.vue:
// Template
<form-child :schema="schema"><form-child>
// JS
data () {
return {
schema: [{ // name: '', value: '', type: '' }, { //etc ... }]
}
}
FormChild.vue:
// Template
<div v-for="field in schema">
<input v-if="field.type === 'text'" #change="updateValue(field.name, field.value)">
<textarea v-if="field.type === 'textarea'" #change="updateValue(field.name, field.value)">/textarea>
</div>
// JS
props: {
schema: Arrary
}
methods: {
updateValue (fieldName, fieldValue) {
this.schema.forEach(field => {
// this makes schema update in Parent.vue
if (field.name === fieldName) field.value = fieldValue
})
}
}
Is this the optimal way? Or maybe there's a better one using emit and v-model? (If so, could you provide a sample code?)
A properly encapsulated child component would be decoupled from the parent data structure. It would take type and value as separate props, plus an opaque id to tell the parent which value the component is emitting about.
By making a settable computed based on the value parameter, the component can use v-model on its form elements. The set function emits an input event with the id and the newValue, and the parent takes it from there.
Update: I decided I didn't like the id going to the component, so I handled that in the input handler: #input="updateField(index, $event).
new Vue({
el: '#app',
data: {
schema: [{
type: 'text',
name: 'one',
value: "1"
},
{
type: 'textarea',
name: 'two',
value: "stuff in the textarea"
}
]
},
methods: {
updateField(index, newValue) {
this.schema[index].value = newValue;
}
},
components: {
formInput: {
props: ['type', 'value'],
computed: {
proxyValue: {
get() { return this.value; },
set(newValue) {
this.$emit('input', newValue);
}
}
}
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js"></script>
<div id="app">
<div v-for="field in schema">
{{field.name}} = {{field.value}}
</div>
<form-input inline-template v-for="field, index in schema" :type="field.type" :key="index" :value="field.value" #input="updateField(index, $event)">
<div>
<input v-if="type === 'text'" v-model="proxyValue">
<textarea v-if="type === 'textarea'" v-model="proxyValue"></textarea>
</div>
</form-input>
</div>
For what you are doing here, there is no need to separate the form into a component. Just make it part of the parent and use v-model.
new Vue({
el: '#app',
data: {
schema: [{
type: 'text',
name: 'one',
value: "1"
},
{
type: 'textarea',
name: 'two',
value: "stuff in the textarea"
}
]
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js"></script>
<div id="app">
<div v-for="field in schema">
{{field.name}} = {{field.value}}
</div>
<div v-for="field in schema">
<input v-if="field.type === 'text'" v-model="field.value">
<textarea v-if="field.type === 'textarea'" v-model="field.value"></textarea>
</div>
</div>
If you want the component for reusability and you don't care about insulating the parent from changes (best practice is not to have anything outside a component change its data), you can just wrap the same thing in a component:
new Vue({
el: '#app',
data: {
schema: [{
type: 'text',
name: 'one',
value: "1"
},
{
type: 'textarea',
name: 'two',
value: "stuff in the textarea"
}
]
},
components: {
formChild: {
props: ['value']
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js"></script>
<div id="app">
<div v-for="field in schema">
{{field.name}} = {{field.value}}
</div>
<form-child inline-template v-model="schema">
<div>
<div v-for="field in value">
<input v-if="field.type === 'text'" v-model="field.value">
<textarea v-if="field.type === 'textarea'" v-model="field.value"></textarea>
</div>
</div>
</form-child>
</div>

Categories

Resources