| name | culture | description | engines | author | contributors | translators | keywords | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Migrating to Aurelia from Angular 1.x |
en-US |
If you have an existing Angular application that you would like to migrate to Aurelia, you can use the conversion guides and implementation notes here to streamline the process. |
|
|
|
Angular 1 has been discarded in favor of Google's new initiative, Angular 2 - leaving many existing applications in need of a rewrite. Rewriting an existing Angular 1 application doesn't have to be difficult, though. With a thorough understanding of modern web standards, emerging language features, and the Javascript ecosystem, you can migrate an existing Angular 1 application to Aurelia.
To ensure that you are ready to begin migrating your application, you will need to have NodeJS and JSPM installed, along with access to your AngularJS application.
Migration will be simpler if your application is:
- using Angular's
controller assyntax in views - assigning properties to
thisinstead of$scopein controllers - separated across files, using one file per Angular module
There are five main things we have to do to migrate an Angular application to Aurelia. First, we need to change the way we load our scripts, adhering to the SystemJS loader model. Second, we'll configure and bootstrap our application in Aurelia. Third, we'll migrate our modules from Angular's dependency injection to Aurelia's injection; fourth, we'll migrate our views and viewmodels to work with Aurelia. Finally (and this is the big one), we'll learn how to migrate Angular directives to Aurelia custom elements.
It's recommended to start with the Aurelia Skeleton Navigation project, as loading and bootstrapping have already been provided.
First, we'll need to change the way we load our scripts. Some existing Angular applications use a loader such as RequireJS, but some applications load their scripts through script tags. Aurelia has been designed to adhere to the ES6 module loading specification; by default, it uses JSPM for package management, Babel for code compilation, and SystemJS for loading.
Second, we'll bootstrap and configure our application in Aurelia. Angular applications typically bootstrap automatically, using the ng-app attribute on an HTML element to call a specific Angular module, like so:
<title>Skeleton Navigation</title>
</head>
<body>
...
</body> </source-code>
Aurelia uses a similar attribute that should feel familiar, like so:
<title>Skeleton Navigation</title>
</head>
<body aurelia-app="main">
...
</body>
</source-code>
Instead of a module name, the aurelia-app attribute takes a filename as its value (without the extension).
Warning:
aurelia-appvsng-appTheaurelia-appattribute will never be on the roothtmltag. Aurelia's bootstrapper will replace the inner HTML of the element containing theaurelia-appattribute.
Configuring an Aurelia application at startup is slightly different than its Angular equivalent. In Angular, the application is configured with a static set of options - Angular configuration, application controllers, services, etc. As such, service configuration in Angular only occurs before Angular bootstraps the applicaiton. Services in Aurelia can be configured at any time - not just at startup.
Angular might look something like this.
angular.config(['$locationProvider', '$routeProvider', function ($locationProvider, $routeProvider) { /* In this configuration block, we might configure certain services - such as interceptors * for HTTP requests, or client-side routing templates - but after Angular has bootstrapped, * configuration is locked. */ ... }]);Aurelia uses a similar configuration model, but services don't have to be configured before they are used; configuration is generally only used for global resource locations and framework plugins.
export function configure(aurelia) { aurelia.use .standardConfiguration() // .plugin('aurelia-animator-css') .feature('someFeature') .globalResources('resources/my-element', 'resources/my-other-element'); aurelia.start().then(a => a.setRoot());
}
</source-code>
This means that certain important service configurations (especially routing and HTTP) can be configured during application runtime. We'll touch upon this later.
Third step - we'll migrate our modules from Angular to Aurelia. Like Aurelia, Angular supported modules and dependency injection; however, Angular used a non-standard, Angular-specific module format. Whether loaded through a loader or through generic script tags, Angular declared a global angular object. Using any controller, service, or other functionality with Angular meant that it had to be registered with an Angular module at bootstrap time in order to be used. An example Angular configuration might look like the code below.
angular.module('myModule')
.service('anotherService', ['$q', '$router',function ($q, $router) {
...
}]);
angular.module('myModule')
.factory('myFactory, ['$storage', function ($storage) {
}]);
angular.module('myModule')
.value('myValue', "value")
... // and so on
</source-code>
Aurelia handles things a little differently. Instead of framework-specific modules, Aurelia leverages native Javascript modules using either the ES6 import statement or the require-style AMD syntax. (These end up transpiled to AMD modules for browser use.) When registering objects with Angular modules Angular made a distinction between service, factory, constant, and value - all syntactic sugar for provider. Aurelia, in contrast, makes no special framework distinction between "services", "factories", and other module types - any viewmodels or services are just simple Javascript modules, simplifying the way you can write your code.
So, the angular code above might end up looking more like this:
@inject(Q, HttpClient) export class myService { constructor (Q, http) { ... } } @inject(Q, Router)
export class anotherService {
constructor (Q, router) {
}
}
@inject(Storage)
export class myFactory {
constructor (storage) {
}
}
export var myValue = value;
</source-code>
As long as you define your exports semantically, Aurelia doesn't need to register or define them in a specific way ("factory" vs "service" vs "value"). Everything is an exported module; everything is generic, and modules can be imported and then injected using Aurelia's dependency injection.
For more information on dependency injection, see the documentation here.
Fourth, we'll migrate our viewmodels.
In Angular, a controller must be registered with a specific name, using the angular global object.
angular.controller('users', ['$scope', '$http', Users]);
</source-code>
Using Aurelia, any object can act as a viewmodel without being explicitly registered, as long as that object is exported as a module. Essentially, this simply means removing the Angular boilerplate registration code. So, essentially, you would have this:
function Users (httpClient) { ... }Note: Dependency Injection Angular was able to inject Angular-specific dependencies based on the argument name (such as
$scopeand$http) through its service locator. Because Aurelia does not couple your application to Aurelia-specific implementations, all dependencies must be explicitly injected.
Aurelia supports client-side routing in the browser. The main difference between Angular's router and Aurelia's router is in the configuration; the Aurelia router can be configured outside of startup, and it can be configured asynchronously. Finally, the Aurelia router supports nested routers (also known as child routers) as well as multiple viewports.
An Angular router configuration, in startup, may look like this:
Alternatively, an Aurelia router is configured inside the viewmodel that uses it. Thus, Aurelia configuration may look like this:
export class App { configureRouter(config, router) { config.title = 'Aurelia'; config.map([ { route: ['', 'welcome'], name: 'welcome', moduleId: 'welcome', nav: true, title: 'Welcome' }, { route: 'users', name: 'users', moduleId: 'users', nav: true, title: 'Users' } ]); this.router = router;
}
}
</source-code>
Note: Router Migration Migration to Aurelia will be easier if your application uses Angular's new router; this router shares many similarities with the router that Aurelia uses, and the configuration will be easier to migrate.
Angular made heavy use of an Angular-specific service named $scope; responsible for computed properties, change detection (through $scope.watch()), signalling (via $scope.broadcast and $scope.emit), and view-to-viewmodel binding, this service was often a staple of an Angular controller.
A typical controller might yield something like this:
angular.controller('Users', ['$scope', '$http', function ($scope, $http) { $scope.heading = 'Welcome to the Angular Navigation App!'; $scope.firstName = 'John'; $scope.lastName = 'Doe'; $scope.previousValue = $scope.fullName(); $scope.fullName = function () {
return $scope.firstName + ' ' + $scope.lastName;
};
$scope.submit = function () {
$scope.previousValue = $scope.fullName();
alert('Welcome, ' + $scope.fullName());
};
}]);
</source-code>
In Aurelia, viewmodel properties are declared as properties on the viewmodel object.
The Aurelia version of our typical Angular controller might look like this:
export class Welcome { heading = 'Welcome to the Aurelia Navigation App!'; firstName = 'John'; lastName = 'Doe'; previousValue = this.fullName; get fullName() {
return `${this.firstName} ${this.lastName}`;
}
submit() {
this.previousValue = this.fullName;
alert(`Welcome, ${this.fullName}!`);
}
}
</source-code>
Note: Controller Migration Later versions of Angular supported the "controller as" syntax, allowing developers to declare viewmodel properties on the
thisproperty. However,$scopewas still necessary for watches and signalling.) Your application will be easier to migrate to Aurelia if you are only using$scopefor signalling.
Fourth step, part two - we'll migrate the views. Aurelia views and Angular views are very similar. However, Aurelia views follow the Web Components specification, requiring views to be wrapped in a <template> tag. Further, references to custom views or custom elements must be included in the view using the <require> tag.
In Angular, a view was often strongly coupled to a viewmodel (or controller) instance, using something similar to the following code:
Aurelia, by default, prefers convention over configuration. Aurelia's default convention is that a viewmodel will have a view with the same name. To take advantage of this, keep a view in the same directory alongside its viewmodel, and make sure that the HTML file and Javascript file share the same filename.
Note: Convention vs configuration This behavior is also configurable using the ViewLocator service. By configuring Aurelia's ViewLocator, you can override Aurelia's defaults. Views and viewmodels can be linked by location, by a file naming convention, or by other means.
Angular, by default, uses a double-bracket syntax for interpolation, like so:
Aurelia makes use of ES6 template strings in its templates, so the Aurelia version becomes this:
Aurelia's template strings use pipe syntax for ValueConverters (filters in Angular).
ng-show becomes show.bind. ng-if becomes if.bind. ng-click becomes click.delegate.
Angular's repeat syntax looks like this:
- {{item.name}}
In Aurelia, the repeat is not on the parent element. It looks like this:
- ${item.name}
Event binding is also a bit different:
myFunction button myFunction buttonFinally, the meat and potatoes - it's time to migrate our Angular directives.
Angular and Aurelia both support the concept of extending HTML by creating custom elements and attributes. However, Aurelia handles the creation of these in a different way.
Creating a custom element in Angular required us to write an Angular directive; we would return a configuration object that told Angular how to create our object through object properties including scope, restrict, template, templateUrl, and link.
Instead of a configuration object, Aurelia takes a class instance.
An Angular directive may have looked like this:
angular.directive('display', ['$window', '$document', 'animate', function ($window, $document, animate) { return { scope: true, restrict: 'A', templateUrl: 'myTemplate.html', link: function (scope, element, attrs) { var windowheight = angular.element($window).height(); scope.holeHeight = function (depth) {
return (depth * 0.2) < windowheight ? (depth * 0.2) + 'px' : undefined;
};
scope.bgPos = function (depth) {
return (depth * 0.2) < windowheight ? 0 : (depth - windowheight) * 0.2;
};
scope.holeWidth = function () {
return Math.min(700, Math.max(350, _(scope.shop)
.reduce(function (p, c) {
return p + c.owned * c.digValue;
}, 0)
));
};
scope.displayArray = [];
scope.$watch('shop', function () {
scope.displayArray = _.values(scope.shop).reverse();
});
(function tick(timestamp) {
angular.element($window).scrollTop($document.height());
animate(tick);
})();
}
};
}])
</source-code>
Converting this directive to an Aurelia custom element requires us to remove the scope references, migrate from link and controller properties to lifecycle methods, and simplify our object. Finally, we'll have to use Aurelia's decorator functions to register our object and its configuration as a custom element instance.
attached () {
}
}
</source-code>
Warning: Scope Custom elements in Aurelia cannot access the outer scope. This means that all custom elements in Aurelia essentially behave as though they had the Angular property
scope: { ... }- custom elements have an isolated scope.
Custom attributes are handled similarly to custom elements in Aurelia. Custom attributes also use an isolate scope - they cannot access the outer scope.
Aurelia provides the content tag to provide transclusion features. This is part of the view itself, instead of being part of the directive.
No equivalent exists in Aurelia.
Instead of $scope.property, use this.property inside of a viewmodel object.
Instead of $scope.$watch(), use a computed property - or, to trigger a function, use the BindingEngine.
Instead of $scope.$emit() and $scope.$broadcast(), use EventAggregator.publish(). Likewise, instead of $scope.$on, use EventAggregator.subscribe().
For signalling (using $emit and $broadcast, use Aurelia's event aggregator. For state that persists across viewmodels, use a separate service.
No equivalent exists in Aurelia; it's not needed as Aurelia's change detection works differently to change detection in Angular. Aurelia will detect changes made from setTimeout, so use that.
Consider using Aurelia's cache plugin.
Use one of the 'aurelia-animate' plugins.
Use window.document.
Use a ValueConverter, providing your own toView and fromView methods.
Use fetch-client. If you need cancellable requests, or IE9 support, use http-client.
Use router.navigateToRoute or router.navigate.
Use Aurelia's logger.
No equivalent exists in Aurelia; Aurelia will detect changes made asynchronously. Use ES6 native promises or a promise library.
Use the DOM object from Aurelia's Platform Abstraction Layer (PAL).