How can I mimic onbeforeunload in a Vue.js 2 application? - javascript

I have a Vue component that is tracking when it is "dirty" (e.g. unsaved). I would like to warn the user before they browse away from the current form if they have unsaved data. In a typical web application you could use onbeforeunload. I've attempted to use it in mounted like this:
mounted: function(){
window.onbeforeunload = function() {
return self.form_dirty ? "If you leave this page you will lose your unsaved changes." : null;
}
}
However this doesn't work when using Vue Router. It will let you navigate down as many router links as you would like. As soon as you try to close the window or navigate to a real link, it will warn you.
Is there a way to replicate onbeforeunload in a Vue application for normal links as well as router links?

Use the beforeRouteLeave in-component guard along with the beforeunload event.
The leave guard is usually used to prevent the user from accidentally
leaving the route with unsaved edits. The navigation can be canceled
by calling next(false).
In your component definition do the following:
beforeRouteLeave (to, from, next) {
// If the form is dirty and the user did not confirm leave,
// prevent losing unsaved changes by canceling navigation
if (this.confirmStayInDirtyForm()){
next(false)
} else {
// Navigate to next view
next()
}
},
created() {
window.addEventListener('beforeunload', this.beforeWindowUnload)
},
beforeDestroy() {
window.removeEventListener('beforeunload', this.beforeWindowUnload)
},
methods: {
confirmLeave() {
return window.confirm('Do you really want to leave? you have unsaved changes!')
},
confirmStayInDirtyForm() {
return this.form_dirty && !this.confirmLeave()
},
beforeWindowUnload(e) {
if (this.confirmStayInDirtyForm()) {
// Cancel the event
e.preventDefault()
// Chrome requires returnValue to be set
e.returnValue = ''
}
},
},

The simplest solution to mimic this fully is as follow:
{
methods: {
beforeWindowUnload (e) {
if (this.form_dirty) {
e.preventDefault()
e.returnValue = ''
}
}
},
beforeRouteLeave (to, from, next) {
if (this.form_dirty) {
next(false)
window.location = to.path // this is the trick
} else {
next()
}
},
created () {
window.addEventListener('beforeunload', this.beforeWindowUnload)
},
beforeDestroy () {
window.removeEventListener('beforeunload', this.beforeWindowUnload)
}
}

Related

How to detect "net::ERR_CONNECTION_REFUSED"

i'm use Vue 2 and i wanna detect sockjs errors and show notification.
(Like 'Connection lost','connection timeout' etc. )
I have no idea how to do it
The browser has a built in method called navigator.onLine, which returns either true or false. Now to watch for connection changes you can add an event listener on window,
window.addEventListener('offline', (e) => { console.log('offline'); });
window.addEventListener('online', (e) => { console.log('online'); });
You can incorporate this into a Vue component with something like:
export default {
data() {
return {
online: navigator.onLine
};
},
mounted() {
window.addEventListener("online", this.onchange);
window.addEventListener("offline", this.onchange);
this.onchange();
},
beforeDestroy() {
window.removeEventListener("online", this.onchange);
window.removeEventListener("offline", this.onchange);
},
methods: {
onchange() {
this.online = navigator.onLine;
this.$emit(this.online ? "online" : "offline");
}
}
};
And then use v-if="!online" to selectively render you're offline banner.
Alternatively, take a look at: v-offline, it instead works by pinging an endpoint, which has the advantage of being able to detect when the user is online but with very poor internet connection (loading), however is an overall less efficient approach.
import offline from 'v-offline';
export default {
components: {
offline
},
methods: {
handleConnectivityChange(status) {
console.log(status);
}
}
}
For most sock.js methods, you can get this information from the Event parameter returned by the callback. But for detecting network connection, and other common tasks, it's usually more robust to do natively, as outlined above.

Vue-router - How to cancel operations when user clicked menu links

Not specific to Vue.js but to Javascript Single Page applications. If you have a form and a rather long running submit action, like saving something. The submit operation should save something and then pushing to a new route for a success message.
While waiting for the result, the user clicks on a different link and is going away.
See this fiddle:
https://jsfiddle.net/hajbgt28/4/
const Home = {
template: '<div><button #click="submit">Save and go Bar!</button></div>',
methods: {
async submit() {
await setTimeout(() => {
this.$router.push("/bar");
}, 5000);
}
}
};
const Foo = { template: '<div>Foo</div>' }
const Bar = { template: '<div>Bar</div>' }
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Home },
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
})
new Vue({
router,
el: '#app',
data: {
msg: 'Hello World'
}
})
Click Home
Click the button
Click on "Foo" immediately, you see "Foo"
Wait a few seconds
The Page changes to "Bar"
I have a two solutions in my mind:
I can check inside the submit operation if I am still on the route I expect and only proceed if the user is still on this page. It is rather complicated to check it every time
Disable all links on the page while loading. But this makes the page useless until the operation is finished.
What is the best practice for situations like this?
You could use a beforeRouteLeave navigation guard to abort that action (i.e., cancel the timer in your example) upon switching routes.
Assuming identifiable submit actions, save the ID of the operation result (i.e., save the timer ID from setTimeout's return value in your example).
Add a beforeRouteLeave handler to the component to cancel the submit action (i.e., clear the timer ID in your example).
const Home = {
methods: {
submit() {
this.timerId /* 1 */ = setTimeout(() => {
this.$router.push("/bar");
}, 5000);
}
},
beforeRouteLeave (to, from, next) {
clearTimeout(this.timerId) /* 2 */
next()
}
};
updated jsfiddle
Here's one idea: make a component that provides (using Vue's provide/inject API):
A function that starts an operation. This is called when a form is sent. It provides a whenDone callback which is either executed or ignored, depending on if the operation is cancelled.
A function that cancels all pending operations. The cancel function could be called when the user navigates away.
The implementation could look like this:
const CancellableOperationProvider = {
name: "CancellableOperationProvider",
props: {},
data: () => ({
pendingOperations: []
}),
/*
* Here we provide the theme and colorMode we received
* from the props
*/
provide() {
return {
$addOperation(func) {
this.pendingOperations.push(func);
func(function whenDone(callback) {
if (this.pendingOperations.includes(func)) callback();
});
},
$cancelAllOperations() {
this.pendingOperations = [];
}
};
},
render() {
return this.$slots.default[0];
}
};
The usage would look like this:
const Home = {
template: '<div><button #click="submit">Save and go Bar!</button></div>',
inject: ['$addOperation', '$cancelAllOperations'],
methods: {
async submit() {
this.$addOperation(whenDone => {
await setTimeout(() => {
whenDone(() => this.$router.push("/bar"));
}, 5000);
});
}
}
};
You could then add a navigation guard to the Vue Router so that $cancelAllOperations is called after clicking any link. Since $cancelAllOperations is only accessible through the inject API you will have to make a component that imperatively adds a navigation guard to the Vue router after mounting and removes it when unmounting.
Let me know if it doesn't work--I haven't done Vue in a while.
I used the answer from tony19 to make solution which fits my needs for use cases without setTimeout too:
const Home = {
template: '<div><button #click="submit">Save and go Bar!</button></div>',
data() {
return {
onThisPage: true
}
},
beforeRouteLeave(to, from, next) {
this.onThisPage = false;
next();
},
methods: {
submit() {
setTimeout(() => {
if (this.onThisPage) {
this.$router.push("/bar");
}
}, 5000);
}
}
};
See here: https://jsfiddle.net/ovmse1jg/

Prevent back event in javascript

I want to catch the back event in javascript, react js and show an alert message before changing the route.
I managed to catch the event wit onpopstate, but the route is changed and I want to prevent changing the route until the alert is confirmed:
window.addEventListener("popstate", (ev) => {
ev.preventDefault(ev);
alert("Are you sure")
})
Also tried with react-route, I found here some examples, but none of them worked.
import { browserHistory } from 'react-router';
componentDidMount() {
super.componentDidMount();
this.onScrollNearBottom(this.scrollToLoad);
this.backListener = browserHistory.listen(location => {
if (location.action === "POP") {
// Do your stuff
}
});
}
componentWillUnmount() {
super.componentWillUnmount();
// Unbind listener
this.backListener();
}
It behaves as in the first case, it doesn't prevent the back event.

React component breaks on browser back/forward button

I have two forms, one is called profile, the other is settings. I'm using introjs to have a guided tour through these two forms. If I only move forward through the tour using the introjs 'Next Step' button there are no issues (first image). But, if I use the browser back or forward button, my forms look like the second image.
Code on profile page that utilize introjs:
runTour() {
if (this.state.showTour === true) {
const tourObj = {
userId: Meteor.userId(),
page: 'profile',
};
introJs.introJs().setOption('doneLabel', 'Next step').start().oncomplete(()
=> {
changeIntroTour.call(tourObj);
browserHistory.push('/user/settings');
})
.onexit(() => {
changeIntroTour.call(tourObj);
});
}
}
componentWillReceiveProps(nextProps) {
this.existingSettings(nextProps);
if (nextProps.intro.length > 0) {
this.setState({
showTour: nextProps.intro[0].profileTour,
}, () => {
this.runTour();
});
}
}
The issue was I was not calling introJs.exit() on componentWillUnmount. This was keeping my instance of introJs running from one component to the next which caused the bug.

Meteor: Security in Templates and Iron Router

I'm enjoying working with Meteor and trying out new things, but I often try to keep security in mind. So while I'm building out a prototype app, I'm trying to find the best practices for keeping the app secure. One thing I keep coming across is restricting a user based on either a roll, or whether or not they're logged in. Here are two examples of issues I'm having.
// First example, trying to only fire an event if the user is an admin
// This is using the alaning:roles package
Template.homeIndex.events({
"click .someclass": function(event) {
if (Roles.userIsInRole(Meteor.user(), 'admin', 'admin-group') {
// Do something only if an admin in admin-group
}
});
My problem with the above is I can override this by typing:
Roles.userIsInRole = function() { return true; } in this console. Ouch.
The second example is using Iron Router. Here I want to allow a user to the "/chat" route only if they're logged in.
Router.route("/chat", {
name: 'chatHome',
onBeforeAction: function() {
// Not secure! Meteor.user = function() { return true; } in the console.
if (!Meteor.user()) {
return this.redirect('homeIndex');
} else {
this.next();
}
},
waitOn: function () {
if (!!Meteor.user()) {
return Meteor.subscribe("messages");
}
},
data: function () {
return {
chatActive: true
}
}
});
Again I run into the same problem. Meteor.user = function() { return true; } in this console blows this pattern up. The only way around this I have found thus far is using a Meteor.method call, which seems improper, as they are stubs that require callbacks.
What is the proper way to address this issue?
Edit:
Using a Meteor.call callback doesn't work for me since it's calling for a response asynchronously. It's moving out of the hook before it can handle the response.
onBeforeAction: function() {
var self = this;
Meteor.call('someBooleanFunc', function(err, res) {
if (!res) {
return self.redirect('homeIndex');
} else {
self.next();
}
})
},
I guess you should try adding a check in the publish method in server.
Something like this:
Meteor.publish('messages') {
if (Roles.userIsInRole(this.userId, 'admin', 'admin-group')) {
return Meteor.messages.find();
}
else {
// user not authorized. do not publish messages
this.stop();
return;
}
});
You may do a similar check in your call methods in server.

Categories

Resources