Angular ui-router resolve value as string
Asked Answered
H

5

12

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?

Halbeib answered 12/6, 2015 at 16:26 Comment(1)
Like mentioned earlier, controller isn't available yet at that point. The design pattern I use is having a Resolver service. Using this.self.name in the resolving functions scope (where you make your Customer.get() request), you can get the name of the state. Then, doing something like return Resolver.prepare( this.self.name ), you can extract all logic into Resolver, and reduce your code to a single (and equal) line in your state declarations.Sexism
C
8

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
  }
});
Cristalcristate answered 12/6, 2015 at 16:41 Comment(2)
config state, it's not required to have full controller object, as you see controller is just a string.Halbeib
It's true, but at runtime, in order for Angular to evaluate the resolve, it will need to create the controller. It cannot create the controller until it has evaluated the resolve.Cristalcristate
C
4

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

Caterinacatering answered 22/6, 2015 at 14:5 Comment(5)
I like it except you get controller from $window instead of $controller. Is it possible to make it use $controller? No matter what I will give you the bounty since you did the most effort to solve the problem.Halbeib
Unfortunately it near to impossible. $controller instantiates controller and tries to resolve dependencies. But because we have custom dependencies(resolve parameters) $controller will say it does not know "customers" parameter. It could be possible to decorate $controllerProvider service and override it's register function. This would give a way to get a list of controller's functions. But current angular do not allow this nowadays. Angular exposes result of $get function instead of actual service. So "register" function is left out of viewCaterinacatering
The next option was to alter $injector. But again, angular doesnt allow to decorate this service and simple overriding of it's methods gives nothing.Caterinacatering
Ok, it seems you can override $controllerProvider with provider function. This way you can add a necessary "getConstructor" to it and avoid using $window object to get controller constructor. I updated the plunker with these new findingsCaterinacatering
I don't recommend implementing this workaround. UI-Router 1.0 no longer uses the $resolve service, so this workaround will have a limited life span.Asquith
E
2

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.

Eyeball answered 15/6, 2015 at 15:31 Comment(4)
how do I make the controller be in the same closure when each file is using IIFE?Halbeib
Add the IIFE around your concatenated js at build time. Uglify has an option to wrap your code in a closure, an IIFE. This way your code will be within the same IIFE.Eyeball
That will work in production, but not in development mode, which does not run any uglifier.Halbeib
I updated my original answer with a solution that should work with or without a build process.Eyeball
J
0

If the intention is to have the resolver in the same file as the controller, the simplest way to do so is to declare the resolver at the controller file as a function:

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

//routing.js
$stateProvider.state('customers.show', {
  url: '/customers/:id',
  template: template,
  controller: 'MyCtrl',
  resolve: resolverMyCtrl_customers
});
Juniper answered 1/7, 2015 at 7:51 Comment(0)
F
0

This should work.

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

//routing.js
$stateProvider
    .state('customers.show', {
        url: '/customers/:id',
        template: template,
        resolve: { 
            customer: function(CustomerService, $stateParams){
                return CustomerService.get($stateParams.id)
            } 
        },
        controller: 'MyCtrl'
});


//service.js
function CustomerService() {
    var _customers = {};

    this.get = function (id) {
        return _customers[id];
    };
}
Freud answered 18/3, 2016 at 21:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.