I'm having difficulty finding why an event bus emits only once between two Vue components.
A button interaction is to open a child components' panel and then dynamically/lazyly loads a pair of chart components. Then, on the second execution, close the panel and destroy the dynamically loaded components. The functions cycle works but only once.
UPDATE:
After adding some UI components inside of the <q-card> directive, I discovered that this.toggleChartPanel() toggles on every interaction with the button event. It appears that the the props loads only on the FIRST-TIME the button is clicked.
A code example with explanation would be greatly appreciated.
Component containing the emit:
<script>
import Store from '#store'
import BaseHrsBtn from './_base-hrs-btn'
export default {
name: 'TotHrs',
parent: 'LogSummaryWidget',
components: {
BaseHrsBtn,
},
data() {
return {
dynamicCharts: {
dynamicChartA: 'test-line-chart',
dynamicChartB: 'test-line-chart'
}
}
},
computed: {
totHrs () {
return Store.state.fetchLogSummary.data.total
},
},
methods: {
emitChartPanelToggle () {
this.$bus.$emit('chart-panel-toggled', this.dynamicCharts)
this.dynamicCharts = {}
},
},
}
</script>
<template>
<base-hrs-btn
class="col-6 col-md-4"
:hours="totHrs"
icon="clock"
title="TOT"
#click.native="emitChartPanelToggle"
/>
</template>
<script>
export default {
name : 'ChartPanel',
parent: 'LogSummaryWidget',
components: {
TestLineChart: () => import("./_charts/test-line-chart"),
},
data () {
return {
chartPanelOpen: false,
dynamicChartA: '',
dynamicChartB: '',
}
},
created() {
this.$bus.$on('chart-panel-toggled', ({ dynamicChartA, dynamicChartB}) => {
this.toggleChartPanel()
this.dynamicChartA = dynamicChartA
this.dynamicChartB = dynamicChartB
});
},
beforeDestroy() {
this.$bus.$off('chart-panel-toggled');
},
methods: {
toggleChartPanel () {
this.chartPanelOpen = !this.chartPanelOpen
}
},
}
</script>
<template>
<q-card
v-show-slide="chartPanelOpen"
class="q-mx-md"
>
<component :is="dynamicChartA"></component>
<component :is="dynamicChartB"></component>
</q-card>
</template>
Also, how can I insure that the $bus.$on fires asynchronously too?
I accidentally left this in:
this.dynamicCharts = {}
Removing it fixed it.
Related
I am trying to make this component reusable so later can install it in any project and via props add needed values e.g. images and function parameters (next, prev, intervals...) inside any component.
<template>
<div>
<transition-group name='fade' tag='div'>
<div v-for="i in [currentIndex]" :key='i'>
<img :src="currentImg" />
</div>
</transition-group>
<a class="prev" #click="prev" href='#'>❮</a>
<a class="next" #click="next" href='#'>❯</a>
</div>
</template>
<script>
export default {
name: 'Slider',
data() {
return {
images: [
'https://cdn.pixabay.com/photo/2015/12/12/15/24/amsterdam-1089646_1280.jpg',
'https://cdn.pixabay.com/photo/2016/02/17/23/03/usa-1206240_1280.jpg',
'https://cdn.pixabay.com/photo/2015/05/15/14/27/eiffel-tower-768501_1280.jpg',
'https://cdn.pixabay.com/photo/2016/12/04/19/30/berlin-cathedral-1882397_1280.jpg'
],
timer: null,
currentIndex: 0,
}
},
mounted: function() {
this.startSlide();
},
methods: {
startSlide: function() {
this.timer = setInterval(this.next, 4000);
},
next: function() {
this.currentIndex += 1
},
prev: function() {
this.currentIndex -= 1
}
},
computed: {
currentImg: function() {
return this.images[Math.abs(this.currentIndex) % this.images.length];
}
}
}
</script>
styles...
So later it would be <Slider... all props, images loop here/> inside other components.
How can be it be achieved?
Just move what needs to come from another component to props. That way other component can pass the relevant info it needs.
export default {
name: 'Slider',
props: {
images: Array,
next: Function
prev: Function,
// and so on
},
...
The parent component would call it like:
<Slider :images="imageArray" :next="nextFunc" :prev="prevFunc" />
EDIT
You can pass an interval value via props:
export default {
name: 'Slider',
props: { intervalVal: Number },
methods: {
startSlide: function() {
this.timer = setInterval(this.next, this.intervalVal);
},
}
You can also pass function from parent to child via props.
export default {
name: 'Slider',
props: { next: Function },
methods: {
someMethod: function() {
this.next() // function from the parent
},
}
I don't really understand your use case 100% but these are possible options.
I have a child component that's basically a search box. When the user types something and presses enter, an event is fired that goes to the parent with the search topic:
export default {
name: "SearchBar",
methods: {
searchRequested(event) {
const topic = event.target.value;
this.$emit('searchRequested', topic);
}
}
};
The parent receives the event and updates a prop connected to other of its children (an image gallery):
<template>
<div id="app">
<SearchBar #searchRequested="onSearchRequested($event)" />
<Images :topic="topic" />
</div>
</template>
<script>
import SearchBar from './components/SearchBar.vue'
import Images from './components/Images.vue'
export default {
name: 'app',
components: {
SearchBar,
Images
},
data() {
return {
topic: ''
};
},
methods: {
onSearchRequested(topic) {
this.topic = topic;
}
}
}
</script>
So far, so good. But now I want the child component load itself with images related to the searched topic whenever the user performs a new search. For that, the child component Images must be aware of a change on its property topic, so I created a computed one:
import { ImagesService } from '../services/images.service.js';
export default {
data() {
return {
topic_: ''
};
},
methods: {
updateImages() {
const images = new ImagesService();
images.getImages(this.topic_).then(rawImages => console.log(rawImages));
}
},
computed: {
topic: {
get: function() {
return this.topic_;
},
set: function(topic) {
this.topic_ = topic;
this.updateImages();
}
}
}
};
But unfortunately, the setter never gets called. I have to say I'm new in Vue, so probably I'm doing something wrong. Any help will be appreciated.
You don't need to create computed in the main component. Images component is already aware of the changes in the topic prop.
You need to watch the changes of topic and do an async operation in 'Images.vue'. It's possible with Vue's watchers.
Vue docs watchers
'./components/Images.vue'
<template>...</template>
<script>
export defult {
props: ['topic'],
data(){
return {
images: []
}
},
watch: {
topic(newVal){
// do async opreation and update data.
// ImageSerice.get(newVal)
// .then(images => this.images = images)
}
}
}
</script>
I’m having a hard time figuring out how to test single file Vue components that need to react to child-sourced events.
I have a Gallery component, which has any number of Card components as children, and must keep track of the Cards that are selected. When a card is clicked, it emits a custom event (card-click) that the Gallery listens for and reacts to by calling a select() function. Select() has some logic based on whether or not the Shift or Meta keys are pressed. I’ve created simplified versions of these components below:
Gallery Component
<template>
<div class="gallery">
<card v-for="(item, index) in items"
:id="item.id"
:key="item.id"
class="galleryCard"
#card-click="select(item.id, $event)">
<h2>{{ item.title }}</h2>
</card>
</div>
</template>
<script>
export default {
name: "Gallery",
props: {
items: {
required: true,
type: Array,
},
},
methods: {
select: function(id, event) {
if(event.metaKey) {
console.log('meta + click')
} else {
if (event.shiftKey) {
console.log('shift + click')
} else {
console.log('click')
}
}
},
},
}
</script>
Card Component
<template>
<article :id="id" #click="handleSelect($event)" class="card">
<slot/>
</article>
</template>
<script>
export default {
name: "Card",
props: {
id: {
type: String,
default: "",
},
},
methods: {
handleSelect: function(event) {
this.$emit("card-click", event)
},
},
}
</script>
Using Jest, I’m trying to test the Gallery component’s select() function, which I’m mocking (selectStub), to make sure the correct Cards are selected when clicked. When I trigger clicks on two of the cards, I expect to see something in the selectStub.mock.calls array, but there is nothing. I have also tried to emit the event How can I capture events that children emit in my test?
Gallery Test
import { createLocalVue, mount } from "#vue/test-utils"
import Gallery from "#/Gallery.vue"
import Card from "#/Card.vue"
const localVue = createLocalVue()
let wrapper
let items = [
{ id: "1", title: "First" },
{ id: "2", title: "Second" },
{ id: "3", title: "Third" },
]
describe("Gallery.vue", () => {
beforeEach(() => {
wrapper = mount(Gallery, {
localVue,
propsData: {
items: items,
},
stubs: ["card"],
})
})
const selectStub = jest.fn();
it("selects the correct items", () => {
wrapper.setMethods({ select: selectStub })
const card1 = wrapper.findAll(".galleryCard").at(0)
const card2 = wrapper.findAll(".galleryCard").at(1)
card1.trigger('click')
card2.trigger('click', {
shiftKey: true
})
console.log(selectStub.mock)
})
})
All properties (calls, instances, and timestamps) of selectStub.mock are empty. I have also tried emitting the card-click event via vue-test-utils wrapper.vm.$emit() function but it doesn't trigger the select.
How can I test the Gallery select() method to make sure it's responding correctly to a child-sourced event?
I'm using chartkick in my Vue project. Right now, the data is loading from Firebase after the chart has rendered, so the chart is blank. When I change the code in my editor, the chart renders as expected, since it's already been retrieved from Firebase. Is there a way to make chartkick wait for the data to load before trying to render the chart? Thanks!
Line-Chart Component:
<template>
<div v-if="loaded">
<line-chart :data="chartData"></line-chart>
</div>
</template>
<script>
export default {
name: 'VueChartKick',
props: ['avgStats'],
data () {
return {
loaded: false,
chartData: this.avgStats
}
},
mounted () {
this.loaded = true
}
}
</script>
Parent:
<template>
...
<stats-chart v-if="avgStatsLoaded" v-bind:avgStats="avgStats" class="stat-chart"></stats-chart>
<div v-if="!avgStatsLoaded">Loading...</div>
...
</template>
<script>
import StatsChart from './StatsChart'
export default {
name: 'BBall',
props: ['stats'],
components: {
statsChart: StatsChart
},
data () {
return {
avgStatsLoaded: false,
avgStats: []
}
},
computed: {
sortedStats: function () {
return this.stats.slice().sort((a, b) => new Date(b.date) - new Date(a.date))
}
},
methods: {
getAvgStats: function () {
this.avgStats = this.stats.map(stat => [stat.date, stat.of10])
this.avgStatsLoaded = true
}
},
mounted () {
this.getAvgStats()
}
}
modify your code of StatsChart component:
you may use props directly
<template>
<div v-if="loaded">
<line-chart :data="avgStats"></line-chart>
</div>
</template>
export default {
name: 'VueChartKick',
props: ['avgStats'],
data () {
return {
loaded: false,
}
},
mounted () {
this.loaded = true
}
}
I tried to implement a simple notification system with notification Store inspired by this snippet form Linus Borg : https://jsfiddle.net/Linusborg/wnb6tdg8/
It is working fine when you add one notification at a time, but when you add a second notification before the first one disappears the notificationMessage emit its "close-notification" event but the parent notificationBox component does not execute the "removeNotification" function. removeNotification is called after the emit if you use the click event on the notification though. So there is probably a issue with the timeout but i can't figure out what.
NotificationStore.js
class NotificationStore {
constructor () {
this.state = {
notifications: []
}
}
addNotification (notification) {
this.state.notifications.push(notification)
}
removeNotification (notification) {
console.log('remove from store')
this.state.notifications.splice(this.state.notifications.indexOf(notification), 1)
}
}
export default new NotificationStore()
App.vue
<template>
<div id="app">
<notification-box></notification-box>
<div #click="createNotif">
create new notification
</div>
</div>
</template>
<script>
import notificationMessage from './components/notificationMessage.vue'
import notificationBox from './components/notificationBox.vue'
import NotificationStore from './notificationStore'
export default {
name: 'app',
methods: {
createNotif () {
NotificationStore.addNotification({
name: 'test',
message: 'this is a test notification',
type: 'warning'
})
}
},
components: {
notificationMessage,
notificationBox
}
}
</script>
notificationBox.vue
<template>
<div :class="'notification-box'">
<notification-message v-for="(notification, index) in notifications" :notification="notification" :key="index" v-on:closeNotification="removeNotification"></notification-message>
</div>
</template>
<script>
import notificationMessage from './notificationMessage.vue'
import NotificationStore from '../notificationStore'
export default {
name: 'notificationBox',
data () {
return {
notifications: NotificationStore.state.notifications
}
},
methods: {
removeNotification: function (notification) {
console.log('removeNotification')
NotificationStore.removeNotification(notification)
}
},
components: {
notificationMessage
}
}
</script>
notificationMessage.vue
<template>
<div :class="'notification-message ' + notification.type" #click="triggerClose(notification)">
{{ notification.message }}
</div>
</template>
<script>
export default {
name: 'notificationMessage',
props: {
notification: {
type: Object,
required: true
},
delay: {
type: Number,
required: false,
default () {
return 3000
}
}
},
data () {
return {
notificationTimer: null
}
},
methods: {
triggerClose (notification) {
console.log('triggerClose')
clearTimeout(this.notificationTimer)
this.$emit('closeNotification', notification)
}
},
mounted () {
this.notificationTimer = setTimeout(() => {
console.log('call trigger close ' + this.notification.name)
this.triggerClose(this.notification)
}, this.delay)
}
}
</script>
thanks for the help
my small fiddle from back in the days is still making the rounds I see :D
That fiddle is still using Vue 1. In Vue 2, you have to key your list elements, and you tried to do that.
But a key should be a unique value identifying a data item reliably. You are using the array index, which does not do that - as soon as an element is removed, indices of the following items change.
That's why you see the behaviour you are seeing: Vue can't reliably remove the right element because our keys don't do their job.
So I would suggest to use a package like nanoid to create a really unique id per notification - but a simple counter would probably work as well:
let _id = 0
class NotificationStore {
constructor () {
this.state = {
notifications: []
}
}
addNotification (notification) {
this.state.notifications.push({ ...notification, id: _id++ })
}
removeNotification (notification) {
console.log('remove from store')
this.state.notifications.splice(this.state.notifications.indexOf(notification), 1)
}
}
and in the notification component:
<notification-message
v-for="(notification, index) in notifications"
:notification="notification"
:key="notification.id"
v-on:closeNotification="removeNotification"
></notification-message>