[javascript] Change route params without reloading in Angular 2

I'm making a real estate website using Angular 2, Google Maps, etc. and when a user changes the center of the map I perform a search to the API indicating the current position of the map as well as the radius. The thing is, I want to reflect those values in the url without reloading the entire page. Is that possible? I've found some solutions using AngularJS 1.x but nothing about Angular 2.

This question is related to javascript angular routes angular2-routing

The answer is


In my case I needed to remove a query param of the url to prevent user to see it.

I found replaceState safer than location.go because the path with the old query params disappeared of the stack and user can be redo the query related with this query. So, I prefer it to do it:

this.location.replaceState(this.router.url.split('?')[0]);

Whit location.go, go to back with the browser will return to your old path with the query params and will keep it in the navigation stack.

this.location.go(this.router.url.split('?')[0]);


For anyone like me finding this question the following might be useful.

I had a similar problem and initially tried using location.go and location.replaceState as suggested in other answers here. However I ran into problems when I had to navigate to another page on the app because the navigation was relative to the current route and the current route wasn't being updated by location.go or location.replaceState (the router doesn't know anything about what these do to the URL)

In essence I needed a solution that DIDN'T reload the page/component when the route parameter changed but DID update the route state internally.

I ended up using query parameters. You can find more about it here: https://angular-2-training-book.rangle.io/handout/routing/query_params.html

So if you need to do something like save an order and get an order ID you can update your page URL like shown below. Updating a centre location and related data on a map would be similar

// let's say we're saving an order. Initally the URL is just blah/orders
save(orderId) {
    // [Here we would call back-end to save the order in the database]

    this.router.navigate(['orders'], { queryParams: { id: orderId } });
    // now the URL is blah/orders?id:1234. We don't reload the orders
    // page or component so get desired behaviour of not seeing any 
    // flickers or resetting the page.
}

and you keep track of it within the ngOnInit method like:

ngOnInit() {
    this.orderId = this.route
        .queryParamMap
        .map(params => params.get('id') || null);
    // orderID is up-to-date with what is saved in database now, or if
    // nothing is saved and hence no id query paramter the orderId variable
    // is simply null.
    // [You can load the order here from its ID if this suits your design]
}

If you need to go direct to the order page with a new (unsaved) order you can do:

this.router.navigate(['orders']);

Or if you need to go direct to the order page for an existing (saved) order you can do:

this.router.navigate(['orders'], { queryParams: { id: '1234' } });

I use this way to get it:

const queryParamsObj = {foo: 1, bar: 2, andThis: 'text'};

this.location.replaceState(
  this.router.createUrlTree(
    [this.locationStrategy.path().split('?')[0]], // Get uri
    {queryParams: queryParamsObj} // Pass all parameters inside queryParamsObj
  ).toString()
);

-- EDIT --

I think that I should add some more informations for this.

If you use this.location.replaceState() router of your application is not updated, so if you use router information later it's not equal for this in your browser. For example if you use localizeService to change language, after switch language your application back to last URL where you was before change it with this.location.replaceState().

If you don't want this behaviour you can chose different method for update URL, like:

this.router.navigate(
  [this.locationStrategy.path().split('?')[0]],
  {queryParams: queryParamsObj}
);

In this option your browser also doesn't refresh but your URL change is also injected into Router of your application, so when you switch language you don't have problem like in this.location.replaceState().

Of course you can choose method for your needs. The first is more lighter because you don't engage your application more than change URL in browser.


I've had similar requirements as described in the question and it took a while to figure things out based on existing answers, so I would like to share my final solution.

Requirements

The state of my view (component, technically) can be changed by the user (filter settings, sorting options, etc.) When state changes happen, i.e. the user changes the sorting direction, I want to:

  • Reflect the state changes in the URL
  • Handle state changes, i.e. make an API call to receive a new result set

additionally, I would like to:

  • Specify if the URL changes are considered in the browser history (back/forward) based on circumstances
  • use complex objects as state params to provide greater flexibility in handling of state changes (optional, but makes life easier for example when some state changes trigger backend/API calls while others are handled by the frontend internally)

Solution: Change state without reloading component

A state change does not cause a component reload when using route parameters or query parameters. The component instance stays alive. I see no good reason to mess with the router state by using Location.go() or location.replaceState().

var state = { q: 'foo', sort: 'bar' }; 
var url = this.router.createUrlTree([], { relativeTo: this.activatedRoute, queryParams: state }).toString();
this.router.navigateByUrl(url);

The state object will be transformed to URL query params by Angular's Router:

https://localhost/some/route?q=foo&sort=bar

Solution: Handling state changes to make API calls

The state changes triggered above can be handled by subscribing to ActivatedRoute.queryParams:

export class MyComponent implements OnInit {

   constructor(private activatedRoute: ActivatedRoute) { }

   ngOnInit()
   {
      this.activatedRoute.queryParams.subscribe((params) => {
         // params is the state object passed to the router on navigation 
         // Make API calls here
      });
   }

}

The state object of the above axample will be passed as the params argument of the queryParams observable. In the handler API calls can be made if necessary.

But: I would prefer handling the state changes directly in my component and avoid the detour over ActivatedRoute.queryParams. IMO, navigating the router, letting Angular do routing magic and handle the queryParams change to do something, completely obfuscates whats happening in my component with regards to maintenability and readability of my code. What I do instead:

Compare the state passed in to queryParams observable with the current state in my component, do nothing, if it hasn't changed there and handle state changes directly instead:

export class MyComponent implements OnInit {

   private _currentState;

   constructor(private activatedRoute: ActivatedRoute) { }

   ngOnInit()
   {
      this.activatedRoute.queryParams.subscribe((params) => {
         // Following comparison assumes, that property order doesn't change
         if (JSON.stringify(this._currentState) == JSON.stringify(params)) return;
         // The followig code will be executed only when the state changes externally, i.e. through navigating to a URL with params by the user
         this._currentState = params;
         this.makeApiCalls();
      });
   }

   updateView()
   {          
      this.makeApiCalls();
      this.updateUri();
   }    

   updateUri()
   {
      var url = this.router.createUrlTree([], { relativeTo: this.activatedRoute, queryParams: this._currentState }).toString();
this.router.navigateByUrl(url);
   }
}

Solution: Specify browser history behavior

var createHistoryEntry = true // or false
var url = ... // see above
this.router.navigateByUrl(url, { replaceUrl : !createHistoryEntry});

Solution: Complex objects as state

This is beyond the original question but adresses common scenarios and might thus be useful: The state object above is limited to flat objects (an object with only simple string/bool/int/... properties but no nested objects). I found this limiting, because I need to distinguish between properties that need to be handled with a backend call and others, that are only used by the component internally. I wanted a state object like:

var state = { filter: { something: '', foo: 'bar' }, viewSettings: { ... } };

To use this state as queryParams object for the router, it needs to be flattened. I simply JSON.stringify all first level properties of the object:

private convertToParamsData(data) {
    var params = {};

    for (var prop in data) {
      if (Object.prototype.hasOwnProperty.call(data, prop)) {
        var value = data[prop];
        if (value == null || value == undefined) continue;
        params[prop] = JSON.stringify(value, (k, v) => {
          if (v !== null) return v
        });
      }
    }
    return params;
 }

and back, when handling the queryParams returned passed in by the router:

private convertFromParamsData(params) {
    var data = {};

    for (var prop in params) {
      if (Object.prototype.hasOwnProperty.call(params, prop)) {
        data[prop] = JSON.parse(params[prop]);
      }
    }
    return data;
}

Finally: A ready-to-use Angular service

And finally, all of this isolated in one simple service:

import { Injectable } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { Location } from '@angular/common';
import { map, filter, tap } from 'rxjs/operators';

@Injectable()
export class QueryParamsService {

  private currentParams: any;

  externalStateChange: Observable<any>;

  constructor(private activatedRoute: ActivatedRoute, private router: Router, private location: Location) {

    this.externalStateChange = this.activatedRoute.queryParams
      .pipe(map((flatParams) => {
        var params = this.convertFromParamsData(flatParams);
        return params
      }))
      .pipe(filter((params) => {
        return !this.equalsCurrentParams(params);
      }))
      .pipe(tap((params) => {
        this.currentParams = params;
      }));
  }

  setState(data: any, createHistoryEntry = false) {
    var flat = this.convertToParamsData(data);
    const url = this.router.createUrlTree([], { relativeTo: this.activatedRoute, queryParams: flat }).toString();
    this.currentParams = data;
    this.router.navigateByUrl(url, { replaceUrl: !createHistoryEntry });
  }

  private equalsCurrentParams(data) {
    var isEqual = JSON.stringify(data) == JSON.stringify(this.currentParams);
    return isEqual;
  }

  private convertToParamsData(data) {
    var params = {};

    for (var prop in data) {
      if (Object.prototype.hasOwnProperty.call(data, prop)) {
        var value = data[prop];
        if (value == null || value == undefined) continue;
        params[prop] = JSON.stringify(value, (k, v) => {
          if (v !== null) return v
        });
      }
    }
    return params;
  }

  private convertFromParamsData(params) {
    var data = {};

    for (var prop in params) {
      if (Object.prototype.hasOwnProperty.call(params, prop)) {
        data[prop] = JSON.parse(params[prop]);
      }
    }
    return data;
  }
}

which can be used like:

@Component({
  selector: "app-search",
  templateUrl: "./search.component.html",
  styleUrls: ["./search.component.scss"],
  providers: [QueryParamsService]
})
export class ProjectSearchComponent implements OnInit {

    filter : any;

    viewSettings : any;

    constructor(private queryParamsService: QueryParamsService) { }

    ngOnInit(): void {

        this.queryParamsService.externalStateChange
          .pipe(debounce(() => interval(500))) // Debounce optional
          .subscribe(params => {
           // Set state from params, i.e.
           if (params.filter) this.filter = params.filter;
           if (params.viewSettings) this.viewSettings = params.viewSettings;

           // You might want to init this.filter, ... with default values here
           // If you want to write default values to URL, you can call setState here
            this.queryParamsService.setState(params, false); // false = no history entry

            this.initializeView(); //i.e. make API calls        
         });
     }

     updateView() {

       var data = {
         filter: this.filter,
         viewSettings: this.viewSettings
       };

       this.queryParamsService.setState(data, true);

       // Do whatever to update your view
     }

  // ...

}

Don't forget the providers: [QueryParamsService] statement on component level to create a new service instance for the component. Don't register the service globally on app module.


As of RC6 you can do the following to change URL without change state and thereby keeping your route history

    import {OnInit} from '@angular/core';

    import {Location} from '@angular/common'; 
    // If you dont import this angular will import the wrong "Location"

    @Component({
        selector: 'example-component',
        templateUrl: 'xxx.html'
    })
    export class ExampleComponent implements OnInit {
        
        constructor( private location: Location )
        {}

        ngOnInit() {    
            this.location.replaceState("/some/newstate/");
        }
    }

For me it was actually a mix of both with Angular 4.4.5.

Using router.navigate kept destroying my url by not respecting the realtiveTo: activatedRoute part.

I've ended up with:

this._location.go(this._router.createUrlTree([this._router.url], { queryParams: { profile: value.id } }).toString())

Use attribute queryParamsHandling: 'merge' while changing the url.

this.router.navigate([], {
        queryParams: this.queryParams,
        queryParamsHandling: 'merge',
        replaceUrl: true,
});

Using location.go(url) is the way to go, but instead of hardcoding the url , consider generating it using router.createUrlTree().

Given that you want to do the following router call: this.router.navigate([{param: 1}], {relativeTo: this.activatedRoute}) but without reloading the component, it can be rewritten as:

const url = this.router.createUrlTree([], {relativeTo: this.activatedRoute, queryParams: {param: 1}}).toString()

 this.location.go(url);

I had major trouble getting this to work in RCx releases of angular2. The Location package has moved, and running location.go() inside constructor() wont work. It needs to be ngOnInit() or later in the lifecycle. Here is some example code:

import {OnInit} from '@angular/core';
import {Location} from '@angular/common';

@Component({
  selector: 'example-component',
  templateUrl: 'xxx.html'
})
export class ExampleComponent implements OnInit
{
  constructor( private location: Location )
  {}

  ngOnInit()
  {    
    this.location.go( '/example;example_param=917' );
  }
}

Here are the angular resources on the matter: https://angular.io/docs/ts/latest/api/common/index/Location-class.html https://angular.io/docs/ts/latest/api/common/index/LocationStrategy-class.html


Examples related to javascript

need to add a class to an element How to make a variable accessible outside a function? Hide Signs that Meteor.js was Used How to create a showdown.js markdown extension Please help me convert this script to a simple image slider Highlight Anchor Links when user manually scrolls? Summing radio input values How to execute an action before close metro app WinJS javascript, for loop defines a dynamic variable name Getting all files in directory with ajax

Examples related to angular

error NG6002: Appears in the NgModule.imports of AppModule, but could not be resolved to an NgModule class error TS1086: An accessor cannot be declared in an ambient context in Angular 9 TS1086: An accessor cannot be declared in ambient context @angular/material/index.d.ts' is not a module Why powershell does not run Angular commands? error: This is probably not a problem with npm. There is likely additional logging output above Angular @ViewChild() error: Expected 2 arguments, but got 1 Schema validation failed with the following errors: Data path ".builders['app-shell']" should have required property 'class' Access blocked by CORS policy: Response to preflight request doesn't pass access control check origin 'http://localhost:4200' has been blocked by CORS policy in Angular7

Examples related to routes

How to open a link in new tab using angular? How to Refresh a Component in Angular laravel Unable to prepare route ... for serialization. Uses Closure How to use router.navigateByUrl and router.navigate in Angular No provider for Router? Difference between [routerLink] and routerLink How to force Laravel Project to use HTTPS for all routes? Change route params without reloading in Angular 2 Angular 2 router no base href set How to redirect to a route in laravel 5 by using href tag if I'm not using blade or any template?

Examples related to angular2-routing

'router-outlet' is not a known element How to reload page the page with pagination in Angular 2? Error: Cannot match any routes. URL Segment: - Angular 2 Can't bind to 'routerLink' since it isn't a known property Passing data into "router-outlet" child components How to determine previous page URL in Angular? How to get parameter on Angular2 route in Angular way? Angular 2 Scroll to top on Route Change Angular routerLink does not navigate to the corresponding component Angular 2 router.navigate