Router: Design for routerCanActivate, an injectable alternative to @CanActivate
See original GitHub issueThis issue describes a design for an injectable alternative to @CanActivate
It is both a fresh start and a continuation of the conversations associated with issues #748 and #4112 and tangentially in #7256, #2965, #7091
I’ve been talking to the team about these router hook methods. I have a good feeling about an emerging design that should arrive before RC. I thought I’d share it here so we can get some feedback.
CAVEAT: this is Ward’s interpretation of what he thinks he heard which may vary from the team’s interpretation and is not necessarily what will happen.
As I understand it, CanActivate
will become an instance method, probably named routerCanActivate
.
The catch is that we implement routerCanActivate
on (what I call) a _routing component_: a component with an attached router and routes and whose template includes a <router-outlet>
.
We didn’t talk about it but I think Angular should ignore it on a non-routing component or perhaps throw an error.
The router will consult this method before pursuing any of the routes that would populate that <router-outlet>
. It gates navigation to any of the routes configured for this component’s router.
To be clear, it has no role in allowing/preventing navigation to the routing component itself; only navigation to the routes configured for this routing component’s <router-outlet>
.
As an instance method, the routerCanActivate
has access to anything we inject into its routing component. The injector (and its scope) is that of this routing component.
I think this addresses many concerns:
- We can inject anything we need without crazy hacks
- We can scope injection at an appropriate level in the component tree. We don’t have to capture the root injector and hope that the service we need has been provided there. We can access services injected “down here”, at the local router configuration level.
- We can make decisions about whether to allow navigation to a lazy loaded component. Before we would have had to load that component and call its
@CanActivate
, perhaps only to belatedly disallow it.
I had a few initial objections:
- I didn’t think the routing component should be responsible for can activate decisions about all of its routes.
- Why should it know all of the details about whether a route’s navigation should be allowed? That’s a decision that belongs to the destination component, right?
- Some people have used
CanActivate
to pre-load data from the server for the destination component. There won’t be an obvious way to do that with this design which doesn’t provide a way to deliver a payload to the destination component. - The can activate logic has access to services at the routing component level and above but not the services (if any) that will be provided by (and therefore scoped at) the destination component itself. That could be too limiting.
Here is why these objections no longer trouble me.
-
If deciding is a burden, the routing component can delegate that burden to a custom injected
CanActivateService
, scoped to the routing component itself. That service can inject much of what it needs to make decisions. The routing component can stay lean. This is our decision as application developers; Angular 2 neither requires it nor blocks it. I like that. -
While I like the idea that the can activate logic is part of the development of the destination component, that logic doesn’t have to be inscribed within the destination component class. We can come up with other patterns that will work well, particularly in concert with the
CanActivateService
approach I just mentioned. I think we can get the allocation of responsibilities right without too much friction. And this approach does put us in position to gate navigation to a lazy-loaded destination component which I think is the more important benefit. -
Actually, I always thought using can activate logic to pre-load data was a hack. Many folks did that kind of thing with resolve in Angular 1. I never liked it. Please don’t bring back
resolve
!But like/dislike aside, we don’t need it in Angular 2. In Angular 2, we have both the
routerOnActivate
and thengOnInit
hooks (missing in Angular 1) which are fine places to pre-load data. -
I couldn’t think of a use case in which it was essential that we consult a service instance created just for the destination component. OTOH, if we could do that, we’d be faced with the obligation to create such a service which brings unwanted complexity of its own.
For example, if we allow navigation, we have to make sure that service (whatever it is), is ultimately injected into the destination component. There is no facility for doing that kind of thing today. Today we can’t access the internal steps that go into hydrating a component … and I rather doubt I’d want to risk what comes with breaking into that sausage factory.
What if we created that service and then rejected the navigation? What did that service do? Did it know it was going to be created and then thrown away? Does it need to be cleaned up before being thrown away. It’s a horrible can of worms.
In the end, I think this design will work well for us. It’s safe, clean, easy to understand once explained.
Angular consults a routing component’s routerCanActivate
method before navigating to any of the routes defined for that component’s router.
I don’t know what the method parameters will be. Obviously something about where the router is going. This is a good time for you all to chime in.
p.s.: I don’t know what will become of the current @CanActivate
decorator. I suppose it could live on as a vestigial relic. Personally, I’d vote for it to be deprecated immediately and removed before release.
p.p.s.: I have stopped thinking that the routerOnActivate
method is or could be a substitute for routerCanActivate
(quite apart from the fact that it doesn’t work as a substitute today).
It should not be involved in the decision about whether we should navigate to this component. It should concentrate on what to do now that we’re here. And in that respect it should differentiate from its companion hook method, routerOnReuse
, which concerns what to do now that we’re back.
Issue Analytics
- State:
- Created 7 years ago
- Reactions:11
- Comments:40 (20 by maintainers)
I’ve never particularly liked the coupling between a component and the router. I’d prefer to have these sorts of things in the RouteConfig:
For things that require services:
The actual activation happens scoped to the router outlet, which appears to have access to the appropriate injector. Just a thought, seems to make things more reusable.
@CaptainCodeman - You make a persuasive argument for evaluating the can activate logic within the target component. Those on the other side of that argument prefer to block activation earlier, especially because the route may involve async loading which they feel is too heavy a price to pay for a failed route even if failed routes are rare. FWIW, the issue has been decided and we do well to move on.
@cquillen2003 - I don’t follow you. I was reflecting the argument in favor of writing router configuration within a component rather than in some other place. If you’re writing “a component without a router” it’s all moot, right? There’s nothing to configure and nothing to put in (or keep out) of the component.
@escardin As attractive as using inputs sounds, it doesn’t hold up in many common use cases. It’s not clear how route or query string parameters should align with input properties nor how the URL material that doesn’t match input properties should become available to the component. I fear for the complexity of whatever might be proposed for matching input properties to parameters buried in a URL.
You may have neglected the critical role of the query string in a URL. These shouldn’t be ignored nor can they be parsed into input properties. I believe this is the essence of @choeller’s critique of your proposal.
Meanwhile, all of the tried-and-true routers in my experience keep the routing constructs separate from the component/controller. We know this separation works.
I think you misunderstand the role of
canReuse
. It participates in the router’s decision about whether a component instance can be recycled or must be discarded and recreated. There is no equivalent among the component lifecycle hooks.The component lifecycle hooks are no substitute for
routerOnActivate
androuterOnReuse
. They provide valuable information about the navigation to and from the target component that is simply not available any other way, neither through the component hooks nor through input params.To be perfectly honest, worthy as some of these ideas may be, they are too nebulous and incomplete. They are very far from addressing concretely the routing scenarios we know we need to handle.
You may disagree. That’s cool. I don’t wish to be cruel and hard-hearted but … these ideas arrive too late. At this point in the game, the design of the router has moved on and I think I can say definitively that
@CanActivate
will become something like acanActivateChildRoutes
instance method of a routing component.routerOnActivate
androuterOnReuse
will live onIt’s time to move on.
It may also be worth remembering that the Component Router is an optional subsystem. It’s not part of the Angular core. If it sucks, Angular will survive, someone will build a better one, and we’ll all move to that.
My personal bet is on the Component Router which I feel soon will overcome its current, most urgent defects.