Data value not updated with tabs in Vue js - javascript

Hi guys Im trying to make my custom tabs, with Vue js but Im having a problem since like my data property is not getting updated :(...
Here is the case Im having trouble with:
When I open 3 tabs, If I open my Modal on the first tab and then close that first tab, I will be switched to second tab but my Modal that was from first tab stays open like it is modal from the first tab instead of second... I would like each tab to have its own modal instance.
Here I posted bellow gif of what is happening. Basically I dont want my modal to apear again on next tab, when previous is closed :)
Seems like my data values, are not destroyed with first tab, and they are just replicated onto the second tab, Im trying to figure out what is the issue for few days now but no succes...
Here is my App.vue
<template>
<div id="app">
<div class="event-tabs wrapper">
<div class="is-flex">
<div class="tabs is-boxed control">
<ul>
<li v-for="(newEvent, index) in newEventList" :key="index" :class="selectedEventClass(index)"
#click.left="selectEvent(index)" #click.middle="discardEvent(index)">
<span class="event-tab-title">
TAB
</span>
<span class="event-tab-close" #click.stop="closeEvent(index)">
<i class="fa fa-times"></i>
</span>
</li>
<li class="add-tab">
<a #click.prevent="createEvent" :title="'Create Tab'">
<span>+</span>
</a>
</li>
</ul>
</div>
</div>
<div class="tab-content">
<tab v-for="(event, index) in newEventList" :event="event" :index="index"
v-if="showEventTab" v-show="index === selectedEvent" :key="index"
ref="eventTab"></tab>
</div>
</div>
</div>
</template>
<script>
import tab from './components/EventTab.vue';
export default {
name: 'app',
components: {
tab,
},
computed: {
newEventList() {
return this.$store.getters['eventModule/getNewList'];
},
selectedEvent() {
return this.$store.getters['eventModule/getSelectedNew'];
},
eventToEdit() {
return this.$store.state.event.eventToEdit;
},
showEventTab() {
return this.newEventList.length > 0;
},
},
methods: {
selectedEventClass(eventIndex) {
return (eventIndex === this.selectedEvent) ? 'is-active' : '';
},
createEvent() {
this.$store.dispatch('eventModule/create');
},
selectEvent(eventIndex) {
this.$store.dispatch('eventModule/select', { eventIndex });
},
closeEvent(eventIndex) {
this.$store.dispatch('eventModule/close', { eventIndex });
},
},
}
</script>
<style lang="scss">
#import './assets/scss/main';
</style>
My Tab component:
<template>
<div class="event-form" v-if="event">
<div class="columns">
<div class="column is-half">
<div class="field">
<h1>This is the TAB number {{ index}} </h1>
</div>
<p class="control">
<button class="button is-danger" #click.prevent="openDialog">
Open Dialog
</button>
</p>
<modalDialog type="none" :show="modal.show"
:className="'eventTabModal'" :title="'Test modal'"
:text="'Test modal'"
#hide="closeDiscardModal">
<h3>Modal is active</h3>
</modalDialog>
</div>
</div>
</div>
</template>
<script>
import modalDialog from './ModalDialog.vue';
export default {
components: {
modalDialog,
},
props: ['event', 'index'],
data() {
return {
eventDefault: {},
/**
* Discard event modal
*/
modal: {
show: false,
},
};
},
computed: {
eventList() {
return this.$store.getters['event/getNewList'];
},
eventTypeList() {
return this.$store.getters['eventType/getList'];
},
},
methods: {
/**
* Opens discarded Modal
*/
closeDiscardModal() {
this.modal = {
show: false,
};
},
openDialog() {
this.modal = {
show: true,
};
},
},
}
</script>
My Modal component for displaying Dialog:
<template>
<transition name="fade">
<div class="modal is-active" v-show="shouldShowModal" :class="className">
<div class="modal-background" #click="hideModal"></div>
<div class="modal-card">
<header class="modal-card-head" v-if="title">
<p class="modal-card-title">{{ title }}</p>
</header>
<section class="modal-card-body">
<slot>
{{ text }}
</slot>
</section>
<footer class="modal-card-foot" v-if="type !== 'none'">
<template v-if="type === 'confirm'">
<a class="button is-success" #click.prevent="buttonClicked('yes')">Yes</a>
<a class="button is-danger" #click.prevent="buttonClicked('no')">No</a>
</template>
<template v-else-if="type === 'info'">
<a class="button" #click.prevent="buttonClicked('ok')">Ok</a>
</template>
</footer>
</div>
<button class="modal-close is-large" #click="hideModal"></button>
</div>
</transition>
</template>
<script>
export default {
props: {
show: {
type: Boolean,
default: false,
},
title: {
type: String,
default: '',
},
text: {
type: String,
default: '',
},
type: {
type: String,
default: 'info',
},
className: {
type: String,
default: '',
},
},
data() {
return {
shouldShowModal: this.show,
};
},
watch: {
show(newValue) {
this.shouldShowModal = newValue;
},
},
methods: {
hideModal() {
this.shouldShowModal = false;
this.$emit('hide');
},
buttonClicked(type) {
this.hideModal();
this.$emit('buttonClicked', type);
},
},
};
</script>
And My store module for Tabs:
const eventModule = {
namespaced: true,
state: {
/**
* List of opened tabs
*/
newList: [],
selectedNew: 0,
savedList: [],
eventToEdit: null,
},
getters: {
getNewList(state) {
return state.newList;
},
getSelectedNew(state) {
return state.selectedNew;
},
getSavedList(state) {
return state.savedList;
},
},
mutations: {
addNew(state, { location } = {}) {
state.newList.push({
typeId: null,
active: true,
logs: [],
});
},
removeNew(state, index) {
state.newList.splice(index, 1);
},
setNew(state, { index = state.selectedNew, event }) {
state.newList.splice(index, 1, event);
},
selectNew(state, selectedNew) {
state.selectedNew = selectedNew;
},
},
actions: {
/**
* opens tab for creating new event
*
* #param context
* #param location
* #param stopProp
* #returns {*}
*/
create(context, { location, stopProp } = {}) {
const newList = context.getters.getNewList;
context.commit('addNew', { location });
context.commit('selectNew', newList.length - 1);
// if (!stopProp) {
// context.dispatch('stateChanged', null, { root: true });
// }
return Promise.resolve();
},
/**
* Saves event
* #param context
* #param event
* #return {Promise|Promise.<TResult>}
*/
save(context, { event, index, hideMessage }) {
const method = (event.id) ? 'patch' : 'post';
// const data = { event, userId: context.rootGetters['user/getData'].id };
const data = { event };
const payload = { method, url: 'event', data, hideMessage };
return context.dispatch('server/http', payload, { root: true })
.then((response) => {
context.commit('setNew', { event: response.data.object, index });
context.dispatch('loadList');
})
.catch(error => Promise.reject(error));
},
select(context, { eventIndex, stopProp }) {
context.commit('selectNew', eventIndex);
},
opened(context) {
const event = JSON.parse(JSON.stringify(context.state.eventToEdit));
context.state.eventToEdit = null;
context.dispatch('create', { stopProp: true });
context.commit('setNew', { event });
},
/**
* Closes for event
* #param context
* #param eventIndex
* #param stopProp
* #return {Promise|Promise.<TResult>}
*/
close(context, { eventIndex, stopProp }) {
const newList = context.getters.getNewList;
const selectedNew = context.getters.getSelectedNew;
context.commit('removeNew', eventIndex);
if (selectedNew >= newList.length && selectedNew > 0) {
context.commit('selectNew', selectedNew - 1);
}
},
},
};
export default eventModule;
Also Here is the link to my github page where full test code is located if someone wants to take a look:
Codesandbox link
Thanx in advance.

Solved it. The problem is with keys in v-for, :key prop, should be unique, so here is how I solved it, in mutations addNew, add new property tabId add like this:
state.newList.push({
tabId: new Date.now(),
typeId: null,
active: true,
briefing: false,
logs: [],
});
and App.vue change :key=“index” to :key=“event.tabId”

Related

Change content on left side when right side scrolled

I would like to create something similiar like tesla.com, content on left side will change when right side is scrolled.
I have tried create it using vuetify intersection, but it still buggy. Is there some way to improve it?
Here is demo for code i already created : https://hyundai-andalan.vercel.app/models/staria
This is code i using:
// page component
<template>
<div>
<ModelDesktop v-if="$vuetify.breakpoint.lgAndUp" />
<ModelMobile v-else />
</div>
</template>
<script>
import model from '~/mixins/model'
export default {
mixins: [model],
head () {
return {
title: this.model.name
}
}
}
</script>
// model desktop component
<template>
<div class="tw-grid tw-grid-cols-3">
<div class="tw-col-span-2">
<ModelCarousel :section="section" class="tw-sticky tw-top-0" />
</div>
<ModelDesktopContentWrapper #change:section="onSectionChange" />
</div>
</template>
<script>
export default {
data () {
return {
section: 'exterior'
}
},
methods: {
onSectionChange (value) {
this.section = value
}
}
}
</script>
// model carousel component
<template>
<v-carousel v-model="carousel" height="100vh">
<v-carousel-item
v-for="(item, i) in model[section].collection"
:key="i"
>
<v-img :src="item.image" aspect-ratio="1" eager />
</v-carousel-item>
</v-carousel>
</template>
<script>
import model from '~/mixins/model'
export default {
mixins: [model],
props: {
section: {
type: String,
default: 'exterior'
}
},
data: () => ({
carousel: 0,
colors: [
'primary',
'secondary',
'yellow darken-2',
'red',
'orange'
]
})
}
</script>
// model desktop component wrapper
<template>
<div class="tw-p-16">
<!-- Specification & Content -->
<div v-intersect="onExterior">
<ModelSpecification />
<ModelContent section="exterior" v-bind="model.exterior" />
</div>
<!-- Specification & Content -->
<!-- Interior -->
<ModelContent v-intersect="onInterior" section="interior" v-bind="model.interior" />
<!-- Interior -->
<!-- Interior -->
<ModelContent v-intersect="onPerformance" section="performance" v-bind="model.performance" />
<!-- Interior -->
<!-- Interior -->
<ModelContent v-intersect="onSafety" section="safety" v-bind="model.safety" />
<!-- Interior -->
</div>
</template>
<script>
import model from '~/mixins/model'
export default {
mixins: [model],
data () {
return {
exterior: false,
interior: false,
performance: false,
safety: false
}
},
watch: {
exterior (newValue) {
if (newValue) {
this.interior = false
this.performance = false
this.safety = false
this.$emit('change:section', 'exterior')
}
},
interior (newValue) {
if (newValue) {
this.exterior = false
this.performance = false
this.safety = false
this.$emit('change:section', 'interior')
}
},
performance (newValue) {
if (newValue) {
this.exterior = false
this.interior = false
this.safety = false
this.$emit('change:section', 'performance')
}
},
safety (newValue) {
if (newValue) {
this.exterior = false
this.interior = false
this.performance = false
this.$emit('change:section', 'safety')
}
}
},
methods: {
onExterior (entries) {
this.exterior = entries[0].isIntersecting
},
onInterior (entries) {
this.interior = entries[0].isIntersecting
},
onPerformance (entries) {
this.performance = entries[0].isIntersecting
},
onSafety (entries) {
this.safety = entries[0].isIntersecting
}
}
}
</script>
// model content component
<template>
<div>
<!-- Main Content -->
<div class="lg:tw-h-screen lg:tw-flex lg:tw-items-center tw-w-full">
<div class="lg:tw-flex-1">
<div class="tw-text-center">
<v-img v-if="$vuetify.breakpoint.mdAndDown" :src="banner" class="tw-mb-2" />
<h2 class="tw-capitalize">
{{ section }}
</h2>
<h3 class="tw-font-medium">
{{ title }}
</h3>
<div class="tw-mb-5" v-text="description" />
<v-btn rounded #click="dialogOpen = true">
feature detail
</v-btn>
</div>
</div>
</div>
<!-- Main Content -->
<ModelContentDialog v-model="dialogOpen" :section="section" :collection="collection" />
</div>
</template>
<script>
export default {
props: {
section: {
type: String,
required: true
},
title: {
type: String,
required: true
},
description: {
type: String,
required: true
},
banner: {
type: String,
default: ''
},
collection: {
type: Array,
required: true
}
},
data () {
return {
dialogOpen: false
}
}
}
</script>

Vue child component not re-rendering on data change

Trying to update a child component on parent state change. When I change the date state in Day.vue I can see that the computed function is working but the <EntryList> component is not updating with the new date I pass in.
Day.vue
<template>
<div id="daily">
<div id="nav">
<div class="nav">
<button #click="changeDay(-1)" class="arrow">←</button>
<h5 class="dayTitle" style="background-color: initial">
{{ dayTitle }}
</h5>
<button #click="changeDay(1)" class="arrow">→</button>
</div>
</div>
<div class="dayDiv">
<div class="details">
<!-- We use v-bind to attach state item to component--->
<EntryList v-bind:listDate="date" />
</div>
</div>
</div>
</template>
<script>
import EntryList from "./EntryList";
export default {
components: {
EntryList,
},
data() {
return {
date: "",
};
},
created() {
this.date = new Date();
},
methods: {
// Change Date here
changeDay(amount) {
let changeDate = new Date(this.date); // had to do this because computed couldn't see that it was updating
changeDate.setDate(this.date.getDate() + amount);
this.date = changeDate;
console.log("Date Changed");
},
},
computed: {
dayTitle() {
let options = { weekday: "short" };
return `${this.date.toLocaleString(
undefined,
options
)} ${this.date.toLocaleDateString()}`;
},
},
};
</script>
EntryList.vue
<template>
<div>
<ul>
<Entry v-for="entry in entries" v-bind:entry="entry" :key="entry.id" />
</ul>
<button :value="dateStamp" class="add">+</button>
</div>
</template>
<style lang="scss">
</style>
<script>
import Entry from "./Entry";
export default {
components: {
Entry,
},
// Eventually pass in props for styling component
props: {
listDate: {
type: Date,
required: true,
},
},
data() {
return {
entries: [],
};
},
created() {
// We do this to get the entries for the date
console.log("Render List");
let dateStamp = this.listDate.toLocaleDateString();
chrome.storage.sync.get([dateStamp], (result) => {
this.entries = Object.values(result[dateStamp]);
});
},
computed: {
dateStamp() {
return this.listDate.toLocaleDateString();
},
},
};
</script>
your entries array fills once on created event. You should move logic from created function, to computed property or use watcher
like this
watch: {
listDate (newValue) {
console.log("Render List");
let dateStamp = this.listDate.toLocaleDateString();
chrome.storage.sync.get([dateStamp], (result) => {
this.entries = Object.values(result[dateStamp]);
});
}
}

How to call a method in a Vue component from programmatically inserted component

I'm trying to call a method from a child component which is programatically inserted.
Here is my code.
MultipleFileUploader.vue
<template>
<div class="form-group" id="multiple-file-uploader">
<div>
<multiple-file-uploader-part
:name="uploadername" :index="1"
#remove="deleteUploader" #fileselected="fileSelected($event)">
</multiple-file-uploader-part>
</div>
</div>
</template>
<script>
import MultipleFileUploaderPart from './MultipleFileUploaderPart.vue';
let index_count = 1;
export default {
components: {
'multiple-file-uploader-part':MultipleFileUploaderPart,
},
props: {
uploadername: {
type: String,
default: 'files',
}
},
data() {
return {
next_id:1,
}
},
methods: {
fileSelected: function (target) {
var UploaderPart = Vue.extend(MultipleFileUploaderPart);
new UploaderPart().$on('fileselected','fileSelected')
.$mount('#multiple-file-uploader');
},
deleteUploader: function (idToRemove) {
this.uploaders = this.uploaders.filter(
uploaders_id => {
return uploaders_id.id !== idToRemove;
}
)
}
},
}
</script>
<style scoped>
</style>
MultipleFileUploaderPart.vue
<template>
<div v-bind:id="name + '['+index+']'">
<div class="input-group margin">
{{index}}
<input type="file" accept="application/pdf,image/jpeg,image/png"
v-bind:name="name + '['+index+']'"
v-on:change="fileSelectedMethod($event.target)">
<div class="input-group-btn">
<button #click="removeClicked"
class="btn btn-danger btn-sm"
v-if="index != 1"
type="button">
Delete{{index}}
</button>
</div>
</div>
<p v-if="size_error" style="color: red">File size must be less than 2MB</p>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
},
index: {
type: Number,
},
},
data() {
return {
size: '',
size_error: false,
}
},
methods: {
removeClicked: function () {
document.getElementById(this.name+'[' + this.index + ']' ).remove();
this.$emit('remove', this.index);
},
fileSelectedMethod: function (target) {
this.size = target.files[0].size;
if (this.size < 2000000) {
this.size_error = false;
this.$emit('fileselected', target);
} else {
target.value = null;
this.size_error = true;
console.log(target.files);
}
}
}
}
</script>
<style scoped>
I'm trying to achieve is that when a file input is filled with a file, a MultipleFileUploaderPart is created. And when the file input element in this component is filled, another MultipleFileUploaderPart is inserted.
I'd like to call MultipleFileUploader 's fileSelected method from newly inserted components so that I can create another component.
I also want to remove a MultipleFileUploaderPart component when the delete button is clicked.
How can I achieve this? or is there a better way?
EDIT:
This is what I originally had.
MultipleFileUploader.vue
<template>
<div class="form-group">
<div>
<multiple-file-uploader-part
v-for="uploader in uploaders"
:name="uploadername" :index="uploader.id"
#remove="deleteUploader" #fileselected="fileSelected($event)">
slot
</multiple-file-uploader-part>
</div>
</div>
</template>
<script>
import MultipleFileUploaderPart from "./MultipleFileUploaderPart";
let index_count = 1;
export default {
//name: "MultipleFileUploader",
components: {MultipleFileUploaderPart},
props: {
uploadername: {
type: String,
default: 'files',
}
},
data() {
return {
uploaders: [
{
id: index_count++,
},
]
}
},
methods: {
fileSelected: function (target) {
if(target.value){
this.uploaders.push({
id: index_count++,
})
}
},
deleteUploader: function (idToRemove) {
this.uploaders = this.uploaders.filter(
uploaders_id => {
return uploaders_id.id !== idToRemove;
}
)
}
},
}
</script>
MultipleFileUploaderPart.vue
<template>
<div class="input-group margin">
{{index}}
<input type="file" accept="application/pdf,image/jpeg,image/png"
v-bind:name="name + '['+index+']'"
v-on:change="fileSelectedMethod($event.target)">
<div class="input-group-btn">
<button #click="$emit('remove',index)"
class="btn btn-danger btn-sm"
v-if="index != 1"
type="button">
Delete{{index}}
</button>
</div>
<br>
<p v-if="size_error" style="color: red">File size must be less than 2MB</p>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
},
index: {
type: Number,
},
},
data() {
return {
size: '',
size_error: false,
}
},
methods: {
checkFileSize: function () {
},
fileSelectedMethod: function (target) {
console.log(target);
console.log(target.files);
this.size = target.files[0].size;
console.log(this.size);
if (this.size < 2000000) {
this.size_error = false;
this.$emit('fileselected', target);
} else {
target.value = null;
this.size_error = true;
console.log(target.files);
}
}
}
}
</script>
And this happens. please click
When I click 'Delete'Button, correct child coponent is deleted but the file in the input form stays there. that's why I'm seeking for another approach.
Declare uploaders as an array of objects that contain all needed props for creation of MultipleFileUploaderPart.
Use v-for on MultipleFileUploaderPart in the main MultipleFileUploader to reactively generate MultipleFileUploaderPart components
Use $emit from MultipleFileUploaderPart to MultipleFileUploader to emit creation and deletion events so that MultipleFileUploader can add or remove elements in the uploaders array.
Please don't delete or create elements from DOM directly, let the VueJs do this work.

How to write a plugin that shows a modal popup using vue. Call should be made as a function()

I am trying to make a VueJS plugin that exports a global method, which when called, will popup a message with an input text field. Ideally, I want to be able to make the following call from any Vue component:
this.$disaplayMessageWithInput("Title","Body","Value");
And a popup should come on the screen.
I've tried building it but when the install() calls this.$ref., it isn't recognized:
DeleteConfirmation.vue
<template>
<b-modal size="lg" ref="deleteConfirmationModal" :title="this.title" header-bg-variant="danger" #ok="confirmDelete" #cancel="confirmCancel">
<p>
{{this.body}}
</p>
</b-modal>
</template>
<script>
export default {
data()
{
return {
title: null,
body: null,
valueCheck: null,
value: null
};
},
install(vue, options)
{
Vue.prototype.$deleteConfirmation = function(title, body, expectedValue)
{
this.title = title;
this.body = body;
this.valueCheck = expectedValue;
this.$refs.$deleteConfirmation.show()
}
},
}
</script>
app.js
import DeleteConfirmation from './components/global/DeleteConfirmation/DeleteConfirmation';
Vue.use(DeleteConfirmation);
The call I am trying to make is:
$vm0.$deleteConfirmation("title","body","val");
I get the below error at the run time:
app.js?id=c27b2799e01554aae7e1:33 Uncaught TypeError: Cannot read property 'show' of undefined
at Vue.$deleteConfirmation (app.js?id=c27b2799e01554aae7e1:33)
at <anonymous>:1:6
Vue.$deleteConfirmation # app.js?id=c27b2799e01554aae7e1:33
(anonymous) # VM1481:1
It looks like, this.$refs in DeleteConfirmation.vue is undefined.
Try to avoiding $ref with vue ( $ref is here for third party and some very special case )
$ref isn't reactive and is populate after the render ...
the best solution for me is using a event bus like this :
const EventBus = new Vue({
name: 'EventBus',
});
Vue.set(Vue.prototype, '$bus', EventBus);
And then use the event bus for calling function of your modal ...
(
this.$bus.on('event-name', callback) / this.$bus.off('event-name');
this.$bus.$emit('event-name', payload);
)
You can create a little wrapper around the bootstrap modal like mine
( exept a use the sweet-modal)
<template>
<div>
<sweet-modal
:ref="modalUid"
:title="title"
:width="width"
:class="klass"
class="modal-form"
#open="onModalOpen"
#close="onModalClose"
>
<slot />
</sweet-modal>
</div>
</template>
<script>
export default {
name: 'TModal',
props: {
eventId: {
type: String,
default: null,
},
title: {
type: String,
default: null,
},
width: {
type: String,
default: null,
},
klass: {
type: String,
default: '',
},
},
computed: {
modalUid() {
return `${this._uid}_modal`; // eslint-disable-line no-underscore-dangle
},
modalRef() {
return this.$refs[this.modalUid];
},
},
mounted() {
if (this.eventId !== null) {
this.$bus.$on([this.eventName('open'), this.eventName('close')], this.catchModalArguments);
this.$bus.$on(this.eventName('open'), this.modalRef ? this.modalRef.open : this._.noop);
this.$bus.$on(this.eventName('close'), this.modalRef ? this.modalRef.close : this._.noop);
}
},
beforeDestroy() {
if (this.eventId !== null) {
this.$off([this.eventName('open'), this.eventName('close')]);
}
},
methods: {
onModalOpen() {
this.$bus.$emit(this.eventName('opened'), ...this.modalRef.args);
},
onModalClose() {
if (this.modalRef.is_open) {
this.$bus.$emit(this.eventName('closed'), ...this.modalRef.args);
}
},
eventName(action) {
return `t-event.t-modal.${this.eventId}.${action}`;
},
catchModalArguments(...args) {
if (this.modalRef) {
this.modalRef.args = args || [];
}
},
},
};
</script>
<style lang="scss" scoped>
/deep/ .sweet-modal {
.sweet-title > h2 {
line-height: 64px !important;
margin: 0 !important;
}
}
</style>
AppModal.vue
<template>
<div class="modal-wrapper" v-if="visible">
<h2>{{ title }}</h2>
<p>{{ text }}</p>
<div class="modal-buttons">
<button class="modal-button" #click="hide">Close</button>
<button class="modal-button" #click="confirm">Confirm</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
visible: false,
title: '',
text: ''
}
},
methods: {
hide() {
this.visible = false;
},
}
}
</script>
Modal.js (plugin)
import AppModal from 'AppModal.vue'
const Modal = {
install(Vue, options) {
this.EventBus = new Vue()
Vue.component('app-modal', AppModal)
Vue.prototype.$modal = {
show(params) {
Modal.EventBus.$emit('show', params)
}
}
}
}
export default Modal
main.js
import Modal from 'plugin.js'
// ...
Vue.use(Modal)
App.vue
<template>
<div id="app">
// ...
<app-modal/>
</div>
</template>
This looks pretty complicated. Why don't you use a ready-to-use popup component like this one? https://www.npmjs.com/package/#soldeplata/popper-vue

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.

Categories

Resources