This is a technical retrospective on migrating a large Angular 1.x application from controller-based routing to component-based architecture using ui-router 1.0.

Project context
I joined a substantial Angular project that started on v1.3.x in 2015. By late 2016, we recognised the need to migrate towards component-based architecture, partly informed by Angular 2 patterns and partly to improve code maintainability.
Our systematic approach involved incremental framework upgrades. The initial upgrades to 1.4.x and early 1.5.x were straightforward with minimal breaking changes. The real opportunity came with Angular 1.5.0 and its component pattern.
Component migration strategy
We started with new features as components rather than converting existing code immediately. Our application was essentially a form builder with reusable widgets, an ideal use case for component architecture.
Resource strategy
- AngularJS component documentation for foundational understanding
- Todd Motto's blog series for practical implementation patterns
- Community examples for edge cases and gotchas
Routing evolution
The migration from controller-based to component-based routing simplified our architecture significantly:
// Legacy approach (ui-router 0.x)
app.state("users", {
url: "/users",
templateUrl: "somefile.html",
controller: "userListController",
controllerAs: "vm",
otherConfigs: {
// 3rd party integrations and promises
},
});
// Component approach (ui-router 1.0.3-beta)
app.state("users", {
url: "/users",
component: "userListComponent",
});
This change eliminated tight coupling between views and controllers while promoting code reusability, exactly what we needed for a widget-based system.
Technical implementation insights
One-way data binding
Component bindings made one-way data flow much more manageable. Starting with one-way binding as the default approach solved most use cases, with two-way binding reserved for specific edge cases.
This architectural decision helped us refactor legacy code from "two-way binding hell" to clean, predictable data flow patterns.
Dynamic component creation
We implemented dynamic widget creation through attribute directives combined with $compile:
function componentWrapperDirective($compile) {
return {
restrict: "A",
transclude: true,
scope: {
master: "=someAttribute",
},
link: function (scope, element, attrs) {
var item = angular.copy(scope.master);
if (item) {
let componentName = item.widgetName.toLowerCase(),
overlayScope = scope.$new(true); // Isolated scope
overlayScope.data = item;
let component =
"<fb-" +
componentName +
' widget-info="data"></fb-' +
componentName +
">";
element.append($compile(component)(overlayScope));
}
},
};
}
This approach let us create components from configuration with proper scope isolation, which was exactly what our form builder needed.
Outcomes
The migration gave us reduced coupling (components eliminated view-controller dependencies), better reusability (widget components could be composed flexibly), and simplified routing (a single component property replaced multiple configuration options). It also aligned our patterns with Angular 2 for the eventual migration.
Lessons learned
Gradual framework upgrades minimised risk and let the team adapt incrementally. Starting new features as components rather than retrofitting existing code worked much better. And ui-router's component support gave us a clear migration path without vendor lock-in.
Future considerations
The next phase involves full ES6 module implementation before eventual Angular 2+ migration. The component-based architecture and reduced state dependencies should make this transition more straightforward.
The approach we took was with incremental upgrades, component-first development, and gradual refactoring, this gave us a sustainable path for large-scale framework evolution without disrupting ongoing development work.
This migration strategy could serve as a template for other large Angular 1.x applications facing similar modernisation challenges.