# Getting Started with Angular Route Parameters: A Beginner's Guide to Accessing and Testing Route Parameters

In this article, I'll show how to extract parameters and data from Angular routes using the native `ActivatedRoute`.

# Setup

If unsure how to set up Angular with Jest please refer to the article: https://barcioch.pro/angular-with-jest-setup.

## helper pipe

I created a very basic pipe to format the `Params`. It's just for the testing purposes.

```typescript
import { Pipe, PipeTransform } from '@angular/core';
import { Params } from '@angular/router';

@Pipe({
  name: 'formatParams',
})
export class FormatParamsPipe implements PipeTransform {

  transform(input: Params): string {
    return Object.keys(input).map(key => `${key}:${input[key]}`).join(',');
  }
}
```

# ActivatedRoute

The first way of accessing route parameters is through the `ActivatedRoute`. It can be injected into a component and contains route information associated with a component. Since this class contains a lot of data, we'll be interested only in the following:

* path params
    
* query params
    
* data - custom data assigned to the routes.
    

These properties are never nullish and are a type of `interface Params {[key: string]: any;}`. This means that the property names are always of `string` type but the values can be of `any` type.

## ActivatedRouteSnapshot

The `ActivatedRoute.snapshot` contains the data from the given moment. This data is read-only and does not change. All the properties are exposed as plain values. That means, if you keep the reference to the snapshot, and then navigate to the same URL with different parameters without re-rendering the component, the values won't change. You'll have to get the newest snapshot from the `ActivatedRoute`.

```typescript
import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-snapshot-data',
  template: `
    <div>{{ activatedRoute.snapshot.params | formatParams }}</div>
    <div>{{ activatedRoute.snapshot.queryParams | formatParams }}</div>
    <div>{{ activatedRoute.snapshot.data | formatParams }}</div>
  `
})
export class SnapshotDataComponent implements OnInit {
  readonly activatedRoute = inject(ActivatedRoute);

  ngOnInit(): void {
    console.log(this.activatedRoute.snapshot.params);
    console.log(this.activatedRoute.snapshot.queryParams);
    console.log(this.activatedRoute.snapshot.data);
  }
}
```

## Observable properties

The `ActivatedRoute` has also 3 observable properties: `params`, `queryParams` and `data`. These properties work as `BehaviourSubject` so the actual values are immediately emitted upon subscription. The most important fact is that they emit values whenever associated route parameters change.

```typescript
import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';

@Component({
  selector: 'app-observable-data',
  template: `
    <div>{{ activatedRoute.params | async | formatParams }}</div>
    <div>{{ activatedRoute.queryParams | async | formatParams }}</div>
    <div>{{ activatedRoute.data | async | formatParams }}</div>
  `
})
export class ObservableDataComponent implements OnInit {
  readonly activatedRoute = inject(ActivatedRoute);

  ngOnInit(): void {
    this.activatedRoute.params.subscribe((params: Params) => console.log(params));
    this.activatedRoute.queryParams.subscribe((queryParams: Params) => console.log(queryParams));
    this.activatedRoute.data.subscribe((data: Params) => console.log(data));
  }
}
```

To access the data in the view I'm using the `async` pipe. In the component it's a basic subscription. Note, that I'm not unsubscribing from the `ActivatedRoute` observables. This is one of the exceptions, where it's not needed because Angular takes care of it.

## Testing the ActivatedRoute

Before testing let's take a look at the `TestComponent`. It's displaying:

* observable params, query params, data
    
* current snapshot params, query params, data
    
* initial snapshot params, query params, data
    

```typescript
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
import { FormatParamsPipeModule } from '../format-params/format-params-pipe.module';

@Component({
  selector: 'app-test',
  template: `
    <div data-test='observable-params'>{{ activatedRoute.params | async | formatParams }}</div>
    <div data-test='observable-query-params'>{{ activatedRoute.queryParams | async | formatParams }}</div>
    <div data-test='observable-data'>{{ activatedRoute.data | async | formatParams }}</div>

    <div data-test='snapshot-params'>{{ activatedRoute.snapshot?.params | formatParams }}</div>
    <div data-test='snapshot-query-params'>{{ activatedRoute.snapshot?.queryParams | formatParams }}</div>
    <div data-test='snapshot-data'>{{ activatedRoute.snapshot?.data | formatParams }}</div>

    <div data-test='initial-snapshot-params'>{{ initialReference?.params | formatParams }}</div>
    <div data-test='initial-snapshot-query-params'>{{ initialReference?.queryParams | formatParams }}</div>
    <div data-test='initial-snapshot-data'>{{ initialReference?.data | formatParams }}</div>
  `,
  standalone: true,
  imports: [CommonModule, FormatParamsPipeModule],
})
export class TestComponent {
  readonly activatedRoute = inject(ActivatedRoute);
  readonly initialReference = this.activatedRoute.snapshot;
}
```

When declaring the routes I'm using `paramsInheritanceStrategy: 'always'` to make sure that the child components can access all the parents' parameters.

```typescript
import { Router } from '@angular/router';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TestComponent } from './modules/test/test.component';
import { RouterTestingModule } from '@angular/router/testing';
import { By } from '@angular/platform-browser';
import { Component } from '@angular/core';


describe('ActivatedRouteTest', () => {
  let router: Router;
  let fixture: ComponentFixture<RouterOutletComponent>;

  beforeEach(async () => {
    TestBed.configureTestingModule({
      declarations: [RouterOutletComponent],
      imports: [
        TestComponent,
        RouterTestingModule.withRoutes([
          {
            path: '',
            component: RouterOutletComponent,
            children: [
              {
                path: `products`,
                children: [
                  {
                    path: ':productId',
                    component: TestComponent,
                    data: {
                      secret: 'product-details',
                    },
                  },
                  {
                    path: '',
                    data: {
                      secret: 'product-list',
                    },
                    component: TestComponent,
                  },
                ]
              },
            ]
          }
        ], {paramsInheritanceStrategy: 'always'}),
      ],
    });

    fixture = TestBed.createComponent(RouterOutletComponent);
    router = TestBed.inject(Router);
    router.initialNavigation();
  });

  describe('when user enters /products url', () => {
    beforeEach(async () => {
      await router.navigateByUrl('/products');
      fixture.detectChanges();
      await fixture.whenStable();
    });

    it('should display route properties', () => {
      const expected: SnapshotDump = {
        params: '',
        queryParams: '',
        data: 'secret:product-list',
        snapshotParams: '',
        snapshotQueryParams: '',
        snapshotData: 'secret:product-list',
        initialSnapshotParams: '',
        initialSnapshotQueryParams: '',
        initialSnapshotData: 'secret:product-list',
      };
      expect(getSnapshotDump()).toEqual(expected);
    });

    describe('and user navigates to /products/123 url', () => {
      beforeEach(async () => {
        await router.navigateByUrl('/products/123');
        fixture.detectChanges();
      });

      it('should display route properties', () => {
        const expected: SnapshotDump = {
          params: 'productId:123',
          queryParams: '',
          data: 'secret:product-details',
          snapshotParams: 'productId:123',
          snapshotQueryParams: '',
          snapshotData: 'secret:product-details',
          initialSnapshotParams: 'productId:123',
          initialSnapshotQueryParams: '',
          initialSnapshotData: 'secret:product-details',
        };
        expect(getSnapshotDump()).toEqual(expected);
      });

      describe('and user navigates to /products/998 url', () => {
        beforeEach(async () => {
          await router.navigateByUrl('/products/998');
          fixture.detectChanges();
        });

        it('should display route properties', () => {
          const expected: SnapshotDump = {
            params: 'productId:998',
            queryParams: '',
            data: 'secret:product-details',
            snapshotParams: 'productId:998',
            snapshotQueryParams: '',
            snapshotData: 'secret:product-details',
            initialSnapshotParams: 'productId:123',
            initialSnapshotQueryParams: '',
            initialSnapshotData: 'secret:product-details',
          };
          expect(getSnapshotDump()).toEqual(expected);
        });

        describe('and user navigates to /products/998?param1=val1 url', () => {
          beforeEach(async () => {
            await router.navigateByUrl('/products/998?param1=val1');
            fixture.detectChanges();
          });

          it('should display route properties', () => {
            const expected: SnapshotDump = {
              params: 'productId:998',
              queryParams: 'param1:val1',
              data: 'secret:product-details',
              snapshotParams: 'productId:998',
              snapshotQueryParams: 'param1:val1',
              snapshotData: 'secret:product-details',
              initialSnapshotParams: 'productId:123',
              initialSnapshotQueryParams: '',
              initialSnapshotData: 'secret:product-details',
            };
            expect(getSnapshotDump()).toEqual(expected);
          });

          describe('and user navigates to /products/998?param2=val2 url', () => {
            beforeEach(async () => {
              await router.navigateByUrl('/products/998?param2=val2');
              fixture.detectChanges();
            });

            it('should display route properties', () => {
              const expected: SnapshotDump = {
                params: 'productId:998',
                queryParams: 'param2:val2',
                data: 'secret:product-details',
                snapshotParams: 'productId:998',
                snapshotQueryParams: 'param2:val2',
                snapshotData: 'secret:product-details',
                initialSnapshotParams: 'productId:123',
                initialSnapshotQueryParams: '',
                initialSnapshotData: 'secret:product-details',
              };
              expect(getSnapshotDump()).toEqual(expected);
            });
          });
        });
      });
    });
  });

  const getSnapshotDump = (): SnapshotDump => {
    return {
      params: fixture.debugElement.query(By.css('[data-test="observable-params"]')).nativeElement.textContent,
      queryParams: fixture.debugElement.query(By.css('[data-test="observable-query-params"]')).nativeElement.textContent,
      data: fixture.debugElement.query(By.css('[data-test="observable-data"]')).nativeElement.textContent,
      snapshotParams: fixture.debugElement.query(By.css('[data-test="snapshot-params"]')).nativeElement.textContent,
      snapshotQueryParams: fixture.debugElement.query(By.css('[data-test="snapshot-query-params"]')).nativeElement.textContent,
      snapshotData: fixture.debugElement.query(By.css('[data-test="snapshot-data"]')).nativeElement.textContent,
      initialSnapshotParams: fixture.debugElement.query(By.css('[data-test="initial-snapshot-params"]')).nativeElement.textContent,
      initialSnapshotQueryParams: fixture.debugElement.query(By.css('[data-test="initial-snapshot-query-params"]')).nativeElement.textContent,
      initialSnapshotData: fixture.debugElement.query(By.css('[data-test="initial-snapshot-data"]')).nativeElement.textContent,
    }
  }
});


@Component({
  selector: 'router-outlet-dummy',
  template: '<router-outlet></router-outlet>',
})
export class RouterOutletComponent {
}

interface SnapshotDump {
  params: string;
  queryParams: string;
  data: string;
  snapshotParams: string;
  snapshotQueryParams: string;
  snapshotData: string;
  initialSnapshotParams: string;
  initialSnapshotQueryParams: string;
  initialSnapshotData: string;
}
```

In this test, I'm navigating between routes and validating the parameters. The test confirms that the params and snapshot params are up-to-date when accessing the activated route. Also confirmed, the initial snapshot parameters don't change upon navigation.

# Source code

https://gitlab.com/barcioch-blog-examples/013-angular-extracting-parameters-from-activated-route
