AngularJS - UI-router - How to configure dynamic views
Solution 1:
There is a plunker showing how we can configure the views dynamically. The updated version of the .run()
would be like this:
app.run(['$q', '$rootScope', '$state', '$http',
function ($q, $rootScope, $state, $http)
{
$http.get("myJson.json")
.success(function(data)
{
angular.forEach(data, function (value, key)
{
var state = {
"url": value.url,
"parent" : value.parent,
"abstract": value.abstract,
"views": {}
};
// here we configure the views
angular.forEach(value.views, function (view)
{
state.views[view.name] = {
templateUrl : view.templateUrl,
};
});
$stateProviderRef.state(value.name, state);
});
$state.go("home");
});
}]);
Check that all in action here
Solution 2:
I have to append an improved version, the one which is even able to do more.
So, now we will still load states dynamically - using $http
, json
and define states in .run()
But now we can navigate to any dynamic state with url (just place it in address bar).
The magic is built in into the UI-Router - see this part of doc:
$urlRouterProvider
The
deferIntercept(defer)
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.
Cited snippet:
var app = angular.module('app', ['ui.router.router']);
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();
}).run(function($rootScope, $urlRouter, UserService) {
$rootScope.$on('$locationChangeSuccess', function(e) {
// UserService is an example service for managing user state
if (UserService.isLoggedIn()) return;
// Prevent $urlRouter's default handler from firing
e.preventDefault();
UserService.handleLogin().then(function() {
// 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();
});
So the updated plunker is here. We can now use even the .otherwise()
to navigate to lately defined state, or go there by url:
The .config()
phase
app.config(function ($locationProvider, $urlRouterProvider, $stateProvider) {
// Prevent $urlRouter from automatically intercepting URL changes;
// this allows you to configure custom behavior in between
// location changes and route synchronization:
$urlRouterProvider.deferIntercept();
$urlRouterProvider.otherwise('/other');
$locationProvider.html5Mode({enabled: false});
$stateProviderRef = $stateProvider;
});
The .run()
phase
app.run(['$q', '$rootScope','$http', '$urlRouter',
function ($q, $rootScope, $http, $urlRouter)
{
$http
.get("myJson.json")
.success(function(data)
{
angular.forEach(data, function (value, key)
{
var state = {
"url": value.url,
"parent" : value.parent,
"abstract": value.abstract,
"views": {}
};
angular.forEach(value.views, function (view)
{
state.views[view.name] = {
templateUrl : view.templateUrl,
};
});
$stateProviderRef.state(value.name, state);
});
// Configures $urlRouter's listener *after* your custom listener
$urlRouter.sync();
$urlRouter.listen();
});
}]);
Check the updated plunker here
Solution 3:
I have investigated different approaches for dynamic adding routes to ui.router
.
First of all I found repositori with ngRouteProvider and main idea of it, is resolving $stateProvider
ans save it in closure on .config
phase and then use this refference in any other time for adding new routs on the fly. It is good idea, and it may be improved. I think better solution is to divide responsibility for saving providers references and manipulating them in any othe time and place. Look at example of refsProvider
:
angular
.module('app.common', [])
.provider('refs', ReferencesProvider);
const refs = {};
function ReferencesProvider() {
this.$get = function () {
return {
get: function (name) {
return refs[name];
}
};
};
this.injectRef = function (name, ref) {
refs[name] = ref;
};
}
Using refsProvider
:
angular.module('app', [
'ui.router',
'app.common',
'app.side-menu',
])
.config(AppRouts)
.run(AppRun);
AppRouts.$inject = ['$stateProvider', '$urlRouterProvider', 'refsProvider'];
function AppRouts($stateProvider, $urlRouterProvider, refsProvider) {
$urlRouterProvider.otherwise('/sign-in');
$stateProvider
.state('login', {
url: '/sign-in',
templateUrl: 'tpl/auth/sign-in.html',
controller: 'AuthCtrl as vm'
})
.state('register', {
url: '/register',
templateUrl: 'tpl/auth/register.html',
controller: 'AuthCtrl as vm'
});
refsProvider.injectRef('$urlRouterProvider', $urlRouterProvider);
refsProvider.injectRef('$stateProvider', $stateProvider);
}
AppRun.$inject = ['SectionsFactory', 'MenuFactory', 'refs'];
function AppRun(sectionsFactory, menuFactory, refs) {
let $stateProvider = refs.get('$stateProvider');
// adding new states from different places
sectionsFactory.extendStates($stateProvider);
menuFactory.extendStates($stateProvider);
}
SectionsFactory
and MenuFactory
is common places for declaration / loading all states.
Other idea it is just using exist angular app phases and extend app states by additional providers, something like:
angular.module('app.side-menu', []);
.provider('SideMenu', SideMenuProvider);
let menuItems = [
{
label: 'Home',
active: false,
icon: 'svg-side-menu-home',
url: '/app',
state: 'app',
templateUrl: 'tpl/home/dasboard.html',
controller: 'HomeCtrl'
}, {
label: 'Charts',
active: false,
icon: 'svg-side-menu-chart-pie',
url: '/charts',
state: 'charts',
templateUrl: 'tpl/charts/main-charts.html'
}, {
label: 'Settings',
active: false,
icon: 'svg-side-menu-settings',
url: '/'
}
];
const defaultExistState = menuItems[0].state;
function SideMenuProvider() {
this.$get = function () {
return {
get items() {
return menuItems;
}
};
};
this.extendStates = ExtendStates;
}
function ExtendStates($stateProvider) {
menuItems.forEach(function (item) {
if (item.state) {
let stateObj = {
url: item.url,
templateUrl: item.templateUrl,
controllerAs: 'vm'
};
if (item.controller) {
stateObj.controller = `${item.controller} as vm`;
}
$stateProvider.state(item.state, stateObj);
} else {
item.state = defaultExistState;
}
});
}
In this case we no need to use .run
phase for extenfing states, and AppRouts
from example above, will changed to:
AppRouts.$inject = ['$stateProvider', '$urlRouterProvider', 'SideMenuProvider'];
function AppRouts($stateProvider, $urlRouterProvider, SideMenuProvider) {
$urlRouterProvider.otherwise('/sign-in');
$stateProvider
.state('login', {
url: '/sign-in',
templateUrl: 'tpl/auth/sign-in.html',
controller: 'AuthCtrl as vm'
})
.state('register', {
url: '/register',
templateUrl: 'tpl/auth/register.html',
controller: 'AuthCtrl as vm'
})
SideMenuProvider.extendStates($stateProvider);
}
Also we still have access to all menu items in any place: SideMenu.items