dotJEM / angular-routing

Enhanced state based routing for Angular applications!
https://dotjem.github.io/angular-routing/
MIT License
75 stars 9 forks source link

State decorators? #125

Open groner opened 10 years ago

groner commented 10 years ago

I'm looking for a way to override a view in a state at runtime to use an alternate template and/or controller.

I think what I'm looking for is a $stateProvider.decorate(stateName, decoratorFn) call.

What do you think of this @jeme? Does this sound like an appropriate API?

jeme commented 10 years ago

Is there any reason you can't use transition handlers?

groner commented 10 years ago

How would that work? I just tried modifying $to.views in the before stage, but it didn't seem to have any effect. I'm running v0.6.1 if that matters.

I guess I might be able to drive $view directly, but I would prefer to keep using the declarative style of $state.

jeme commented 10 years ago

Ideally it would make sense that you could just modify the state object in the transition handlers.

It's a bit tricky though, because we don't wan't to to be a permanent change in the configuration, that is why we do copies along the way.

And views adds a bit to that complexity as we traverse the state tree for views.

groner commented 10 years ago

I'm not looking for completely dynamic configuration, I just want to overlay some of the state configuration from a separate module that is only loaded for some users. Once the configuration phase is over, I don't need the states to change.

jeme commented 10 years ago

So that sounds a bit similar to what people wan't to do with server side configurations. See: https://github.com/dotJEM/angular-routing/issues/19

Perhaps that could be of use to you?...

groner commented 10 years ago

Hmmm, I'm not sure. I'm really looking for a way to amend a state without having to restate the details I'm not changing.

jeme commented 10 years ago

Yes I understand that, but what triggered me towards the initialization feature is that you just wanted to do it once.

But I guess the feature isn't explained well enough (I have not gotten around to document that, so many other things to do) But all loading of states are deferred to happen at run-time today, so your already using that feature, you just don't know it.

//So if you have:
$state.state('myState', { /* configure */ });

//You can replace that by: (This happens behind the scene for the above anyways, but without using an injector)
$state.state(['$register', function(reg) {
    reg('myState', { /* configure */ });
}]);

Now given you are talking about specific users..

$state.state(['$register', '$user', function(reg, user) {
    var myState = { /* configure */ }
    if(user.needsExtraStuff) {
      myState.views.extra = { /*..*/ }
    }
    reg('myState', myState);

    if(user.needsExtraStates) {
      reg('extraState', { /* configure */ });
      reg('extraState.child', { /* configure */ });
    }
}]);
groner commented 10 years ago

$register does give some extra flexibility by allowing you to defer configuration until other services are available, but I don't need that for what I'm trying to do here. By the time angular bootstraps, we have selected and loaded the modules needed for the current user.

Conditional blocks don't really help me here because this code is in other modules. I could redefine the state in the other modules, but that requires me to copy (and maintain) the parts of the configuration I'm not changing too. Last time I looked, redefining extraState would cause any child states of it to need ot be redefined as well.

This is what I'm using to allow extra modules to override views right now. I think it will work ok, but dotjem could provide a cleaner way to do it if it would be useful to others.

angular.module('core')

  // This services provides an extension point for states that can have their views overriden.
  .provider('stateViewOverrides', function() {
    var stateViews = {};

    this.define = define;
    this.extend = extend;
    this.$get = angular.noop;

    function define(name, views) {
      stateViews[name] = views;
      return views;
    }

    function extend(name, views) {
      angular.extend(stateViews[name], views);
      return this;
    }
  });
angular.module('core')

  .config(function($stateProvider, stateViewOverridesProvider) {
    $stateProvider

      // Here a state registers it's views with the overrides service
      .state('whatever', {
        views: stateViewOverridesProvider.define('whatever', {
          main: {
            template: 'whatever.html',
            controller: 'WhateverCtrl',
          },
        }),
      });
  });
angular.module('ext')

  // For certain users the ext module is loaded to provide certain customizations.
  .config(function(stateViewOverridesProvider) {
    stateViewOverridesProvider

      .extend('whatever', {
        main: {
          template: 'alt/whatever.html',
          controller: 'WhateverCtrl',
        },
      });
  });
jeme commented 10 years ago

We are really talking about server bound configurations in your case though (based on the user that which only the server can rationale about), which the state loaders was meant to handle, it's just that the way you do it through modules is really a different approach that what angular-routing provides.

It's always super interesting to see different takes on solving the same problem, but I am at a stage where we can't add them all into the core of the framework as that would just generate bloat (there is already plenty of bloat)

Back to what you have done... I honestly never saw that as being a bad pattern, putting decorators around the $stateProvider... I encourage it, and there is a ton of examples:

And I think it's better to do this over adding it into the core. However there could maybe be a more clear way of pushing these decorators directly into the $stateProvider so you could get back to calling: $stateProvider.extend but under the hoods that would be a decorator kicking in.

You can actually already do this , this is javascript after all, there is nothing blocking you from doing:

angular.module('core')
  //Or we could just use config here
  .provider('stateDecorator', function($stateProvider) {
    var views = {};
    var stateFn = $stateProvider.state;
    $stateProvider.state = function(nameOrFn, state) {
       if(angular.isString(nameOrFn) {
         views[nameOrFn] = state.views;
       }
       stateFn(nameOrFn, state);
    }

    $stateProvider.extend = function extend(name, views) {
      angular.extend(stateViews[name], views);
      return this;
    }

    this.$get = angular.noop;
  });

But it would perhaps be better to provide a pattern similar to $provide.decorator(name, decorator) which sadly is for services only.