Automatically sync url path and query params with Angular component fields

Wouldn't it be nice if a user of you Angular application could just send a link to another person ant they would end up seeing the exact same things on his screen?
The user could also just bookmark the url and get back to the same view whenever he wants to.

Using the Angular router you can accomplish that even if you have very dynamic views.
However, the effort and boilerplate code for this is quite huge and can easily be avoided with a library I wrote. (My first npm package ❤️)

The concept behind this is deep linking.

What is deep linking?

If you are already familiar with deep linking and how to implement it with the Angular Router continue here.

Imagine you have built a webapp for a library, and a user is browsing a list of available books. He selects a book and wants to show it to a friend.

With deep linking the information of the selected book is part of the url and can easily be shared between users. The same thing can be done with any parameter that affects the current view in your Angular application. (e.g. additionally with a search string as shown below)

As shown in the example above you can write these parameters to the url as

  • path parameter library/:location/books/:selectedBookId or
  • query parameter ...?searchString=jon&secondParameter=1.

Path parameters can be strings or numbers while query parameters also could be complex json objects

How to implement "deep linking" with the Angular router?

In your components you can get the current path and query params by injecting the ActivatedRouteSnapshot:

export class YourComponent implements OnInit {
    private yourPathParam: number;
    private yourQueryParam: string;

    constructor(private readonly activatedRouteSnapshot: ActivatedRouteSnapshot) {}

    ngOnInit() {
        const pathParams: Params[] = this.activatedRouteSnapshot.params;
        this.yourPathParam = pathParams['yourPathParam'];

        const queryParams: Params[] = this.activatedRouteSnapshot.queryParams;
        this.yourQueryParam = queryParams['yourQueryParam'];
    }
}

You can even subscribe to changes in the url params by using the ActivatedRoute instead of the ActivatedRouteSnapshot:

this.activatedRoute.params.subscribe((newPathParams) => console.log(newPathParams));
this.activatedRoute.queryParams.subscribe((newQueryParams) => console.log(newQueryParams));

What about the other way around? If a field in you component changes you can navigate to the new url by injecting the Router:

this.router.navigateByUrl(newUrl);

The hard part here is figuring out the new url. You might not want to change existing parameters but ony change the one that is of interest in your component.
However, this is a crucial part for implementing "deep linking" as we need an updated url whenever the view changes.

The downsides of the Angular router

As you can see synchronizing of url params with your component fields is quite hard and requires a lot of "boilerplate code".

The problems of the Angular router are

  1. not providing a way to linking url parameters to component inputs via configuration
  2. not providing a simple way to update the url on component field changes (e.g. via component outputs)

The simple way with ngx-deep-linking

What if you could simply configure synchronization of component fields with the url?
Ngx-deep-linking provides a way to do this in the route configuration.

In the end you just need three things:

  1. You need to install @jdrks/ngx-deep-linking:
    • npm install @jdrks/ngx-deep-linking or
    • yarn add @jdrks/ngx-deep-linking
  2. You need to provide @Input and @Output for the fields you want to deep link.
    • You must provide an @Input to initially populate the component field and change it if the url changes
    • You may provide an @Output to change the url whenever that component output emits. Make sure to follow the naming convention for Angular two way data binding by appending 'Change' to the field name.
  3. You need to provide a router config with the names of all fields you want to "deep link":
    • See example below
    • You can use the provided interface DeepLinkingRoute for a type safe configuration
import {DeepLinkingRoute, DeepLinkingWrapperComponent} from '@jdrks/ngx-deep-linking';
...

const routes: DeepLinkingRoute[] = [
  {
    path: 'books/:selectedBookId',
    component: DeepLinkingWrapperComponent,
    wrappedComponent: BookListComponent,
    deepLinking: {
      params: [{name: 'selectedBookId', type: 'number'}],
      queryParams: [{name: 'searchString', type: 'string'}],
    }
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class BookListRoutingModule {
}

The book list component could look like this:

@Component({
  templateUrl: 'book-list.component.html'
})
export class BookListComponent {

  private _selectedBookId: number = 0;

  @Output()
  private selectedBookIdChange: EventEmitter<number> = new EventEmitter<number>();

  @Input()
  get selectedBookId(): number {
    return this.selectedBookId;
  }

  set selectedBookId(value: number) {
    this.selectedBookId = value;
    this.selectedBookIdChange.next(bookId);
  }

  private _searchString = '';

  @Output()
  searchStringChange: EventEmitter<string> = new EventEmitter<string>();

  @Input()
  get searchString(): string {
    return this._searchString;
  }

  set searchString(value: string) {
    this._searchString = value;
    this.searchStringChange.next(value);
  }

  ...
}

So basically it's just like two-way data binding. The above implementation will automatically emit on the @Output fields when using the setters, but this is just convenience.

Features of ngx-deep-linking

  • Easily synchronize your component fields with the url by simple route configuration
  • Type safe configuration
  • Supports synchronizing fields of type number, string and even complex types as json
  • Supports lazy loading for Angular modules
  • Supports hash routing strategy

How does the library work under the hood?

The library wraps you component into the DeepLinkingWrapperComponent. The fields that should be "deep linked" are synced with the DeepLinkingWrapperComponent via "manual" two-way data binding. The wrapper reacts to changes on both url and wrapped component and updates vice versa.

Links

36