I have an AngularJS application, that uses ui-router and ui-router-extras, where some transitions between states are intercepted and redirected to another ones.
So basically, when a $stateChangeStart event is triggered, the app checks if that transition is allowed or needs to be blocked and then redirect the user somewhere else (eg: a login dialog, a missing step in the process, etc...). Transitions can be also cancelled.
The interception works this way:
$rootScope.$on("$stateChangeStart", function(e,to,toParams,from,fromParams){
if( <mustBeIntercepted> || <mustBeCancelled> ){
// this prevents the actual state change from happening
e.preventDefault();
if( <redirectionRequired> ){
$state.go( <stateName>, <stateParams>);
}
}
});
And this works nicely so far. The problem is that when cancelling a state change, $previousState is not aware of the cancellation, and registers the current one as previous, in the same way that if you do not cancel the transition. So basically $previousState is unaware of the transition cancellation. This causes that when trying to return to the previous state programmatically, it does not move because $state == $previousState.
I created a live demo here: http://plnkr.co/edit/dXjj2iSwDU1DtPMuqW1W?p=preview
Basically, you click on Step1, and then on Confirm, the app will cancel the transition and prompt you if you want to confirm. If you confirm, then it will transit to the originally desired state. Note that in the real app this is not done with confirm but with a custom overlay that cannot block for an answer in the same way that confirm does.
As you will notice, in the top bar there are two labels that show the content of $state and $previousState. If you cancel the confirm prompt, you will see that $previousState changes to "Step1" despite of that the transition didn't happen.
How can I avoid this?
The bug was reported and it has been resolved: https://github.com/christopherthielen/ui-router-extras/issues/120
Related
I need to simply update the URL params in some cases (through code) and not trigger any events. But, I still need to trigger certain actions if the user goes back using the browser back button or if he changes the url params manually.
Prior to UI-Router v1.x, I used to use { notify: false } in combination with the $stateChangeSuccess event for this kind of stuff. But now that the dynamic params are the way to go I can't figure out how to make this work any more.
I have defined all the params of the route as dynamic, and when I execute the $state.go(".", params), the controller does not get refreshed. Which is expected. The $transition.onSuccess does however still fire which I find slightly odd and I don't expect this event to fire in this case.
When I move back with the back button, or if I change a param manually, the same thing happens. The controller doesn't get refreshed and the $transition.onSuccess fires.
My main problem is how do I know one event came from the user, and the other one came from the code? What am I missing here? I checked out the transition param of the onSuccess callback, but couldn't find anything on it that would help me. Is there a different event/hook I should use or is this simply not possible any more with the latest UI-Router?
Thnx.
Seems like I found a way to find out where the transition came from. If somebody has a different/better solution, please post it, I don't find this the neatest solution ever but it works and considering I couldn't find a better one, I'll stick with it until someone shows me a better way.
// The "to:" ensures the event doesn't trigger for other routes
this.$transitions.onSuccess({ to: "your.route" }, (trans) => {
let changedParams = trans._changedParams();
if (trans._options.source === "url" && // transition came from URL change
changedParams && changedParams.length > 0) { // at least one param changed
// do something
}
});
I was able to prevent navigation as per the v4 docs, but I'm trying to hook up a function so that I can use a modal instead of an alert.
Function:
abandonForm = (route) => {
this.props.showModal('confirm');
console.log('leaving..');
}
In my page:
<NavigationPrompt when={true} message={(location) => this.abandonForm('confirm')} />
this.props.showModal('confirm') activates the modal successfully, but behind the modal the page still transitions - how can I prevent transition until a button in the modal is clicked?
Browsers only allow navigation cancellation by means of the alert box that you've mentioned. This restriction is motivated by phishing/scamming sites that try to use javascript gimmicks to create user experiences that convincingly mimic something that a browser or the OS would do (whom the user trusts). Even the format of the text shown in the alert box is crafted so that it's obvious that it originates from the site.
Of course, as long as the current URL stays within your app, you have control over it using react-router's history. For example you can do the following on navigation:
allow the navigation without confirmation
immediately navigate back to the previous location, but now with a modal on top
navigate away for real this time when the user clicks on a button in the modal.
The disadvantage of this approach (leaving out the sheer complexity of it) is that the user will not get a confirmation dialog if they try to navigate to a different site entirely.
Use:
this.unBlock = this.props.history.block((location, navigateToSelectedRoute) => {
// save navigateToSelectedRoute eg this.navigateToSelectedRoute =
// navigateToSelectedRoute;
// use this.navigateToSelectedRoute() afterwards to navigate to link
// show custom modal using setState
});
and when unblocking is done then call this.unBlock() to remove the listener.
Documentation here for history api
If a user attempts to change the active page (window/tab/etc) from our page to another while myState is true, we want to notify/alert the user in React JS using react-router.
I tried implementing it with the help TransitionHook and React-router's Confirming Navigation article. Though these only point out / work when user wants to close the page or reload it. Whereas I need to know when user just temporarily leaves the page without necessarily closing it.
How can I achieve this?
If by "leaving the page", you mean that the page is open, but a different window has popped up, you could consider using the document.hasFocus property. Here is one way I handled a problem that was tangentially related:
componentDidMount: function (){
setInterval(()=>{
if (document.hasFocus()){
this.checkServerState();
}
},
},
I am making a cordova app, using angularjs and ui-router , the problem is when i use the cordova back button event ,my app doesn't go to the previous page, but if i comment the back button event everything works fine , i want to track the back button event but its seems not to work for me , i tried the below code also doesn't seems to help me.
document.addEventListener("backbutton", (e) => {
e.preventDefault();
e.stopPropagation();
}, true);
please help me with the issue
e.preventDefault() as it's name suggests prevents the default behaviour. Therefore you stay on the same page.
If you want to control route switches / state switches imho it's better to listen for the stateChangeStart event. There you can also prevent state changes and you have the extra bonus of knowing which state it came from and where it's going.
The justification
In my BB app, I allow rapid input from users which gets queued & sent off periodically in the background to the server. The problem I currently have is if a user leaves the page they effectively discard any pending changes sitting in the queue.
So basically what I want to do is inform the user before they leave to give them the opportunity to wait for the changes to be saved rather than just exiting & discarding.
The nitty gritty
So for the general cases where the user refreshes or attempts to navigate to an external URL we can handle the onbeforeunload event. Where it becomes slightly tricky is when we are in the context of an SPA whereby switching between pages does not cause a page refresh.
My immediate thought was to use a global click event handler for all anchors and validate whether or not I want to allow the click, which would work for in-site link navigation. However, where this falls over is navigating via the browsers Back/Forward buttons.
I also had a look at Backbone.routefilter, which at first glance appeared to do exactly what I needed. However, using the simple case as described in the docs, the route was still being executed.
The question
How do we intercept navigation for all scenarios within a Backbone SPA?
Direct link navigation
Use a global event handler to capture all click events
$(document).on('click', 'a[href^="/"]', function (e) {
var href = $(e.currentTarget).attr('href');
e.preventDefault();
if (doSomeValidation()) {
router.navigate(href, { trigger: true });
}
});
Page refreshing / external URL navigation
Handle the onbeforeunload event on the window
$(window).on('beforeunload', function (e) {
if (!doSomeValidation()) {
return 'Leaving now will may result in data loss';
}
});
Browser back/forward button navigation
Behind the scenes Backbone.Router uses the Backbone.history which ultimately leverages the HTML5 pushstate API. Depending on what options you pass to Backbone.history.start, and what your browser is capable of, the API will hook into either the onhashchange event or the onpopstate event.
Delving into the source for Backbone.history.start it becomes apparent that regardless of whether you are using push state or not, the same event handler is used i.e. checkUrl.
if (this._hasPushState) {
addEventListener('popstate', this.checkUrl, false);
} else if (this._wantsHashChange && this._hasHashChange && !this.iframe) {
addEventListener('hashchange', this.checkUrl, false);
} else if (this._wantsHashChange) {
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}
Therefore, we can override this method & perform our validation in there
var originalCheckUrl = Backbone.history.checkUrl;
Backbone.history.checkUrl = function (e) {
if (doSomeValidation()) {
return originalCheckUrl.call(this, e);
} else {
// re-push the current page into the history (at this stage it's been popped)
window.history.pushState({}, document.title, Backbone.history.fragment);
// cancel the original event
return false;
}
};