Skip to content

Custom UrlMatcher types cannot be decoded asynchronously #1634

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
schmod opened this issue Dec 16, 2014 · 12 comments
Closed

Custom UrlMatcher types cannot be decoded asynchronously #1634

schmod opened this issue Dec 16, 2014 · 12 comments

Comments

@schmod
Copy link

schmod commented Dec 16, 2014

The example contained within the documentation for custom UrlMatcherFactory types implies that I should be able to implement a custom type that resolves some data from an AJAX call:

// Defines a custom type that gets a value from a service,
// where each service gets different types of values from
// a backend API:
$urlMatcherFactoryProvider.type('dbObject', {}, function(Users, Posts) {
  return {
    encode: function(object) {
      // Represent the object in the URL using its unique ID
      return object.id;
    },
    decode: function(value, key) {
      // Look up the object by ID, using the parameter
      // name (key) to call the correct service
      return services[key].findById(value);
    ....

After browsing the code, it doesn't appear that there is any sort of support for asynchronously decoding custom URL parameters.

  1. This functionality would be very nice to have. For my use cases, this would be a much cleaner and reusable way to asynchronously load things than resolve.
  2. We should update the documentation to avoid suggesting that this functionality currently exists.

It sounds like the new transitionProvider magic explained in #1399 might be a prerequisite to getting this to work. Thoughts?

@christopherthielen
Copy link
Contributor

I think that example is jumping the gun a bit. I haven't worked this through to full resolution, but you could of course return a promise from your custom type, which you could then resolve. This, at least, encapsulates the async code into the custom type, e.g., UserType and makes the resolve declaration "boilerplate"

http://plnkr.co/edit/4HvP0zCiQOHKvPwY781o?p=preview

  $urlMatcherFactoryProvider.type('UserType', { pattern: /[0-9]+/ }, function($timeout) {
    return {
      encode: function(user) { return user.id },
      decode: function(userIdStr) {
        if (this.is(userIdStr)) return userIdStr;
        return $timeout(function() {
          return { id: userIdStr, name: 'john doe' };
        }, 2000);
      },
      is: function(val) { return val && angular.isFunction(val.then); }
    };
  });

  $stateProvider.state({ 
    name: 'user', 
    url: '/user/{userid:UserType}', 
    resolve: { user: unwrapStateParam('userid') },
    controller: function(user, $scope) { $scope.user = user; },
    template: 'User: <pre>{{user | json }}</pre>'
  });

You could even register all state parameters as resolves using a decorator and skip the boilerplate resolve declarations:

http://plnkr.co/edit/3MGHlZjR0zBPhQxky0sZ?p=preview

  $urlMatcherFactoryProvider.type('UserType', { pattern: /[0-9]+/ }, function($timeout) {
    return {
      encode: function(user) { return user.id },
      decode: function(userOrStr) {
        if (this.is(userOrStr)) return userOrStr;
        return $timeout(function() {
          return { id: userOrStr, name: 'john doe' };
        }, 2000);
      },
      is: function(val) { return val && angular.isFunction(val.then); }
    };
  });


  $stateProvider.decorator('ownParams', function(state, paramsFn) {
    // Make a resolve fn that returns a state param
    function unwrapStateParam(paramName) {
      return [ '$stateParams', function($stateParams) {
        return $stateParams[paramName];
      }];
    }

    var params = paramsFn(state); 
    angular.forEach(params.$$keys(), function(key) {
      // Add each 'ownParams' as a resolve
      state.resolve[key] = unwrapStateParam(key); 
    });
    return params;
  });

  $stateProvider.state({ 
    name: 'user',
    url: '/user/{user:UserType}', 
    // inject 'user' typed state param
    controller: function(user, $scope) { $scope.user = user; },
    template: 'User: <pre>{{user | json }}</pre>'
  });

Unfortunately this means you are passing around a promise as the state param, not a typed user object, which is not ideal.

@schmod
Copy link
Author

schmod commented Dec 17, 2014

Interesting. It seems like the logical thing to do would be to process params the same way that we process resolve, which actually might not be terribly scary from an architectural point of view.

@christopherthielen
Copy link
Contributor

Yeah, I've been tossing that idea around (add all params to resolve), specifically for this use case, and also for ease-of-use (injecting a param directly, if you want, as opposed to $stateParams)

@schmod
Copy link
Author

schmod commented Dec 17, 2014

Agh. I had a long reply typed out, and lost it.

My thought is basically that we should use the infrastructure that we already have (resolve) for cases where we want to directly inject things into controllers via DI.

I've shied away from extensively using revolve in my own code, because it tends to be brittle, and leads to a lot of duplication and repetition if I want to use a controller in more than one place. Splitting the logic between the controller and the state definition isn't DRY, and breaks encapsulation. It also makes it awkward/difficult to instantiate controllers outside of a state, because Angular's DI isn't particularly forgiving.

Typed URL parameters seem like they could be a huge step in the right direction, as they allow me to consolidate a lot of my code, and significantly streamline the currently-awkward process of passing data between states (or inheriting things asynchronously from parent states).

It also provides a graceful fallback for controllers that could potentially be instantiated without a full set of stateParams or resolves (letting us do something like if ( !$stateParams.user ) { loadUser() } ) -- Doing the same thing with resolves requires a lot of try { $injector.get('User')... }

However, I'm still left with the problem that this encourages me to write controllers that don't know how to load all of the data that they need to run. If I decide that I want to use the same controller in a state and a directive, it would be nice if ui-router defined an idiomatic way to "fill in" missing data that might have otherwise been provided via a resolve or state parameter.

Basically, I want to be able to write properly-encapsulated controllers, but also take advantage of the nice things that resolve and state parameters provide, especially so that I can deduplicate data loading in nested states.

@schmod
Copy link
Author

schmod commented Dec 17, 2014

Tangentially, this is starting to sound like a good case for adding hooks for ui-router to interact with a more comprehensive data-persistence layer like $resource or angular-data

@eddiemonge
Copy link
Contributor

@schmod
Copy link
Author

schmod commented Sep 11, 2015

I think that this is still a valid issue.

Resolves are a powerful feature, but I think they need to be used sparingly. Decoding URL params into objects (possibly via an AJAX call, or a call to some other persistence layer) seems like it could potentially be a much more maintainable solution.

@nateabele
Copy link
Contributor

Yeah, we can leave this open as a potential design issue. I'm not sure what injecting individual parameters would look like, but accessing unwrapped async values without additional ceremony would definitely be handy.

@christopherthielen
Copy link
Contributor

I think this will be possible using transition hooks. A hook can add resolves on the fly. The hook could inspect the state params and create a resolve that fetches the data based on the param.

@christopherthielen
Copy link
Contributor

(I don't think this belongs in the params layer, by the way)

@nateabele nateabele self-assigned this Sep 12, 2015
@trisys3
Copy link

trisys3 commented Feb 15, 2017

One reason for it to still belong in the params layer is ui-sref-active. For example, if we have a dropdown with a bunch of users, and the URL may not have the correct user, and the URL user may even be empty, how do you choose the correct item? I know we could do fancy things like re-resolve the user in the controller, then have ng-class instead of ui-sref-active, but it would be easier if this were done in the params.

One obvious pitfall is how to decide when the state is finally transitioned to/resolved, or whether it is OK for the user to not be known right away and what to show in the UI in the meantime.

@stale
Copy link

stale bot commented Jan 24, 2020

This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs.

This does not mean that the issue is invalid. Valid issues
may be reopened.

Thank you for your contributions.

@stale stale bot added the stale label Jan 24, 2020
@stale stale bot closed this as completed Feb 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants