ui-router / angular

UI-Router for Angular: State-based routing for Angular (v2+)
https://ui-router.github.io/ng2/
MIT License
354 stars 135 forks source link

Redirect abstract state to pre-configured "default" child state #196

Closed litera closed 6 years ago

litera commented 6 years ago

Using Angular 5 and UIRouter state routing. I'm using an additional custom route state property as per this interface.

interface AugmentedNg2RouteDefinition extends Ng2StateDeclaration {
    default?: string | ((...args: any[]) => string | Promise<string>);
}

When I define an abstract state, I can now add a default property to it as well, so when one would try to route to an abstract state, the default should redirect them to configured default child state.

As can be understood from the interface above, the default may be defined as any of the following:

// relative state name
default: '.child',
// absolute state name
default: 'parent.child',
// function with DI injectables
default: (auth: AuthService, stateService: StateService) => {
    if (auth.isAuthenticated) {
        return '.child';
    } else {
        return stateService.target('.login', { ... });
    }
}
// function with DI injectables returning a promise
default: (items: ItemsService) => {
    return items
        .getTotal()
        .then((count) => {
            return count > 7
                ? '.simple'
                : '.paged';
        });
}

To actually make the default work, I have to configure route transition service:

@NgModule({
  imports: [
    ...
    UIRouterModule.forChild({  // or "forRoot"
      states: ...
      // THIS SHOULD PROCESS "default" PROPERTY ON ABSTRACT STATES
      config: (uiRouter: UIRouter, injector: Injector, module: StatesModule) => {
        uiRouter.transitionService.onBefore(
          // ONLY RUN THIS ON ABSTRACTS WITH "default" SET
          {
            to: state => state.abstract === true && !!state.self.default
          },
          // PROCESS "default" VALUE
          transition => {
            let to: transition.to();
            if (angular.isFunction(to.default)) {
              // OK WE HAVE TO EXECUTE THE FUNCTION WITH INJECTABLES SOMEHOW
            } else {
              // this one's simple as "default" is a string
              if (to.default[0] === '.') {
                  to.default = to.name + to.default;
              }
              return transition.router.stateService.target(to.default);
            }
          }
        );
      }
    })
  ]
})
export class SomeFeatureModule { }

So the problem is invoking the default when it's a function that likely has some injectable services/values...

Configuration function's injector (config: (uiRouter: UIRouter, injector: Injector, module: StatesModule)) can only be used to get service instances, but can't invoke functions with injectable parameters.

In AngularJS, this would be accomplished by $injector.invoke(...) which would call the function and inject its parameters.

Questions

  1. How should I handle default when it's defined as a function with injectable which may be singleton or not?
  2. Is there a difference if I configure my UIRouter using forRoot or forChild? I can only see the main difference in having more configuration possibilities when using the former, but when it comes to routing upper configuration would run for any routing transition...
christopherthielen commented 6 years ago

First off, I should mention that it looks like you're re-implementing redirectTo link (but adding support for relative states).

So the problem is invoking the default when it's a function that likely has some injectable services/values... In AngularJS, this would be accomplished by $injector.invoke(...) which would call the function and inject its parameters.

Angular doesn't support arbitrary injection of functions like AngularJS does. The injection supported by Angular is similar to how AngularJS handles service instantiation (the injectable has to actually be registered with DI).

You'll have to wire the injector into your callback, then within the callback, ask the injector for each service you depend on. See redirectTo docs for an example of getting the Injector from the Transition.

redirectTo: (trans: Transition) => {
    let auth: AuthService = trans.injector.get(AuthService);
    let stateService: StateService = trans.injector.get(StateService);

    if (auth.isAuthenticated) {
        return 'fully.qualified.child';
        // or return trans.to().name + '.child';
    } else {
        return stateService.target('.login', { ... });
        // or return Promise.resolve(stateService.target('.login'));
    }
}

Is there a difference if I configure my UIRouter using forRoot or forChild?

The difference is that .forRoot creates and initializes the UIRouter instance, as well as creates all the global ui-router services that can be injected, such as StateService. Just remember to add one (and only one) forRoot in your app, as an import to the top level NgModule.

Hope this was helpful.

christopherthielen commented 6 years ago

As a side note, supporting relative states in redirectTo seems like a nice feature. If this is something you'd like out of the box, feel free to make a PR. https://github.com/ui-router/core/blob/c22f69476c01faf5b22efe7f3e3611e177ace38f/src/hooks/redirectTo.ts#L25