Emit event from App.vue and catch in component - vue.js

I am trying to implement sign out handling in Vue. I redirect to Home which works fine on all pages except Home which is not refreshed. So I decided to emit a signal and refresh data once I catch it.
App.vue
<b-dropdown-item href="#0" v-on:click="signMeOut()">Sign out</b-dropdown-item>
methods: {
signMeOut() {
this.$store.dispatch('SIGN_USER_OUT');
if (this.$route.path === '/') {
this.$emit('sign-out');
} else {
this.$router.push({ name: 'home' });
}
},
Home.vue
<b-container fluid="true" class="pt-3 w-75 m-auto" v-on:sign-out="reload">
created() {
this.$store.dispatch('INIT_STREAM');
},
methods: {
reload() {
console.log('reload');
this.$store.dispatch('INIT_STREAM');
},
},
but the signal does not reaches the Home.vue or is ignored. How can I fix it? Or do you have a better solution of this sign out procedure?

When you use the hook $emit.
You should listen to this event in $root instance from your vuejs application, $root.
So for achieve the desired result you just have to change your code to:
In your component home (I'm putting only the session script from a .vue file)
<script>
export default {
name: 'Home',
components: {
HelloWorld
},
created(){
this.$root.$once('mylogouthandler', this.logoutEventHandler)
},
methods: {
logoutEventHandler() {
console.log('exit')
//do your stuff here.
}
}
}
</script>
your component with action logout.
<template>
<div class="about">
<button #click="handleButtonClick()">logout</button>
</div>
</template>
<script>
export default {
name: 'About',
methods: {
handleButtonClick(){
console.log('clicked')
this.$root.$emit('mylogouthandler')
}
}
}
</script>
If you would like to know more, here is the documentation for handling events in vuejs.

Related

Vue 3: Wait until parent is done with data fetching to fetch child data and show loader

I'm looking for a reusable way to display a full page loader (Sidebar always visible but the loader should cover the content part of the page) till all necessary api fetches has been done.
I've got a parent component LaunchDetails wrapped in a PageLoader component
LaunchDetails.vue
<template>
<PageLoader>
<router-link :to="{ name: 'launches' }"> Back to launches </router-link>
<h1>{{ name }}</h1>
<section>
<TabMenu :links="menuLinks" />
</section>
<section>
<router-view />
</section>
</PageLoader>
</template>
<script>
import TabMenu from "#/components/general/TabMenu";
export default {
data() {
return {
menuLinks: [
{ to: { name: "launchOverview" }, display_name: "Overview" },
{ to: { name: "launchRocket" }, display_name: "Rocket" },
],
};
},
components: {
TabMenu,
},
created() {
this.$store.dispatch("launches/fetchLaunch", this.$route.params.launch_id);
},
computed: {
name() {
return this.$store.getters["launches/name"];
},
},
};
</script>
PageLoader.vue
<template>
<Spinner v-if="isLoading" full size="medium" />
<slot v-else></slot>
</template>
<script>
import Spinner from "#/components/general/Spinner.vue";
export default {
components: {
Spinner,
},
computed: {
isLoading() {
return this.$store.getters["loader/isLoading"];
},
},
};
</script>
The LaunchDetails template has another router-view. In these child pages new fetch requests are made based on data from the LaunchDetails requests.
RocketDetails.vue
<template>
<PageLoader>
<h2>Launch rocket details</h2>
<RocketCard v-if="rocket" :rocket="rocket" />
</PageLoader>
</template>
<script>
import LaunchService from "#/services/LaunchService";
import RocketCard from "#/components/rocket/RocketCard.vue";
export default {
components: {
RocketCard,
},
mounted() {
this.loadRocket();
},
data() {
return {
rocket: null,
};
},
methods: {
async loadRocket() {
const rocket_id = this.$store.getters["launches/getRocketId"];
if (rocket_id) {
const response = await LaunchService.getRocket(rocket_id);
this.rocket = response.data;
}
},
},
};
</script>
What I need is a way to fetch data in the parent component (LaunchDetails). If this data is stored in the vuex store, the child component (LaunchRocket) is getting the necessary store data and executes the fetch requests. While this is done I would like to have a full page loader or a full page loader while the parent component is loading and a loader containing the nested canvas.
At this point the vuex store is keeping track of an isLoading property, handled with axios interceptors.
All code is visible in this sandbox
(Note: In this example I could get the rocket_id from the url but this will not be the case in my project so I'm really looking for a way to get this data from the vuex store)
Im introduce your savior Suspense, this feature has been added in vue v3 but still is an experimental feature. Basically how its work you create one suspense in parent component and you can show a loading when all component in any depth of your application is resolved. Note that your components should be an async component means that it should either lazily loaded or made your setup function (composition api) an async function so it will return an async component, with this way you can fetch you data in child component and in parent show a fallback if necessary.
More info: https://vuejs.org/guide/built-ins/suspense.html#suspense
You could use Events:
var Child = Vue.component('child', {
data() {
return {
isLoading: true
}
},
template: `<div>
<span v-if="isLoading">Loading …</span>
<span v-else>Child</span>
</div>`,
created() {
this.$parent.$on('loaded', this.setLoaded);
},
methods: {
setLoaded() {
this.isLoading = false
}
}
});
var Parent = Vue.component('parent', {
components: { Child },
data() {
return {
isLoading: true
}
},
template: `<div>
Parent
<Child />
</div>`,
mounted() {
let request1 = new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
let request2 = new Promise((resolve, reject) => {
setTimeout(resolve, 2000);
});
Promise.all([ request1, request2 ]).then(() => this.$emit('loaded'))
}
});
new Vue({
components: { Parent },
el: '#app',
template: `<Parent />`
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
This may be considered an anti-pattern since it couples the parent with the child and events are considered to be sent the other way round. If you don't want to use events for that, a watched property works just fine, too. The non-parent-child event emitting was removed in Vue 3 but can be implemented using external libraries.

Passing data from route to router-view body

I have a single page application with the structure below.
|- App.vue
|- + Views
| |- Page.vue
|- + Components
| |- Slider.vue
EDIT 1: Solution thanks to #gengar.value
I solved the issue by passing params from Page.vue with
methods: {
emitIndex: function (index) {
this.$router.push({
name: "visualization",
params: { imgCat: "visualization", imgIndex: index },
});
},
}
App.vue containing the router-view container that is routing the Page.vue
Slider.vue is a component of App.vue
I want to pass index of clicked image and whole images data from Page.vue to App.vue then to Slider.vue in order to achieve decoupling Slider from Page for reusability purposes.
How can I pass user selected index from Page.vue too App.vue
I have tried to use, params, props and emit but failed.
Sample Page.vue
<template>
<div v-for="(item, index) in 3" :key="index"></div>
</template>
<script>
export default ({
data() {
return {
urls: ['url1', 'url2', 'url3']
}
}
})
</script>
Thanks in advance
EDIT 1: Solution thanks to #gengar.value
Problem solved by pushing params to router via Page.vue and listening it from Slider.vue as follows:
Page.vue
methods: {
passIndex: function (index) {
this.$router.push({
name: "visualization",
params: { imgCat: "visualization", imgIndex: index },
});
},
}
Slider.vue
watch: {
"$route.params.imgCat": function (val) {
this.state = val;
},
"$route.params.imgIndex": function (newVal) {
if (newVal != -1) this.imgState = newVal;
this.$router.push({ params: { imgIndex: -1 } });
}
My solution is a little bit complicated, but quite native since I only used Props and Emit.
You want to pass value between brother components, so you could simply try below:
App.vue
<template>
<div>
<Page :data="data" #syncData="syncData" />
<Slider :data="data" />
</div>
<template>
<script>
import Page from './Views/Page.vue'
import Slider from './Components/Slider.vue'
export default ({
components: {
Page,
Silder
},
data() {
return {
data: [] // init data in the parent component
}
},
methods: {
syncData(updatedImages) {
this.data = updatedImages
}
}
})
</script>
Page.vue
<template>
<div></div>
<template>
<script>
export default ({
props: {
data: { type: Array, default: () => [] }
},
methods: {
onSelectImage(images) {
this.$emit('syncData', images) // update selected data to App.vue
}
}
})
</script>
Slider.vue
<template>
<div></div>
<template>
<script>
export default ({
props: {
data: { type: Array, default: () => [] }
},
watch: {
data: {
handler(val) {
// when Page.vue emits updated data to App.vue,
// App.vue will pass data to Slider.vue
// and you could receive the updated data 'val' here
},
deep: true
}
}
})
</script>
Update: Sorry I misunderstood before. If you are using vue-router components and assigned different paths (eg. '/page' and '/slider'), you can use
this.$router.push({ path: '/path', query: selectedImage })
in Page.vue and get url query in Slider.vue.
Alternative methods could be using Cookie.js or sessionStorage (not pretty tho). Also you could try Vuex if the specific condition suits you.

Reuse Quasar Dialog plugin with custom component on another component

I'm taking the first steps with Quasar.
I want to build a modal window to be reused in forms.
I am using Dialog plugin and q-layout in a custom component.
However, when I use this custom component in another component the dialog plugin methods do not work.
Is there any way to solve this?
util/modal.js
import { Dialog } from "quasar";
export function ModalWindow(CustomComponent) {
Dialog.create({
component:CustomComponent,
ok: {
push: true
},
cancel: {
color: 'negative'
},
persistent: true
})
}
modal/ModalWindow.vue (custom component):
<template>
<q-dialog persistent ref="dialog" #hide="onDialogHide">
<q-layout view="lhh LpR lff" container style="height: 400px" class="bg-grey-3">
<q-toolbar class="bg-primary text-white q-mb-sm">
<q-toolbar-title>
<slot name="titelWindow"></slot>
</q-toolbar-title>
<q-btn v-close-popup flat round dense icon="close" />
</q-toolbar>
<q-form #submit.prevent="submitForm">
<q-card-section>
<slot></slot>
</q-card-section>
<q-card-actions align="right">
<slot name="toolbarBottom"></slot>
</q-card-actions>
</q-form>
</q-layout>
</q-dialog>
</template>
<script>
export default {
methods: {
show () {
this.$refs.dialog.show()
},
hide () {
this.$refs.dialog.hide()
},
onDialogHide () {
this.$emit('hide')
}
}
}
</script>
Call method ModalWindow on a page:
<script>
import { ModalWindow } from 'src/util/modal'
import CustomComponent from "components/modal/MyModalWindow.vue"
export default {
methods: {
showUWin: function(id) {
ModalWindow(CustomComponent)
}
}
}
</script>
So far it works well.
However, as I said,when I use this custom component in another component the dialog plugin methods do not work.
render custom component in another component: MyModalForm.vue
<template>
<MyModalWindow>
<!--Dialog's show method doesn't work-->
</MyModalWindow>
</template>
<script>
export default {
name: 'MyModalForm',
components: {
'MyModalWindow': require('components/modal/MyModalWindow.vue').default,
}
}
</script>
Call method ModalWindow on a page:
<script>
import { ModalWindow } from 'src/util/modal'
import CustomComponent from "components/modal/MyModalForm.vue"
export default {
methods: {
showWin: function(id) {
ModalWindow(CustomComponent)
}
}
}
</script>
I get on de console:
[Vue warn]: Error in mounted hook: "TypeError: this.$refs.dialog.show
is not a function"
I recently got into the same error.
My understanding is that, when you use something like:
Dialog.create({
component: CustomComponent,
...
})
// or
this.$q.dialog({
component: CustomComponent
})
the CustomComponent must directly implement the required show/hide/... methods, as per documentation.
So you have to repeat in each custom component this code (adapting it to the right "ref", specific to your component):
methods: {
show () {
this.$refs.dialog.show()
},
hide () {
this.$refs.dialog.hide()
},
onDialogHide () {
this.$emit('hide')
}
}
and propagate onOk and onCancel events appropriately.
For instance, summarizing everything:
<template>
<MyModalWindow ref="myModalForm" #ok="onOk" />
</template>
<script>
export default {
name: 'MyModalForm',
components: {
'MyModalWindow'
},
methods: {
show() {
this.$refs.myModalForm.show();
},
hide() {
this.$refs.myModalForm.hide();
},
onHide() {
this.$emit('hide');
},
onOk() {
this.$emit('ok');
this.hide();
}
}
}
</script>

Reload navbar component on every this.$router.push() call

I developing a login/registration system in my Vue.js app. I want the items in navbar to be updated when I call this.$router.push('/').
App.vue:
<template>
<div id="app">
<Navbar></Navbar>
<router-view></router-view>
<Footer></Footer>
</div>
</template>
Navbar component:
export default {
name: "Navbar",
data: function() {
return {
isLoggedIn: false,
currentUser: null
}
},
methods: {
getAuthInfo: function() {
this.isLoggedIn = this.auth.isLoggedIn();
if (this.isLoggedIn) {
this.currentUser = this.auth.currentUser();
}
}
},
mounted: function() {
this.getAuthInfo();
},
updated: function() {
this.getAuthInfo();
}
}
Here is how I redirect to another page:
const self = this;
this.axios
.post('/login', formData)
.then(function(data) {
self.auth.saveToken(data.data.token);
self.$router.push('/');
})
.catch(function(error) {
console.log(error);
self.errorMessage = 'Error!';
});
SUMMARY: The problem is that isLoggedIn and currentUser in Navbar don't get updated when I call self.$router.push('/');. This means that functions mounted and updated don't get called. They are updated only after I manually refresh the page.
I solved the problem with adding :key="$route.fullPath" to Navbar component:
<template>
<div id="app">
<Navbar :key="$route.fullPath"></Navbar>
<router-view></router-view>
<Footer></Footer>
</div>
</template>
Check this out from the docs:
beforeRouteUpdate (to, from, next) {
// called when the route that renders this component has changed,
// but this component is reused in the new route.
// For example, for a route with dynamic params `/foo/:id`, when we
// navigate between `/foo/1` and `/foo/2`, the same `Foo` component instance
// will be reused, and this hook will be called when that happens.
// has access to `this` component instance.
},
I expect your Navbar component is reused across routes so its mounted and updated are not called. Try using beforeRouteUpdate if you want to do some processing on route change.

Why this.$listeners is undefined in Vue JS?

Vue.js version: 2.4.2
Below component always print this.$listeners as undefined.
module.exports = {
template: `<h1>My Component</h1>`,
mounted() {
alert(this.$listeners);
}
}
I register the component and put it inside a parent component.
Can someone tell me why?
You have to understand what $listeners are.
this.$listeners will be populated once there are components that listen to events that your components is emitting.
let's assume 2 components:
child.vue - emits an event each time something is written to input field.
<template>
<input #input="emitEvent">
</input>
</template>
<script>
export default {
methods: {
emitEvent() {
this.$emit('important-event')
console.log(this.$listeners)
}
}
}
</script>
parent.vue - listen to the events from child component.
<template>
<div class="foo">
<child #important-event="doSomething"></child>
</div>
</template>
<script>
import child from './child.vue'
export default {
data() {
return {
newcomment: {
post_id: 'this is default value'
}
}
},
components: { child },
methods: {
doSomething() {
// do something
}
}
}
</script>
With this setup, when you type something to the input field, this object should be written to the console:
{
`important-event`: function () { // some native vue.js code}
}
I added the following alias to my webpack.config.js file and this resolved the issue for me:-
resolve: {
alias: {
'vue$': path.resolve(__dirname, 'node_modules/vue/dist/vue.js')
}
},