I have implemented an angularjs single page application using ui-router.
Originally I identified each state using a distinct url however this made for unfriendly, GUID packed urls.
So I have now defined my site as a much simpler state-machine. The states are not identified by urls but are simply transitioned to as required, like this:
Define Nested States
angular
.module 'app', ['ui.router']
.config ($stateProvider) ->
$stateProvider
.state 'main',
templateUrl: 'main.html'
controller: 'mainCtrl'
params: ['locationId']
.state 'folder',
templateUrl: 'folder.html'
parent: 'main'
controller: 'folderCtrl'
resolve:
folder:(apiService) -> apiService.get '#base/folder/#locationId'
Transition to a Defined State
#The ui-sref attrib transitions to the 'folder' state
a(ui-sref="folder({locationId:'{{folder.Id}}'})")
| {{ folder.Name }}
This system works very well and I love its clean syntax. However, as I am not using urls the back button does not work.
How do I keep my neat ui-router state-machine but enable the back button functionality?
This question is related to
javascript
angularjs
coffeescript
angular-ui-router
If you are looking for the simplest "back" button, then you could set up a directive like so:
.directive('back', function factory($window) {
return {
restrict : 'E',
replace : true,
transclude : true,
templateUrl: 'wherever your template is located',
link: function (scope, element, attrs) {
scope.navBack = function() {
$window.history.back();
};
}
};
});
Keep in mind this is a fairly unintelligent "back" button because it is using the browser's history. If you include it on your landing page, it will send a user back to any url they came from prior to landing on yours.
browser's back/forward button solution
I encountered the same problem and I solved it using the popstate event
from the $window object and ui-router's $state object
. A popstate event is dispatched to the window every time the active history entry changes.
The $stateChangeSuccess
and $locationChangeSuccess
events are not triggered on browser's button click even though the address bar indicates the new location.
So, assuming you've navigated from states main
to folder
to main
again, when you hit back
on the browser, you should be back to the folder
route. The path is updated but the view is not and still displays whatever you have on main
. try this:
angular
.module 'app', ['ui.router']
.run($state, $window) {
$window.onpopstate = function(event) {
var stateName = $state.current.name,
pathname = $window.location.pathname.split('/')[1],
routeParams = {}; // i.e.- $state.params
console.log($state.current.name, pathname); // 'main', 'folder'
if ($state.current.name.indexOf(pathname) === -1) {
// Optionally set option.notify to false if you don't want
// to retrigger another $stateChangeStart event
$state.go(
$state.current.name,
routeParams,
{reload:true, notify: false}
);
}
};
}
back/forward buttons should work smoothly after that.
note: check browser compatibility for window.onpopstate() to be sure
After testing different proposals, I found that the easiest way is often the best.
If you use angular ui-router and that you need a button to go back best is this:
<button onclick="history.back()">Back</button>
or
<a onclick="history.back()>Back</a>
// Warning don't set the href or the path will be broken.
Explanation: Suppose a standard management application. Search object -> View object -> Edit object
Using the angular solutions From this state :
Search -> View -> Edit
To :
Search -> View
Well that's what we wanted except if now you click the browser back button you'll be there again :
Search -> View -> Edit
And that is not logical
However using the simple solution
<a onclick="history.back()"> Back </a>
from :
Search -> View -> Edit
after click on button :
Search -> View
after click on browser back button :
Search
Consistency is respected. :-)
Can be solved using a simple directive "go-back-history", this one is also closing window in case of no previous history.
Directive usage
<a data-go-back-history>Previous State</a>
Angular directive declaration
.directive('goBackHistory', ['$window', function ($window) {
return {
restrict: 'A',
link: function (scope, elm, attrs) {
elm.on('click', function ($event) {
$event.stopPropagation();
if ($window.history.length) {
$window.history.back();
} else {
$window.close();
}
});
}
};
}])
Note: Working using ui-router or not.
app.run(['$window', '$rootScope',
function ($window , $rootScope) {
$rootScope.goBack = function(){
$window.history.back();
}
}]);
<a href="#" ng-click="goBack()">Back</a>
history.back()
and switch to previous state often give effect not that you want. For example, if you have form with tabs and each tab has own state, this just switched previous tab selected, not return from form. In case nested states, you usually need so think about witch of parent states you want to rollback.
This directive solves problem
angular.module('app', ['ui-router-back'])
<span ui-back='defaultState'> Go back </span>
It returns to state, that was active before button has displayed. Optional defaultState
is state name that used when no previous state in memory. Also it restores scroll position
Code
class UiBackData {
fromStateName: string;
fromParams: any;
fromStateScroll: number;
}
interface IRootScope1 extends ng.IScope {
uiBackData: UiBackData;
}
class UiBackDirective implements ng.IDirective {
uiBackDataSave: UiBackData;
constructor(private $state: angular.ui.IStateService,
private $rootScope: IRootScope1,
private $timeout: ng.ITimeoutService) {
}
link: ng.IDirectiveLinkFn = (scope, element, attrs) => {
this.uiBackDataSave = angular.copy(this.$rootScope.uiBackData);
function parseStateRef(ref, current) {
var preparsed = ref.match(/^\s*({[^}]*})\s*$/), parsed;
if (preparsed) ref = current + '(' + preparsed[1] + ')';
parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/);
if (!parsed || parsed.length !== 4)
throw new Error("Invalid state ref '" + ref + "'");
let paramExpr = parsed[3] || null;
let copy = angular.copy(scope.$eval(paramExpr));
return { state: parsed[1], paramExpr: copy };
}
element.on('click', (e) => {
e.preventDefault();
if (this.uiBackDataSave.fromStateName)
this.$state.go(this.uiBackDataSave.fromStateName, this.uiBackDataSave.fromParams)
.then(state => {
// Override ui-router autoscroll
this.$timeout(() => {
$(window).scrollTop(this.uiBackDataSave.fromStateScroll);
}, 500, false);
});
else {
var r = parseStateRef((<any>attrs).uiBack, this.$state.current);
this.$state.go(r.state, r.paramExpr);
}
});
};
public static factory(): ng.IDirectiveFactory {
const directive = ($state, $rootScope, $timeout) =>
new UiBackDirective($state, $rootScope, $timeout);
directive.$inject = ['$state', '$rootScope', '$timeout'];
return directive;
}
}
angular.module('ui-router-back')
.directive('uiBack', UiBackDirective.factory())
.run(['$rootScope',
($rootScope: IRootScope1) => {
$rootScope.$on('$stateChangeSuccess',
(event, toState, toParams, fromState, fromParams) => {
if ($rootScope.uiBackData == null)
$rootScope.uiBackData = new UiBackData();
$rootScope.uiBackData.fromStateName = fromState.name;
$rootScope.uiBackData.fromStateScroll = $(window).scrollTop();
$rootScope.uiBackData.fromParams = fromParams;
});
}]);
The Back button wasn't working for me as well, but I figured out that the problem was that I had html
content inside my main page, in the ui-view
element.
i.e.
<div ui-view>
<h1> Hey Kids! </h1>
<!-- More content -->
</div>
So I moved the content into a new .html
file, and marked it as a template in the .js
file with the routes.
i.e.
.state("parent.mystuff", {
url: "/mystuff",
controller: 'myStuffCtrl',
templateUrl: "myStuff.html"
})
Source: Stackoverflow.com