fix(router): empty outlet component respecting the current outlet#30444
fix(router): empty outlet component respecting the current outlet#30444matheo wants to merge 3 commits intoangular:masterfrom
Conversation
This comment has been minimized.
This comment has been minimized.
|
I signed it! |
|
CLAs look good, thanks! ℹ️ Googlers: Go here for more info. |
|
Well @jasonaden dirty but works for my lazy-loaded outlets: This case is not covered by the current |
atscott
left a comment
There was a problem hiding this comment.
Hi @matheo, are you still interested in moving this forward? If so, could you please add some tests for this? It looks like there are two changes that need testing - the change which updates router_outlet to use @Input instead of @Attribute and the change in EmptyOutletComponent. Also, please submit an issue with a reproduction since the linked issue is closed and the specific comment you linked to doesn't have a reproduction either.
|
Hi @atscott, glad you're looking to this PR! I see that you are working on #38379, can you help me to setup some tests please? |
8a7c869 to
6431d8c
Compare
6431d8c to
8151f7a
Compare
atscott
left a comment
There was a problem hiding this comment.
Hey @matheo, thanks for the example! I can definitely see what you're saying. As for the tests, take a look at the ones that were added to integration.spec.ts in the original commit for EmptyOutletComponent: 5731d07. It's pretty clear that those tests also didn't consider anything but a single empty path match in the child for the auxiliary route. If you can add another one that has a non-empty path which would need to have the correct name on the router-outlet like in your example, that should be good.
Running the tests would be relatively simple.
- install required node modules with
yarn - run the tests:
yarn bazel test //packages/router/test
Additionally, what's the motivation behind the change from @Attribute to @Input on router-outlet? Not that I don't think it's a more common way of doing things and that it probably should have been @Inputfrom the start, but that we should limit the changes here to what's necessary and only fix one thing at a time.
Also FYI, I rebased your PR so you'll want to fetch the latest from your remote before making any changes.
There was a problem hiding this comment.
| @Input() name!: string; | |
| @Input() name?: string; |
| * to this `EmptyOutletComponent`. | ||
| */ | ||
| @Component({template: `<router-outlet></router-outlet>`}) | ||
| @Component({template: `<router-outlet [name]="route.outlet"></router-outlet>`}) |
There was a problem hiding this comment.
So this is not going to work in all cases, and causes the tests to fail. Consider the following config:
{
path: 'parent',
outlet: 'nav',
children: [
{path: '', component: SomeComponent}
]
}
This is the config that the tests are using, which currently works because it's able to match the primary outlet to the EmptyOutlet's RouterOutlet. Unfortunately, I think that's the only type of route that's going to work.
{
path: 'parent',
outlet: 'nav',
children: [
{path: '', component: SomeComponent},
{path: 'somethingElse', component: SomeComponent},
{path: 'somethingElse', outlet: 'nav', component: SomeComponent},
]
}
From what I can tell, neither the second nor third Route above will have any chance of activating. The second route is kind of just wrong--arguably, the first is as well - I don't really understand activating a primary route inside a secondary, but since it's an empty path it works even if it shouldn't. The third route can't ever activate because the fix you have here isn't present.
I think what this means is that you're going to have to also update standardizeConfig to account for this. Child Routes of auxiliary routes should not be the primary outlet.
export function standardizeConfig(r: Route, parent?: Route): Route {
if (parent && parent.outlet !== PRIMARY_OUTLET && (r.outlet === PRIMARY_OUTLET || r.outlet === undefined)) {
r.outlet = parent.outlet;
}
...
}
This change is a bit scary -- it feels like it should work but I don't know if there are any weird scenarios I'm not considering.
|
@atscott thanks for the info, I've tried to workaround the Looking at the implications of this, now I understand that The router is a whole universe, maybe we could ask @vsavkin for hints |
That would explain why
Victor isn't on the team any longer and neither is Jason so we have to learn the universe ourselves. The change you have for Edit: That may just be how it works though. Child routes under a named outlet seem to be written as primary routes, without the outlet name of their parent. Maybe that makes sense too so you're not writing named outlets within named outlets and you don't have to remember the parent outlet name. The routes of the children could then be used inside of a named outlet or on their own. If that's the case then I suppose the named outlets aren't intended to be used the way your example is trying to use them. |
|
@atscott thanks for this pair-programming async session hehe I've also included your considerations around I will think about it again. Thanks for the feedback tho! |
It's still not clear why you need to change it to an
Let's refactor this to be
Yes, but the question really is: why doesn't this work for you? Unless you need the outlet name to be correct (which you can still do by looking at the |
There was a problem hiding this comment.
Rather than adding lazy as an attribute, can you change name to be string|null? If the outlet is using name as an input, what is the @Attribute value? Is it null?
There was a problem hiding this comment.
yes, the name attribute is fixed in the HTML of the current Angular Apps, if it's not present it defaults to primary and currently there's no way to wait for ngOnInit to activate the route. For backwards-compatibility I thought we leave the name attribute as it is.
But yeah, we could wait for ngOnInit to check the [name] and then initialize the primary outlet, but I'm afraid that we will pollute the existing Apps with the A router outlet has not been instantiated during routes activation warning in devMode, which is not ideal.
There was a problem hiding this comment.
This lazy attribute is clean from the backward-compatility perspective IMHO, and only advanced apps would use it to exploit the power of the Router.
The reason I didn't use any other workaround,
is because I do load/unload different components via outlets programatically using this in my container component:
this.router.navigate(
[{ outlets: { chat: [uid], emails: [uid, 'emails' } ],
{
relativeTo: this.route,
queryParamsHandling: 'preserve',
skipLocationChange: true
}
)
and the Router takes care of that complexity while I just style the outlets in my container.
There was a problem hiding this comment.
The reason I didn't use any other workaround,
is because I do load/unload different components via outlets programatically using this in my container component:this.router.navigate( [{ outlets: { chat: [uid], emails: [uid, 'emails' } ], { relativeTo: this.route, queryParamsHandling: 'preserve', skipLocationChange: true } )
This doesn't seem explicitly incompatible with the way things work without your change though. Can you share your Routes and explain why you can't modify them to work as they are? Loading separate components seems possible like that; you would just need separate modules rather than sharing one like you have in your example.
None of this discussion is because I think your change isn't correct. I just want to make sure we're totally understanding what's going on and only making the change if we're convinced the current implementation is wrong, there isn't a reasonable workaround, that the change is not breaking, and that it doesn't cause additional confusion. I haven't seen any issue reports for this and nobody else has commented/reacted to this PR so it's not clear that anyone else is expecting things to work this way.
There was a problem hiding this comment.
Indeed, and thanks for this @atscott
I think that with this exercise I learned more about the Router original concept.
The outlets are not supposed to open a whole new "branch" in the routing-tree,
but a little "slot" in a specific level of the hierarchy, am I right?
I will try to experiment with this a bit more in a Nx monorepo when I have some time
and come back to you with a more complete application to evaluate. Thanks again!
atscott
left a comment
There was a problem hiding this comment.
The tests in integration.spec.ts should be expanded to test additional configurations rather than the single empty path in the children. Add some with a path that's not empty and verify things work as they did before. Try adding some with outlets other than the primary as well.
RouterOutlet now includes the `name` Input and the `lazy` attribute to activate a secondary route from its ngOnInit, so EmptyOutletComponent can do its job properly.
Non primary routes will inherit their outlet name to their primary direct children
| name!: string; | ||
|
|
||
| constructor(public route: ActivatedRoute) { | ||
| // lazy loaded children are handled as primary routes |
There was a problem hiding this comment.
What's the motivation behind the distinction here? Don't lazy loaded children act the same way as regular children after they're loaded?
There was a problem hiding this comment.
With the previous attempt to modify standardizeConfig I realized that lazy loaded modules are supposed to use the primary outlet, no needing to know the parent outlet to load their children, so basically I will figure out the routing-hierarchy and what happens with the outlets in the same hierarchy level.
So I will test my use-case for different outlets in a lazy-loaded module.
There was a problem hiding this comment.
The reason I didn't use any other workaround,
is because I do load/unload different components via outlets programatically using this in my container component:this.router.navigate( [{ outlets: { chat: [uid], emails: [uid, 'emails' } ], { relativeTo: this.route, queryParamsHandling: 'preserve', skipLocationChange: true } )
This doesn't seem explicitly incompatible with the way things work without your change though. Can you share your Routes and explain why you can't modify them to work as they are? Loading separate components seems possible like that; you would just need separate modules rather than sharing one like you have in your example.
None of this discussion is because I think your change isn't correct. I just want to make sure we're totally understanding what's going on and only making the change if we're convinced the current implementation is wrong, there isn't a reasonable workaround, that the change is not breaking, and that it doesn't cause additional confusion. I haven't seen any issue reports for this and nobody else has commented/reacted to this PR so it's not clear that anyone else is expecting things to work this way.
|
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Currently, the
EmptyOutletComponentdoesn't include arouter-outletfor the current named route, and it doesn't display the component of the current route.Issue Number: #10981
Refs: #10981 (comment)
What is the new behavior?
It will pass the current outlet name to the
router-outletDoes this PR introduce a breaking change?
Other information
@jasonaden Do you know if moving the
RouterOutlet.namefrom@Attributeto@Inputhas a lifecycle implication? any collateral issue will be seen in the tests? Thanks in advance!