ngUpgraders/ng-forward

Routing components with ui-router

reppners opened this issue · 13 comments

This issue is here to discuss how a decorator could look like and work under the hood to enable routing to components with ui-router.

Most recent (and thus valid) information about the component router can be taken from the talk of Brian Ford and the corresponding slides:
https://www.youtube.com/watch?v=z1NB-HG0ZH4
https://docs.google.com/presentation/d/1bGyuMqYWYXPScnldAZuxqgnCdtgvXE54HhKds8IIPyI/edit#slide=id.

Component router aims to be universal for ng1/ng2. I don't know the exact release plans. Maybe in the near future the component router will be available for ng1 but still I think there is some value in being able to use ui-router with ng-forward and the component oriented approach.

I think we should try to polyfill the @RouteConfig decorator as much as possible.

@pbastowski were you working on this still? Is it something we should mark as 'help wanted'?

In another issue (which I can't find at the moment) I proposed a @StateConfig decorator that looked and functioned similarly to @RouteConfig:

@StateConfig([
 { path: '/home', component: HomeCmp, as: 'home' }
]}
class App{ }

My hesitation for not calling it @RouteConfig is that the new router should be out in time for Angular 1.5, at which point we could separate between ui-router and component-router based on the route decorator used.

Yeah you are right.

As for how this will work, take this quick example app:

@Component({ selector: 'contact-marketing-page' })
class MarketingCmp{ ... }

@Component({ selector: 'contact-page' })
@StateConfig([
  { 
    path: '/marketing', 
    component: MarketingCmp, 
    as: 'marketing',
    resolve: {
      something: 123
    }
  }
])
class ContactCmp{ ... }

@Component({ selector: 'press-page' })
class PressCmp{ ... }

@Component({ selector: 'home-page' })
class HomeCmp{ ... }

@Component({ selector: 'app' })
@StateConfig([
  {
    path: '/contact',
    component: ContactCmp,
    as: 'contact',
    onEnter: () => {},
    params: {}
  },
  {
    path: '/press',
    component: PressCmp,
    as: 'press'
  },
  {
    path: '/',
    component: HomeCmp,
    as: 'home'
  }
])
class App{ ... }

During our bundle step, we would traverse the state metadata to create a route graph:

let routes = [
  {
    name: 'contact',
    component: ContactCmp,
    path: '/contact',
    config: {
      onEnter: () => {},
      params: {}
    },
    children: [
      {
        name: 'marketing',
        component: MarketingCmp,
        path: '/marketing',
        config: {
          resolve: {
            something: 123
          }
        }
      }
    ]
  },
  {
    name: 'press',
    component: PressCmp,
    path: '/press'
    config: {}
  },
  {
    name: 'home',
    component: HomeCmp,
    path: '/',
    config: {}
  }
];

We would then add a config function to the bundled module that sets up all of the ui-router states generated by walking the graph:

$stateProvider
  .state('contact', {
    template: `<contact-page [params]="$stateParams"></contact-page>`,
    url: '/contact',
    onEnter: () => {},
    params: {}
  })
  .state('contact.marketing', {
    template: `<contact-marketing-page [params]="$stateParams" [something]="something"></contact-marketing-page>`,
    url: '/marketing',
    resolve: {
      something: 123
    }
  })
  .state('press', {
    template: `<press-page [params]="params"></press-page>`
  })
  .state('home', {
    template: `<home-page [params]="params"></home-page>`
  })

@brandonroberts: I'd like to hear your thoughts on this proposal.

Looks coherent to me 👍 I like the concept of passing the params and resolves as inputs to the component.

Two things come to my mind:

Can we support ui-router named views?

ContactCmp template

<div ui-view>
<div ui-view="marketing-outlet">
@StateConfig([
  { 
    path: '/marketing', 
    component: MarketingCmp, 
    as: 'marketing',
    resolve: {
      something: 123
    },
    in: 'marketing-outlet'
  }
])
class ContactCmp{ ... }
let routes = [
  ...
      {
        name: 'marketing',
        component: MarketingCmp,
        path: '/marketing',
        in:'marketing-outlet',
        config: {
          resolve: {
            something: 123
          }
        }
      }
    ...
];
$stateProvider
  .state('contact', {
    template: `<contact-page [params]="$stateParams"></contact-page>`,
    url: '/contact',
    onEnter: () => {},
    params: {}
  })
  .state('contact.marketing', {
    url: '/marketing',
    views: {
      'marketing-outlet': {
        template: `<contact-marketing-page [params]="$stateParams" [something]="something"></contact-  marketing-page>`,
        resolve: {
          something: 123
        }
      }
    }
  })

How can we enable DI in resolve, onEnter(), onExit()?

@Injectable()
class MyInjectableService { ... }

@StateConfig([
  { 
    path: '/marketing', 
    component: MarketingCmp, 
    as: 'marketing',
    resolve: {
      something: [ "$http", MyInjectableService, function ($http, MyInjectableService) {
        ...
      }]
    }
  }
])
class ContactCmp{ ... }

Would DI with an array work, just as usual but with the ability to use Injectables directly? It should be possible to resolve the DI to plain strings in the bundle-step, right?

Maybe we could do something similar to your config proposal?

@Component({ selector: 'home-page' })
class HomeCmp{
  @Resolves('SomeValue')
  @Inject('$q', MyInjectableService)
  static resolveSomeValue($q, MyInjectableService){

  }
}

@MikeRyan52 @reppners I like the proposed StateConfig option. Will the path allow you to define a nested url that is referenced from a root path? (e.g. /contact instead of /contact/marketing). I am a little torn on importing the components for the states, as it may limit what you can do with lazy loading if people want to break their app into multiple modules. Also, what happens to features like state decorators that are available in ui-router?

The best part of ng-forward is its still totally just angular 1. So you can still use regular angular ui-router syntax if you want.

Maybe we could do something similar to your config proposal?

This would be a solution. But it is kind of decoupling something that should be in one place. Also it introduces a "magic string" to target the resolve variable which is bad for maintenance. Could ng-forward throw an error when a resolve-variable is present that is not defined as an input on the component?

Will the path allow you to define a nested url that is referenced from a root path? (e.g. /contact instead of /contact/marketing).

Do you mean something like absolute routes? Should be working just fine since its passed through as state url.

I am a little torn on importing the components for the states, as it may limit what you can do with lazy loading if people want to break their app into multiple modules.

Thats something we should have in mind. I would be a fan of being able to have an @AsyncStateConfig to stick to the way the ComponentRouter attributes are distinguished between static and dynamic loading. I think we should look at the way future states work to get an idea of how this could be implemented.

I had a look at the merge, I can't seem to find anything in the code (or the docs) regarding passing the resolves / route parameters to the child components.

I think both should just be added as attributes to the component.

Example:

@Component({ selector: 'contact-page' })
@StateConfig([
  { 
    path: '/:contactId', 
    component: ContactDetailCmp, 
    as: 'detail',
    resolve: {
      something: 123
    }
  }
])
class ContactCmp{ ... }

This should become:

<contact-detail [contact-id]="$stateParams.contactId" [something]="resolves.something"></contact-detail>

I would prefer this over simply passing all params as an object as it would keep the interface to the components cleaner I think.

@eXaminator we don't want to do large features that aren't direct polyfills of angular 2. For what you are asking for I am waiting on the result of this thread on the ng2 forums. angular/angular#4452

Ok so they JUST closed that issue over in the ng2 forums. And they suggest the following approach for populating state component inputs:

@ViewChild query for the component and setting the data on afterViewInit

So hopefully we can polyfill both @ViewChild and afterViewInit well enough to cover this for state and route components as well.