AngularJs UI-Router - Page Refresh $state.current is now empty - angularjs

I have an Angular application using ui-router and I am having issues whenever I refresh the page. I am using nested views, named views to build the application. Whenever I refresh the page, ui-router doesn't reload the current state and just leaves the page blank.
On page load $state.current is equal to
Object {name: "", url: "^", views: null, abstract: true}
I am reading my navigation from a .json file via $http and looping through the states. So this is what I can show:
stateProvider.state(navElement.stateName, {
url: navElement.regexUrl ? navElement.regexUrl : url,
searchPage: navElement.searchPage, //something custom i added
parent: navElement.parent ? navElement.parent : "",
redirectTo: navElement.redirectTo,
views: {
'subNav#index': {
templateUrl: defaults.secondaryNavigation,
controller: 'secondaryNavigationController as ctrl' //static
},
'pageContent#index': {
template: navElement.templateUrl == null
? '<div class="emptyContent"></div>'
: undefined,
templateUrl: navElement.templateUrl == null
? undefined
: navElement.templateUrl,
controller: navElement.controller == null
? undefined
: navElement.controller + ' as ctrl'
}
}
});
This code gets executed for each item in the nested json object. If there is anything else that would be helpful, let me know.

There is a question: AngularJS - UI-router - How to configure dynamic views with one answer, which shows how to do that.
What is happening? On refresh, the url is evaluated sooner, then states are registered. We have to postpone that. And solution is driven by UI-Router native feature deferIntercept(defer)
$urlRouterProvider.deferIntercept(defer)
As stated in the doc:
Disables (or enables) deferring location change interception.
If you wish to customize the behavior of syncing the URL (for example, if you wish to defer a transition but maintain the current URL), call this method at configuration time. Then, at run time, call $urlRouter.listen() after you have configured your own $locationChangeSuccess event handler.
In a nutshell, we will stop URL handling in config phase:
app.config(function ($urlRouterProvider) {
// Prevent $urlRouter from automatically intercepting URL changes;
// this allows you to configure custom behavior in between
// location changes and route synchronization:
$urlRouterProvider.deferIntercept();
})
And we will re-enable that in .run() phase, once we configured all dynamic states from JSON:
.run(function ($rootScope, $urlRouter, UserService) {
...
// Once the user has logged in, sync the current URL
// to the router:
$urlRouter.sync();
// Configures $urlRouter's listener *after* your custom listener
$urlRouter.listen();
});
There is a plunker from the linked Q & A

I don't know how are all your routes.. but if you refresh a page of a child state, you need to pass all parameters of the parents states to be resolved correctly.

Related

ui-router is reloading controller

Using ui-router 1.0.6.
Every time I return to an url (using ui-sref) it reloads the controller. I would like to avoid that and to load the controller only the first time it is accessed.
In this example Plunkr: every time I switch repeatedly between Hello and About it logs the console.
It can be wrapped in a parent controller to track who's already loaded
Here is a working example: Plnkr
Basically you create another controller that holds an object with an empty list:
myApp.controller('ModuleNumCtrl', function() {
loadedCtrl = {};
});
And set it to be parent by setting the abstract attribute to true:
var parentState = {
abstract: true,
name: 'parent',
controller: 'ModuleNumCtrl'
};
Then you set the the exiting controllers to be his children by prefixing their names with 'parent.'
var helloState = {
name: 'parent.hello',
url: '/hello',
template: '<h3>hello world!</h3>',
controller: 'ModuleTwoCtrl'
};
var aboutState = {
name: 'parent.about',
url: '/about',
template: '<h3>Its the UI-Router hello world app!</h3>',
controller: 'ModuleOneCtrl'
};
$stateProvider.state(parentState);
$stateProvider.state(helloState);
$stateProvider.state(aboutState);
Then on each controller you want to load only once, you can add it to the list the first time it's loaded and the code that you want to run only once put in an if statement:
myApp.controller('ModuleOneCtrl', function() {
if (!loadedCtrl.one) {
console.log("One");
}
loadedCtrl.one = true;
});
Last thing, don't forget to change the HTML with the new controllers names:
<a ui-sref="parent.hello" ui-sref-active="active">Hello</a>
<a ui-sref="parent.about" ui-sref-active="active">About</a>
There's a plugin for ui-router which can do that, named sticky-states: https://github.com/ui-router/sticky-states
I would build on top of your plunker, but i can't find a CDN that's hosting sticky states. I found a CDN for ui-router-extras which is the equivalent for sticky states in ui-router 0.x, but for 1.x that won't work.
What you'll need to do is
1) Add the plugin. The github page for sticky-states gives instructions on how to do this, which i'll replicate here:
import {StickyStatesPlugin} from "ui-router-sticky-states";
angular.module('myapp', ['ui.router']).config(function($uiRouterProvider) {
$uiRouterProvider.plugin(StickyStatesPlugin);
});
2) For the state definitions that you want to remain active, add the property sticky: true, as in:
var aboutState = {
name: 'about',
url: '/about',
template: '<h3>Its the UI-Router hello world app!</h3>',
controller : 'ModuleOneCtrl',
sticky: true
}
With this flag, moving from a state to a sibling state will not exit the old state, but rather will "inactivate" it. The controller remains loaded. If you try to enter that old state, it will be "reactivated". The state is now active, but the existing controller is reused.
Note that sticky states will still be exited if you do one of the following:
1) exit the parent of the sticky state
2) directly activate the parent of the sticky state
So you'll need to arrange your tree of states so that that either can't happen , or only happens when you want it to.

Angularjs UI bootstrap temporarily change URL on open and revert to original URL on close

I want to temporarily change the browser url when the ui bootstrap modal is opened ( The page behind should remain as is, only the url changes ). When the modal is closed the url should be reverted back to the original one.
Steps :
User loads the page
url : xyz.com/home
User clicks a link opens a modal
url : xyz.com/detail/123
possible solution : changing url with html5 push state
problem : Angular ui-router tries to run its routes as per the changed url, eventually changing the background page.
User closes the modal
url : xyz.com/home
possible solution : html5 pop state
problem : Reloads the background page, which kills the purpose
Example implementation : Pinterest pins and their pin details popup.
You can use ui-router-extras sticky state to solve your problem. There is simple example with modal by the link. You should create two named views, one for main content (background) and one for modal.
<div ui-view="app"></div>
<div ui-view="modal"></div>
Mark the state, from what you want to access to modal as sticky: true in route definition.
.state('main', {
abstract: true,
url: '/',
templateUrl: '_layout.html'
})
.state('main.index', {
url: '',
sticky: true,
views: {
'app': {
templateUrl: 'index.html'
}
}
})
.state('main.login', {
url: 'login/',
views: {
'modal': {
templateUrl: 'login.html'
}
}
})
Also add an event for stateChangeSuccess:
$rootScope.$on('$stateChangeSuccess', function (ev, to, toParams, from, fromParams) {
if ((from.views && !from.views.modal) || !from.views) {
$rootScope.from = from;
$rootScope.fromParams = fromParams;
}
});
so, when you need to close modal, you can just
$state.go($rootScope.from, $rootScope.fromParams);
There is small problem for that solution. If you reload page on the modal state, then the app ui-view will be empty.
This can be achieved by having a nested state and triggering the modal using onEnter callback:
$stateProvider
.state('contacts', {
url: '/home',
templateUrl: 'home.html',
controller: function($scope, MyService){
$scope.contacts = MyService.getContacts();
}
})
.state('contacts.details', {
url: "^/details/:id", // using the absolute url to not have the "/home" prepended
onEnter: function($state, $uibModal) {
var modal = $uibModal.open({
templateUrl: 'details.html',
controller: function($scope, $stateParams, MyService) {
// get data from service by url parameter
$scope.contact = MyService.getContact($stateParams.id);
}
});
modal.result.finally(function() {
$state.go('^'); // activate the parent state when modal is closed or dismissed
});
}
});
This technique is described in the ui-router's FAQ.
Here the plunk. In this example the modal's scope is created as a child of the $rootScope - the default $uibModal's behavior when no scope is passed to it. In this case we should use the service in the modal's controller to obtain the data by url parameter.
To have master and details URLs look like these - xyz.com/home and xyz.com/detail/123 - we should use the absolute URL (^/details/:id) in the child state.
Using this solution you can open the detail URLs directly and still have both, master and detail states, activated properly, so sharing the detail URL is possible.
I think you can achive that with ngSilent module
https://github.com/garakh/ngSilent
using $ngSilentLocation.silent('/new/path/');
(once you open modal and again after closing it)
Managed to implement this using https://github.com/christopherthielen/ui-router-extras/tree/gh-pages/example/stickymodal

AngularJS ui.router root state should be called always during app init

I am working on web application using AngularJS and have used ui.router for routing.
I have configured the app
.state('init', {
url: '/',
controller: 'LocalizationCtrl',
templateUrl: 'partials/common/init.html'
})
.state('login', {
url: '/login',
templateUrl: 'partials/auth/login.html',
controller: 'LoginCtrl',
resolve: {
skipIfLoggedIn: skipIfLoggedIn
}
});
In init I load the localization json from server
If I hit the following URL it all works fine
http://localhost/app/index.html
However if I hit the following URL or any other state directly the localization files do not load
http://localhost/app/index.html#/login
How can I make sure that when app is loaded first using any URL the localization code should execute and not bypassed.
/ Bu default go to state -
angular.module('yourModuleName').run(["$location", function ($location) {
$location.url('/');
}]);
So, when you refresh or take your web application, your site will go to url /, so it should invoke your state, init to work.
Or you can use $state to go to a state upon starts
/ Bu default go to state -
angular.module('yourModuleName').run(["$state", function ($state) {
$state.go('init');
}]);
Its very simple there is a property "Resolve" you can use that.
In your parent state you can write this -
.state('parentState', {
resolve : {
localize : function() {
//Localization code here
}
}
};
Resolve will ensure that your localization work will be done before controller is loaded.

Ui-router: how to block url navigation but accept state navigation

I try to do some functionality that accepts a state navigation (using $state.go(otherState)), refreshing the associated url in the adress/url bar but it blocks (or redirects to not allowed page) if user directly puts this url in the adress/url bar.
Could it be done by ui-router rules or something inside ui-router module?
I put the example code:
$stateProvider.state("main", {
url: "/index.html",
templateUrl: "main.html"
}).state("notAccessibleScreenByBar", {
url: "/private/example.html",
templateUrl: "example.html"
});
From main view (index.html), the next angular code will be executed:
$state.go("notAccessibleScreenByBar");
This action changes the view, loading example.html and refreshing the url bar to /private/example.html.
If user puts /private/example.html in the adress/url bar, ui-router must block this request (or redirect to not-allowed page).
What you are trying to do seems very similar to any web authentication standard, but, if you don't want that, you could use $locationChangeSuccess (docs here)
A basic example inspired by this sample:
$rootScope.$on('$locationChangeSuccess', function(e, newUrl, oldUrl) {
// Prevent $urlRouter's default handler from firing
e.preventDefault();
if(isThisTransitionValid(newUrl, oldUrl)) {
// Ok, let's go
$urlRouter.sync();
});
});

UI Router conditional ui views?

I can't figure out a reasonable way, which doesn't feel like a hack, to solve this rather trivial problem.
I want a guest to see a splash page when they access the index of the website and a logged in user to see their profile, with each page having it's own template and controller. Ideally, there would be two states for one url, and somehow I would be able to automatically alter the active one depending on the loggin status. Both of these views will have their own nested views so ng-include cannot be used (I assume).
I'm quite new to angular and ui router and think I might be overlooking an easy solution to the problem.
Could it be done with named views and ng-show?
If you're using UI Router, just create three states: the root state, with the '/' URL, and two direct descendant states with no URLs. In the onEnter of the root state, you detect the state of the user and transition to the correct child state accordingly. This gives the appearance of keeping the same URL for both child states, but allows you to have to separate states with separate configurations.
The templateUrl can be a function as well so you can check the logged in status and return a different view and define the controller in the view rather than as part of the state configuration
My Solution:
angular.module('myApp')
.config(function ($stateProvider) {
$stateProvider
.state('main', {
url: '/',
controller: function (Auth, $state) {
if (someCondition) {
$state.go('state1');
} else {
$state.go('state2');
}
}
});
});
where state 1 and state 2 are defined elsewhere.
For docs purposes, I used:
$rootScope.$on('$stateChangeStart', function(event, toState) {
if ((toState.name !== 'login') && (!$localStorage.nickname)) {
event.preventDefault();
$state.go('login');
}
});
Using $routeChangeStart didn't work for me.
It is used for me conditional view in ui-route
$stateProvider.state('dashboard.home', {
url: '/dashboard',
controller: 'MainCtrl',
// templateUrl: $rootScope.active_admin_template,
templateProvider: ['$stateParams', '$templateRequest','$rootScope', function ($stateParams, templateRequest,$rootScope) {
var templateUrl ='';
if ($rootScope.current_user.role == 'MANAGER'){
templateUrl ='views/manager_portal/dashboard.html';
}else{
templateUrl ='views/dashboard/home.html';
}
return templateRequest(templateUrl);
}]
});
If I understand the question; you want to make sure that the user who hasn't logged in cannot see a page that requires log in. Is that correct?
I've done so with code like this inside a controller:
if(!'some condition that determines if user has access to a page'){
$location.path( "/login" );
}
Anywhere (probably in some high-level controller) you should be able to just bind a '$routeChangeStart' event to the $rootScope and do your check then:
$rootScope.$on('$routeChangeStart', function(next, current){
if(next != '/login' && !userLoggedIn){
$location.path( "/login" );
}
});
This will get fired every time a new route is set, even on the first visit to the page.
The way I've done this is pretty simple. I made one for our A/B testing strategy. This is the gist:
resolve: {
swapTemplate: function(service) {
// all of this logic is in a service
if (inAbTest) {
this.self.templateUrl = '/new/template.html';
}
}
... other resolves
}
This gets called before the template is downloaded and therefor you're allowed to swap out the template url.
In my case, if two states can share logic of same controller, conditional template is a good choice. Otherwise, creating separate states is a good option.

Resources