angular: Detect hashchange from outside

During my work I came up with an issue regarding detecting the location change of an Angular SPA from outside of the application. The project setup is the following: The SPA runs in an iframe (thats the functionality provided by the platform to integrate SPAs). The applications’ state (URL) should be propagated to the parent page and its browser URL to even make the SPA “deep-linkable”. This functionality is achieved by the platform by using the iframe-hash-manager library.

About iframe-hash-manager

It detects location changes by listening to windows’ hashchange event. This event is only triggered when setting the hash via HTML DOM Location API. No other events are taken into account.

Routing in Angular

Strategies

Generally there are two location strategies in Angular. The HashLocationStrategy produces URLs like https://mydomain.com/#anything while the PathLocationStrategy produces https://mydomain.com/anything. So in fact you stay at the same resource when using HashLocationStrategy and you “move” to an other resource (even though you physically stay at the same) when using PathLocationStrategy.

Technical Details on Routing Implementation

Internally, Angular uses the HTML5 History API to navigate in browser. The reason for using this API is that it provides state binding to each history entry.

Take a look at the concrete implementation (same for pushState method), you can find the full code at github:

// see https://github.com/angular/angular/blob/7.2.x/packages/common/src/location/hash_location_strategy.ts
replaceState(state: any, title: string, path: string, queryParams: string) {
    // ...
    this._platformLocation.replaceState(state, title, url);
}

The _platformLocation is getting injected automatically by Angular. In case of running in the browser this is derived by the injected BrowserPlatformLocation and its implementation uses the History API.

Decisions made

Since the platform uses the HashLocationStrategy and the asset server is not currently supporting deeplinks without hash, I decided to use the HashLocationStrategy for my use case.

Potential Solutions

From researching about solutions it became obvious that the solution can either be implemented in the iframe-hash-manager or within the app to trigger the events manually.

To come up with a final solution for notifying the parent about the SPAs’ location change I tried the following potential solutions whereas only solution 3 really works out and makes sense.

1. Use Location API in Angular

One possibility would be to make Angular use the HTML 5 Location API when manipulating routes. In fact this does not work out since Angular relies on states as provided by History API.

2. Overwrite Browsers’ History pushState/replaceState method

Since there is no option to listen to History APIs changes made through replaceState/pushState methods, an other option is overwriting both methods as suggested in this stackoverflow answer to manually trigger an event after the methods have been executed.

This attempt may cause side effects on other application parts and cross-browser issues, so I decided not to use it.

3. Add new LocationStrategy firing hashchange Event

Inspired by solution 2 I thought about an “Angular only” solution.

So how about creating a new custom LocationStrategy?

I was really worried about creating a new LocationStrategy since I do not want to change Angular behavior and implementation details for the current available strategies. I did some research in the implementation (see above) itself and came up with the decision that creating an additional strategy makes only sense if most of the present strategy is used.

So what I did is only overwriting the two methods pushState and replaceState in the HashLocationStrategy by still using the original implementation for navigating and firing the hashchange event afterwards:

Implementation Details

// see https://github.com/mbenzenhoefer/ngx-hashchange-location-strategy/blob/master/projects/ngx-hashchange-location-strategy/src/lib/hashchange-location-strategy.ts
export class HashChangeLocationStrategy extends HashLocationStrategy {
  // ...
  pushState(
    state: any,
    title: string,
    path: string,
    queryParams: string
  ): void {
    const oldUrl: string = this._getCurrentUrl();
    super.pushState(state, title, path, queryParams);
    this._fireHashChangeEvent(oldUrl);
  }
  // ...
  private _fireHashChangeEvent(oldUrl: string): void {
    const changeEvt = new Event("hashchange");
    // ...
    window.dispatchEvent(changeEvt);
  }
}

Now this “new” LocationStrategy has to be provided in the SPA as it is usually done in Angular:

// ...
import { HashChangeCompatibleHashLocationStrategy } from "ngx-hashchange-location-strategy";

@NgModule({
  // ...
  providers: [
    { provide: LocationStrategy, useClass: HashChangeLocationStrategy }
    // ...
  ]
  // ...
})
export class AppModule {}

This approach caused issues when multiple routings are going on at the same time, because Angular itself listens to the hashchange event. So I came up with the solution to only fire the event once per cycle. This is achieved by setting a timeout and clearing it if the method accessed again.

if (this.hashChangeFireTimeout) {
  clearTimeout(this.hashChangeFireTimeout);
}
this.hashChangeFireTimeout = setTimeout(() => {
  window.dispatchEvent(changeEvt);
});

Throwing this event really seems to be the solution for handling the hashchange from the Angular SPA and really worked out well for me thus I created a library for that, which you can find on github.

Issues / Limitations

As described above, due to the fact that there may be multiple routings ongoing (one after an other) in the same Angular cycle, only the last hashchange event is propagated. Reason: Angular listens to HashChangeEvents and if we throw it again and again, the application will stuck in an infinite routing loop.

Summary

To detect hashchanges from outside the Angular SPA I created a library which overwrites the default HashLocationStrategy and adds manual event triggering for the hashchange event.

You can find the library here.