Keep your URL clean
Tags: angular, angularrouter
Published: 2020-05-21 16:07:04

In our app we can set the filters of a page through the URL. This is especially helpful when the user wants to share complex filters with peers.

Once the user has navigated into the page, we store the filter settings in session and we don't need the URL parameters any more. More over, if the user changes her filter settings, we update the session, but if she reloads the page with the URL, it would take precedence over the session settings and would give her a bad experience.

The solution is to remove the filters while storing them in the settings. But... We can't just remove all the url params because there may be others that do not belong to the filters. We need to remove each parameter at a time.

Let's get down to code. 

```typescript
ngOnInit() {
  this.route.queryParamMap.subscribe(paramMap => {
    this.filter(paramMap, 'f1');
    this.filter(paramMap, 'f2');
 
    // ...
  
  });
}
```
We subscribe to all the query params when our component initializes. Every time the queryMap changes when we're on this page, our filters will update. _Don't forget to unsubscribe `onDestroy`!_

```typescript
private filter(paramMap:ParamMap, key:string) {
  if (!paramMap.has(key)) {return;}

  const value = paramMap.get(key);

  // Perform filtering here
  // ...
  
  // Set filter to session here
  // ..

  this.removeParamFromUrl(paramMap, key);
}
```
I like doing all the negative checks at the entrance to the function and exit if no work is to be done. It saves us some nice levels of indentation, at the cost of a slightly less readable negative-condition.

Once we're done with all the filtering and the session work, we can now safely remove the parameter from our URL.

```typescript
private removeParamFromUrl(paramMap: ParamMap, key: string) {
  if (!paramMap.has(key)) { return; }

  const queryParams = {};
  paramMap.keys.filter(k => k != key).forEach(k => (queryParams[k] = paramMap.get(k)));
  this.router.navigate([], { queryParams, replaceUrl: true, relativeTo: this.route });
}
```

The `if` statement when we enter the function is just to make sure that we're not trying to remove a parameter that doesn't exist. It wouldn't have any visual effect if we didn't have this protection, but it saves us some cycles. Just to be sure, I looked up the [source code for ParamMap](https://github.com/angular/angular/blob/master/packages/router/src/shared.ts#L69) and behind the scenes it uses a pure object, so `~O(1)` to check. I digress.

We create a new query param dictionary and place in it only the keys we want. Finally, we navigate to the new route with one parameter less.

###Hold On!! This is terrible! 
1. We loop over the param map `~O(n!)`.
2. We are calling the router to navigate again and again.

Let's see if we can make this a little better? We'll work backwards now, starting from `removeParamFromUrl`:

```typescript
private removeParamFromUrl(paramMap: ParamMap, keysToRemove: string[]) {
    const queryParams = {};
    const keysToKeep = paramMap.keys.filter(k => !keysToRemove.includes(k));
    keysToKeep.forEach(k => (queryParams[k] = paramMap.get(k)));

    this.router.navigate([], { queryParams, replaceUrl: true, relativeTo: this.route });
}
```

Now we're getting an array of keys that we want to get rid of, instead of a single key each time. We iterate over the keys of `paramMap` and keep only those that are not in included.

If we want to finish up quickly here, we can have our subscription call `removeParamFromUrl` with all our filters:

```typescript
ngOnInit() {
  this.route.queryParamMap.subscribe(paramMap => {
    this.filter(paramMap, 'f1');
    this.filter(paramMap, 'f2');
 
    // ...
    this.removeParamFromUrl(paramMap, ['f1', 'f2' ...]);
  });
}

private filter(paramMap:ParamMap, key:string) {
  if (!paramMap.has(key)) {return;}

  const value = paramMap.get(key);

  // Perform filtering here
  // ...
  
  // Set filter to session here
  // ..

  // We got rid of the local call to removeParamFromUrl
}
```
And we'd be done.

But let's make it just a little cleaner? Again, at the cost of some readability. 

```typescript
private filter(paramMap:ParamMap, key:string):boolean {
  if (!paramMap.has(key)) {return false;}

  const value = paramMap.get(key);

  // Perform filtering here
  // ...
  
  // Set filter to session here
  // ..

  // We got rid of the local call to removeParamFromUrl
  // And returning true if the filter was applied.
  // Notice that we returned `false` in the `if` above!
  return true;
}
```

Now we'll handle the list filters in `ngOnInit`:
```typescript
ngOnInit() {
  this.route.queryParamMap.subscribe(paramMap => {
    const filterKeys = ['f1', 'f2' ...];
    const keysToRemove = [];

    // filter and apply keys for removal
    filterKeys.forEach(k => {
      // filter and remove key only if filtered.
      this.filter(paramMap, k) && keysToRemove.push(k);
    });

    this.removeParamFromUrl(paramMap, keysToRemove);
  });
}
```

Here we iterate over the keys to remove. We make use the boolean condition - if the filter returned `true` the key gets added to `keysToRemove`. If the filter returned `false`, the second part of the condition will not evaluate **at all** and the key will not be added.

_A final note about Readability_: The question of readability is often personal and may depend on your own experience and convenience. But most importantly - think of the engineers who will come after you and will have to read and change your code. Will a junior developer be comfortable reading this? Will they notice and understand the condition?

Being a senior developer isn't only about writing the most efficient code. It's also about writing code that others can understand and change easily.