From Donkey to Unicorn, A new approach to AngularJS migration.
AngularConnect 2017 · London, UK
by Asim Hussain · 30 June 2017
I’d been giving this talk all over the place — ngConf, Barcelona, Israel, AngularUP, workshops, meetups — and the same thing kept happening: people would come up afterwards, frustrated that the official ng-upgrade path just wasn’t working for their app. So at AngularConnect I laid out an alternative I was calling “bulletproof migration” (the name changed weekly), built on a colleague’s offhand suggestion: just iFrame the old app into the new one.
AI-generated summary of my talk
Jump into the talk
- 0:02 Why I'm here: ng-upgrade frustration
- 2:11 A quick recap of the ng-upgrade approach
- 3:12 Why migrations fail #1: other baggage
- 5:18 Why migrations fail #2: clean house
- 8:22 The iFrame idea (and the demo)
- 11:23 Route ownership: who owns the URL?
- 16:43 Shared state via local storage
- 18:50 Summary, and three frameworks living in peace
Why ng-upgrade isn’t enough
Let me be clear up front: ng-upgrade is great. It’s a technical marvel — no other framework has invested this much in helping people move from one version to the next. The idea is that you dual boot the application, running both the AngularJS and the new Angular libraries at once, then upgrade one entity at a time — a service here, a controller there — working through the leaves of your app until eventually everything’s Angular and you drop AngularJS entirely.
But I kept meeting people for whom it simply wasn’t working. After enough of those conversations I started to think the failures clustered around two problems. I call them other baggage and clean house.
Other baggage
We don’t build apps in isolation — we lean on a pile of third-party libraries. Your AngularJS app might be on Bootstrap 2.3.2, ui-router, angular-strap and so on. When you migrate, some of those get absorbed into Angular proper (ui-router becomes the Angular router), and some you simply want to upgrade — Bootstrap 2 to Bootstrap 4, say.
Here’s the rub: ng-upgrade runs one application in one global namespace, so both versions share the same modules. You can’t have two versions of Bootstrap running. That leaves two bad options. Keep the baggage, and you end up with a shiny new Angular app still on Bootstrap 2 — and nobody’s building a Bootstrap 2 date picker with modern Angular support. Or migrate the legacy app to Bootstrap 4 first, pouring effort into code you’re about to throw away. What you actually want is for the old app to stay on Bootstrap 2 while the new one is born on Bootstrap 4. Neither ng-upgrade option gives you that.
Clean house
The second problem is that ng-upgrade really wants you to tidy up before you start. When AngularJS first landed, none of us knew how to architect it well — we leaned on controllers instead of components, scope inheritance, far too much $watch, far too much $emit and $broadcast. The expectation is that you bring all of that up to today’s standards — TypeScript, sane file structure, controller-as — before migrating.
But most of us are staring at complex architectures with years of investment, maybe tens of thousands of hours. Telling a team they need to refactor everything to modern standards first is, at best, a deeply depressing request and, at worst, flatly impossible. As more than one person said to me: that’s basically the same as rewriting it from scratch.
The iFrame idea
I didn’t come up with the alternative myself. On my last engagement before Microsoft I’d spent six months wrestling an ugly framework onto Angular, and I was training up my replacement. After a few minutes he turned to me and asked, “Why don’t you just use an iFrame?” Build a modern Angular SPA, and when you want to show the old content, iFrame the old AngularJS app in.
My first reaction was to object — loudly. But a few minutes later I realised it wasn’t a bad idea at all. I actually started feeling a bit dumb for not thinking of it myself. So I built a demo: a Bootstrap 4 / Angular app and a Bootstrap 2 / AngularJS 1.6 app, where the new app iFrames the old one in. Inspect the page and the AngularJS route is literally just an iFrame inside a router outlet. And crucially, a shared counter stays in sync across both — two completely separate applications, each with its own global namespace, sharing routing and state. No baggage problem, no clean-house tax: the legacy app keeps its wonderful, unique architecture, untouched.
Route ownership and shared state
Two core concepts make it work. The first is route ownership. You start with the whole thing iFramed in, so AngularJS owns every URL. Then Angular starts taking over, one route at a time — you write the route in Angular, and that URL now renders the new app instead. The thorny bit is the handoff: for a while both apps are live, so who owns a given route?
Most cases are trivial — an AngularJS page linking to another AngularJS page is handled internally, same for Angular to Angular. The interesting case is crossing the boundary. Going Angular to AngularJS, I use a fallback route — the ** wildcard at the end of the Angular config — which, when nothing else matches, injects an iFrame component pointing at the right legacy URL (with a little URL massaging and de-sanitising so Angular allows it in an iFrame). Going the other way, AngularJS to Angular, you can’t inject an iFrame — you need to talk from the child iFrame up to the parent. So in UI router’s otherwise I call parent.postMessage(...) with the URL I want, and the iFrame component listens for that event and calls navigateByUrl on its own router.
For shared state I keep both apps on the same domain. That means they share cookies — so logging in on one logs you in on both — and they share local storage. Whenever the counter changes I write it to local storage, and a storage event fires; the other app listens for that event and updates its own copy. Same trick that syncs data across browser tabs, used here to sync across the iFrame boundary.
The part I find most exciting is that none of this is Angular-specific. It’s just an iFrame. React to Angular, Vue to Angular, Angular to Vue — it all works. I was getting tired of the framework wars: if we can live in a world with multiple cultures, religions and beliefs, surely we can live in a world with three JavaScript frameworks. All the code and samples are on my blog at codecraft.tv.