Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular ui-router resolve value as string

With ui-router, I add all resolve logic in state function like this;

    //my-ctrl.js
    var MyCtrl = function($scope, customers) {
      $scope.customers = customers;
    }

    //routing.js
    $stateProvider.state('customers.show', {
      url: '/customers/:id',
      template: template,
      controller: 'MyCtrl',
      resolve: {   // <-- I feel this must define as like controller
        customers: function(Customer, $stateParams) {
          return Customer.get($stateParams.id);
        }
      }
    });

However IMO, resolve object must belong to a controller, and it's easy to read and maintain if it is defined within a controller file.

    //my-ctrl.js
    var MyCtrl = function($scope, customers) {
      $scope.customers = customers;
    }
    MyCtrl.resolve = {
      customers: function(Customer, $stateParams) {
        return Customer.get($stateParams.id);
      };
    };

    //routing.js
    $stateProvider.state('customers.show', {
      url: '/customers/:id',
      template: template,
      controller: 'MyCtrl',
      resolve: 'MyCtrl.resolve'   //<--- Error: 'invocables' must be an object.
    });

However, When I define it as MyCtrl.resolve, because of IIFE, I get the following error.

Failed to instantiate module due to: ReferenceError: MyCtrl is not defined

When I define that one as string 'MyCtrl.resolve', I get this

Error: 'invocables' must be an object.

I see that controller is defined as string, so I think it's also possible to provide the value as string by using a decorator or something.

Has anyone done this approach? So that I can keep my routings.js clean and putting relevant info. in a relevant file?

like image 563
allenhwkim Avatar asked Jun 12 '15 16:06

allenhwkim


3 Answers

It sounds like a neat way to build the resolve, but I just don't think you can do it.

Aside from the fact that "resolve" requires an object, it is defined in a phase where all you have available are providers. At this time, the controller doesn't even exist yet.

Even worse, though, the "resolve" is meant to define inputs to the controller, itself. To define the resolve in the controller, then expect it to be evaluated before the controller is created is a circular dependency.

In the past, I have defined resolve functions outside of the $stateProvider definition, at least allowing them to be reused. I never tried to get any fancier than that.

var customerResolve = ['Customer', '$stateParams',
    function(Customer, $stateParams) {
        return Customer.get($stateParams.id);
    }
];

// ....

$stateProvider.state('customers.show', {
  url: '/customers/:id',
  template: template,
  controller: 'MyCtrl',
  resolve: {
    customers: customerResolve
  }
});
like image 65
Jeff Fairley Avatar answered Nov 20 '22 12:11

Jeff Fairley


This question is about features of ui-router package. By default ui-router doesn't support strings for resolve parameter. But if you look at the source code of ui-router you will see, that it's possible to implement this functionality without making direct changes to it's code.

Now, I will show the logic behind suggested method and it's implementation

Analyzing the code

First let's take a look at $state.transitionTo function angular-ui-router/src/urlRouter.js. Inside that function we will see this code

  for (var l = keep; l < toPath.length; l++, state = toPath[l]) {
    locals = toLocals[l] = inherit(locals);
    resolved = resolveState(state, toParams, state === to, resolved, locals, options);
  }

Obviously this is where "resolve" parameters are resolved for every parent state. Next, let's take a look at resolveState function at the same file. We will find this line there:

dst.resolve = $resolve.resolve(state.resolve, locals, dst.resolve, state);
var promises = [dst.resolve.then(function (globals) {
    dst.globals = globals;
})];

This is specifically where promises for resolve parameters are retrieved. What's good for use, the function that does this is taken out to a separate service. This means we can hook and alter it's behavior with decorator.

For reference the implementation of $resolve is in angular-ui-router/src/resolve.js file

Implementing the hook

The signature for resolve function of $resolve is

this.resolve = function (invocables, locals, parent, self) {

Where "invocables" is the object from our declaration of state. So we need to check if "invocables" is string. And if it is we will get a controller function by string and invoke function after "." character

//1.1 Main hook for $resolve
$provide.decorator('$resolve', ['$delegate', '$window', function ($delegate, $window){ 
  var service = $delegate; 



  var oldResolve = service.resolve;
  service.resolve = function(invocables, locals, parent, self){
     if (typeof(invocables) == 'string') {
       var resolveStrs = invocables.split('.');

       var controllerName = resolveStrs[0];
       var methodName = resolveStrs[1];

       //By default the $controller service saves controller functions on window objec
       var controllerFunc = $window[controllerName];
       var controllerResolveObj = controllerFunc[methodName]();

       return oldResolve.apply(this, [controllerResolveObj, locals, parent, self]);

     } else {
       return oldResolve.apply(this, [invocables, locals, parent, self]);
     }
  };

  return $delegate;
}]);

EDIT:

You can also override $controllerProvider with provider like this:

app.provider("$controller", function () {

}

This way it becomes possible to add a new function getConstructor, that will return controller constructor by name. And so you will avoid using $window object in the hook:

$provide.decorator('$resolve', ['$delegate', function ($delegate){ 
    var service = $delegate; 

    var oldResolve = service.resolve;
    service.resolve = function(invocables, locals, parent, self){
       if (typeof(invocables) == 'string') {
         var resolveStrs = invocables.split('.');

         var controllerName = resolveStrs[0];
         var methodName = resolveStrs[1];

         var controllerFunc = $controllerProvider.getConstructor(controllerName);
         var controllerResolveObj = controllerFunc[methodName]();

         return oldResolve.apply(this, [controllerResolveObj, locals, parent, self]);

       } else {
         return oldResolve.apply(this, [invocables, locals, parent, self]);
       }
    }; 

Full code demonstrating this method http://plnkr.co/edit/f3dCSLn14pkul7BzrMvH?p=preview

like image 28
Dmitry Tolmachov Avatar answered Nov 20 '22 11:11

Dmitry Tolmachov


You need to make sure the controller is within the same closure as the state config. This doesn't mean they need to be defined in the same file.

So instead of a string, use a the static property of the controller:

resolve: MyCtrl.resolve,

Update

Then for your Controller file:

var MyCtrl;
(function(MyCtrl, yourModule) {

    MyCtrl = function() { // your contructor function}
    MyCtrl.resolve = { // your resolve object }

    yourModule.controller('MyCtrl', MyCtrl);

})(MyCtrl, yourModule)

And then when you define your states in another file, that is included or concatenated or required after the controller file:

(function(MyCtrl, yourModule) {

    configStates.$inject = ['$stateProvider'];
    function configStates($stateProvider) {

        // state config has access to MyCtrl.resolve
        $stateProvider.state('customers.show', {
            url: '/customers/:id',
            template: template,
            controller: 'MyCtrl',
            resolve: MyCtrl.resolve
        });
    }

    yourModule.config(configStates);

})(MyCtrl, yourModule);

For production code you will still want to wrap all these IIFEs within another IIFEs. Gulp or Grunt can do this for you.

like image 2
Martin Avatar answered Nov 20 '22 13:11

Martin