问题
I want to load distinct components for similar structured paths.
Means:
example.com/whatever-post-slug --> Load Post Component
example.com/whatever-page-slug --> Load Page Component
example.com/hello-i-am-a-string --> Load Post Component (because that slug belongs to a post)
example.com/about-us --> Load Page Component (because that slug belongs to a page)
Why would I do this?
One may think: hey but why don't you just define two prefixed different paths, which is what paths are there for?
The answer is: I am creating (by the way an open source) Angular WordPress theme and I want the routing to be something the user decides, without forcing any hardcoded structure.
In one of the multiple possibilities, Posts and Pages (if you don't know WordPress, just know those are two distinct content types which I want to use independent Modules and Components for) may share slugs at a root level.
And at a first glance, I cannot know if the slug belongs to a post or a page or something else (so UrlMatcher
cannot help here).
Did I think of a solution?
Yes, of three.
First solution
I could create a Wrapper Component that will load for the catchall route, and then, inside this component do something like:
<whatever-a-component *ngIf="showComponentA()"><whatever-a-component>
<whatever-b-component *ngIf="showComponentB()"></whatever-b-component>
And let the Wrapper Component do all the logic.
This adds an extra intermediate Component into the game.
Second solution
Use a resolver for the catchall and then do all the logic in the resolver.
The problem is, I need to subscribe to an http communication to know what kind of content type I am dealing with, and since the Resolver resolve() method needs to return an Observable, it's not nice to subscribe in there.
Of course, it works, if I return a placeholder observable that waits for a while, like so:
// ... inside resolve()
// Force an observable return to allow suscriptions take their time
return Observable.create(observer => {
setTimeout(() => {
console.log("Resolver observable is done");
observer.complete();
}, 15000);
});
... or if I pipe my subscription with mergeMap()
and return EMPTY when I get the results from my subscription.
And once I get my data back I can set new routes including the current path that must point to its specific component.
That does not seem a very clean approach to me.
Third solution
Just load a normal Dispatcher Component that will do all the checkings at OnInit()
and then navigate to a "secret" component-specific url but using { skipLocationChange: true }
, so the user will have the correct route and also the correct component will be loaded.
But again, this adds an extra intermediate component into the game.
I think this is the cleanest solution, since at the App Routing module I can do something like:
{
path: 'wp-angular/view-post/:id',
loadChildren: () => import('./view-post/view-post.module').then(m => m.ViewPostModule)
},
{
path: 'wp-angular/view-page/:id',
loadChildren: () => import('./view-page/view-page.module').then(m => m.ViewPageModule)
}
So, I will have those modules lazy loaded only if the user actually visits one of those two content types.
Also, that content type component will already be available if the user then visits a second content of the same type.
And the fact that I can use { skipLocationChange: true }
will make it possible to keep the path as expected.
Plus, this allows displaying navigation loading feedback without the need to subscribe to the Router Events.
Question
What would you do and why?
Maybe I am missing some magic Angular feature that allows doing this in a straight-to-the-point way.
回答1:
Once I had a similar problem and I resolved like this:
You can use a Module that handles what component should be load by providing the ROUTES of the RouterModule using the useFactory provider of Angular.
The code could be something like this:
// HandlerModule
@NgModule({
declarations: [],
imports: [
CommonModule,
RouterModule
],
providers: [
{
provide: ROUTES,
useFactory: configHandlerRoutes,
deps: [CustomService],
multi: true
}
]
})
export class HandlerModule {}
export function configHandlerRoutes(customService: CustomService) {
let routes: Routes = [];
if (customService.whatever()) {
routes = [
{
path: '', component: AComp
}
];
} else {
routes = [
{
path: '', component: BComp
}
];
}
return routes;
}
Then in your AppRoutingModule the module of the path '' is going to be the HandlerModule:
// AppRoutingModule
{
path: '',
loadChildren: () => import('app/handler/handler.module').then(mod => mod.HandlerModule)
}
After in the CustomService you have to update the Router.config when the value that provides the method .whatever() changes because the application will only load the component that had loaded the first time. This is because the function “configHandlerRoutes” use by the useFactory provider in the HandlerModule is only executed the first time we navigate to the “” path and after that, the Angular Router already know which component he has to load.
In conclusion in the CustomService you have to do:
export class CustomService {
private whateverValue: boolean;
constructor(private router: Router) {
}
public whatever(): boolean {
return this.whateverValue;
}
public setWhatever(value: boolean): void {
const previous = this.whateverValue;
this.whateverValue = value;
if (previous === this.whateverValue) {
return;
}
const i = this.router.config.findIndex(x => x.path === '');
this.router.config.splice(i, 1);
this.router.config.push(
{path: '', loadChildren: () => import('app/handler/handler.module').then(mod => mod.HandlerModule)}
);
}
}
That's it. I used the "" path for the example but you can use any path you want.
Also, you can use the same approach if you want to load Modules instead of components.
If you want another reference here is an article where they use the same approach: https://medium.com/@german.quinteros/angular-use-the-same-route-path-for-different-modules-or-components-11db75cac455
来源:https://stackoverflow.com/questions/58468758/create-a-routes-path-that-loads-a-specific-component-based-on-condition