AngularAMD + ui-router + dynamic controller name?

Sayak Banerjee picture Sayak Banerjee · Dec 14, 2014 · Viewed 10.1k times · Source

I'm trying to write a generalized route in my application and resolve the view and controller names on the fly based on the route params.

I have the following code that works:

$stateProvider.state('default', angularAMD.route({
    url: '/:module/:action?id',

    templateUrl: function (params) {
        var module = params.module;
        var action = module + params.action.charAt(0).toUpperCase() 
                            + params.action.substr(1);

        return 'app/views/' + module + '/' + action + 'View.html';
    },

    controller: 'userController',
}));

However, I'm unable to figure out a way to resolve the controller name dynamically. I tried using resolve as described here, but ui-router seems to handle resolve differently than angular-route.

Any pointers?

EDIT: I've already tried using controllerProvider but it doesn't work for me (for instance, the following code just returns a hard coded controller name to test whether it actually works):

controllerProvider: function () {
    return 'userController';
}

Gives me the following error:

Error: [ng:areq] Argument 'userController' is not a function, got undefined http://errors.angularjs.org/1.3.3/ng/areq?p0=userController&p1=not%20aNaNunction%2C%20got%20undefined

Answer

Radim Köhler picture Radim Köhler · Dec 14, 2014

This is a link to working plunker.

solution

We need two features of the UI-Router:

  • resolve (to load the missing pieces of js code)
  • controllerProvider (see cites from documentation below)

angularAMD - main.js definition

This would be our main.js, which contains smart conversion controllerName - controllerPath:

require.config({

    //baseUrl: "js/scripts",
    baseUrl: "",

    // alias libraries paths
    paths: {
        "angular": "angular",
        "ui-router": "angular-ui-router",
        "angularAMD": "angularAMD",

        "DefaultCtrl": "Controller_Default",
        "OtherCtrl": "Controller_Other",
    },

    shim: {
        "angularAMD": ["angular"],
        "ui-router": ["angular"],
    },

    deps: ['app']
});

controllers:

// Controller_Default.js
define(['app'], function (app) {
    app.controller('DefaultCtrl', function ($scope) {
        $scope.title = "from default"; 
    });
}); 

// Controller_Other.js
define(['app'], function (app) {
    app.controller('OtherCtrl', function ($scope) {
        $scope.title = "from other";
    });
});

app.js

Firstly we would need some method converting the param (e.g. id) into controller name. For our test purposes let's use this naive implementation:

var controllerNameByParams = function($stateParams)
{
    // naive example of dynamic controller name mining
    // from incoming state params

    var controller = "OtherCtrl";

    if ($stateParams.id === 1) {
        controller = "DefaultCtrl";
    }

    return controller;
}

.state()

And that would be finally our state definition

$stateProvider
    .state("default", angularAMD.route({
        url: "/{id:int}",
        templateProvider: function($stateParams)
        {
            if ($stateParams.id === 1)
            {
                return "<div>ONE - Hallo {{title}}</div>";
            }
            return "<div>TWO - Hallo {{title}}</div>";
        },
        resolve: {
            loadController: ['$q', '$stateParams',
                function ($q, $stateParams)
                {
                    // get the controller name === here as a path to Controller_Name.js
                    // which is set in main.js path {}
                    var controllerName = controllerNameByParams($stateParams);

                    var deferred = $q.defer();
                    require([controllerName], function () { deferred.resolve(); });
                    return deferred.promise;
                }]
        },
        controllerProvider: function ($stateParams)
        {
            // get the controller name === here as a dynamic controller Name
            var controllerName = controllerNameByParams($stateParams);
            return controllerName;
        },
    }));

Check it here, in this working example

documentation

As documented here: $stateProvider, for a state(name, stateConfig) we can use controller and controllerProvider. Some extract from documentation:

controllerProvider

...

controller (optional) stringfunction

Controller fn that should be associated with newly related scope or the name of a registered controller if passed as a string. Optionally, the ControllerAs may be declared here.

controller: "MyRegisteredController"

controller:
"MyRegisteredController as fooCtrl"}

controller: function($scope, MyService) {
$scope.data = MyService.getData(); }

controllerProvider (optional) function

Injectable provider function that returns the actual controller or string.

controllerProvider:
  function(MyResolveData) {
    if (MyResolveData.foo)
      return "FooCtrl"
    else if (MyResolveData.bar)
      return "BarCtrl";
    else return function($scope) {
      $scope.baz = "Qux";
    }
  }

...

resolve

resolve (optional) object

An optional map<string, function> of dependencies which should be injected into the controller. If any of these dependencies are promises, the router will wait for them ALL to be resolved before the controller is instantiated...

I.e. let's use controllerProvider:

... to resolve the controller name dynamically...

In case, that you managed to get here, maybe you'd like to check another similar solution with RequireJS - angular-ui-router with requirejs, lazy loading of controller