Skip to content

Instantly share code, notes, and snippets.

@zaenk
Last active August 13, 2024 16:07
Show Gist options
  • Save zaenk/21e8bee3071f339aed27f411822bb8d9 to your computer and use it in GitHub Desktop.
Save zaenk/21e8bee3071f339aed27f411822bb8d9 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>
@bensgroi
Copy link

Thanks! This worked for me, though this API is for an older version of UI-Router, so I had to make some changes.

$urlRouterProvider has been deprecated, replaced by $urlServiceProveder, whose rules.otherwise callback doesn't receive an injector. So instead of creating factories to inject the Angular services into AngularJS, I had to create providers and consume them like this:

// src/app/app.module.ts
AppModule.config((
   $stateProvider,
   $urlServiceProvider,
   angularRouterProvider,
   urlHandlingStrategyProvider
) => {
   $urlServiceProvider.rules.otherwise((matchValue, urlParts, router) => {
      const urlHandlingStrategy = urlHandlingStrategyProvider.$get(),
         angularRouter = angularRouterProvider.$get(),
         url = angularRouter.parseUrl(router.locationService.url());

         if (urlHandlingStrategy.shouldProcessUrl(url)) {
            ...

Looks like this is built on the ideas in the ideas in Victor Savkin's Upgrading Angular ebook? I appreciated your fleshing the code out more, because the examples in the book are a little thin.

@nguyentk90
Copy link

Thanks for your instruction.

When I navigate from AngularJS to Angular router, the template of Angular router has not rendered under . The router only works when I refresh the whole page. Do you have any idea for this problem?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment