Angular2: Router v3: have components manage their own route

203 views
Skip to first unread message

Steven Luke

unread,
Jul 7, 2016, 9:17:25 PM7/7/16
to AngularJS
HI all,

I have been working through the MEAP book Angular2 Development with Typescript.  It is a pre-release book and is using the Router v2, so isn't current with the newer Router v3 in the latest Angular release candidates.  It has an interesting task in chapter 3 about building routes, and specifically, having nested routes.  The version in the book and the downloadable code doesn't work in the latest Angular2 RC, so I did some research and got a version that functions.  You can see it working in this plunker: https://embed.plnkr.co/rPp0tQOJ4z93Wt8fQoRk/.

The problem I have with it is that there is one super route which must know all children which may have routes of their own.  I would rather have each component manage its own routing, and I haven't been able to figure that out.  Is there a way to do it?

Here is the general outline


  1. You have a main view with a router-outlet which takes one of two views, the "Home" view, and the "Product" view.
  2. The "Product" view has its own router-outlet which, again, takes two views, the "Details" view the "Seller" view.
Pretty simple.  To start, I will show you the Product code (relevant parts:
product.ts
import {Router, ROUTER_DIRECTIVES} from '@angular/router';

@Component({
    selector: 'product',
    directives: [ROUTER_DIRECTIVES],
    template: `
        <div class="product">
            <h1>Product Detail for: {{ productID }}</h1>

            <router-outlet></router-outlet>
            <p><a [routerLink]="['seller', 'abc']">Seller Info</a></p>
        </div>
    `,
    styles: ['.product {background: cyan}']
})
export class ProductDetailComponent { 
  ...
}

Just some space for the router-outlet and a link to switch to the seller view.

product_routes.ts
import { provideRouter, RouterConfig } from '@angular/router';

export const ProductRoutes: RouterConfig = [
    { path: 'product/:id', component: ProductDetailComponent,
      children: [
        { path: 'seller/:id', component: SellerInfoComponent },
        { path: '' , component: ProductDescriptionComponent }
      ]} 
];

export const PRODUCT_ROUTER_PROVIDERS = [ provideRouter(ProductRoutes)];

The path mapping for the product view, and child routes for the two sub views.

main.ts
import {bootstrap} from '@angular/platform-browser-dynamic';
import {Component} from '@angular/core';
import {LocationStrategy, HashLocationStrategy} from '@angular/common';
import {ROUTER_DIRECTIVES} from '@angular/router';
import {APP_ROUTER_PROVIDERS} from './app_routes';

@Component({
    selector: 'basic-routing',
    directives: [ROUTER_DIRECTIVES],
    template: `
        <a [routerLink]="['/']">Home</a>
        <a [routerLink]="['/product', '1234']">Product Details</a>
        <router-outlet></router-outlet>
    `
})

class RootComponent{
}

bootstrap(RootComponent, 
    [  APP_ROUTER_PROVIDERS,
       {provide: LocationStrategy, useClass: HashLocationStrategy}
    ]);

Two links to switch between Home and Product views.

app_routes.ts
import { provideRouter, RouterConfig } from '@angular/router';
import { ProductRoutes } from './product_routes';

export const appRoutes: RouterConfig = [
    { path: '' , component: HomeComponent },
    ...ProductRoutes
];

export const APP_ROUTER_PROVIDERS = [ provideRouter(appRoutes)];

This is the part that I really don't like.  These are the routes for the overall app.  I would like it to be just the top level view routes, to navigate between /home and /product/:id.  Then let the Product component deal with the routes between the /product/:id (product details) and /product/:id/seller/:id (seller) views.  As it is, this top level RouteConfig needs to know all components which will want their own RouteConfigs, so it can include them in the final provider.  I haven't found a way that lets me navigate both the AppRoutes and the ProductRoutes, or lets me keep the ProductRoutes private.  Any ideas?

Thanks is advance,
Steve

Steven Luke

unread,
Jul 8, 2016, 5:46:31 PM7/8/16
to AngularJS
Okay, I figured out a way that I don't like, but in so doing, I think it is generally not a nice idea (having child components manage their own routes), I think we need to manage the routes from the top level.  Let me explain.

First, a working solution is on Plunker here: https://embed.plnkr.co/s9PqXLvpee5HuVUXTbOM/

The key changes are in the app_routes.ts file and the product.ts file.

app_routes.ts
import { provideRouter, RouterConfig } from '@angular/router';
import { ProductDetailComponent } from './components/product';
import { HomeComponent } from './components/home';

export const appRoutes: RouterConfig = [
    { path: '' , component: HomeComponent },
    { path: 'product/:id', component: ProductDetailComponent }
];

export const APP_ROUTER_PROVIDERS = [ provideRouter(appRoutes)];

It is changed to just have the navigation routes for the top level.  

product.ts (relevant parts):
const navRouteChildren: Route [] = [
        { path: 'seller/:id', component: SellerInfoComponent },
        { path: '' , component: ProductDescriptionComponent }];


@Component({
    selector: 'product',
...
})
export class ProductDetailComponent {
    productID: string;
    private sub: any;

...
    ngOnInit() {
        this.modifyRoutes(appRoutes);
        this.sub = this.route.params.subscribe(params => { this.productID = params['id']; });
        this.router.navigate([]);
    }

    ngOnDestroy() {
        this.sub.unsubscribe();
    }

    modifyRoutes(routes: RouterConfig) {
        routes.forEach(routeToCheck => {
            this.addChildrenIfMatch('product/:id', routeToCheck);
        });
    }

    addChildrenIfMatch(targetPath: string, targetRoute: Route) {
        if(targetRoute.path === targetPath) {
            targetRoute.children = navRouteChildren;
        }
    }

}

The component holds a collection of child routes in an array, then it uses the ngOnInit() method to find the route the children belong to (the modifyRoutes and addChildrenIfMatch methods) and adds the children to the route.  Then it has to force navigate (with no path changes) to ensure the new routing options are applied.

This works in that both the parent and child navigation routes are maintained.  The components would have to know about the overall RouterConfig, and if nested, their parent RouterConfigs (or at least path) but siblings wouldn't need to know each other, and parents wouldn't need to know children.

The problem is deep linking.  If I start navigation at http://home.address/, then the whole app will work fine.  If I start at one layer deep in the navigation: http://home.address/#/product/1234 then everything is still fine.  But if I have a deep link to the bottom of the tree: http://home.address/#/product/1234/seller/abc then this doesn't work.  Since the top level router configuration doesn't know how to handle the seller/:id path, the whole thing breaks, you have to incrementally navigate through the children until you get to the component you want.

I am not sure how you fix that without pulling the child routes into the top level route (which is what all the published documents that I could find do, and what my previous example did).  It seems like my desired design is destined to fail.  So I don't think this is a working solution, and I don't think what I want is really feasible if I want to maintain deep links.
Reply all
Reply to author
Forward
0 new messages