31

I have had difficultly finding any documentation on utilizing the ui-router dynamically via a database. Par for the course, everything is hard coded.

My Json:

[
   {
       "name": "root",
       "url": "/",
       "parent": "",
       "abstract": true,
       "views": [
            {"name": "header", "templateUrl": "/app/views/header.html"},            
            {"name" :"footer", "templateUrl": "/app/views/footer.html" }
       ]
   },
    {
        "name": "home",
        "url": "",
        "abstract" : false,
        "parent": "root",
        "views": [
            {"name": "container@", "templateUrl": "/app/views/content1.html"},            
            {"name" :"left@", "templateUrl": "/app/views/left1.html" }
       ]
    },
    {
        "name": "about",
        "url": "/about",
        "abstract": false,
        "parent": "root",
        "views": [
             {"name": "container@", "templateUrl": "/app/views/content2.html"},            
             {"name" :"left@", "templateUrl": "/app/views/left2.html" }
            ]
    }
]

My App:

'use strict';

var $stateProviderRef = null;
var $urlRouterProviderRef = null;

var app = angular.module('app', ['ngRoute', 'ui.router']);


 app.factory('menuItems', function ($http) {
    return {
      all: function () {
        return $http({
            url: '/app/jsonData/efstates.js',
            method: 'GET'
        });
    }
  };
 });


  app.config(function ($locationProvider, $urlRouterProvider, $stateProvider) {
    $urlRouterProviderRef = $urlRouterProvider;
    $stateProviderRef = $stateProvider;
    $locationProvider.html5Mode(false);
    $urlRouterProviderRef.otherwise("/");
  });


  app.run(['$q', '$rootScope', '$state', 'menuItems',
  function ($q, $rootScope, $state, menuItems) {
      menuItems.all().success(function (data) {
          angular.forEach(data, function (value, key) {                
              $stateProviderRef.state(name = value.name, {
                  "url": value.url,
                  "parent" : value.parent,
                  "abstract": value.abstract,
                  "views": {
                     // do not want the below hard coded, I want to loop
                     // through the respective json array objects and populate state & views 
                     // I can do this with everything but views.

                     // first loop
                     'header': { 'templateUrl': '/app/views/header.html' },
                     'footer': { 'templateUrl': '/app/views/footer.html' },

                     // second loop
                     'left@':  { 'templateUrl': '/app/views/left1.html' },
                     'container@': { 'templateUrl': '/app/views/container1.html' },

                     // third loop
                     'left@':  { 'templateUrl': '/app/views/left2.html' },
                     'container@': { 'templateUrl': '/app/views/container2.html' },
                }
            });
        });
        $state.go("home");
    });
 }]);

I am having difficultly configuring my views dynamically. Any ideas?


UPDATE:

I made a Plunker per Radim Köhler's answer for anyone interested. I appreciate the help.

I think ui-router is the defacto router for angular and by being dynamic it will make a large app much easier to manage.

  • have you considered passing the data as variable in js file loaded directly in page and avoiding asynch call to get it? – charlietfl Jul 13 '14 at 22:11
  • Yes but that reduces the security I would like to have in place. Your proposal would leave a trail in the browser's cache. –  Jul 13 '14 at 22:12
  • don't really see why it is any less secure than passing it through ajax call, could manually bootstrap angular though after retrieving data also – charlietfl Jul 13 '14 at 22:15
  • If I am understanding you correctly, you suggest I create a js file with the data from the db and read from the js file. Why? Why the addt'l resources/time to print something that can be read str8 from the db? Why have a js file in cache for anyone to read? There has got to be a way to loop thru the views str8 from the db. –  Jul 13 '14 at 22:19
  • what is your specific problem then? My suggestions are based on you not being able to resolve `run()` and the config will fire before you return your data unless you have data available right away or manually bootstrap. Nothing is going to prevent anyone seeing the data – charlietfl Jul 13 '14 at 22:28

3 Answers3

25

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

Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
  • Thank you so much, why do you have a non-named ui-view as well as the other 4 named ui-views? –  Jul 14 '14 at 07:07
  • Great if that helped ;) yeah... that was... for testing ;) sorry, it should not be there! great spot sir ;) – Radim Köhler Jul 14 '14 at 07:08
  • Hey, this is a nice solution but what if i want to go to the current state, not $state.go("home"); I want ui-router to parse and find out which one the current state is and go there. Is there a way to do that? – Kitze Jan 21 '15 at 11:41
  • 1
    Any time you do have access to $state.current. There should be exatly current state. You can use this like `$state.current.name` – Radim Köhler Jan 21 '15 at 11:59
  • 1
    Nope, the current state is Object {name: "", url: "^", views: null, abstract: true}. I don't know in which state i'm in yet because the router was just configured after the $http request... – Kitze Jan 22 '15 at 08:49
  • @RadimKöhler I tried the same example, and the problem that I'm facing is that when I refresh the page it takes me directly to the home page, and as it was said by Kitze, the $state.current.name have no content so it can't be used. is there any way to fix this? Thank you in advance. – Mohamed NAOUALI Nov 08 '16 at 11:10
  • @MohamedNAOUALI YES, because the below answer is much better than this. Please check and read and use this approach http://stackoverflow.com/a/29013914/1679310. That will work even after REFRESH.. because it is designed that way (URL provider is postponing its url resolution, till it is dynamically set) – Radim Köhler Nov 08 '16 at 11:26
19

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

Community
  • 1
  • 1
Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
  • Hi there, thanks for your explanation, very usefull. but what im trying to do now, is to specify the controller also in the json file. but it won't initialize the controller. any idea? – Raphael Müller Mar 20 '15 at 12:46
  • I am not fully sure, what could mean "controller is also in json file" if it is a name, it should not be an issue. If it is implemenation, then I tried to deeply describe how to lazyl load stuff here http://stackoverflow.com/q/22627806/1679310... Hope it gives at least some direction... – Radim Köhler Mar 20 '15 at 12:50
  • thanks for the quick reply. in fact, it's a string stored also in my database, or so i thought i would implement it. I'll take a look at your other answer. – Raphael Müller Mar 20 '15 at 12:57
  • Life saver, I struggled so much with this, why is it not easy to have dynamic states with ui-router... – Tonio Feb 21 '17 at 12:20
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

Pencroff
  • 1,009
  • 9
  • 13