258

Is it possible to have an optional route parameter in the Angular 2 route? I tried the Angular 1.x syntax in RouteConfig but received below error:

"ORIGINAL EXCEPTION: Path "/user/:id?" contains "?" which is not allowed in a route config."

@RouteConfig([
{
    path: '/user/:id?',
    component: User,
    as: 'User'
}])
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
Jeroen
  • 3,443
  • 2
  • 13
  • 15

14 Answers14

395

You can define multiple routes with and without parameter:

@RouteConfig([
    { path: '/user/:id', component: User, name: 'User' },
    { path: '/user', component: User, name: 'Usernew' }
])

and handle the optional parameter in your component:

constructor(params: RouteParams) {
    var paramId = params.get("id");

    if (paramId) {
        ...
    }
}

See also the related github issue: https://github.com/angular/angular/issues/3525

Ondra Žižka
  • 43,948
  • 41
  • 217
  • 277
rerezz
  • 4,080
  • 1
  • 11
  • 4
  • 13
    Correct me if I'm wrong, but this solution worked for me only when the order of routes in the array was reversed, i.e. the route with the parameter occurred before the other one. Until I did that, the router only matched the route without the parameter. – Aviad P. Jan 22 '16 at 12:29
  • @Aviad P The order didn't matter for me when I called `router.navigate(['/user', {id:2}]);`, but it _did_ matter when I called `router.navigate(['/user', 2]);`. In the latter case, the route with the ID parameter needed to be defined before the route without any parameters. – reduckted May 20 '16 at 05:48
  • 14
    does this solution still apply ? I noticed moving from "User" to "UserNew" will re-instantiate the 'User' component – teleaziz Sep 21 '16 at 23:38
  • Best answer, because the solution recommended by the official Angular2 documentation for optional parameters (matrix URL notation) is not an HTML standard, and does not work by default with certain web servers, such as nginx: https://angular.io/docs/ts/latest/guide/router.html#!#optional-route-parameters So yes, define multiple routes. – KTCO Oct 02 '16 at 13:23
  • 1
    What is the `as` for? When I specify that attr I get an error from the router. When I omit things work as intended. – rynop Oct 13 '16 at 03:50
  • 32
    old but a major problem with this approach is that each route is treated as unique route and it makes component reuse impossible. – Agony May 24 '17 at 10:53
  • Invalid configuration of route '/companies/:data': path cannot start with a slash – Hooman Limouee Nov 14 '17 at 06:55
  • 1
    This approach does have implications... i was able to resolve this another way ... see this q/a: https://stackoverflow.com/questions/50128779/in-angular-what-is-the-correct-way-to-implement-a-route-with-an-optional-parame – DevMike May 02 '18 at 10:04
  • 6
    As noted by @teleaziz, appending the parameter will re-render the component. To avoid this, Martin Cremer's answer; adding a 'redirectTo' root with a blank parameter value, worked great for me: https://stackoverflow.com/a/49159166/1364650 - but that's quite hacky, I think they should just support optional route parameters properly. – Vincent Sels Jul 13 '18 at 10:00
  • 7
    For those who are wondering why `RouteParams` not importing to component check this: https://stackoverflow.com/a/36792261/806202. The solution is to use `ActivatedRoute`: `route.snapshot.params['routeParam']` – Arsen Khachaturyan Jul 24 '18 at 20:37
  • This answer is outdated since Angular 8. See @Yanis Yakuza's answer – Eneko Oct 21 '21 at 11:11
184
{path: 'users', redirectTo: 'users/', pathMatch: 'full'},
{path: 'users/:userId', component: UserComponent}

This way the component isn't re-rendered when the parameter is added.

Martin Cremer
  • 5,191
  • 2
  • 32
  • 38
  • 15
    This answer is the best. It doesn't re-render the same component and it doesn't require multiple components. – Rex Aug 08 '18 at 12:45
  • 5
    The best answer, but I added `pathMatch: 'full'` to redirect, otherwise paths like `users/admin` is also redirected in my case – Valeriy Katkov Aug 09 '18 at 09:08
  • 7
    This answer is only the best if you're ok with trailing slashes on your URLs as viewed in the browser. Consider maybe a value that represents 'an undefined id', for example `/users/all` or `/users/home`, read the 'all' or 'home' as the `id` and simply ignore it if it matches your magic value. Then the first line above becomes `redirectTo: 'users/home'` or whatever you decide. To me a trailing slash really stands out as something being wrong. – Simon_Weaver Feb 16 '19 at 08:40
  • 1
    @Simon_Weaver I agree. I found another solution using a matcher which doesn't have this problem: https://stackoverflow.com/a/56391974/664533 – Wayne Maurer May 31 '19 at 09:09
  • 1
    it's a simple spell but quite unbreakable :D The best solution! – Verri Oct 16 '19 at 10:27
  • As Valeriy says this needs `pathMatch: 'full'`, otherwise `users/26` is going to redirect to `users//26` – MDave Feb 20 '20 at 03:55
  • thanks @ MDave and Valeriy, i've added that to the answer. – Martin Cremer Feb 20 '20 at 08:36
  • how do I retrieve the value :userId ? – Evan Apr 28 '20 at 08:56
  • https://stackoverflow.com/a/69420764/7186739 – Billu Oct 05 '21 at 08:01
53

It's recommended to use a query parameter when the information is optional.

Route Parameters or Query Parameters?

There is no hard-and-fast rule. In general,

prefer a route parameter when

  • the value is required.
  • the value is necessary to distinguish one route path from another.

prefer a query parameter when

  • the value is optional.
  • the value is complex and/or multi-variate.

from https://angular.io/guide/router#optional-route-parameters

You just need to take out the parameter from the route path.

@RouteConfig([
{
    path: '/user/',
    component: User,
    as: 'User'
}])
Jp_
  • 5,973
  • 4
  • 25
  • 36
  • 6
    Changing optional route params rerenders components, but changing queryParams doesn't. Also if you resolve some data before route navigation, it will be requested each time when you change optional route params. – Rakhat Dec 06 '16 at 12:19
  • 1
    FYI, that anchor link doesn't work anymore. The new link appears to be [Route Parameters: Required or optional?](https://angular.io/guide/router#optional-route-parameters) – spottedmahn May 23 '18 at 19:22
31

Angular 4 - Solution to address the ordering of the optional parameter:

DO THIS:

const appRoutes: Routes = [
  {path: '', component: HomeComponent},
  {path: 'products', component: ProductsComponent},
  {path: 'products/:id', component: ProductsComponent}
]

Note that the products and products/:id routes are named exactly the same. Angular 4 will correctly follow products for routes with no parameter, and if a parameter it will follow products/:id.

However, the path for the non-parameter route products must not have a trailing slash, otherwise angular will incorrectly treat it as a parameter-path. So in my case, I had the trailing slash for products and it wasn't working.

DON'T DO THIS:

...
{path: 'products/', component: ProductsComponent},
{path: 'products/:id', component: ProductsComponent},
...
Adrita Sharma
  • 21,581
  • 10
  • 69
  • 79
ObjectiveTC
  • 2,477
  • 30
  • 22
  • If both are going to the ProductsComponent, how do you deal with the optional parameter there? – Arwin Jan 12 '18 at 00:33
  • 1
    You can access the :id1, :id2, etc parameters as well as the requested url in ProductsComponent like so: this.route.url.first() .mergeMap((url) => { // console.log('1: url change detected' + url); return this.route.params.do((params) => { // console.log('2: url + params change detected' + params["id1"] + ' ' + params["id2"]); this.id1 = params["id1"]; this.id2 = params["id2"]; }) }) – ObjectiveTC Jan 14 '18 at 02:26
  • 2
    Remember you can pass `data` to the component too, which can be different for each route even to the same component. Example `{path: 'products', component: ProductsComponent, data: { showAllProducts: true} },` could be used and then you check for `showAllProducts`. A little nicer then checking for a null, but for simpler cases either is probably fine. – Simon_Weaver Jan 31 '18 at 04:39
  • 4
    Unfortunately, this solution will prevent Angular from reusing the component between products and products/:id. The component will be re-instanciated. – Kodiak Aug 13 '18 at 10:55
  • @Kodiak - I don't believe that is correct. My understanding is that in app.module.ts, the ProductsComponent is instantiated once, and that the angular engine then re-uses that instantiated ProductsComponent upon each navigable event (products and products/:id etc). Can you explain or demonstrate how ProductsComponent might be re-instantiating in the code above, and how you would go about preventing re-instantiation? – ObjectiveTC Aug 31 '18 at 20:33
  • @ObjectiveTC no, Kodiak is right; I have the same problem as them, which is why I landed on this page. I have two routes, `/projects` and `/projects/:id`, using the same ProjectsComponent. Being in projects and selecting a project "reloads" the page and re-instantiates the component. :( – ANeves Nov 02 '18 at 11:33
  • @ANeves - What version of Angular are you using? Also note that your anchors must use angular's "RouterLink" directive in order so that angular knows not to reload the page. Eg: {{product.product_id}} – ObjectiveTC Nov 03 '18 at 21:10
16

The suggested answers here, including the accepted answer from rerezz which suggest adding multiple route entries work fine.

However the component will be recreated when changing between the route entries, i.e. between the route entry with the parameter and the entry without the parameter.

If you want to avoid this, you can create your own route matcher which will match both routes:

export function userPageMatcher(segments: UrlSegment[]): UrlMatchResult {
    if (segments.length > 0 && segments[0].path === 'user') {
        if (segments.length === 1) {
            return {
                consumed: segments,
                posParams: {},
            };
        }
        if (segments.length === 2) {
            return {
                consumed: segments,
                posParams: { id: segments[1] },
            };
        }
        return <UrlMatchResult>(null as any);
    }
    return <UrlMatchResult>(null as any);
 }

Then use the matcher in your route config:

const routes: Routes = [
    {
        matcher: userPageMatcher,
        component: User,
    }
];
Wayne Maurer
  • 12,333
  • 4
  • 33
  • 43
  • @KevinBeal I have implemented quite a few matchers which do work with AOT. What's the error you are getting here? – Wayne Maurer Aug 24 '19 at 21:14
  • Oops. It was something else. My matcher works with AOT. – Kevin Beal Aug 26 '19 at 00:56
  • this is a little bit tricky but the best solution to this problem – fedor.belov Jan 18 '20 at 20:18
  • IMO, this is the most correct answer to this question. I wish I could give more votes to this answer. I can't believe there aren't more posts about this around the internet. I wish that Angular docs were structured better. Currently you pretty much have to read all their tutorials so you don't miss useful features. – rooby Oct 12 '21 at 01:14
14

rerezz's answer is pretty nice but it has one serious flaw. It causes User component to re-run the ngOnInit method.

It might be problematic when you do some heavy stuff there and don't want it to be re-run when you switch from the non-parametric route to the parametric one. Though those two routes are meant to imitate an optional url parameter, not become 2 separate routes.

Here's what I suggest to solve the problem:

const routes = [
  {
    path: '/user',
    component: User,
    children: [
      { path: ':id', component: UserWithParam, name: 'Usernew' }
    ]
  }
];

Then you can move the logic responsible for handling the param to the UserWithParam component and leave the base logic in the User component. Whatever you do in User::ngOnInit won't be run again when you navigate from /user to /user/123.

Don't forget to put the <router-outlet></router-outlet> in the User's template.

matewka
  • 9,912
  • 2
  • 32
  • 43
  • Avoiding the component being recreated is a good thing if performance is critical. I have another solution which also avoids avoids the component being recreated: https://stackoverflow.com/a/56391974/664533 – Wayne Maurer May 31 '19 at 09:12
6

With angular4 we just need to organise routes together in hierarchy

const appRoutes: Routes = [
  { 
    path: '', 
    component: MainPageComponent 
  },
  { 
    path: 'car/details', 
    component: CarDetailsComponent 
  },
  { 
    path: 'car/details/platforms-products', 
    component: CarProductsComponent 
  },
  { 
    path: 'car/details/:id', 
    component: CadDetailsComponent 
  },
  { 
    path: 'car/details/:id/platforms-products', 
    component: CarProductsComponent 
  }
];

This works for me . This way router know what is the next route based on option id parameters.

Juan Antonio
  • 2,451
  • 3
  • 24
  • 34
Ravi Jadhav
  • 81
  • 1
  • 11
6

There are three ways to send route parameter(s) from one component to other component through routes. But first import these libraries in components related .ts files and define in constructor

private route: ActivatedRoute
private router: Router

1st Way: Required routing parameters

//Route Configuration
{path: 'user/:id', component: UserDetailComponent}

//Set Hyperlink/routerLink
<a [routerLink]="['/user', user.id]"></a> 

 //Requesting Url after click on hyperlink
 http://localhost:4200/user/6

//Now you can read id value in navigated component
this.route.snapshot.paramMap.get('id');

2nd Way: Optional path parameters

//Route Configuration
    {path: 'user', component: UserDetailComponent}
    
    //Set Hyperlink/routerLink
    <a [routerLink]=['/user', {name: userName, status: true}]"></a>


//Requesting Url after click on hyperlink
    http://localhost:4200/user;name:userNameValue;status:true

//Now you can read values in navigated component
    this.route.snapshot.paramMap.get('userId');
    this.route.snapshot.paramMap.get('userName');

3rd Way: Optional path parameters

//Route Configuration
    {path: 'user', component: UserDetailComponent}
    
    //Set Hyperlink/routerLink
    <a [routerLink]="['/user']"  [queryParms]="{userId:'911', status:true}"></a>

    //Requesting Url after click on hyperlink
    http://localhost:4200/user?userId=911&status=true

    
    //Now you can read values in navigated component
    this.route.snapshot.paramMap.get('userId');
    this.route.snapshot.paramMap.get('userName');

Reference: https://qastack.mx/programming/44864303/send-data-through-routing-paths-in-angular

Billu
  • 2,733
  • 26
  • 47
3

In Angular 8, ou can simply add the parameter without changing your router config.

Angular Doc Optional param

In yourModule.routing.module.ts

const routes: Routes = [
  { path: 'somePath/:RequiredParam', component: Yourcomponent }
];

In your template :

<div [RouterLink] = ['somePath', requiredParamValue, {optionalParam: value}]></div>
Yanis Yakuza
  • 111
  • 1
  • 11
1

Ran into another instance of this problem, and in searching for a solution to it came here. My issue was that I was doing the children, and lazy loading of the components as well to optimize things a bit. In short if you are lazy loading the parent module. Main thing was my using '/:id' in the route, and it's complaints about '/' being a part of it. Not the exact problem here, but it applies.

App-routing from parent

...
const routes: Routes = [
  {
    path: '',
    children: [
      {
        path: 'pathOne',
        loadChildren: 'app/views/$MODULE_PATH.module#PathOneModule'
      },
      {
        path: 'pathTwo',
        loadChildren: 'app/views/$MODULE_PATH.module#PathTwoModule'
      },
...

Child routes lazy loaded

...
const routes: Routes = [
  {
    path: '',
    children: [
      {
        path: '',
        component: OverviewComponent
      },
      {
        path: ':id',
        component: DetailedComponent
      },
    ]
  }
];
...
L.P.
  • 406
  • 5
  • 17
1

With this matcher function you can get desirable behavior without component re-rendering. When url.length equals to 0, there's no optional parameters, with url.length equals to 1, there's 1 optional parameter. id - is the name of optional parameter.

  const routes: Routes = [
  {
    matcher: (segments) => {
      if (segments.length <= 1) {
        return {
          consumed: segments,
          posParams: {
            id: new UrlSegment(segments[0]?.path || '', {}),
          },
        };
      }
      return null;
    },
    pathMatch: 'prefix',
    component: UserComponent,
  }]
Denis R
  • 266
  • 2
  • 3
1

Had the same issue with a Master Detail view. The master view can be visible without the :elementId parameter, but should still be displayed with detail selection open and with the :elementId in the url.

I solved it as follows:

const routes: Routes = [
  {
    path: '',
    component: MasterDetailComponent,
    children: [
      {
        path: ':elementId',
        children: [
          {
            path: 'details',
            component: DetailComponent
          },
          {
            path: '',
            redirectTo: 'details'
          }
        ]
      }
    ]
  }
];

Then in MasterDetailComponent (e.g. in ngOnInit method) you can get the :elementId using the child route:

const childRouteWithElementId = this.route.snapshot.children[0];
const elementIdFromUrl = childRouteWithElementId.params.elementId;
if (!!elementIdFromUrl ) {
  // Do what you need to with the optional parameter
}

Of course you could do the same thing without the child routes and only have the optional elementId at the end of the url.

Nas3nmann
  • 480
  • 1
  • 6
  • 13
0

I can't comment, but in reference to: Angular 2 optional route parameter

an update for Angular 6:

import {map} from "rxjs/operators"

constructor(route: ActivatedRoute) {
  let paramId = route.params.pipe(map(p => p.id));

  if (paramId) {
    ...
  }
}

See https://angular.io/api/router/ActivatedRoute for additional information on Angular6 routing.

0

Facing a similar problem with lazy loading I have done this:

const routes: Routes = [
  {
    path: 'users',
    redirectTo: 'users/',
    pathMatch: 'full'
  },
  {
    path: 'users',
    loadChildren: './users/users.module#UserssModule',
    runGuardsAndResolvers: 'always'
  },
[...]

And then in the component:

  ngOnInit() {
    this.activatedRoute.paramMap.pipe(
      switchMap(
        (params: ParamMap) => {
          let id: string = params.get('id');
          if (id == "") {
            return of(undefined);
          }
          return this.usersService.getUser(Number(params.get('id')));
        }
      )
    ).subscribe(user => this.selectedUser = user);
  }

This way:

  • The route without / is redirected to the route with. Because of the pathMatch: 'full', only such specific full route is redirected.

  • Then, users/:id is received. If the actual route was users/, id is "", so check it in ngOnInit and act accordingly; else, id is the id and proceed.

  • The rest of the componect acts on selectedUser is or not undefined (*ngIf and the things like that).

Javier Sedano
  • 923
  • 3
  • 11
  • 28