Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Alec Irwin
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Alec Irwin

Changing The Hash With The Location Service In Angular 4.4.0-RC.0

By Ben Nadel on

The Angular framework provides two ways of working with the browser location: the Location service for direct URL manipulation; and, the Router for more advanced URL manipulation, parsing, and view rendering. In an application, I would use the Router; but, in my Angular demos, the Router is way overkill; so I often reach for the Location service. Now, one thing that's not obvious in the Location documentation is how to change the location Hash or fragment. As it turns out, you can include the hash in the "path" argument when navigating (as long as you also include the optional query-string in the path as well).


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

The Location service in Angular 4.4 has a .go() method with the following signature:

go( path: string, query: string ): void

To look at this, one would logically have to draw the conclusion that there is no way to change the hash or fragment portion of the application URL. After all, if we're breaking query-string apart from the path, then it would stand to reason there would also be a "hash" argument if the hash could be changed programmatically.

If you look under the hood, however, you'll see that the LocationStrategy implementations in Angular (Hash and Path) are simply normalizing and concatenating the path and the query-string before passing them through to the underlying platform location. Roughly speaking, the implementations look like this:

let url = ( path + Location.normalizeQueryParams( queryParams ) );

This means that if the "queryParams" argument is empty, "path" becomes the value that's passed to the underlying platform implementation. Which means, the "path" argument should be able to contain the pathname, the query-string, and the hash. And, in fact, if you treat the "path" argument as the entire application URL, omitting the "query" argument altogether, you can use the .go() method to change the all three segments of the location path, including the fragment.

To see this in action, I've put together a rather trite demo that passes full, hash-containing URLs into the location.go() method and then queries the location to see if the changes took place:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  • import { Location } from "@angular/common";
  • import { PopStateEvent } from "@angular/common";
  •  
  • @Component({
  • selector: "my-app",
  • styleUrls: [ "./app.component.css" ],
  • template:
  • `
  • <h2>
  • HREF-Based URL Changes
  • </h2>
  •  
  • <ul>
  • <li>
  • <a href="#/route-one?hello=world#hashification">
  • #/route-one?hello=world#hashification
  • </a>
  • </li>
  • <li>
  • <a href="#/route-two?cool=beans#hashitation">
  • #/route-two?cool=beans#hashitation
  • </a>
  • </li>
  • </ul>
  •  
  • <h2>
  • Location-Based URL Changes
  • </h2>
  •  
  • <ul>
  • <li>
  • <a (click)="navigate( '/route-three?meep=moop#hashmatash' )">
  • Route three
  • </a>
  • </li>
  • <li>
  • <a (click)="navigate( '/route-four?king=kong#hashmania' )">
  • Route four
  • </a>
  • </li>
  • </ul>
  • `
  • })
  • export class AppComponent {
  •  
  • public location: Location;
  •  
  • // I initialize the app component.
  • constructor( location: Location ) {
  •  
  • this.location = location;
  • // While the PopStateEvent won't trigger when we call location.go(), it will
  • // trigger when we use the HREF-based navigation. Let's listen for those location
  • // changes that occur due to external changes in the browser URL.
  • this.location.subscribe(
  • ( event: PopStateEvent ) : void => {
  •  
  • if ( event.type === "hashchange" ) {
  •  
  • console.group( "PopState" );
  • console.log( event.url );
  • console.groupEnd();
  •  
  • }
  •  
  • }
  • );
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I navigate to the given URL. This URL is expected to be a "complete" URL; meaning,
  • // it contains all the necessary components: pathname, query-string, and hash.
  • public navigate( newPath: string ) : void {
  •  
  • // In the documentation, the .go() method accepts two arguments: "path" and
  • // "query". This leaves you wondering about the "hash" - where does that go?
  • // Well, it turns out that the path and query don't really need to be broken out
  • // into separate components. Under the hood, they are just concatenated. As such,
  • // we can include them in the "path" argument, along with any desired HASH value.
  • this.location.go( newPath );
  •  
  • // Since the PopStateEvent doesn't fire when we programmatically navigate, let's
  • // turn around and query the location.
  • console.group( "Internal Navigation" );
  • console.log( this.location.path() );
  • console.groupEnd();
  •  
  • }
  •  
  • }

As you can see, I'm including two HREF-based links as a control-test alongside two (click)-based links. In my (click)-based links, though, you can see that the value being passed to location.go() contains a comprehensive URL that includes the pathname, the query-string, and the hash. And, if we click through these URLs, we can see that the hash is properly set using location.go():


 
 
 

 
 Changing the application url hash / fragment using the location service in Angular 4. 
 
 
 

This works quite nicely. But, one word of caution: once you start putting full URLs in the "path" argument, you have to omit the "query" argument. If you try to use the query argument, you may end up inserting the query-string after the embedded hash, which will not work.

For completeness, I will say that could also include the hash inside the query-string, such that your location.go() method looks like this:

location.go( "path.htm", "foo=bar#hash" )

This works because the values are, ultimately, just concatenated under the hood; however, this feels a bit more janky. I'd rather have one overloaded "path" argument than try to overload the "query" argument.

If you're building an Angular application, you'll likely end up using the Router; however, in cases where you want to use the Location service directly, it may not be clear how to change the hash / fragment. Luckily, if we just put the pathname, query-string, and fragment inside the "path" argument, then location.go() will change the location hash.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

@All,

While working on this post, I was feeling very nostalgic for the $location service in Angular 1.x - particularly the ability to access and mutate portions of the URL independently (ex, path, search, hash). As such, I tried to re-create some of that goodness:

https://www.bennadel.com/blog/3332-creating-an-angular-1-x-location-inspired-retrolocation-service-in-angular-4-4-0-rc-0.htm

I created a "RetroLocation" service that works on top of the LocationStrategy implementation in an Angular 4.x application.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.