Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save powellmichael/c674e4070d353ef3a5df51a145f38238 to your computer and use it in GitHub Desktop.
Save powellmichael/c674e4070d353ef3a5df51a145f38238 to your computer and use it in GitHub Desktop.
Migrating AngularJS UI Router to Angular Router

Migrating AngularJS UI Router to Angular Router

This migration path focuses on AngularJS UI Router and Angular Router interopability,

This method not good for everyone! The main characteristics of this path is that the AngularJS and Angular apps does not share the same layout. So new components are always introduced in Angular, old components are rewritten and downgraded for AngularJS. Migration of old modules happens at once, when almost all components are updated. UI Router states cannot be reused in Angular, so every state and listener should bre rewriten to routes and event listeners or strategies for Angular Router.

Prerequisites

  • AngularJS 1.6.5, UI Router 0.4.2, Angular 4.3.5
  • AngularJS app architecture follows John Papa's AngularJS Styleguide
  • Webpack and ES6 introduced to the AngularJS project
  • Started migration to Angular (at least bootstrap and module exports are done)

UI Router - Angular Router interopability

First, configure an UrlHandlingStrategy in the Angular application:

// src/app/app.module.ts
import { NgModule, Component } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';

import { RouterModule, UrlHandlingStrategy } from '@angular/router';

class Ng1Ng2UrlHandlingStrategy implements UrlHandlingStrategy {
	shouldProcessUrl(url) {
		console.log('Should process called for: ' + url);
		return url.toString().startsWith('/usage');
	}
	extract(url) { return url; }
	merge(url, whole) { return url; }
}

@Component({
	selector: 'app-root',
	template: `<div class="ui-view"></div>
    <router-outlet></router-outlet>
  `
})
export class AppRootComponent {}

@NgModule({
	imports: [
		BrowserModule,
		UpgradeModule,
		RouterModule.forRoot([], {useHash: false, initialNavigation: true}),
		/* other modules */
	],
	bootstrap: [AppRootComponent],
	declarations: [AppRootComponent],
	providers: [
		{ provide: UrlHandlingStrategy, useClass: Ng1Ng2UrlHandlingStrategy }
	],
})
export class AppModule {
	constructor(private upgrade: UpgradeModule) { }
}

RouterModule configuration initialNavigation: true will enable us to navigate to Angular routes directly, without going into the AngularJS app first.

Next, we want to inject Angular Router and UrlHandlingStrategy components into our AngualrJS app. Modify the bootstrapping code as follows:

// src/main.ts
import 'angular';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { UpgradeModule } from '@angular/upgrade/static';

import { AppModule } from './app/app.module';

import { Router, UrlHandlingStrategy } from '@angular/router';

import { modules } from './app/index'; // exports [ RootModule ]
// import { modules } from '../e2e/mock_backend/index'; // exports [ MocksModule, RootModule ]

platformBrowserDynamic().bootstrapModule(AppModule).then(ref => {
  // add factories to RootModule
	modules[modules.length - 1].factory('ng2Injector', () => ref.injector)
		.factory('ng2UrlHandlingStrategy', () => ref.injector.get(UrlHandlingStrategy))
		.factory('ng2Router', () => ref.injector.get(Router));
	(<any>ref.instance).upgrade.bootstrap(document.body, modules.map(module => module.name), { strictDi: true });
});

At this point, the Angular Router could intercept navigation, but those events always handeled by UI Router.

In the next steps we want to achive the following behaviour, when navigating to URL that is not a UI Router state, then unload AngularJS content for app root component and delegate routing to Angular.

I have achived it with the following configuration:

// AngularJS states
*
|- guest (abstract)
|  |- login
|  |- 2FA verify
|- auhtenticated (abstract)
   |- app (abstract, provide standard layout)
   |  |- // all the states
   |- ng2 (without template*)

I introduced an ng2 state, which has no template and only servers as a monkey patch for UI Router to unload the content from the top level ui-view. See the configuration below:

// src/app/components/auth/auth.module.ts
AuthModule.config(($stateProvider, $httpProvider) => {
  $stateProvider
    .state('guest', {
      url: '',
      template: main,
      redirectTo: 'guest.login'
  })
  .state('guest.login', {
    url: '/login',
    template: '<login></login>'
  })
  .state('guest.verify', {
    url: '/verify',
    template: `
    <verification
      credentials="$resolve.credentials">
    </verification>
  `})
  .state('authenticated', {
    redirectTo: 'app',
    template: '<ui-view/>'
  });
  // ...
});

// src/app/app.module.ts
AppModule.config(($stateProvider, $urlRouterProvider) => {
  // ...
  $stateProvider
    .state('app', {
      url: '',
      parent: 'authenticated',
      template: '<app></app>',
      redirectTo: 'home'
    })
    .state('ng2', { // state to clean up view
      parent: 'authenticated',
    });
    //...

Next we have to hook into UI Router $urlRouter to chatch events, where it did not found state defined for the URL:

// src/app/app.module.ts
AppModule.config(($stateProvider, $urlRouterProvider) => {
  $urlRouterProvider.otherwise(($injector, $location) => {
    const $state = $injector.get('$state');
    const ng2UrlHandlingStrategy = $injector.get('ng2UrlHandlingStrategy');
    const ng2Router = $injector.get('ng2Router');
    const url = $location.url();
    if (ng2UrlHandlingStrategy.shouldProcessUrl(url)) {
      $state.go('ng2');
      ng2Router.navigate([url]);
    } else {
      $state.go('app');
    }
  });
  // ...
}

And finally, we provide nice directive to AngularJS, that will execute routing to the Angular Router:

// src/app/app.module.ts
AppModule.directive('routerLink', (ng2Router) => {
  return {
    restrict: 'A',
    scope: {
      routerLink: '@'
    },
    link: function(scope, element, attr) {
      element.on('click', () => {
        ng2Router.navigate([scope.routerLink]);
      });
    }
  };
});

Then we can route to Angular from AngularJS template by:

<a router-link="/usage">Usage report</a>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment