1297 lines
58 KiB
Plaintext
1297 lines
58 KiB
Plaintext
include ../../../../_includes/_util-fns
|
|
|
|
:marked
|
|
Having an existing Angular 1 application doesn't mean that we can't
|
|
begin enjoying everything Angular 2 has to offer. That's beause
|
|
Angular 2 comes with built-in tools for migrating Angular 1 projects
|
|
over to the Angular 2 platform.
|
|
|
|
One of the keys to a successful upgrade is to do it incrementally,
|
|
by running the two frameworks side by side in the same application,
|
|
and porting Angular 1 components to Angular 2 one by one. This makes
|
|
it possible to upgrade even large and complex applications without
|
|
disrupting other work. The `upgrade` module in Angular 2 has
|
|
been designed to make incremental upgrading seamless.
|
|
|
|
In this chapter we will look at a complete example of preparing and
|
|
upgrading an application using the `upgrade` module. The app we're going
|
|
to work on is [Angular PhoneCat](https://github.com/angular/angular-phonecat)
|
|
from [the original Angular 1 tutorial](https://docs.angularjs.org/tutorial),
|
|
which is where many of us began our Angular adventures. Now we'll see how to
|
|
bring that application to the brave new world of Angular 2.
|
|
|
|
During the process we'll learn
|
|
|
|
- How to prepare and align an Angular 1 application with Angular 2
|
|
|
|
- How to use the SystemJS module loader and TypeScript with Angular 1
|
|
|
|
- How to develop and test a hybrid Angular 1+2 application
|
|
|
|
- How to migrate an application to Angular 2 one component at a time
|
|
|
|
To follow along with the tutorial, clone the
|
|
[angular-phonecat](https://github.com/angular/angular-phonecat) repository
|
|
and apply the steps as we go
|
|
|
|
.alert.is-important
|
|
:marked
|
|
If you do clone this repository, note that it doesn't look like this guide
|
|
assumes yet. There's [a pull request](https://github.com/angular/angular-phonecat/pull/289)
|
|
that will change this. Meanwhile, you'll find a good starting point from
|
|
[this commit](https://github.com/teropa/angular-phonecat/commit/d6fb83e1c2db9d1812c7c478fdb8d92301ef0061).
|
|
|
|
.l-main-section
|
|
:marked
|
|
## Preparing for the Upgrade
|
|
|
|
In terms of project structure, this is where our work begins
|
|
|
|
.filetree
|
|
.file angular-phonecat
|
|
.children
|
|
.file bower.json
|
|
.file package.json
|
|
.file app
|
|
.children
|
|
.file js
|
|
.children
|
|
.file core
|
|
.children
|
|
.file checkmark.filter.js
|
|
.file core.module.js
|
|
.file phone.factory.js
|
|
.file phone_detail
|
|
.children
|
|
.file phone_detail.html
|
|
.file phone_detail.module.js
|
|
.file phone_detail.controller.js
|
|
.file phone_list
|
|
.children
|
|
.file phone_list.html
|
|
.file phone_list.module.js
|
|
.file phone_list.controller.js
|
|
.file app.module.js
|
|
.file css
|
|
.children
|
|
.file animations.css
|
|
.file app.css
|
|
.file img
|
|
.children
|
|
.file ...
|
|
.file phones
|
|
.children
|
|
.file ...
|
|
.file index.html
|
|
.file test
|
|
.children
|
|
.file e2e
|
|
.children
|
|
.file scenarios.js
|
|
.file unit
|
|
.children
|
|
.file checkmark.filter.spec.js
|
|
.file phone_detail.controller.spec.js
|
|
.file phone.factory.spec.js
|
|
.file phone_list.controller.spec.js
|
|
.file karma.conf.js
|
|
.file protractor-conf.js
|
|
|
|
:marked
|
|
This is actually a pretty good starting point. In particular, this organization
|
|
follows the [Angular Style Guide](https://github.com/johnpapa/angular-styleguide),
|
|
which is an important [preparation step](preparation.html) before a successful upgrade.
|
|
|
|
* Each controller, factory, and filter is in its own source file, as per the
|
|
[Rule of 1](https://github.com/johnpapa/angular-styleguide#single-responsibility).
|
|
* The `core`, `phoneDetail`, and `phoneList` modules are each in their
|
|
own subdirectory. Those subdirectories contain the JavaScript code as well as
|
|
the HTML templates that go with each particular feature. This is in line with the
|
|
[Folders-by-Feature Structure](https://github.com/johnpapa/angular-styleguide#style-y152)
|
|
and [Modularity](https://github.com/johnpapa/angular-styleguide#modularity)
|
|
rules.
|
|
|
|
:marked
|
|
## TypeScript And Module Loading
|
|
|
|
Since we're going to be writing our Angular 2 code in TypeScript, it makes sense to
|
|
bring in the TypeScript compiler even before we begin upgrading.
|
|
|
|
In order to use TypeScript's ES2015 module system to `import` and `export` code, we're
|
|
going to need a JavaScript module loader. Our application doesn't currently
|
|
use one, and is just using plain old `<script>` tags and the global `window` scope
|
|
instead. We'll replace this approach with the
|
|
[SystemJS loader](https://github.com/systemjs/systemjs).
|
|
|
|
.alert.is-helpful
|
|
:marked
|
|
Angular 2 itself doesn't require either TypeScript or SystemJS.
|
|
There will soon be other editions of this guide that show how to
|
|
do the upgrade using ES5.
|
|
|
|
:marked
|
|
We will also start to gradually phase out the Bower package manager in favor
|
|
of NPM. We'll install all new dependencies using NPM, and will eventually be
|
|
able to remove Bower from the project.
|
|
|
|
Let's begin by installing the SystemJS and TypeScript packages to the project:
|
|
```
|
|
npm i systemjs --save
|
|
npm i typescript --save-dev
|
|
```
|
|
|
|
The Angular 1 framework doesn't come with built-in TypeScript type definitions.
|
|
This means that if we want to have type checks for the calls we make to Angular 1
|
|
APIs, we should install its type definitions separately.
|
|
For that we'll use the [tsd TypeScript definition manager](http://definitelytyped.org/tsd/).
|
|
Let's make sure we have it globally installed:
|
|
```
|
|
npm i -g tsd
|
|
```
|
|
|
|
We can then use tsd to install the type definitions for Angular 1 and the Jasmine
|
|
unit test framework. This will add a `typings` directory to the project and install
|
|
a number of `.d.ts` files under it:
|
|
```
|
|
tsd install angular angular-route angular-resource angular-mocks jasmine
|
|
```
|
|
|
|
In `index.html`, let's now enable SystemJS. Add a `<script>` tag that loads
|
|
the SystemJS library and a second `<script>` tag that initializes it. These
|
|
will *replace* the various `<script>` tags we had earlier for loading the
|
|
application components:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/app/index.html', 'scripts', 'app/index.html')
|
|
|
|
:marked
|
|
This configuration tells SystemJS that we have a module called `app.module` that
|
|
resides in the `js` subdirectory (relative to the `index.html` page). We then load that
|
|
module using `System.import`. This will load and execute the `app/app.module.js` file.
|
|
|
|
We should also configure the TypeScript compiler so that it can understand our
|
|
project. We'll add a `tsconfig.json` file to the project directory, just like we did
|
|
in the [Quickstart](../quickstart.html). It instructs the TypeScript compiler how
|
|
to interpret our source files.
|
|
|
|
+makeJson('upgrade/ts/typescript-conversion/tsconfig.1.json', null, 'tsconfig.json')
|
|
|
|
:marked
|
|
We are telling the TypeScript compiler to turn our TypeScript files to ES5 code
|
|
bundled into SystemJS modules. In other words, our compiler target is something
|
|
SystemJS can load and all major browsers are able to run.
|
|
|
|
Also add a `tsc` run script to `package.json`. We'll use it to start the TypeScript
|
|
compiler:
|
|
|
|
+makeJson('upgrade/ts/typescript-conversion/package.1.json', {paths: 'scripts'}, 'package.json', {otl: /(\"tsc.*)/})
|
|
|
|
:marked
|
|
We can now launch the TypeScript compiler from the command line. It will watch
|
|
our `.ts` source files and compile them to JavaScript on the fly. Those compiled
|
|
`.js` files are then loaded into the browser by SystemJS. This is a process we'll
|
|
want to have continuously running in the background as we go along.
|
|
```
|
|
npm run tsc
|
|
```
|
|
|
|
The next thing we'll do is convert our JavaScript files to TypeScript and define
|
|
their imports and exports. Each file should now explicitly export the things
|
|
it wants to expose, and import the things it needs from other files. This is a
|
|
departure from the previous approach which just relied on things being available
|
|
on the global `window` scope.
|
|
|
|
Since TypeScript is a superset of ECMAScript 2015, which in turn is a superset
|
|
of ECMAScript 5, we can simply switch the file extensions from `.js` to `.ts`
|
|
and define the imports and exports. We don't need to make other changes to
|
|
our existing code. Instead we'll introduce type annotations and other new
|
|
features gradually over time.
|
|
|
|
Let's begin by adding references to the Angular 1.x `.d.ts` typing files to the
|
|
main application file. Rename `app.module.js` to `app.module.ts` and add the following on top
|
|
of the file:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/app/js/app.module.ts', 'typings', 'app/js/app.module.ts')
|
|
|
|
:marked
|
|
The TypeScript compiler will now know what we mean when we reference
|
|
Angular 1 APIs. It should already at this point be able to compile the
|
|
`app.module.ts` file successfully.
|
|
|
|
Let's then go through the rest of our source files and convert them.
|
|
We'll rename each one to a `.ts` file, and add the imports and exports it needs.
|
|
|
|
Beginning from the checkmark filter, here are the converted contents:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/app/js/core/checkmark.filter.ts', null, 'app/js/core/checkmark.filter.ts')
|
|
|
|
:marked
|
|
This file now has the filter factory function as the default export. Apart from
|
|
the export, there's one other major change we've applied to the file, which
|
|
is that it does *not* contain the registration of the filter into an Angular
|
|
module. We will do that later in the `core` module's main file.
|
|
|
|
Moving to the `Phone` factory file, it now has the factory function as the default
|
|
export:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/app/js/core/phone.factory.ts', null, 'app/js/core/phone.factory.ts')
|
|
|
|
:marked
|
|
The `core` module's main module file will now import both the checkmark filter
|
|
and the Phone factory. This is where we actually register them into the Angular module.
|
|
We then export the module itself as this file's default export:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/app/js/core/core.module.ts', null, 'app/js/core/core.module.ts')
|
|
|
|
:marked
|
|
Notice that with this organization pattern, the files that hold the application
|
|
components themselves - filters and factories - aren't concerned with the makeup
|
|
of Angular modules. That's just something we previously *had* to do because there
|
|
were no other good solutions. Now we use a separate file just for the purpose
|
|
of forming the Angular module.
|
|
|
|
Now switching to the phone detail module, we'll make similar changes here. In the
|
|
controller file we export the controller function as the default export:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/app/js/phone_detail/phone_detail.controller.ts', null, 'app/js/phone_detail/phone_detail.controller.ts')
|
|
|
|
:marked
|
|
In the main module file we import the controller and register it into the Angular
|
|
module, which itself is then exported:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/app/js/phone_detail/phone_detail.module.ts', null, 'app/js/phone_detail/phone_detail.module.ts')
|
|
|
|
:marked
|
|
Then we'll repeat the same steps once more for the phone list module.
|
|
The controller file exports the controller function:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/app/js/phone_list/phone_list.controller.ts', null, 'app/js/phone_list/phone_list.controller.ts')
|
|
|
|
:marked
|
|
And the main module file imports the controller and registers it:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/app/js/phone_list/phone_list.module.ts', null, 'app/js/phone_list/phone_list.module.ts')
|
|
|
|
:marked
|
|
Finally, we can now pull everything together in `app.module.ts`. It here we'll
|
|
import each of the three submodule files and register them as dependencies
|
|
of the main application module:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/app/js/app.module.ts', 'pre-bootstrap', 'app/js/app.module.ts')
|
|
|
|
:marked
|
|
Note that we don't have to repeat the submodule name strings here. Since the
|
|
modules export themselves, we can just refer to the `name` attribute of each
|
|
of them.
|
|
|
|
Before this converted version of the application will run, we need to change the
|
|
way we're bootstrapping it. It is currently bootstrapped using the `ng-app` directive
|
|
attached to the `<html>` element of the host page. This will no longer work because
|
|
`ng-app` is processed when the page loads, and our application code will not
|
|
be available at that point yet. It is loaded asynchronously by SystemJS instead.
|
|
|
|
We should switch to a JavaScript-driven bootstrap instead. As it happens, this is
|
|
also how Angular 2 apps are bootstrapped, so the switch brings us one step closer
|
|
to Angular as well. So, remove the `ng-app` attribute from `index.html`, and add
|
|
this at the end of `app.module.ts`:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/app/js/app.module.ts', 'bootstrap', 'app/js/app.module.ts')
|
|
|
|
:marked
|
|
We now have a fully functional version of the application, all converted
|
|
into TypeScript code and a modern module system! If you start the project HTTP
|
|
server with `npm start`, you should see the fully functional application in
|
|
your browser. On the other hand, if you were to try running the *test suite*,
|
|
things wouldn't look quite that good yet. We also have to make our tests
|
|
support our new module organization.
|
|
|
|
:marked
|
|
## Preparing Tests
|
|
|
|
Our project has both E2E Protractor tests and some Karma unit tests in it.
|
|
Both of those are going to need a bit of work.
|
|
|
|
Of these two, E2E tests are a lot easier to convert. By definition,
|
|
E2E tests access our application from the *outside* by interacting with
|
|
the various UI elements the app puts on the screen. E2E tests aren't really that
|
|
concerned with the internal structure of the application components. That
|
|
also means that although we've modified our project quite a bit, the E2E
|
|
test suite should keep passing just as it was before. We haven't changed
|
|
how the app behaves from the user's point of view.
|
|
|
|
What we *can* do is convert our E2E test code to TypeScript, like we've
|
|
done with the production code. To do this, you can just rename the
|
|
`scenarios.js` file to `scenarios.ts`. After that, you'll want to declare
|
|
the global Protractor variables used in the file, so that the TypeScript
|
|
compiler knows we're accessing them on purpose:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/test/e2e/scenarios.ts', 'declares', 'test/e2e/scenarios.ts')
|
|
|
|
:marked
|
|
Once we add Angular 2 to the project, we'll be able to add more type safety
|
|
to this file, because Angular 2 ships with the type definitions of the
|
|
Protractor framework as well.
|
|
|
|
That pretty much takes care of E2E tests for now. For unit tests we're going
|
|
to do a bit more. What we'll do is convert our existing unit tests to TypeScript
|
|
as well as have them use `import`s to load in the code they need. We'll also need
|
|
to tweak our Karma configuration so that it'll let SystemJS load the application
|
|
files.
|
|
|
|
But first we should have some additional type definitions loaded, so that the TypeScript
|
|
compiler can understand the Jasmine and ngMock APIs we're using in unit tests.
|
|
Add a file called `test_helper.ts` to the test directory and add a reference
|
|
to the Jasmine and mock type definitions we already installed earlier:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/test/test_helper.ts', null, 'test/test_helper.ts')
|
|
|
|
:marked
|
|
For Karma's SystemJS support we'll use a shim file that will tweak the way
|
|
files get loaded, so that it happens through SystemJS:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/test/karma_test_shim.js', null, 'test/karma_test_shim.js')
|
|
|
|
.alert.is-important The shim is likely to be replaced by improved tooling, but is currently needed.
|
|
|
|
:marked
|
|
We'll then update the Karma configuration file, so that it loads SystemJS and the
|
|
shim file. We'll also change how the app and unit tests files themselves are loaded.
|
|
We will *watch* them so that the test suite is triggered when changes occur, but we
|
|
won't have Karma *include* them because that is now done by SystemJS and the shim.
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/test/karma.conf.1.js', 'files', 'test/karma.conf.js')
|
|
|
|
:marked
|
|
Now we have the infrastructure in place and can convert the test files themselves.
|
|
This mainly just consists for changing the file extensions of those files, and adding
|
|
the necessary imports to them.
|
|
|
|
In the checkmark filter spec, we'll import the core module file, so that it is
|
|
available when we load the corresponding Angular module:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/test/unit/checkmark.filter.spec.ts', 'top', 'test/unit/checkmark.filter.spec.ts')
|
|
|
|
:marked
|
|
We'll do the exact same thing for the phone factory spec:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/test/unit/phone.factory.spec.ts', 'top', 'test/unit/phone.factory.spec.ts')
|
|
|
|
:marked
|
|
In the phone detail controller spec, on the other hand, we should import
|
|
the phone detail module:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/test/unit/phone_detail.controller.spec.ts', 'top', 'test/unit/phone_detail.controller.spec.ts')
|
|
|
|
:marked
|
|
Finally, the phone list controller spec should import the phone list
|
|
module:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/test/unit/phone_list.controller.spec.ts', 'top', 'test/unit/phone_list.controller.spec.ts')
|
|
|
|
:marked
|
|
There's one more issue we have in our controller tests, which is that TypeScript
|
|
isn't happy about compiling them at the moment. This is because we're using
|
|
the custom Jasmine matcher `toEqualData` in both of them. Because this is something
|
|
we define ourselves, it isn't included in the Jasmine type definitions that we
|
|
installed using `tsd`.
|
|
|
|
We can add our own little type definition file for that extension, which extends
|
|
the `jasmine.Matchers` interface and adds our custom matcher to it. This will
|
|
satisfy the compiler and let us use our custom matcher while retaining the nice
|
|
type safety features of TypeScript:
|
|
|
|
+makeExample('upgrade/ts/typescript-conversion/test/jasmine_matchers.d.ts', null, 'test/jasmine_matchers.d.ts')
|
|
|
|
:marked
|
|
And now we have a fully functional test suite for our TypeScript-enabled
|
|
application as well.
|
|
|
|
:marked
|
|
## Enjoying The Benefits of TypeScript
|
|
|
|
Now that we have TypeScript, we can start benefiting from some of its
|
|
other features in addition to the imports and exports that we're already using.
|
|
There's a lot of value the language can provide in Angular 1 applications.
|
|
|
|
For one thing, TypeScript is a superset of ES2015. Any app that has previously
|
|
been written in ES5 - like the PhoneCat example has - can with TypeScript
|
|
start incorporating all of the JavaScript features that are new to ES2015.
|
|
These include things like `let`s and `const`s, default function parameters,
|
|
and destructuring assignments.
|
|
|
|
Another thing we can do is start adding *type safety* to our code, by
|
|
adding type annotations. For instance, we can annotate the checkmark
|
|
filter so that it expects booleans as arguments and returns strings.
|
|
This makes it clearer what the filter is supposed to do, and makes it
|
|
possible for the TypeScript compiler to notify us when we're trying to
|
|
use it with incompatible types.
|
|
|
|
+makeExample('upgrade/ts/classes/app/js/core/checkmark.filter.ts', null, 'app/js/core/checkmark.filter.ts', {otl: /(:\w+)/g})
|
|
|
|
.l-sub-section
|
|
:marked
|
|
The [Angular 1.x type definitions](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/angularjs)
|
|
we installed from TSD are not officially maintained by the Angular team,
|
|
but are quite comprehensive. Though we're not going to do it in this
|
|
tutorial, it is possible to make an Angular 1.x application fully
|
|
type-annotated with the help of these definitions.
|
|
|
|
If this is something we wanted to do, it would be a good idea to enable
|
|
the `noImplicitAny` configuration option in `tsconfig.json`. This would
|
|
cause the TypeScript compiler to display a warning when there's any code that
|
|
does not yet have type annotations. We could use it as a guide to inform
|
|
us about how close we are to having a fully annotated project.
|
|
|
|
:marked
|
|
Another TypeScript feature we can make use of is *classes*. In particular, we
|
|
can turn our controllers into classes. That way they'll be a step
|
|
closer to becoming Angular 2 component classes, which will make our life
|
|
easier once we do the upgrade.
|
|
|
|
Angular 1 expects controllers to be constructor functions, and that's what
|
|
ES2015/TypeScript classes really are, and that means we can just register a
|
|
class as a controller and Angular 1 will happily use it. We also won't
|
|
need to make any changes to our test suite as the external behavior of the
|
|
controllers will not change.
|
|
|
|
Here's what our new class for the phone list controller looks like.
|
|
|
|
+makeExample('upgrade/ts/classes/app/js/phone_list/phone_list.controller.ts', null, 'app/js/phone_list/phone_list.controller.ts')
|
|
|
|
:marked
|
|
What was previously done in the controller function is now done in the class
|
|
constructor function. The class additionally declares three members: The
|
|
array of phones, the name of the current sort key, and the search query. These
|
|
are all things we have already been attaching to the controller,
|
|
but that weren't explicitly declared anywhere. The last one of these isn't actually
|
|
used in the TypeScript code since it's only referred to in the template, but for
|
|
the sake of clarity we want to define all the members our controller will have.
|
|
|
|
In the Phone detail controller we'll have two members: One for the phone
|
|
that the user is looking at and another for the URL of the currently displayed image.
|
|
We can additionally introduce a TypeScript interface that explicitly defines
|
|
what we expect the `$routeParams` object to contain when it is
|
|
passed to the controller. This interface is not exported and is just used internally
|
|
inside this module:
|
|
|
|
+makeExample('upgrade/ts/classes/app/js/phone_detail/phone_detail.controller.ts', null, 'app/js/phone_detail/phone_detail.controller.ts')
|
|
|
|
:marked
|
|
This makes our controller code look a lot more like Angular 2 already. We're
|
|
all set to actually introduce Angular 2 into the project.
|
|
|
|
If we had any Angular 1 services in the project, those would also be
|
|
a good candidate for converting to classes, since like controllers,
|
|
they're also constructor functions. But we only have the `Phone` factory
|
|
in this project, and that's a bit special since it's an `ngResource`
|
|
factory. So we won't be doing anything to it in the preparation stage,
|
|
but will instead turn it directly into an Angular 2 service in the
|
|
next section.
|
|
|
|
.l-main-section
|
|
:marked
|
|
## Gradually Upgrading to Angular 2
|
|
|
|
Having completed our preparation work, let's get going with the Angular 2
|
|
upgrade itself. We'll do this incrementally with the help of the `upgrade` module
|
|
that comes with Angular 2. By the time we're done, we can remove Angular 1
|
|
from the project completely, but the key is to do it piece by piece
|
|
without breaking the application.
|
|
|
|
.alert.is-important The project also contains some animations, which we are not yet upgrading in this version of the guide. This will change in a later release.
|
|
|
|
:marked
|
|
Let's install Angular 2 into the project. Add the Angular 2 dependencies
|
|
to `package.json` as described in the package.json appendix of the
|
|
[Quickstart](../quickstart.html).
|
|
Then run:
|
|
|
|
```
|
|
npm i
|
|
```
|
|
|
|
We can then load Angular 2 into the application by adding some `<script>`
|
|
tags to `index.html`. They should go before the `<script>` tag with the
|
|
`System.config()` invocation:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/app/index.html', 'ng2')
|
|
|
|
:marked
|
|
The first two scripts are for adding some ES6 features to older browsers
|
|
that don't natively support them. The last three bring in Angular 2
|
|
itself.
|
|
|
|
While we're at it, let's also load the same files into unit tests by
|
|
updating the Karma config:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/test/karma.conf.1.js', 'ng2', 'test/karma.conf.js', {otl: /(.*\.\.\/node\_modules\/angular2.*)/})
|
|
|
|
.alert.is-important
|
|
:marked
|
|
After installing Angular 2, the TypeScript compiler will complain
|
|
about a clash in the global `$` variable: The jQuery typings used
|
|
by the Angular 1 typings introduce one, and the Protractor typings
|
|
used by Angular 2 introduce another. This will be resolved in a
|
|
later release. See [issue #5459](https://github.com/angular/angular/issues/5459)
|
|
for some workarounds.
|
|
|
|
:marked
|
|
## Bootstrapping Hybrid 1+2 Applications
|
|
|
|
What we'll do next is bootstrap the application as a *hybrid application*
|
|
that supports both Angular 1 and Angular 2 components. Once we've done that
|
|
we can start converting the individual pieces to Angular 2.
|
|
|
|
At this point we need to do add the Angular 2 type definitions
|
|
into `app.ts`, so that the TypeScript compiler knows what we're talking about
|
|
when we use Angular 2 APIs. Unlike with Angular 1, we don't need to install
|
|
or refer to these type definitions in our source code, because Angular 2
|
|
comes with them included. What we do need to do is set the TypeScript
|
|
compiler's `moduleResolution` option to `node`, so that it knows to look
|
|
for these definitions from the `angular2` NPM package.
|
|
|
|
+makeJson('upgrade/ts/ng2_initial/tsconfig.1.json', null, 'tsconfig.json', {otl: /(\"moduleResolution.*)/})
|
|
|
|
:marked
|
|
Angular 2 bundles the Jasmine type definitions we need in tests, which means
|
|
that the Jasmine type definitions we installed with `tsd` are now redundant.
|
|
We might as well remove them so that there's no confusion about which ones
|
|
we're using.
|
|
```
|
|
rm -r typings/jasmine
|
|
```
|
|
Also remove the reference to those type definitions from
|
|
`test/test_helper.ts`.
|
|
|
|
The same also goes for Protractor: Angular 2 comes with the types of the
|
|
Protractor APIs, so it's safe to remove the `declare var` line from
|
|
`scenarios.ts`.
|
|
|
|
:marked
|
|
To boostrap a hybrid application, we first need to initialize an `UpgradeAdapter`.
|
|
This is an object that comes with the `angular2/upgrade` module. It provides the
|
|
glue that joins the two versions of the framework together. It can be used to
|
|
|
|
* Upgrade Angular 1 directives to Angular 2 components
|
|
* Downgrade Angular 2 components to Angular 1 directives
|
|
* Upgrade Angular 1 factories, services, and providers to Angular 2 services
|
|
* Downgrade Angular 2 services to Angular 1 factories
|
|
* Bootstrap and manage hybrid Angular 1+2 applications
|
|
|
|
Together these features allow us to very flexibly mix and match the two
|
|
frameworks in our apps. This flexibility is great because there are many
|
|
different kinds of Angular code out there. Even within a single app,
|
|
we may need different strategies to deal with different kinds of components.
|
|
|
|
Let's import the `UpgradeAdapter` class into `app.module.ts`:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/app/js/app.module.ts', 'adapter-import', 'app/js/app.module.ts')
|
|
|
|
:marked
|
|
We can then make an adapter by instantiating the class:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/app/js/core/upgrade_adapter.ts', 'adapter-init')
|
|
|
|
:marked
|
|
Now we can use that adapter to bootstrap our application as a hybrid.
|
|
Instead of calling `angular.bootstrap`, we must call
|
|
`upgradeAdapter.bootstrap`, but the function arguments remain the same:
|
|
They are still the element that will become the root of the application,
|
|
and the names of the root Angular 1.x modules that we want to include:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/app/js/app.module.ts', 'bootstrap')
|
|
|
|
:marked
|
|
We are now running both Angular 1 and 2 at the same time. That's pretty
|
|
exciting! We're not running any actual Angular 2 components yet though,
|
|
so let's do that next.
|
|
|
|
|
|
:marked
|
|
## Upgrading Services
|
|
|
|
The first piece we'll port over to Angular 2 is the `Phone` factory, which
|
|
resides in `app/js/core/phones.factory.ts` and makes it possible for controllers
|
|
to load phone information from the server. Right now it's implemented with
|
|
ngResource and we're using it for two things:
|
|
|
|
* For loading the list of all phones into the phone list controller
|
|
* For loading the details of a single phone into the phone detail controller.
|
|
|
|
We can replace this implementation with an Angular 2 service class, while
|
|
keeping our controllers in Angular 1 land. In the new version we'll just use
|
|
the the `Http` service from Angular 2 instead of ngResource.
|
|
|
|
The `Http` service isn't included in the main Angular 2 bundle, so we need to
|
|
include it in `index.html` separately. As the service uses RxJS Observables,
|
|
we also need to bring in the RxJS bundle:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/app/index.html', 'ng2-http')
|
|
|
|
:marked
|
|
We'll also do the same in `karma.conf.js` so that `Http` will be available in
|
|
unit tests:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/test/karma.conf.1.js', 'ng2-http')
|
|
|
|
:marked
|
|
Before the `Http` service is available for injection, we still need to register
|
|
it into our application's dependency injector. We should import the `HTTP_PROVIDERS`
|
|
constant in `app.module.ts`:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/app/js/app.module.ts', 'http-import')
|
|
|
|
:marked
|
|
In a regular Angular 2 application we would now pass `HTTP_PROVIDERS` into
|
|
the application bootstrap function. But we can't do that in a hybrid
|
|
application such as the one we're working on. That's because the `bootstrap`
|
|
method of `UpgradeAdapter` expects Angular 1 modules as dependencies,
|
|
not Angular 2 providers.
|
|
|
|
What we must do instead is register `HTTP_PROVIDERS` into the `UpgradeAdapter`
|
|
separately. It has a method called `addProvider` for that purpose:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/app/js/app.module.ts', 'add-http-providers')
|
|
|
|
:marked
|
|
Now we're ready to upgrade the Phones factory itself. We'll put the Angular 2
|
|
implementation in a new file called `Phones.ts` in the core module. It will be a TypeScript
|
|
class decorated as `@Injectable`:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/app/js/core/Phones.ts', 'class', 'app/js/core/Phones.ts')
|
|
|
|
:marked
|
|
Note that with Angular 2 we're switching to a new file naming scheme, and
|
|
won't be using the `feature.type.ts` naming convention anymore.
|
|
|
|
The `@Injectable` decorator will attach some dependency injection metadata
|
|
to the class, letting Angular 2 know about its dependencies. As described
|
|
by our [Dependency Injection Guide](../guide/dependency-injection.html),
|
|
this is a marker decorator we need to use for classes that have no other
|
|
Angular 2 decorators but still need to have their dependencies injected.
|
|
|
|
In its constructor the class expects to get the `Http` service. It will
|
|
be injected to it and it is stored as a private field. The service is then
|
|
used in the two instance methods, one of which loads the list of all phones,
|
|
and the other the details of a particular phone:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/app/js/core/Phones.ts', 'fullclass')
|
|
|
|
:marked
|
|
The methods now return Observables of type `Phone` and `Phone[]`. This is
|
|
a type we don't have yet, so let's add a simple interface for it:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/app/js/core/Phones.ts', 'phone-interface', 'app/js/core/Phones.ts')
|
|
|
|
:marked
|
|
Here's the full, final code for the service:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/app/js/core/Phones.ts', 'full', 'app/js/core/Phones.ts')
|
|
|
|
:marked
|
|
Notice that we're importing the `map` operator of the RxJS `Observable` separately.
|
|
We need to do this for all RxJS operators that we want to use, since Angular 2
|
|
does not load all of them by default.
|
|
|
|
The new `Phones` service now has the same features that the original, ngResource based
|
|
service did. You can remove the old `phones.factory.ts` file. Now we just
|
|
need to register the new service into the application, so that our Angular 1
|
|
controllers will be able to use it.
|
|
|
|
`UpgradeAdapter` has a `downgradeNg2Provider` method for the purpose of making
|
|
Angular 2 services available to Angular 1 code. The problem is that we don't have
|
|
our `UpgradeAdapter` available in `core.module.ts` where the `Phones` service should
|
|
be registered. We only have it in `app.module.ts`. There should only be one
|
|
`UpgradeAdapter` in an application, so we need to find a way to share our
|
|
instance between the two code modules.
|
|
|
|
What we'll do is create a new module that instantiates `UpgradeAdapter`
|
|
and exports the instance. We can then just pull it in wherever we need it,
|
|
so that we're using the same object everywhere. Let's put this new file
|
|
under `core`:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/app/js/core/upgrade_adapter.ts', 'full', 'app/js/core/upgrade_adapter.ts')
|
|
|
|
:marked
|
|
In `app.module.ts` we should now just import this adapter instead of making a separate one:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/app/js/app.module.ts', 'adapter-state-import')
|
|
|
|
:marked
|
|
Also remove the line from `app.module.ts` that is instantiating `UpgradeAdapter`. It's no
|
|
longer needed since we import the instance from elsewhere.
|
|
|
|
We'll then do the same in `core.module.ts` as well. Then we can register the `Phones` service into it.
|
|
While doing that, we can remove the module's dependency to `ngResource`, which
|
|
we're no longer using.
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/app/js/core/core.module.ts', null, 'app/js/core/core.module.ts')
|
|
|
|
:marked
|
|
Note that we actually needed to do two registrations here:
|
|
|
|
1. Register `Phones` as an **Angular 2 provider** with the `addProvider`
|
|
method. That's the same method that we used earlier for `HTTP_PROVIDERS`.
|
|
2. Register an **Angular 1 factory** called `phones`, which will be a *downgraded*
|
|
version of the `Phones` service.
|
|
|
|
At this point we can switch our two controllers to use the new service
|
|
instead of the old one. We `$inject` it as the downgraded `phones` factory,
|
|
but it's really an instance of the `Phones` class and we can annotate its type
|
|
accordingly:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/app/js/phone_detail/phone_detail.controller.ts', null, 'app/js/phone_detail/phone_detail.controller.ts')
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/app/js/phone_list/phone_list.controller.ts', null, 'app/js/phone_list/phone_list.controller.ts')
|
|
|
|
:marked
|
|
What we have here are two Angular 1 controllers using an Angular 2 service!
|
|
The controllers don't need to be aware of this, though the fact that the
|
|
service returns Observables and not Promises is a bit of a giveaway.
|
|
In any case, what we've achieved is a migration of a service to Angular 2
|
|
without having to yet migrate the controllers that use it.
|
|
|
|
To bring our test suite up to speed with the changes, we should first enable
|
|
the Angular 2 test support library in our unit test suite. We first need to
|
|
add the angular `testing` bundle to list of files that Karma is loading:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/test/karma.conf.1.js', 'ng2-testing')
|
|
|
|
:marked
|
|
Then we'll update the Karma test shim. It should load the Angular 2
|
|
browser adapter before getting to the spec files:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/test/karma_test_shim.js', null, 'test/karma_test_shim.js')
|
|
|
|
.alert.is-important The shim is likely to be replaced by improved tooling, but is needed right now.
|
|
|
|
:marked
|
|
Now, let's look at the tests for the service itself. What we used to have in
|
|
`phones_factory_spec.js` was a fairly simple test that simply checks if
|
|
the factory exists and is available for injection. We can now do that same
|
|
test in Angular 2. Rename `phones.factory.spec.ts` to `Phones.spec.ts` and
|
|
set the contents as follows:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/test/unit/Phones.spec.ts', null, 'test/unit/Phones.spec.ts')
|
|
|
|
:marked
|
|
Here we first load the `Phones` provider and then test that an instance of
|
|
`Phones` can in fact be injected. We also need to load `HTTP_PROVIDERS` since
|
|
it is a dependency of `Phones`.
|
|
|
|
For the controller tests, we can first of all at this point get rid of the
|
|
custom `toEqualData` custom matcher. It was added because `ngResource` attaches
|
|
attributes to the data that we don't want to compare in tests. We're no longer
|
|
using `ngResource`, so we can simply use the built-in `toEqual` for comparisons.
|
|
This means we can remove the `test/jasmine_matchers.d.ts` file at this point.
|
|
|
|
Now, in the phone detail controller we have been testing that the phone details
|
|
with the id given in the route params are fetched over HTTP and put on the
|
|
scope. We can continue doing that, but we'll need to change the structure of the
|
|
test a bit. Instead of using the Angular 1 mock HTTP backend, we'll just mock out
|
|
the `get` method of the `Phones` service, which is what the controller is now
|
|
using to load what it needs. As the mocked value, we're returning an Observable
|
|
that will emit a single value - the mock phone data:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/test/unit/phone_detail.controller.spec.ts', null, 'test/unit/phone_detail.controller.spec.ts')
|
|
|
|
.alert.is-important
|
|
:marked
|
|
We're doing a manual `Phones` instantiation because hybrid apps can't be
|
|
bootstrapped for unit tests at the moment, which means that Angular 2
|
|
dependencies can't be made available. This is likely to change.
|
|
|
|
:marked
|
|
In the phone list controller we'll do something very similar: We mock out the `query`
|
|
method of the `Phones` service, and check that the controller makes the resulting
|
|
value available:
|
|
|
|
+makeExample('upgrade/ts/ng2_initial/test/unit/phone_list.controller.spec.ts', null, 'test/unit/phone_list.controller.spec.ts')
|
|
|
|
:marked
|
|
## Upgrading Controllers to Components
|
|
|
|
Next, let's upgrade our Angular 1 controllers to Angular 2 components. We'll
|
|
do it one at a time, while still keeping the application in hybrid mode.
|
|
As we make these conversions, we'll also be defining our first Angular 2 *pipes*.
|
|
|
|
Let's look at the phone list controller first. Right now it is a TypeScript class,
|
|
which is paired with an HTML template by the route configuration in `app.ts`.
|
|
We'll be turning it into an Angular 2 component.
|
|
|
|
Rename `phone_list.controller.ts` to `PhoneList.ts`. Then rename the controller class
|
|
inside to just `PhoneList` and decorate it as a `@Component`:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/phone_list/PhoneList_without_pipes.ts', 'top', 'app/js/phone_list/PhoneList.ts')
|
|
|
|
:marked
|
|
The `selector` attribute is a CSS selector that defines where on the page the component
|
|
should go. It will match elements by the name of `pc-phone-list`. It is a good idea
|
|
to always use application-specific prefixes in selectors so that they never clash with
|
|
built-in ones, and here we're using `pc-`, which is short for "PhoneCat".
|
|
|
|
The `templateUrl` defines the location of the component template. It points to our existing
|
|
template file
|
|
|
|
Both of these attributes are things that were defined *externally* for the controller,
|
|
but for the component are things that it defines *itself*. This will affect how we use
|
|
the component in the router.
|
|
|
|
:marked
|
|
We now also need to convert the template of this component into Angular 2 syntax.
|
|
In the search controls we need to use Angular 2 syntax for the two `ngModel`s
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/phone_list/phone_list_without_pipes.html', 'controls', 'app/js/phone_list/phone_list.html')
|
|
|
|
:marked
|
|
In the list we need to replace the `ng-repeat` with an `*ngFor` and its
|
|
`#var of iterable` syntax, as described in our
|
|
[Template Syntax guide](../guide/template-syntax.html).
|
|
|
|
For the images, we can replace `ng-src` with the standard `src`, but will use a
|
|
property binding. Note that we're also adding a `name` CSS class for the phone name.
|
|
This is something we'll need for our Protractor tests:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/phone_list/phone_list_without_pipes.html', 'list', 'app/js/phone_list/phone_list.html')
|
|
|
|
:marked
|
|
In the module file we're going to plug this component into our application. Instead
|
|
of registering a controller, we register a `pcPhoneList` directive.
|
|
The directive is a downgraded version of our component, and the `UpgradeAdapter`
|
|
handles the bridging between the two:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/phone_list/phone_list.module.ts', null, 'app/js/phone_list/phone_list.module.ts')
|
|
|
|
:marked
|
|
The `<angular.IDirectiveFactory>` type annotation here is to let the TypeScript compiler
|
|
know that the return value of the downgrade method call will be something that can be
|
|
used as a directive factory.
|
|
|
|
To complete the switch, we should change our route configuration in `app.module.ts`.
|
|
Instead of using the controller and template, it can just instantiate our component.
|
|
We can do that by using a simple template that uses the directive
|
|
we just registered:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/app.module.ts', 'list-route')
|
|
|
|
:marked
|
|
When the application runs, the Angular 1.x directive compiler will match
|
|
the element in the template to the `pcPhoneList` directive, which is actually
|
|
an Angular 2 component!
|
|
|
|
The remaining issue with the phone list is the use of filters in its
|
|
template: It is referring to the `filter` filter and the `orderBy` filter,
|
|
and relying on them to filter and sort the phone list, respectively.
|
|
These pipes do not exist in Angular 2, so we're going to need to do
|
|
the filtering and sorting ourselves. Let's define a couple of pipes that
|
|
get the job done.
|
|
|
|
.alert.is-helpful
|
|
:marked
|
|
If you want to learn more about how pipes in Angular 2
|
|
work, we have [a whole guide on the subject](../guide/pipes.html)
|
|
available!
|
|
|
|
:marked
|
|
For filtering, we'll have a pipe called `PhoneFilterPipe`. It works like
|
|
the `filter` filter in Angular 1 in that it filters a collection of objects,
|
|
matching properties within the objects. But, as opposed to `filter`,
|
|
this pipe is specialized to filter `Phone` objects and we can use
|
|
type annotations to make this explicit:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/phone_list/PhoneFilterPipe.ts', null, 'app/js/phone_list/PhoneFilterPipe.ts')
|
|
|
|
:marked
|
|
Since we're adding new code, it's a good idea to add some unit tests for
|
|
it too. Here are a few tests for `PhoneFilterPipe`:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/test/unit/PhoneFilterPipe.spec.ts', null, 'test/unit/PhoneFilterPipe.spec.ts')
|
|
|
|
:marked
|
|
For sorting, we'll use a more generic pipe, just called `OrderBy`. It
|
|
takes an array of objects, and a property to order the array by. It returns
|
|
an array of the same type of thing it was given. In the implementation we
|
|
copy the input array, sort the copy, and return it.
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/phone_list/OrderByPipe.ts', null, 'app/js/phone_list/OrderByPipe.ts')
|
|
|
|
:marked
|
|
Here's a unit test for `OrderByPipe` as well:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/test/unit/OrderByPipe.spec.ts', null, 'test/unit/OrderByPipe.spec.ts')
|
|
|
|
:marked
|
|
We can now integrate these new pipes with our component. Before the pipes
|
|
are available there, we need to declare them in the `@Component` decorator.
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/phone_list/PhoneList.ts', 'top', 'app/js/phone_list/PhoneList.ts')
|
|
|
|
:marked
|
|
In the template we need to use the `phoneFilter` pipe instead of `filter`.
|
|
No changes are needed for the `orderBy`
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/phone_list/phone_list_without_async.html', 'list', 'app/js/phone_list/phone_list.html')
|
|
|
|
:marked
|
|
Now that the phone list is an Angular 2 component, there's one more neat trick
|
|
we can apply to make its code a little bit simpler. Earlier, as we upgraded
|
|
the `Phones` service, we needed to add a `subscribe` callback to the list
|
|
response, which populated the `phones` array on the component.
|
|
With Angular 2, we can instead just put the Observable itself on the
|
|
component, and can skip the subscription callback:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/phone_list/PhoneList.ts', 'full', 'app/js/phone_list/PhoneList.ts')
|
|
|
|
:marked
|
|
This is made possible by the `async` pipe, which we can apply in the template.
|
|
It knows how to turn an Observable to the (latest) value it has emitted:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/phone_list/phone_list.html', 'list', 'app/js/phone_list/phone_list.html')
|
|
|
|
:marked
|
|
That takes care of the phone list. Here's the updated unit test file for
|
|
that component to complete the migration:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/test/unit/PhoneList.spec.ts', null, 'test/unit/PhoneList.spec.ts')
|
|
|
|
:marked
|
|
Before this test will run, we'll need to augment our Karma configuration
|
|
so that component HTML templates are loaded properly. We didn't need them
|
|
before when we were testing the controller in isolation, but our new test
|
|
exercises the component as a whole, which includes the template.
|
|
|
|
+makeExample('upgrade/ts/ng2_components/test/karma.conf.1.js', 'html', 'test/karma.conf.js')
|
|
|
|
:marked
|
|
Now we can start looking at our other controller, which is the one for
|
|
the phone details. Rename `phone_detail.controller.ts` to `PhoneDetail.ts`, and set the
|
|
contents as follows:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/phone_detail/PhoneDetail_without_pipes.ts', null, 'app/js/phone_detail/PhoneDetail.ts')
|
|
|
|
:marked
|
|
This is pretty similar to what we did with the phone list. The one new change
|
|
here is the use of `@Inject` for the `$routeParams` dependency. It tells the
|
|
Angular 2 injector what this dependency should map to. We have a dependency called
|
|
`$routeParams` in the Angular 1 injector, where it is provided by the Angular 1 router.
|
|
That is what we were already using when `PhoneDetails` was still an Angular 1 controller.
|
|
The things is though, Angular 1 dependencies are not made automatically available to
|
|
Angular 2 components, so if we were to run this now, it would not work.
|
|
|
|
We explicitly need to tell the `UpgradeAdapter` to upgrade `$routeParams` so that
|
|
it is available for injection in Angular 2. We can do it in `app.module.ts`:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/app.module.ts', 'upgrade-route-params', 'app/js/app.module.ts')
|
|
|
|
|
|
|
|
:marked
|
|
We now also need to convert the template of this component into Angular 2 syntax.
|
|
Here is the new template in its entirety:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/phone_detail/phone_detail.html', null, 'app/js/phone_detail/phone_detail.html')
|
|
|
|
:marked
|
|
There are several notable changes here:
|
|
|
|
* We've removed the `vm.` prefix from all expressions.
|
|
* Just like we did in the phone list, we've replaced `ng-src` with property
|
|
bindings for the standard `src`.
|
|
* We're using the property binding syntax around `ng-class`. Though Angular 2
|
|
does have a very similar `ngClass` as Angular 1 does, its value is not
|
|
magically evaluated as an expression. In Angular 2 we always specify
|
|
in the template when an attribute's value is a property expression, as opposed
|
|
to a literal string.
|
|
* We've replaced `ng-repeat`s with `*ngFor`s.
|
|
* We've replaced `ng-click` with an event binding for the standard `click`.
|
|
* In all references to `phone`, we're using the elvis operator `?.` for
|
|
safe property navigation. We need it because when the component first loads,
|
|
we don't have `phone` yet and the expressions will refer to a non-existing
|
|
value. Unlike in Angular 1, Angular 2 expressions do not fail silently when
|
|
we try to refer to properties on undefined objects. We need to be explicit
|
|
about cases where this is expected.
|
|
|
|
:marked
|
|
In the module file we'll now register a `pcPhoneDetail` directive instead of a
|
|
controller. The directive is a downgraded version of the `PhoneDetail` component.
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/phone_detail/phone_detail.module.ts', null, 'app/js/phone_detail/phone_detail.module.ts')
|
|
|
|
:marked
|
|
In the router configuration in `app.module.ts`, we'll switch the details route to
|
|
instantiate a component as well:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/app.module.ts', 'detail-route')
|
|
|
|
:marked
|
|
There's one additional step we need to take, which is to upgrade the
|
|
`checkmark` filter that the template is using. We need an Angular 2
|
|
pipe instead of an Angular 1 filter.
|
|
|
|
While there is no upgrade method in the upgrade adapter for filters, we
|
|
can just transform the filter function into a class that fulfills
|
|
the contract for Angular 2 Pipes. The implementation is the same as before.
|
|
It just comes in a different kind of package. While changing it, also
|
|
rename the file to `CheckmarkPipe.ts`:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/core/CheckmarkPipe.ts', null, 'app/js/core/CheckmarkPipe.ts')
|
|
|
|
:marked
|
|
As we apply this change, we should also remove the registration of the filter
|
|
from the core module file. The module's content becomes:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/core/core.module.ts', null, 'app/js/core/core.module.ts')
|
|
|
|
:marked
|
|
The unit test file for the filter also now becomes the unit test filter
|
|
for the pipe. While we're still testing the same thing, we need to change
|
|
how we set things up:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/test/unit/CheckmarkPipe.spec.ts', null, 'test/unit/CheckmarkPipe.spec.ts')
|
|
|
|
:marked
|
|
In the component we should now import and declare our newly created pipe:
|
|
|
|
+makeExample('upgrade/ts/ng2_components/app/js/phone_detail/PhoneDetail.ts', 'top', 'app/js/phone_detail/PhoneDetail.ts')
|
|
|
|
:marked
|
|
With the phone detail component now migrated as well, we can go and migrate
|
|
its unit tests too.
|
|
|
|
+makeExample('upgrade/ts/ng2_components/test/unit/PhoneDetail.spec.ts', null, 'test/unit/PhoneDetail.spec.ts')
|
|
|
|
:marked
|
|
As we discussed earlier, Protractor tests should largely remain functional
|
|
as we are making changes, since we're not really changing the user-visible
|
|
behavior of the application. Now that we've migrated some components and
|
|
their templates, however, there are a few changes we need to make. Apply
|
|
the following replacements to `scenarios.ts`:
|
|
|
|
table
|
|
tr
|
|
th Previous code
|
|
th New code
|
|
th Notes
|
|
tr
|
|
td
|
|
:marked
|
|
`by.repeater('phone in vm.phones').column('phone.name')`
|
|
td
|
|
:marked
|
|
`by.css('.phones .name')`
|
|
td
|
|
:marked
|
|
The repeater matcher relies on Angular 1 `ng-repeat`
|
|
tr
|
|
td
|
|
:marked
|
|
`by.repeater('phone in vm.phones')`
|
|
td
|
|
:marked
|
|
`by.css('.phones li')`
|
|
td
|
|
:marked
|
|
The repeater matcher relies on Angular 1 `ng-repeat`
|
|
tr
|
|
td
|
|
:marked
|
|
`by.model('vm.query')`
|
|
td
|
|
:marked
|
|
`by.css('input')`
|
|
td
|
|
:marked
|
|
The model matcher relies on Angular 1 `ng-model`
|
|
tr
|
|
td
|
|
:marked
|
|
`by.model('vm.orderProp')`
|
|
td
|
|
:marked
|
|
`by.css('select')`
|
|
td
|
|
:marked
|
|
The model matcher relies on Angular 1 `ng-model`
|
|
tr
|
|
td
|
|
:marked
|
|
`by.binding('vm.phone.name')`
|
|
td
|
|
:marked
|
|
`by.css('h1')`
|
|
td
|
|
:marked
|
|
The binding matcher relies on Angular 1 data binding
|
|
tr
|
|
td
|
|
:marked
|
|
`li:nth-child(1)` and `li:nth-child(3)`
|
|
td
|
|
:marked
|
|
`li:nth-of-type(1)` and `li:nth-of-type(3)`
|
|
td
|
|
:marked
|
|
Angular 2 may inject empty `<script>` tags to the page for its internal purposes so we should not rely on the number of siblings being predictable.
|
|
|
|
:marked
|
|
## Switching To The Angular 2 Router And Bootstrap
|
|
|
|
At this point we've replaced all our Angular 1 application components with
|
|
their Angular 2 counterparts. The application is still bootstrapped as a hybrid,
|
|
but there isn't really any need for that anymore, and we can begin to
|
|
pull out the last remnants of Angular 1.
|
|
|
|
There are just two more things to do: We need to switch the router to
|
|
the Angular 2 one, and then bootstrap the app as a pure Angular 2 app.
|
|
|
|
Let's do the routing part first. Angular 2 comes with a shiny new router,
|
|
but it isn't included by default. Just like we did with `Http`, we need to
|
|
include it in `index.html` before the `System.config()` script first:
|
|
|
|
+makeExample('upgrade/ts/ng2_final/app/index.html', 'ng2-router', 'app/index.html')
|
|
|
|
:marked
|
|
In the main app module we need to import a few things from the router module:
|
|
|
|
+makeExample('upgrade/ts/ng2_final/app/js/app.ts', 'router-import', 'app/js/app.module.ts')
|
|
|
|
:marked
|
|
Angular 2 applications all come with a *root component*, which, among other
|
|
things, is where we should plug in the router. We don't yet have such a root
|
|
component, because our app is still managed as an Angular 1 app.
|
|
Let's change this now and add an `AppComponent` class, which replaces the
|
|
`configure` function in `app.module.ts`:
|
|
|
|
+makeExample('upgrade/ts/ng2_final/app/js/app.ts', 'appcomponent')
|
|
|
|
:marked
|
|
This is a component that plugs in to an `<pc-app>` element on the page,
|
|
and has a simple template that only includes the router outlet component
|
|
of the Angular router. This means that the component just renders the contents
|
|
of the current route and nothing else. The `@RouteConfig` decorator defines
|
|
the Angular 2 counterparts of our two routes. They refer directly to the
|
|
two components.
|
|
|
|
We should put this `<pc-app>` element in the HTML so that the root component
|
|
has something to attach to. It replaces the old Angular 1 `ng-view` directive:
|
|
|
|
+makeExample('upgrade/ts/ng2_final/app/index.html', 'body', 'app/index.html')
|
|
|
|
:marked
|
|
In the `PhoneDetail` component we now need to change how the phone id parameter
|
|
is received. There will be no more `$routeParams` injection available, because
|
|
that comes from the Angular 1 router. Instead, what we have is a `RouteParams`
|
|
object provided by the Angular 2 router. We use it to obtain the `phoneId` from
|
|
the params:
|
|
|
|
+makeExample('upgrade/ts/ng2_final/app/js/phone_detail/PhoneDetail.ts', null, 'app/js/phone_detail/PhoneDetail.ts')
|
|
|
|
:marked
|
|
We should also make the corresponding change in the unit test. We provide
|
|
an instance of the `RouteParams` class instead of the `$routeParams` object:
|
|
|
|
+makeExample('upgrade/ts/ng2_final/test/unit/PhoneDetail.spec.ts', 'routeparams', 'test/unit/PhoneDetail.spec.ts')
|
|
|
|
:marked
|
|
With that, we're ready to switch the bootstrap method of the application from that
|
|
of the `UpgradeAdapter` to the main Angular 2 `bootstrap`. Let's import it together
|
|
with a couple of other things in `app.module.ts`
|
|
|
|
+makeExample('upgrade/ts/ng2_final/app/js/app.ts', 'importbootstrap')
|
|
|
|
:marked
|
|
We'll now use the regular Angular 2 `bootstrap` function to bootstrap the app
|
|
instead of using `UpgradeAdapter`. The first argument to `bootstrap` is the
|
|
application's root component `AppComponent`, and the second
|
|
is an array of the Angular 2 providers that we want to make available for
|
|
injection. In that array we include all the things we have been registering
|
|
with `upgradeAdapter.addProvider` until now, as well as the providers and
|
|
directives of the router:
|
|
|
|
+makeExample('upgrade/ts/ng2_final/app/js/app.ts', 'bootstrap')
|
|
|
|
:marked
|
|
We are now running a pure Angular 2 application!
|
|
|
|
But there's actually one more cool thing we can do with the new router.
|
|
We no longer have to hardcode the links to phone details from the phone
|
|
list, because the Angular 2 router is able to generate them for us with
|
|
its `routerLink` directive. We just need to refer to the route names we
|
|
used in the `@RouteConfig`:
|
|
|
|
+makeExample('upgrade/ts/ng2_final/app/js/phone_list/phone_list.html', 'list', 'app/js/phone_list/phone_list.html')
|
|
|
|
:marked
|
|
For this to work the directive just needs to be declared in the component:
|
|
|
|
+makeExample('upgrade/ts/ng2_final/app/js/phone_list/PhoneList.ts', 'top')
|
|
|
|
:marked
|
|
To bring our Protractor test suite up to speed with the latest changes,
|
|
there are a few remaining things we need to do. Firstly, now that we're
|
|
no longer running Angular 1 at all, we should let Protractor know it
|
|
should not be looking for one but instead find *Angular 2 apps* from
|
|
the page. Add the following configuration option to `protractor-conf.js`:
|
|
|
|
+makeExample('upgrade/ts/ng2_final/test/protractor-conf.js', 'ng2')
|
|
|
|
:marked
|
|
Also, there are a couple of Protractor API calls in our test code that
|
|
are using the Angular 1 `$location` service under the hood. As that
|
|
service is no longer there, we need to replace those calls with ones
|
|
that use WebDriver's generic URL APIs instead. The first of these is
|
|
the redirection spec:
|
|
|
|
+makeExample('upgrade/ts/ng2_final/test/e2e/scenarios.ts', 'redirect')
|
|
|
|
:marked
|
|
And the second is the phone links spec:
|
|
|
|
+makeExample('upgrade/ts/ng2_final/test/e2e/scenarios.ts', 'links')
|
|
|
|
:marked
|
|
Now our E2E test suite is passing too, and we're ready to remove
|
|
Angular 1 from the project!
|
|
|
|
:marked
|
|
## Saying Goodbye to Angular 1
|
|
|
|
It is time to take off the training wheels and let our application begin
|
|
its new life as a pure, shiny Angular 2 app. The remaining tasks all have to
|
|
do with removing code - which of course is every programmer's favorite task!
|
|
|
|
First, rename `app.module.ts` to `app.ts`. It will no longer be setting up
|
|
an Angular 1 module, so it doesn't really make sense to call it a module.
|
|
Then remove all references to the `UpgradeAdapter` from `app.ts`. Also remove
|
|
the Angular 1 bootstrap code, type definitions, and the imports of the `core`,
|
|
`phoneList`, and `phoneDetail` modules. Instead import the `PhoneList` and `PhoneDetail`
|
|
components directly - they are needed in the route configuration.
|
|
|
|
When you're done, this is what `app.ts` should look like:
|
|
|
|
+makeExample('upgrade/ts/ng2_final/app/js/app.ts', null, 'app/js/app.ts')
|
|
|
|
:marked
|
|
You may also completely remove the following files. They are Angular 1
|
|
module configuration files and type definition files, and not required
|
|
in Angular 2:
|
|
|
|
* `app/js/core/core.module.ts` (also `app/js/core/upgrade_adapter.ts`)
|
|
* `app/js/phone_detail/phone_detail.module.ts`
|
|
* `app/js/phone_list/phone_list.module.ts`
|
|
* `test/test_helper.ts`
|
|
|
|
Finally, from `index.html` and `karma.conf.js`, remove all references to
|
|
Angular 1 scripts as well as jQuery. When you're done, this is what `index.html`
|
|
should look like:
|
|
|
|
+makeExample('upgrade/ts/ng2_final/app/index.html', null, 'app/index.html')
|
|
|
|
:marked
|
|
And this is what `karma.conf.js` should look like:
|
|
|
|
+makeExample('upgrade/ts/ng2_final/test/karma.conf.1.js', null, 'test/karma.conf.js')
|
|
|
|
:marked
|
|
That is the last we'll see of Angular 1! It has served us well but now
|
|
it's time to say goodbye.
|