Vuejs 'beforeunload' event not triggered as expected - vue.js

I have registered 'beforeunload' event on created hook of the component used by routes of vue router.
I want to call this event handler in order to remove user on browser tab close or browser tab refresh or browser close.
On ComponentA
created (){
window.addEventListener('beforeunload', () => {
this.removeUser()
return null
})
}
Smilarly on ComponentB
created (){
window.addEventListener('beforeunload', () => {
this.removeUser()
return null
})
}
And my router.js
{
path: '/staff/call/:session_key',
name: 'Staff Call',
component: ComponentA,
meta: {auth: true}
},
{
path: '/consumer/call/:session_key',
name: 'Consumer Call',
component: ComponentB
},
Here 'beforeunload' event handler is triggered randomly. That is sometimes it get triggered and sometimes not. I count find any pattern when it is triggered and when it is not.
What am I missing here?

Edit
I'd guess the most likely culprit then is exactly what #PatrickSteele said. From MDN:
Note: To combat unwanted pop-ups, some browsers don't display prompts
created in beforeunload event handlers unless the page has been
interacted with; some don't display them at all. For a list of
specific browsers, see the Browser_compatibility section.
I'd say it's likely you're seeing inconsistent behavior because you are sometimes not interacting with the page.
This may be a syntax error. created should be a method
created () {
window.addEventListener('beforeunload', this.removeUser)
},
methods: {
removeUser () {
//remove user here
}
}
A fiddle working: https://jsfiddle.net/e6m6t4kd/3/

It's work for me. while do something before reload or close in
vue.js
created() {
window.onbeforeunload = function(){
return "handle your events or msgs here";
}
}

I had to do some fiddling on the above examples, I believe this is the most robust solution:
let app1 = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
dirty_form: true,
},
created () {
console.log('created')
window.addEventListener('beforeunload', this.confirm_leaving)
},
methods: {
confirm_leaving (evt) {
if (this.dirty_form) {
const unsaved_changes_warning = "You have unsaved changes. Are you sure you wish to leave?";
evt.returnValue = unsaved_changes_warning;
return unsaved_changes_warning;
};
};
},
});

If you want detect page refresh/change in Vue whenever you press F5 or Ctrl + R, You may need to use Navigation Timing API.
The PerformanceNavigation.type, will tell you how the page was accessed.
created() {
// does the browser support the Navigation Timing API?
if (window.performance) {
console.info("window.performance is supported");
}
// do something based on the navigation type...
if(performance.navigation.type === 1) {
console.info("TYPE_RELOAD");
this.removeUser();
}
}

Not sure why none of the above were fully working for me in vue 3 composition api. Abdullah's answer partially works but he left out how to remove the listener.
setup() {
const doSomething = (e) => {
// do stuff here
return true
}
onBeforeMount(() => {
window.onbeforeunload = handleLeaveWithoutSaving
})
onUnmounted(() => {
window.onbeforeunload = null
})
}

Related

Vue reactivity issues in component making asynchronous server requests

A small amount of context: I have a Vue view called Articles. When the component is mounted to the DOM, I fetch all posts from the database using the axios library (in conjunction with Laravel controllers and API routes). The articles view contains a data property called active, which points towards the post that is currently selected. Clicking on a different post in the sidebar updates active and subsequently the new post is shown.
Now, every post has many comments, and those comments in turn can be linked to subcomments if you will. However, the mounted lifecycle hook in Articles.vue gets invoked only once and when I try to place the server request in updated(), everything seemingly works but I'd eventually get a 429 status (too many requests). My guess is that for each comment that is retrieved, the code in updated() get's invoked again.
I guess my question is as follows: How can I make Post.vue reactive, since right now the mounted lifecycle hook will be invoked only once even when another post is selected.
Here's the code:
Articles.vue
export default {
name: "Articles",
components: {SidebarLink, PageContent, Sidebar, Post, Searchbar, Spinner},
data() {
return {
posts: [],
active: undefined,
loading: true
}
},
mounted() {
this.fetchPosts();
},
methods: {
async fetchPosts() {
const response = await this.$http.get('/api/posts');
this.posts = response.data;
this.active = this.posts[0];
setTimeout(() => {
this.loading = false;
}, 400);
},
showPost(post) {
this.active = post;
}
}
}
Post.vue
export default {
name: "Post",
components: {Tag, WennekesComment},
props: ['post'],
data() {
return {
expanded: true,
comments: []
}
},
mounted() {
this.fetchComments();
},
methods: {
async fetchComments() {
let response = await this.$http.get('/api/posts/' + this.post.id + '/comments');
this.comments = response.data;
}
}
}
WennekesComment.vue
export default {
name: "WennekesComment",
props: ['comment'],
data() {
return {
subComments: []
}
},
mounted() {
this.fetchSubcomments();
},
methods: {
fetchSubcomments() {
let response = this.$http.get('/api/comments/' + this.comment.id).then((result) => {
// console.log(result);
});
}
}
}
Template Logic
<wennekes-comment v-for="comment in comments" :key="comment.id" :comment="comment"></wennekes-comment>
<post v-if="!loading" :post="active" :key="active.id"/>
Thanks in advance, and my apologies if this question is somewhat unclear, I'm somewhat at a loss.
Regards,
Ryan
UPDATE
I think I got it to work. In Articles.vue, I have appended a key to the post component. I think this is Vue's way of knowing which specific instance of a component to update.
I think I got it to work. In Articles.vue, I have appended a key to the post component. I think this is Vue's way of knowing which specific instance of a component to update.

When passing data from parent component to child component via props, the data appears to be undefined in the mounted hook of the child component

In my parent component:
<UsersList :current-room="current_room" />
In the child component:
export default {
props: {
currentRoom: Object
},
data () {
return {
users: []
}
},
mounted () {
this.$nextTick( async () => {
console.log(this.currentRoom) // this, weirdly, has the data I expect, and id is set to 1
let url = `${process.env.VUE_APP_API_URL}/chat_room/${this.currentRoom.id}/users`
console.log(url) // the result: /api/chat_room/undefined/users
let response = await this.axios.get(url)
this.users = response.data
})
},
}
When I look at the page using vue-devtools, I can see the data appears:
I've run into this issue in the past – as have many others. For whatever reason, you can't rely on props being available in the component's mounted handler. I think it has to do with the point at which mounted() is called within Vue's lifecycle.
I solved my problem by watching the prop and moving my logic from mounted to the watch handler. In your case, you could watch the currentRoom property, and make your api call in the handler:
export default {
props: {
currentRoom: Object
},
data() {
return {
users: []
}
},
watch: {
currentRoom(room) {
this.$nextTick(async() => {
let url = `${process.env.VUE_APP_API_URL}/chat_room/${room.id}/users`
let response = await this.axios.get(url)
this.users = response.data
})
}
},
}
I don't think you really need to use $nextTick() here, but I left it as you had it. You could try taking that out to simplify the code.
By the way, the reason console.log(this.currentRoom); shows you the room ID is because when you pass an object to console.log(), it binds to that object until it is read. So even though the room ID is not available when console.log() is called, it becomes available before you see the result in the console.

Why declared field in data with props initial value is undefined?

Since mutating a prop is an antipattern I do the following as one of the solutions to that, however when I console.log my new data field I get undefined. What's wrong?
export default {
name: "modal",
props: ["show"],
data() {
return {
sent: false,
mutableShow: this.show
};
},
methods: {
closeModal: function() {
this.mutableShow = false;
},
sendTeam: function() {
var self = this;
let clientId = JSON.parse(localStorage.getItem("projectClient")).id;
axios({
method: "get",
url: "/send-project-team/" + clientId,
data: data
})
.then(function(response) {
self.sent = true;
$("h3").text("Wooo");
$(".modal-body").text("Team was sent succesfully to client");
setTimeout(function() {
console.log(this.mutableShow);
self.closeModal();
}, 3000);
})
.catch(function(error) {
console.log(error);
});
}
}
};
Your timeout handler is establishing a new context. Instead of
setTimeout(function() {
console.log(this.mutableShow);
self.closeModal();
}, 3000);
you could use
setTimeout(() => {
console.log(this.mutableShow);
self.closeModal();
}, 3000);
And you'd need to make a similar change to
.then(function(response) {
to
.then(response => {
having said that, though, I'm not sure the code is going to behave as you might want it. Once the users closes the modal, it won't be possible to open it again since there is no way to make mutableShow equal to true.
Edited to add:
Since you're defining the self variable, you could also use that.
console.log(self.mutableShow);
Edited to add:
Without knowing specifically what behavior is intended, the best suggestion I can offer is to follow accepted Vue practices. Namely, after the AJAX request succeeds, emit a custom event. Have the parent component listen for that event and, when triggered, change the show prop.

vuejs2: how can i destroy a watcher?

How can i destroy this watcher? I need it only one time in my child component, when my async data has loaded from the parent component.
export default {
...
watch: {
data: function(){
this.sortBy();
},
},
...
}
gregor ;)
If you construct a watcher dynamically by calling vm.$watch function, it returns a function that may be called at a later point in time to disable (remove) that particular watcher.
Don't put the watcher statically in the component, as in your code, but do something like:
created() {
var unwatch = this.$watch(....)
// now the watcher is watching and you can disable it
// by calling unwatch() somewhere else;
// you can store the unwatch function to a variable in the data
// or whatever suits you best
}
More thorough explanation may be found from here: https://codingexplained.com/coding/front-end/vue-js/adding-removing-watchers-dynamically
Here is an example:
<script>
export default {
data() {
return {
employee: {
teams: []
},
employeeTeamsWatcher: null,
};
},
created() {
this.employeeTeamsWatcher = this.$watch('employee.teams', (newVal, oldVal) => {
this.setActiveTeamTabName();
});
},
methods: {
setActiveTeamTabName() {
if (this.employee.teams.length) {
// once you got your desired condition satisfied then unwatch by calling:
this.employeeTeamsWatcher();
}
},
},
};
</script>
If you are using vue2 using the composition-api plugin or vue3, you can use WatchStopHandle which is returned by watch e.g.:
const x = ref(0);
setInterval(() => {
x.value++;
}, 1000);
const unwatch = watch(
() => x.value,
() => {
console.log(x.value);
x.value++;
// stop watch:
if (x.value > 3) unwatch();
}
);
For this kind of stuff, you can investigate the type declaration of the API, which is very helpful, just hover the mouse on it, and it will show you a hint about what you can do:

Show loading spinner for async Vue 2 components

I have a fairly heavy component which I would like to load asynchronously, while at the same time showing the user a loading spinner when it's loading.
This is my first attempt, using loading defined in data linked to a spinner component with v-if="loading". Unfortunately this doesn't work because it seems that Vue doesn't rebind this properly for functions inside components -
export default {
data: {
return {
loading: false,
};
},
components: {
// ...
ExampleComponent: (resolve) => {
// Doesn't work - 'this' is undefined here
this.loading = true;
require(['./ExampleComponent'], (component) => {
this.loading = false;
resolve(component);
});
},
},
};
I've also found some Vue 1.0 examples, but they depended on $refs - in 2.0 $refs is no longer reactive, and cannot be used for this. The only way left is for the child component itself to do something on its mount lifecycle event to the application data state to remove the loading spinner, but that seems a bit heavy. Is there a better way to do this?
You could declare a variable outside the object scope (but still is within module scope) then use the created hook to attach this. So your updated code would look like:
let vm = {}
export default {
// add this hook
created () {
vm = this;
},
data: {
return {
loading: false,
};
},
components: {
// ...
ExampleComponent: (resolve) => {
// since 'this' doesn't work, we reference outside 'vm'
vm.loading = true;
require(['./ExampleComponent'], (component) => {
vm.loading = false;
resolve(component);
});
},
},
};