AngularJS ui-router default state for states without a URL - javascript

We have a couple of states for one page, so the states don't have any URL assigned to them. Is there a way to specify a default state for URL-less states? Maybe similar to $urlRouterProvider.otherwise('/');.
I tried going to a state in Angular's run statement or within the controller but that causes a transition superseded error, which is probably due to the fact that $state hasn't been initialized.
Below is a short example. I would like to start directly in stateA.
angular
.module('myModule')
.config([
'$stateProvider',
function($stateProvider) {
$stateProvider
.state('stateA', {
controller: function () {
// Do some magic related to state A
}
})
.state('stateB', {
controller: function () {
// Do some magic related to state B
}
});
}
])
.controller(['$state', '$timeout', function($state, $timeout){
// My global controller
// To set a default state I could do:
// $timout(function () {
// $state.go('stateA');
// }, 0);
// But that doesn't feel right to me.
}]);
Update:
Thanks to this answer I figured out that I have to wrap $state.go into a $timeout, to avoid the transition superseded error.

This will call a custom rule that goes to a default state without tampering with a URL.
$urlRouterProvider.otherwise(function($injector) {
var $state = $injector.get('$state');
$state.go('stateA');
});`

Related

$resolve not added to $scope until after controller creation

I'm trying to take advantage of the (new in 1.5.0) feature that adds the resolve map to the scope of the route. However, it seems like the map isn't actually getting added to the scope until after the controller has loaded.
Here's a very simple module that demonstrates the issue (from this plunk):
var app = angular.module("app", ['ngRoute']);
app.config(['$routeProvider', '$locationProvider',
function($routeProvider, $locationProvider) {
$routeProvider.otherwise({
controller: "editController",
controllerAs: "ec",
templateUrl: "edit.html",
resolve: {
resolveData: function() {
return {
foo: "bar",
blah: "halb"
}
}
}
});
}
]);
app.controller("editController", ["$scope", function($scope) {
// undefined
console.log($scope.$resolve);
setTimeout(function() {
// the object is there, including $scope.$resolve.resolveData
console.log($scope.$resolve);
}, 0)
}]);
If you watch the console, you'll note that $scope.$resolve is undefined when the controller is created. However, it's there immediately afterwards, as demonstrated by the setTimeout.
I can't find anything in the documentation that suggests this should be the case. Is this a bug? Or am I just not getting something fundamental about how angular works?
In case someone else comes across this - it was already reported on the angular github and resolved as intended behavior. $scope.$resolve isn't really meant to be used in controllers. You are supposed to inject it, or use $route.current.locals.
(Or you can wrap the code using $scope.$resolve in a $timeout.)
Notice your object is much bigger than you expect, and actually your object is on $resolve. This is mostly explained in the docs, however, could be more elaborate with examples...
the resolve map will be available on the scope of the route, under
$resolve
No need to dig into this, when you resolve, the named object becomes an injectable you can then place on $scope, in this case, resolveData. Observe the following...
app.controller('editController', ['$scope', 'resolveData', function($scope, resolveData) {
console.log(resolveData);
// -- $scope.data = resolveData; // -- assign it here
}]);
Plunker - updated demo
Investigation of why you are getting undefined is due to the nature of awaiting digest cycles in the framework. An acceptable workaround to get a handle on $resolve would include injecting and wrapping the call in a $timeout, which forces a digest cycle in which your object will be available in the asynchronous callback. If this approach is not ideal, forego injecting $timeout and either call $route.current.locals directly for your resolved object, or inject your object as a named parameter of which it will have resolved immediately as the example above demonstrates.

Angular ui-router: stop controller from reloading on press of back button in the browser

It seems, by default, the controller of the previous state is reloaded when you press the back button in the browser to go to a previous state.
(this is not true in case of parent-child states)
How can I prevent that from happening?
Since I am not going to change any data in my current state which can affect the previous state, I don't want the previous state to reload again.
Here is a small plunker:
http://plnkr.co/edit/xkQcEywRZVFmavW6eRGq?p=preview
There are 2 states: home and about. If you go to about state and then press back button, you will see that the home state controller is called again.
.state('home', {
url: '/home',
templateUrl: 'partial-home.html',
controller: function($scope) {
console.log('i was called');
}
})
I believe this is the expected behavior, but I want to prevent it because my previous state (home in this case) is doing some visualizations which take some time to be created again.
Let's start with a global controller like GlobalCtrl which is added to the <body> or <html> tag like ng-controller="GlobalCtrl.
Doing this will enable us to keep the scope of this GlobalCtrl throughout your single page Angular app (as you are using ui-router).
Now, inside your GlobalCtrl define something like this:
$rootScope.globalData = {preventExecution: false};
// This callback will be called everytime you change a page using ui-router state
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState) {
$scope.globalData.preventExecution = false;
// Just check for your states here
if (toState.name == "home" && fromState.name == "about") {
$scope.globalData.preventExecution = true;
}
});
Now, in your state configuration, you can use this $scope.globalData.preventExecution;
.state('home', {
url: '/home',
templateUrl: 'partial-home.html',
controller: function($scope) {
if ($scope.globalData.preventExecution) {
return;
}
console.log('i was called');
}
});
Answer to the question: The scope that we refer in the GlobalCtrl and the scope that we use in the State controller, how are they related?
Well, it is a very good question but it's simple. Every time a new scope is created in Angular, it always inherits its parent scope (unless isolated). So when your home state controller instantiated, its scope created using parent state i.e. $rootScope here in this case and we are instantiating the globalData in the $rootScope which is an Object (an Object in Javascript can be used to it's any nested object. Read this). So now when we are setting the globalData.preventExecution true/false, the same data can be used in the $scope of your home state controller. This is how both scopes are related or using the same data.
Answer to the question: is there some flag or setting in the ui-router which can accomplish this in general
If you want to achieve the above behaviour code for multiple states then you can write something like this:
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState) {
$scope.globalData.preventExecution = false;
if (toState.name == "home" && fromState && fromState.preventHomeReExecution) {
$scope.globalData.preventExecution = true;
}
});
Now, your states can be written like this:
.state('about', {
url: '/about',
templateUrl: 'partial-about.html',
preventHomeReExecution: true
})
.state('foo', {
url: '/foo',
templateUrl: 'partial-foo.html',
})
.state('bar', {
url: '/bar',
templateUrl: 'partial-bar.html'
preventHomeReExecution: true
})
Basically, we are using preventHomeReExecution: true as a flag you wanted.

how to make a default nested state for a non abstracted state

I'm using ui-router as follow:
3 main states home, profile and settings like this:
The 2nd state profile have 2 nested states profile.about and profile.main
I need that the profile state clickable on the index page loads the profile view and redirects to the default nested state which is profile.about here
Here's a plunker with what I got so far.
I'm aware of this solutions:
how to set up a defaultindex child state
how to set default child view with angular-ui-router
And others but all require an abstract parent which is unclickable hence abstract.
So, to summaries my question is how to make a nested default view for a non abstracted state?
As discussed in comment, solution here would usually be with the .when() defintion as desribed here:
Angular UI-Router $urlRouterProvider .when not working *anymore*
Angular UI-Router $urlRouterProvider .when not working when I click <a ui-sref="...">
In version before 2.0.13 we could use the native solution like this (myService is a place for some default value... not needed if no params):
var whenConfig = ['$urlRouterProvider', function($urlRouterProvider) {
$urlRouterProvider
.when('/app/list', ['$state', 'myService', function ($state, myService) {
$state.go('app.list.detail', {id: myService.Params.id});
}])
.otherwise('/app');
}];
...
app.config(whenConfig)
But after a fix in version 2.0.13 this is not working, but we can make it with this workaround:
var onChangeConfig = ['$rootScope', '$state',
function ($rootScope, $state) {
$rootScope.$on('$stateChangeStart', function (event, toState) {
if (toState.name === "app.list") {
event.preventDefault();
$state.go('app.list.detail', {id: 2});
}
});
}]
See also this bug report for some other examples: https://github.com/angular-ui/ui-router/issues/1584

Why does $timeout affect how $stateParams is resolved in a ui-bootstrap modal?

Following the ui-router FAQ entry, I recently implemented a generic AngularJS service that turns any ui-router state definition into a state definition that wraps a ui-bootstrap modal.
As a part of this, the resolve object needs to be pushed down from the state definition to the $modal.open() options. The major problem with this is that the $stateParams that is injected into these resolve functions are those of the previous state. After many hacky attempts at solving this, I found that simply wrapping the $modal.open() call in a $timeout block results in the desired behavior.
In general, I'd like to understand why this works the way that it does, whether or not it's an acceptable solution, and if there are any caveats involved. In the past I've been able to resolve several Angular timing issues by simply wrapping a block of code in $timeout, and it makes me nervous since I'm really not sure why it works. I'm guessing that a $timeout forces the block to run after the current digest cycle ends, but I'm not too confident about that.
I created a Plunker that demonstrates this -- if you remove the $timeout and invoke the modal, the parameter will not resolve.
Note: As a caveat, I MUST be able to resolve $stateParams properly in the modal resolves -- I have many existing controllers and state definitions that I'd rather not have to go back and refactor.
Follow-up: I have created ui-router issue 1649 to request a resolution to this issue -- also linked there is a minimal Plunker that only uses $injector.invoke to demonstrate the issue with no modal at all.
This is happening because you have not transitioned yet to the new state when secondparam is being resolved. The $timeout puts your code at the end of the queue, the state transition happens then executes your code with the expected state. You can tell by logging or alerting the current state in your resolve config:
secondparam: ['$stateParams', function($stateParams) {
alert($state.$current.url); //here
return $stateParams.secondparam;
}]
Unfortunately the documentation for onenter and onexit does not clearly tell when in the lifecycle they are invoked: https://github.com/angular-ui/ui-router/wiki#onenter-and-onexit-callbacks. However this post gives some indication:
These callbacks give us the ability to trigger an action on a new view
or before we head out to another state.
I think you'd be better off using a controller and opening your modal there instead of the $timeout (where I believe your context will be window/global).
$stateProvider.state('demo.modal', {
url: '/modal/:secondparam',
template: 'showing modal',
controller: function($scope, $modal, $state){
var modalInstance = $modal.open({
template: '<div class="alert alert-success" role="alert">Remove the $timeout and this will not resolve: {{secondparam}}</div>',
resolve: {
secondparam: ['$stateParams', function($stateParams) {
console.log($state.$current.url, $stateParams); //you are in the new state
return $stateParams.secondparam; //'secondparam' IS resolved, even without $timeout
}]
},
controller: function($scope, secondparam) {
$scope.secondparam = secondparam;
}
});
modalInstance.result.finally(function() {
$state.go('^');
});
}
});
Note that I had to add <div ui-view></div> to the demo state to get the controller to instantiate (ui-router nested route controller isn't being called).
$stateProvider.state('demo', {
url: '/demo/:firstparam',
template: '<button class="btn btn-primary" ui-sref=".modal({ secondparam: 2 })">Show Modal</button>' +
'<button class="btn btn-primary" ui-sref="demo.contacts">Show Contacts</button><div ui-view></div>'
});
http://plnkr.co/edit/zdSkAsEyIef0tWxSTC5p?p=preview
It could be an unintended behavior, or something "by design" of onEnter.
The solution is to inject $stateParams into the onEnter handler - it would point to the params of the second state (as per documentation) of onEnter.
$stateProvider.state('demo.modal', {
url: '/modal/:secondparam',
onEnter: showModal,
// ...
});
function showModal($state, $stateParams) {
var secondparam = $stateParams.secondparam; // equals 2
...
// BUT, the state has not yet transitioned
var secondParamFromState = $state.params.secondparam; // is undefined
...
}
The $timeout allows the state to transition, and the the resolve of the $modal gets injected the current params, which would be for the second state.
EDIT: updated plunker

Angularjs - refresh controller scope on route change

Was wondering what is the best way to refresh controller scope on route change?
Any help would be appreciated!
I would refactor to have all my initialisation of data properties on the scope in a single function, called something like initScope() that is called when the controller is first run, and also on the $routeChangeSuccess (and probably also $routeChangeUpdate if you want to handle changes to the URL that resolve to the same route) event(s).
e.g.
app.controller('MyCtrl', ['$scope', function ($scope) {
function initScope() {
$scope.foo = 1;
$scope.bar = { ram: 'ewe' };
}
initScope();
$scope.$on('$routeChangeUpdate', initScope);
$scope.$on('$routeChangeSuccess', initScope);
}

Categories

Resources