How do I get the Back Button to work with an AngularJS ui-router state machine?
Note
The answers that suggest using variations of $window.history.back()
have all missed a crucial part of the question: How to restore the application's state to the correct state-location as the history jumps (back/forward/refresh). With that in mind; please, read on.
Yes, it is possible to have the browser back/forward (history) and refresh whilst running a pure ui-router
state-machine but it takes a bit of doing.
You need several components:
Unique URLs. The browser only enables the back/forward buttons when you change urls, so you must generate a unique url per visited state. These urls need not contain any state information though.
A Session Service. Each generated url is correlated to a particular state so you need a way to store your url-state pairs so that you can retrieve the state information after your angular app has been restarted by back / forward or refresh clicks.
A State History. A simple dictionary of ui-router states keyed by unique url. If you can rely on HTML5 then you can use the HTML5 History API, but if, like me, you can't then you can implement it yourself in a few lines of code (see below).
A Location Service. Finally, you need to be able manage both ui-router state changes, triggered internally by your code, and normal browser url changes typically triggered by the user clicking browser buttons or typing stuff into the browser bar. This can all get a bit tricky because it is easy to get confused about what triggered what.
Here is my implementation of each of these requirements. I have bundled everything up into three services:
The Session Service
class SessionService
setStorage:(key, value) ->
json = if value is undefined then null else JSON.stringify value
sessionStorage.setItem key, json
getStorage:(key)->
JSON.parse sessionStorage.getItem key
clear: ->
@setStorage(key, null) for key of sessionStorage
stateHistory:(value=null) ->
@accessor 'stateHistory', value
# other properties goes here
accessor:(name, value)->
return @getStorage name unless value?
@setStorage name, value
angular
.module 'app.Services'
.service 'sessionService', SessionService
This is a wrapper for the javascript sessionStorage
object. I have cut it down for clarity here. For a full explanation of this please see: How do I handle page refreshing with an AngularJS Single Page Application
The State History Service
class StateHistoryService
@$inject:['sessionService']
constructor:(@sessionService) ->
set:(key, state)->
history = @sessionService.stateHistory() ? {}
history[key] = state
@sessionService.stateHistory history
get:(key)->
@sessionService.stateHistory()?[key]
angular
.module 'app.Services'
.service 'stateHistoryService', StateHistoryService
The StateHistoryService
looks after the storage and retrieval of historical states keyed by generated, unique urls. It is really just a convenience wrapper for a dictionary style object.
The State Location Service
class StateLocationService
preventCall:[]
@$inject:['$location','$state', 'stateHistoryService']
constructor:(@location, @state, @stateHistoryService) ->
locationChange: ->
return if @preventCall.pop('locationChange')?
entry = @stateHistoryService.get @location.url()
return unless entry?
@preventCall.push 'stateChange'
@state.go entry.name, entry.params, {location:false}
stateChange: ->
return if @preventCall.pop('stateChange')?
entry = {name: @state.current.name, params: @state.params}
#generate your site specific, unique url here
url = "/#{@state.params.subscriptionUrl}/#{Math.guid().substr(0,8)}"
@stateHistoryService.set url, entry
@preventCall.push 'locationChange'
@location.url url
angular
.module 'app.Services'
.service 'stateLocationService', StateLocationService
The StateLocationService
handles two events:
locationChange. This is called when the browsers location is changed, typically when the back/forward/refresh button is pressed or when the app first starts or when the user types in a url. If a state for the current location.url exists in the
StateHistoryService
then it is used to restore the state via ui-router's$state.go
.stateChange. This is called when you move state internally. The current state's name and params are stored in the
StateHistoryService
keyed by a generated url. This generated url can be anything you want, it may or may not identify the state in a human readable way. In my case I am using a state param plus a randomly generated sequence of digits derived from a guid (see foot for the guid generator snippet). The generated url is displayed in the browser bar and, crucially, added to the browser's internal history stack using@location.url url
. Its adding the url to the browser's history stack that enables the forward / back buttons.
The big problem with this technique is that calling @location.url url
in the stateChange
method will trigger the $locationChangeSuccess
event and so call the locationChange
method. Equally calling the @state.go
from locationChange
will trigger the $stateChangeSuccess
event and so the stateChange
method. This gets very confusing and messes up the browser history no end.
The solution is very simple. You can see the preventCall
array being used as a stack (pop
and push
). Each time one of the methods is called it prevents the other method being called one-time-only. This technique does not interfere with the correct triggering of the $ events and keeps everything straight.
Now all we need to do is call the HistoryService
methods at the appropriate time in the state transition life-cycle. This is done in the AngularJS Apps .run
method, like this:
Angular app.run
angular
.module 'app', ['ui.router']
.run ($rootScope, stateLocationService) ->
$rootScope.$on '$stateChangeSuccess', (event, toState, toParams) ->
stateLocationService.stateChange()
$rootScope.$on '$locationChangeSuccess', ->
stateLocationService.locationChange()
Generate a Guid
Math.guid = ->
s4 = -> Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1)
"#{s4()}#{s4()}-#{s4()}-#{s4()}-#{s4()}-#{s4()}#{s4()}#{s4()}"
With all this in place, the forward / back buttons and the refresh button all work as expected.
ui-router browser back button changes url even abort transition is called
for a workaround, you can do $location.path($urlRouter.location); when you abort the transition to have the same url for the state, this even preserves the browser history.
angularjs ui router not supporting browser back and forward buttons
I got the resolution.
I didn't set the default route, that's the reason it was not working.
After setting default route, all was working fine.
How to differentiate between browser back button and $state.go (dynamic params)?
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
}
});
angular ui router, back button does cause state change
The normal behaviour of ui-router
is, that the browser back button is working out of the box (exactly as we would expect).
Try to play with this example, there is also the listener '$stateChangeStart'
writing into console
.run(function($rootScope, $location){
$rootScope.$on('$stateChangeStart',
function (event, toState, toParams){
console.log(toParams)
});
})
(click the blue icon in the right top corner, to open the preview in separated window) ... you can navigate among few details and then use backspace keyobard to mimic the back button... all is correctly re-trigerred... check it here
Related Topics
JavaScript Equivalent of Jquery's Extend Method
How to Get the Containing Form of an Input
Get the Visible Height of a Div with Jquery
Why Does a Module Level Return Statement Work in Node.Js
Using Await Outside of an Async Function
How to Create a Session Using JavaScript
Jquery Drop Down Menu Closing by Clicking Outside
Jquery Ajax Requests Are Getting Cancelled Without Being Sent
Checking Whether Something Is Iterable
How to Create a Jquery Clock/Timer
JavaScript Ie Detection, Why Not Use Simple Conditional Comments
How Does the Messages-Count Example in Meteor Docs Work
How to Handle Circular Dependencies with Requirejs/Amd
Wrapping Lines/Polygons Across the Antimeridian in Leaflet.Js
React - How to Force a Function Component to Render
Stop Cursor from Jumping to End of Input Field in JavaScript Replace