# Understanding Data Passing in Angular Using Router and Component Binding (ComponentInputBinding)

In this article, I'll show how to extract parameters and data from Angular routes using the component input binding feature.

# Setup

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

# Component Input Binding

## Enable it

There are two ways of enabling this feature.

1. import `RouterModule.forRoot` and pass the option `bindToComponentInputs: true` and `paramsInheritanceStrategy: 'always'`
    

```typescript
imports: [
  ...
  RouterModule.forRoot(routes, {bindToComponentInputs: true, paramsInheritanceStrategy: 'always'})
]
```

1. Use provider function `provideRouter` and pass the `withComponentInputBinding()` and `withRouterConfig({paramsInheritanceStrategy: 'always'})` features
    

```typescript
providers: [
  ...
  provideRouter(routes, withComponentInputBinding(), withRouterConfig({paramsInheritanceStrategy: 'always'}))
]
```

## Use it

To use this feature define component inputs using the well-known `@Input` decorator.

```typescript
@Component({
  [...]
})
export class TestComponent {
  @Input() testId?: string;
  @Input() sort?: string;
  @Input() secretData?: string;
}
```

Also, this component has to be `routed`. That means, one of the application routes has to point directly to it.

```typescript
export const routes: Routes = [
  {
    path: 'test/:testId',
    component: TestComponent,
    data: {secretData: 'secret'}
  },
];
```

Now, `path params`, `query params` and `data` will be assigned to the inputs by matching their names. If the name collision occurs the route parameters are assigned in the following order: `data` &gt; `path param` &gt; `query param`. Also, they won't override each other, i.e: you cannot override `path param` with `query param` and so on.

# Test it

## Routes

```typescript
import { Routes } from '@angular/router';
import { RoutedComponentComponent } from '../routed-component/routed-component.component';


export const routes: Routes = [
  {
    path: 'params/:pathParam',
    component: RoutedComponentComponent,
    data: {dataParam: 'secret'}
  },
];
```

## Test components

I'll be using two components. Their purpose is to test, that route information is bound only in the the routed components. The non-routed behave as before - you have to pass parameters explicitly.

The routed one:

```typescript
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-routed-component',
  template: `
    <span data-test="routed-pathParam">{{ pathParam }}</span>
    <span data-test="routed-queryParam">{{ queryParam }}</span>
    <span data-test="routed-nonExistingParam">{{ nonExistingParam }}</span>
    <span data-test="routed-dataParam">{{ dataParam }}</span>
    
    <app-non-routed-component></app-non-routed-component>
  `
})
export class RoutedComponentComponent {
  @Input() pathParam?: string;
  @Input() nonExistingParam?: string;
  @Input() queryParam?: string;
  @Input() dataParam?: string;
}
```

and non-routed:

```typescript
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-non-routed-component',
  template: `
    <span data-test="non-routed-pathParam">{{ pathParam }}</span>
    <span data-test="non-routed-queryParam">{{ queryParam }}</span>
    <span data-test="non-routed-nonExistingParam">{{ nonExistingParam }}</span>
    <span data-test="non-routed-dataParam">{{ dataParam }}</span>
  `
})
export class NonRoutedComponentComponent {
  @Input() pathParam?: string;
  @Input() nonExistingParam?: string;
  @Input() queryParam?: string;
  @Input() dataParam?: string;
}
```

## Test suite

The test suite employs `RouterTestingHarness` to reduce the boilerplate code. The testing procedure is simple: navigate through the routes and verify the bound information from routed and non-routed components. Also, test the name collision.

```typescript
import { provideRouter, withComponentInputBinding, withRouterConfig } from '@angular/router';
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { routes } from './app-routing';
import { RouterTestingHarness } from '@angular/router/testing';
import { RoutedComponentComponent } from '../routed-component/routed-component.component';
import { NonRoutedComponentComponent } from '../non-routed-component/non-routed-component.component';


describe('ComponentInputBinding', () => {
  let harness: RouterTestingHarness;

  beforeEach(async () => {
    TestBed.configureTestingModule({
      declarations: [RoutedComponentComponent, NonRoutedComponentComponent],
      providers: [provideRouter(routes, withComponentInputBinding(), withRouterConfig({ paramsInheritanceStrategy: 'always' }))],
    });

    harness = await RouterTestingHarness.create();
  });

  describe('Test standard route params', () => {
    describe('when user enters /params/123?queryParam=paramFromQuery url', () => {
      beforeEach(async () => {
        await harness.navigateByUrl('/params/123?queryParam=paramFromQuery');
        harness.fixture.detectChanges();
      });

      it('should display routed component params', () => {
        const expected: ComponentParams = {
          pathParam: '123',
          queryParam: 'paramFromQuery',
          nonExistingParam: '',
          dataParam: 'secret',
        };
        expect(getRoutedComponentParams()).toEqual(expected);
      });

      it('should display empty input values in non-routed component', () => {
        const expected: ComponentParams = {
          pathParam: '',
          queryParam: '',
          nonExistingParam: '',
          dataParam: '',
        };
        expect(getNonRoutedComponentParams()).toEqual(expected);
      });

      describe('and user navigates to same route with different pathParam and queryParam (/params/456?queryParam=aNewValue)', () => {
        beforeEach(async () => {
          await harness.navigateByUrl('/params/456?queryParam=aNewValue');
          harness.fixture.detectChanges();
        });

        it('should display routed component params', () => {
          const expected: ComponentParams = {
            pathParam: '456',
            queryParam: 'aNewValue',
            nonExistingParam: '',
            dataParam: 'secret',
          };
          expect(getRoutedComponentParams()).toEqual(expected);
        });

        it('should display empty input values in non-routed component', () => {
          const expected: ComponentParams = {
            pathParam: '',
            queryParam: '',
            nonExistingParam: '',
            dataParam: '',
          };
          expect(getNonRoutedComponentParams()).toEqual(expected);
        });
      });
    });
  });

  describe('Test name collision', () => {
    describe('when user enters url with query params named same as pathParam and dataParam (/params/123?pathParam=pathParamFromQuery&dataParam=dataParamFromQuery)', () => {
      beforeEach(async () => {
        await harness.navigateByUrl('/params/123?pathParam=pathParamFromQuery&dataParam=dataParamFromQuery');
        harness.fixture.detectChanges();
      });

      it('should not override path params and data', () => {
        const expected: ComponentParams = {
          pathParam: '123',
          queryParam: '',
          nonExistingParam: '',
          dataParam: 'secret',
        };
        expect(getRoutedComponentParams()).toEqual(expected);
      });

      it('should display empty input values in non-routed component', () => {
        const expected: ComponentParams = {
          pathParam: '',
          queryParam: '',
          nonExistingParam: '',
          dataParam: '',
        };
        expect(getNonRoutedComponentParams()).toEqual(expected);
      });
    });
  });

  const getRoutedComponentParams = (): ComponentParams => {
    return {
      pathParam: harness.fixture.debugElement.query(By.css('[data-test="routed-pathParam"]')).nativeElement.textContent,
      queryParam: harness.fixture.debugElement.query(By.css('[data-test="routed-queryParam"]')).nativeElement.textContent,
      nonExistingParam: harness.fixture.debugElement.query(By.css('[data-test="routed-nonExistingParam"]')).nativeElement.textContent,
      dataParam: harness.fixture.debugElement.query(By.css('[data-test="routed-dataParam"]')).nativeElement.textContent,
    }
  }

  const getNonRoutedComponentParams = (): ComponentParams => {
    return {
      pathParam: harness.fixture.debugElement.query(By.css('[data-test="non-routed-pathParam"]')).nativeElement.textContent,
      queryParam: harness.fixture.debugElement.query(By.css('[data-test="non-routed-queryParam"]')).nativeElement.textContent,
      nonExistingParam: harness.fixture.debugElement.query(By.css('[data-test="non-routed-nonExistingParam"]')).nativeElement.textContent,
      dataParam: harness.fixture.debugElement.query(By.css('[data-test="non-routed-dataParam"]')).nativeElement.textContent,
    }
  }
});

interface ComponentParams {
  pathParam: string;
  queryParam: string;
  nonExistingParam: string;
  dataParam: string;
}
```

# Source code

https://gitlab.com/barcioch-blog-examples/014-angular-component-input-binding
