2227 lines
109 KiB
Plaintext
2227 lines
109 KiB
Plaintext
include ../_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 because Angular 2
|
||
comes with built-in tools for migrating Angular 1 projects over to the
|
||
Angular 2 platform.
|
||
|
||
已经有了一个Angular 1的程序并不表示我们就不能喜欢Angular 2提供的一切。
|
||
这是因为Angular 2带来了一些内建工具帮助我们把Angular 1的项目迁移到Angular 2平台。
|
||
|
||
Some applications will be easier to upgrade than others, and there are
|
||
ways in which we can make it easier for ourselves. It is possible to
|
||
prepare and align Angular 1 applications with Angular 2 even before beginning
|
||
the upgrade process. These preparation steps are all about making the code
|
||
more decoupled, more maintainable, and up to speed with modern development
|
||
tools. That means the preparation work will not only make the eventual upgrade
|
||
easier, but will also generally improve our Angular 1 applications.
|
||
|
||
有些应用可能比其它的升级起来简单,还有一些方法能让我们自己把这项工作变得更简单。
|
||
即使在正式开始升级过程之前,让Angular 1的程序提前做些准备,并向Angular 2看齐也是有可能的。
|
||
这些准备步骤几乎都是关于如何让代码更加松耦合、更有可维护性,以及用现代开发工具提高速度的。
|
||
这意味着,这种准备工作不仅能让最终的升级变得更简单,而且还能提升Angular 1程序的质量。
|
||
|
||
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
|
||
business, because the work can be done collaboratively and spread over
|
||
a period of time. The `upgrade` module in Angular 2 has been designed to
|
||
make incremental upgrading seamless.
|
||
|
||
成功升级的关键之一是增量式的实现它,通过在同一个应用中一起运行这两个框架,并且逐个把Angular 1的组件迁移到Angular 2中。
|
||
这意味着可以在不必打断其他业务的前提下,升级更大、更复杂的应用程序,因为这项工作可以多人协作完成,并能在一段时间内逐渐铺开。
|
||
Angular 2 `upgrade`模块的设计目标就是让你渐进、无缝的完成升级。
|
||
|
||
1. [Preparation](#preparation)
|
||
1. [准备工作](#preparation)
|
||
1. [Following The Angular Style Guide](#following-the-angular-style-guide)
|
||
1. [遵循Angular风格指南](#following-the-angular-style-guide)
|
||
2. [Using a Module Loader](#using-a-module-loader)
|
||
2. [使用模块加载器](#using-a-module-loader)
|
||
3. [Migrating to TypeScript](#migrating-to-typescript)
|
||
3. [迁移到TypeScript](#migrating-to-typescript)
|
||
4. [Using Component Directives](#using-component-directives)
|
||
4. [使用组件型指令](#using-component-directives)
|
||
2. [Upgrading with The Upgrade Adapter](#upgrading-with-the-upgrade-adapter)
|
||
2. [通过升级适配器进行升级](#upgrading-with-the-upgrade-adapter)
|
||
1. [How The Upgrade Adapter Works](#how-the-upgrade-adapter-works)
|
||
1. [升级适配器如何工作](#how-the-upgrade-adapter-works)
|
||
2. [Bootstrapping Hybrid Angular 1+2 Applications](#bootstrapping-hybrid-angular-1-2-applications)
|
||
2. [引导Angular 1和2的混合(hybrid)应用](#bootstrapping-hybrid-angular-1-2-applications)
|
||
3. [Using Angular 2 Components from Angular 1 Code](#using-angular-2-components-from-angular-1-code)
|
||
3. [从Angular 1的代码中使用Angular 2的组件](#using-angular-2-components-from-angular-1-code)
|
||
4. [Using Angular 1 Component Directives from Angular 2 Code](#using-angular-1-component-directives-from-angular-2-code)
|
||
4. [从Angular 2的代码中使用Angular 1的组件](#using-angular-1-component-directives-from-angular-2-code)
|
||
5. [Projecting Angular 1 Content into Angular 2 Components](#projecting-angular-1-content-into-angular-2-components)
|
||
5. [把Angular 1的内容投影(project)进Angular 2组件中](#projecting-angular-1-content-into-angular-2-components)
|
||
6. [Transcluding Angular 2 Content into Angular 1 Component Directives](#transcluding-angular-2-content-into-angular-1-component-directives)
|
||
6. [把Angular 2的内容透传(transclude)到Angular 1的组件型指令中](#transcluding-angular-2-content-into-angular-1-component-directives)
|
||
7. [Making Angular 1 Dependencies Injectable to Angular 2](#making-angular-1-dependencies-injectable-to-angular-2)
|
||
7. [让Angular 1提供的依赖可以被注入到Angular 2](#making-angular-1-dependencies-injectable-to-angular-2)
|
||
8. [Making Angular 2 Dependencies Injectable to Angular 1](#making-angular-2-dependencies-injectable-to-angular-1)
|
||
8. [让Angular 2提供的依赖可以被注入到Angular 1](#making-angular-2-dependencies-injectable-to-angular-1)
|
||
3. [PhoneCat Preparation Tutorial](#phonecat-preparation-tutorial)
|
||
3. [PhoneCat准备工作教程](#phonecat-preparation-tutorial)
|
||
1. [Switching to TypeScript And Module Loading](#switching-to-typescript-and-module-loading)
|
||
1. [切换到TypeScript和模块加载器](#switching-to-typescript-and-module-loading)
|
||
2. [Preparing Unit and E2E Tests](#preparing-unit-and-e2e-tests)
|
||
2. [准备单元测试和E2E测试](#preparing-unit-and-e2e-tests)
|
||
3. [Enjoying The Benefits of TypeScript](#enjoying-the-benefits-of-typescript)
|
||
3. [享受TypeScript带来的好处](#enjoying-the-benefits-of-typescript)
|
||
|
||
4. [PhoneCat Upgrade Tutorial](#phonecat-upgrade-tutorial)
|
||
4. [PhoneCat升级教程](#phonecat-upgrade-tutorial)
|
||
1. [Bootstrapping A Hybrid 1+2 PhoneCat](#bootstrapping-a-hybrid-1-2-phonecat)
|
||
1. [引导Angular 1+2的混合版PhoneCat](#bootstrapping-a-hybrid-1-2-phonecat)
|
||
2. [Upgrading the Phone factory](#upgrading-the-phone-factory)
|
||
2. [升级Phone工厂](#upgrading-the-phone-factory)
|
||
3. [Upgrading Controllers to Components](#upgrading-controllers-to-components)
|
||
3. [把控制器升级为组件](#upgrading-controllers-to-components)
|
||
4. [Switching To The Angular 2 Router And Bootstrap](#switching-to-the-angular-2-router-and-bootstrap)
|
||
4. [切换到Angular 2的路由器并引导](#switching-to-the-angular-2-router-and-bootstrap)
|
||
5. [Saying Goodbye to Angular 1](#saying-goodbye-to-angular-1)
|
||
5. [再见,Angular 1!](#saying-goodbye-to-angular-1)
|
||
|
||
.l-main-section
|
||
:marked
|
||
# Preparation
|
||
# 准备工作
|
||
|
||
There are many ways to structure Angular 1 applications. When we begin
|
||
to upgrade these applications to Angular 2, some will turn out to be
|
||
much more easy to work with than others. There are a few key techniques
|
||
and patterns that we can apply to future proof our apps even before we
|
||
begin the migration.
|
||
|
||
Angular 1应用程序的组织方式有很多种。当我们想把它们升级到Angular 2的时候,
|
||
有些做起来会比其它的更容易些。即使在我们开始升级之前,也有一些关键的技术和模式可以让我们将来升级时更轻松。
|
||
|
||
## Following The Angular Style Guide
|
||
## 遵循Angular风格指南
|
||
|
||
The [Angular Style Guide](https://github.com/johnpapa/angular-styleguide)
|
||
collects patterns and practices that have been proven to result in
|
||
cleaner and more maintainable Angular 1 applications. It contains a wealth
|
||
of information about how to write and organize Angular code - and equally
|
||
importantly - how **not** to write and organize Angular code.
|
||
|
||
[Angular风格指南](https://github.com/johnpapa/angular-styleguide)收集了一些
|
||
已证明能写出干净且可维护的Angular 1程序的模式与实践。
|
||
它包含了很多关于如何书写和组织Angular代码的有价值信息,同样重要的是,**不应该**如何书写和组织Angular代码。
|
||
|
||
Angular 2 is a reimagined version of the best parts of Angular 1. In that
|
||
sense, its goals are the same as the Angular Style Guide's: To preserve
|
||
the good parts of Angular 1, and to avoid the bad parts. There's a lot
|
||
more to Angular 2 than just that of course, but this does mean that
|
||
*following the style guide helps make your Angular 1 app more closely
|
||
aligned with Angular 2*.
|
||
|
||
Angular 2是一个基于Angular 1中最好的部分构思出来的版本。在这种意义上,它的目标和Angular风格指南是一样的:
|
||
保留Angular 1中好的部分,去掉坏的部分。当然,Angular 2还做了更多。
|
||
说这些的意思是:*遵循这个风格指南可以让你写出的Angular 1程序更接近Angular 2程序*。
|
||
|
||
There are a few rules in particular that will make it much easier to do
|
||
*an incremental upgrade* using the Angular 2 `upgrade` module:
|
||
|
||
特别是某些规则会让使用Angular 2的`upgrade`模块进行*增量升级*变得更简单:
|
||
|
||
* The [Rule of 1](https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md#single-responsibility)
|
||
states that there should be one component per file. This not only makes
|
||
components easy to navigate and find, but will also allow us to migrate
|
||
them between languages and frameworks one at a time. In this example application,
|
||
each controller, factory, and filter is in its own source file.
|
||
|
||
* [规则1](https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md#single-responsibility)
|
||
规定应该每个文件中放一个组件。这不仅让组件更容易浏览和查找,而且还将允许我们逐个迁移它们的语言和框架。
|
||
在这个范例程序中,每个控制器、工厂和过滤器都在它自己的源文件中。
|
||
|
||
* The [Folders-by-Feature Structure](https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md#folders-by-feature-structure)
|
||
and [Modularity](https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md#modularity)
|
||
rules define similar principles on a higher level of abstraction: Different parts of the
|
||
application should reside in different directories and Angular modules.
|
||
|
||
* [按特性分目录的结构](https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md#folders-by-feature-structure)
|
||
和[模块化](https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md#modularity)
|
||
规则在较高的抽象层定义了一些相似的原则:应用程序中的不同部分应该被分到不同的目录和Angular模块中。
|
||
|
||
When an application is laid out feature per feature in this way, it can also be
|
||
migrated one feature at a time. For applications that don't already look like
|
||
this, applying the rules in the Angular style guide is a highly recommended
|
||
preparation step. And this is not just for the sake of the upgrade - it is just
|
||
solid advice in general!
|
||
|
||
如果应用程序能用这种方式把每个特性分到一个独立目录中,它也就能每次迁移一个特性。
|
||
对于那些还没有这么做的程序,强烈建议把应用这条规则作为准备步骤。而且这也不仅仅对升级有价值,
|
||
它还是一个通用的规则,可以让你的程序更“坚实”。
|
||
|
||
## Using a Module Loader
|
||
## 使用模块加载器
|
||
|
||
When we break application code down into one component per file, we often end
|
||
up with a project structure with a large number of relatively small files. This is
|
||
a much neater way to organize things than a small number of large files, but it
|
||
doesn't work that well if you have to load all those files to the HTML page with
|
||
`<script>` tags. Especially when you also have to maintain those tags in the correct
|
||
order. That's why it's a good idea to start using a *module loader*.
|
||
|
||
当我们把应用代码分解成每个文件中放一个组件之后,我们通常会得到一个由大量相对较小的文件组成的项目结构。
|
||
这比组织成少量大文件要整洁得多,但如果你不得不通过`<script>`标签在HTML页面中加载所有这些文件,那就不好玩儿了。
|
||
尤其是当你不得不按正确的顺序维护这些标签时更是如此。
|
||
这就是为什么开始使用*模块加载器*是一个好主意了。
|
||
|
||
Using a module loader such as [SystemJS](https://github.com/systemjs/systemjs),
|
||
[Webpack](http://webpack.github.io/), or [Browserify](http://browserify.org/)
|
||
allows us to use the built-in module systems of the TypeScript or ES2015 languages in our apps.
|
||
We can use the `import` and `export` features that explicitly specify what code can
|
||
and will be shared between different parts of the application. For ES5 applications
|
||
we can use CommonJS style `require` and `module.exports` features. In both cases,
|
||
the module loader will then take care of loading all the code the application needs
|
||
in the correct order.
|
||
|
||
使用模块加载器,比如[SystemJS](https://github.com/systemjs/systemjs)、
|
||
[Webpack](http://webpack.github.io/)或[Browserify](http://browserify.org/),
|
||
可以让我们在程序中使用TypeScript或ES2015语言内置的模块系统。
|
||
我们可以使用`import`和`export`特性来明确指定哪些代码应该以及将会被在程序的不同部分之间共享。
|
||
对于ES5程序来说,我们可以改用CommonJS风格的`require`和`module.exports`特性代替。
|
||
无是论哪种情况,模块加载器都会按正确的顺序加载程序中用到的所有代码。
|
||
|
||
When we then take our applications into production, module loaders also make it easier
|
||
to package them all up into production bundles with batteries included.
|
||
|
||
当我们的应用程序投入生产环境时,模块加载器也会让把所有这些文件打成完整的产品包变得更容易。
|
||
|
||
:marked
|
||
## Migrating to TypeScript
|
||
## 迁移到TypeScript
|
||
|
||
If part of our Angular 2 upgrade plan is to also take TypeScript into use, it makes
|
||
sense to bring in the TypeScript compiler even before the upgrade itself begins.
|
||
This means there's one less thing to learn and think about during the actual upgrade.
|
||
It also means we can start using TypeScript features in our Angular 1 code.
|
||
|
||
Angular 2升级计划的一部分是引入TypeScript,即使在开始升级之前,引入TypeScript编译器也是有意义的。
|
||
这意味着等真正升级的时候需要学习和思考的东西更少。
|
||
它还意味着我们可以在Angular 1代码中开始使用TypeScript的特性。
|
||
|
||
Since TypeScript is a superset of ECMAScript 2015, which in turn is a superset
|
||
of ECMAScript 5, "switching" to TypeScript doesn't necessarily require anything
|
||
more than installing the TypeScript compiler and switching renaming files from
|
||
`*.js` to `*.ts`. But just doing that is not hugely useful or exciting, of course.
|
||
Additional steps like the following can give us much more bang for the buck:
|
||
|
||
因为TypeScript是ECMAScript 2015的一个超集,而ES2015又是ECMAScript 5的一个超集。
|
||
这意味着除了安装一个TypeScript编译器,并把文件名都从`*.js`改成`*.ts`之外,其实什么都不用做。
|
||
当然,如果仅仅这样做也没什么大用,也没什么令人兴奋之处。
|
||
下面这些额外步骤可以让我们抖擞起精神来:
|
||
|
||
* For applications that use a module loader, TypeScript imports and exports
|
||
(which are really ECMAScript 2015 imports and exports) can be used to organize
|
||
code into modules.
|
||
|
||
* 对那些使用了模块加载器的程序,TypeScript的导入和导出(这实际上是ECMAScript 2015导入和导出)可以把代码组织到模块中。
|
||
|
||
* Type annotations can be gradually added to existing functions and variables
|
||
to pin down their types and get benefits like build-time error checking,
|
||
great autocompletion support and inline documentation.
|
||
|
||
* 类型注解可以逐步添加到已存在的函数和变量上,以固定它们的类型,并获得其优点:比如编译期错误检查、更好的支持自动完成,以及内联式文档等。
|
||
|
||
* JavaScript features new to ES2015, like `let`s and `const`s, default function
|
||
parameters, and destructuring assignments can also be gradually added to make
|
||
the code more expressive.
|
||
|
||
* 那些ES2015中新增的特性,比如`let`、`const`、默认函数参数、解构赋值等也能逐渐添加进来,让代码更有表现力。
|
||
|
||
* Services and controllers can be turned into *classes*. That way they'll be a step
|
||
closer to becoming Angular 2 service and component classes, which will make our
|
||
life easier once we do the upgrade.
|
||
|
||
* 服务和控制器可以转成*类*。这样我们就能一步步接近Angular 2的服务和组件类了,这样等到我们开始升级时,也会更简单。
|
||
|
||
## Using Component Directives
|
||
## 使用组件型指令
|
||
|
||
In Angular 2, components are the main primitive from which user interfaces
|
||
are built. We define the different parts of our UIs as components, and then
|
||
compose the UI by using components in our templates.
|
||
|
||
在Angular 2中,组件是用来构建用户界面的主要元素。我们把UI中的不同部分定义成组件,然后通过在模板中使用这些组件最终合成为UI。
|
||
|
||
You can also do this in Angular 1, using *component directives*. These are
|
||
directives that define their own templates, controllers, and input/output bindings -
|
||
the same things that Angular 2 components define. Applications built with
|
||
component directives are much easier to migrate to Angular 2 than applications
|
||
built with lower-level features like `ng-controller`, `ng-include`, and scope
|
||
inheritance.
|
||
|
||
我们在Angular 1中也能这么做。那就是一种定义了自己的模板、控制器和输入/输出绑定的指令 —— 跟Angular 2中对组件的定义是一样的。
|
||
要迁移到Angular 2,通过组件型指令构建的应用程序会比直接用`ng-controller`、`ng-include`和作用域继承等底层特性构建的要容易得多。
|
||
|
||
To be Angular 2 compatible, an Angular 1 component directive should configure
|
||
these attributes:
|
||
|
||
要做成与Angular 2兼容的,Angular 1的组件型指令应该配置下列属性:
|
||
|
||
* `restrict: 'E'`. Components are usually used as elements.
|
||
|
||
* `restrict: 'E'`。组件通常会以元素的方式使用。
|
||
|
||
* `scope: {}` - an isolate scope. In Angular 2, components are always isolated
|
||
from their surroundings, and we should do this in Angular 1 too.
|
||
|
||
* `scope: {}` - 一个独立作用域。在Angular 2中,组件永远是从它们的环境中被隔离出来的,在Angular 1中,我们也应该这么做。
|
||
|
||
* `bindToController: {}`. Component inputs and outputs should be bound
|
||
to the controller instead of using the `$scope`.
|
||
|
||
* `bindToController: {}`。组件的输入和输出应该绑定到控制器,而不是`$scope`。
|
||
|
||
* `controller` and `controllerAs`. Components have their own controllers.
|
||
|
||
* `controller`和`controllerAs`。组件有它们自己的控制器。
|
||
|
||
* `template` or `templateUrl`. Components have their own templates.
|
||
|
||
* `template`或`templateUrl`。组件有它们自己的模板。
|
||
|
||
Component directives may also use the following attributes:
|
||
|
||
组件型指令还可能使用下列属性:
|
||
|
||
* `transclude: true`, if the component needs to transclude content from elsewhere.
|
||
|
||
* `transclude: true`:如果组件需要从其它地方透传内容,就设置它。
|
||
|
||
* `require`, if the component needs to communicate with some parent component's
|
||
controller.
|
||
|
||
* `require`:如果组件需要和父组件的控制器通讯,就设置它。
|
||
|
||
Component directives **may not** use the following attributes:
|
||
|
||
组件型指令**不能**使用下列属性:
|
||
|
||
* `compile`. This will not be supported in Angular 2.
|
||
|
||
* `compile`。它在Angular 2中将不再被支持。
|
||
|
||
* `replace: true`. Angular 2 never replaces a component element with the
|
||
component template. This attribute is also deprecated in Angular 1.
|
||
|
||
* `replace: true`。Angular永远不会用组件模板替换一个组件元素。这个特性在Angular 1中也同样不建议使用了。
|
||
|
||
* `priority` and `terminal`. While Angular 1 components may use these,
|
||
they are not used in Angular 2 and it is better not to write code
|
||
that relies on them.
|
||
|
||
* `priority`和`terminal`。虽然Angular 1的组件可能使用这些,但它们在Angular 2中已经没用了,并且最好不要再写依赖它们的代码。
|
||
|
||
An Angular 1 component directive that is fully aligned with the Angular 2
|
||
architecture may look something like this:
|
||
|
||
Angular 1中一个完全向Angular 2架构看齐过的组件型指令看起来有点像这样:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/hero-detail.directive.ts')
|
||
|
||
:marked
|
||
Angular 1.5 introduces the [component API](https://docs.angularjs.org/api/ng/type/angular.Module)
|
||
that makes it easier to define directives like these. It is a good idea to use
|
||
this API for component directives for several reasons:
|
||
|
||
Angular 1.5引入了[组件API](https://docs.angularjs.org/api/ng/type/angular.Module),它让像这样定义指令变得更简单了。
|
||
为组件型指令使用这个API是一个好主意,因为:
|
||
|
||
* It requires less boilerplate code.
|
||
|
||
* 它需要更少的样板代码。
|
||
|
||
* It enforces the use of component best practices like `controllerAs`.
|
||
|
||
* 它强制使用组件的最佳实践,比如`controllerAs`。
|
||
|
||
* It has good default values for directive attributes like `scope`,
|
||
`restrict`, and `transclude`.
|
||
|
||
* 对于指令中像`scope`、`restrict`和`transclude`这样的属性,它有良好的默认值。
|
||
|
||
The component directive example from above looks like this when expressed
|
||
using the component API:
|
||
|
||
如果使用这个组件API进行快捷定义,那么上面看到的组件型指令就变成了这样:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/upgrade-io/hero-detail.component.ts')
|
||
|
||
|
||
|
||
.l-main-section
|
||
:marked
|
||
# Upgrading with The Upgrade Adapter
|
||
# 使用升级适配器进行升级
|
||
|
||
The `upgrade` module in Angular 2 is a very useful tool for upgrading
|
||
anything but the smallest of applications. With it we can mix and match
|
||
Angular 1 and 2 components in the same application and have them interoperate
|
||
seamlessly. That means we don't have to do the upgrade work all at once,
|
||
since there's a natural coexistence between the two frameworks during the
|
||
transition period.
|
||
|
||
不管要升级什么,Angular 2中的`upgrade`模块都会是一个非常有用的工具 —— 除非是小到没功能的应用。
|
||
借助它,我们可以在同一个应用程序中混用并匹配Angular 1和2的组件,并让它们实现无缝的互操作。
|
||
这意味着我们不用必须一次性做完所有升级工作,因为在整个演进过程中,这两个框架可以很自然的和睦相处。
|
||
|
||
## How The Upgrade Adapter Works
|
||
## 升级适配器如何工作
|
||
|
||
The primary tool provided by the upgrade module is called the `UpgradeAdapter`.
|
||
This is a service that can bootstrap and manage hybrid applications that support
|
||
both Angular 2 and Angular 1 code.
|
||
|
||
`upgrade`模块提供的主要工具叫做`UpgradeAdapter`。这是一个服务,它可以引导并管理同时支持Angular 2和Angular 1的混合应用程序。
|
||
|
||
When we use `UpgradeAdapter`, what we're really doing is *running both versions
|
||
of Angular at the same time*. All Angular 2 code is running in the Angular 2
|
||
framework, and Angular 1 code in the Angular 1 framework. Both of these are the
|
||
actual, fully featured versions of the frameworks. There is no emulation going on,
|
||
so we can expect to have all the features and natural behavior of both frameworks.
|
||
|
||
当使用`UpgradeAdapter`时,我们实际做的是*同时运行两个版本的Angular*。所有Angular 2的代码运行在Angular 2框架中,
|
||
而Angular 1的代码运行在Angular 1框架中。所有这些都是真实的、全功能的框架版本。
|
||
没有进行任何仿真,所以我们可以期待同时存在这两个框架的所有特性和天生的行为。
|
||
|
||
What happens on top of this is that components and services managed by one
|
||
framework can interoperate with those from the other framework. This happens
|
||
in three main areas: Dependency injection, the DOM, and change detection.
|
||
|
||
所有这些事情的背后,本质上是一个框架中管理的组件和服务能和来自另一个中的进行互操作。
|
||
这发生在三个主要的领域:依赖注入、DOM和变更检测。
|
||
|
||
### Dependency Injection
|
||
### 依赖注入
|
||
|
||
Dependency injection is front and center in both Angular 1 and
|
||
Angular 2, but there are some key differences between the two
|
||
frameworks in how it actually works.
|
||
|
||
无论是在Angular 1中还是在Angular 2中,依赖注入都处于前沿和中心的位置,但在两个框架的工作原理上,却存在着一些关键的不同之处。
|
||
|
||
table
|
||
tr
|
||
th Angular 1
|
||
th Angular 2
|
||
tr
|
||
td
|
||
:marked
|
||
Dependency injection tokens are always strings
|
||
|
||
依赖注入的令牌(Token)永远是字符串(译注:指服务名称)。
|
||
td
|
||
:marked
|
||
Tokens [can have different types](../guide/dependency-injection.html).
|
||
They are often classes. They may also be strings.
|
||
|
||
令牌[可能有不同的类型](../guide/dependency-injection.html)。
|
||
通常是类,也可能是字符串。
|
||
tr
|
||
td
|
||
:marked
|
||
There is exactly one injector. Even in multi-module applications,
|
||
everything is poured into one big namespace.
|
||
|
||
只有一个注入器。即使在多模块的应用程序中,每样东西也都被装入一个巨大的命名空间中。
|
||
td
|
||
:marked
|
||
There is a [tree hierarchy of injectors](../guide/hierarchical-dependency-injection.html),
|
||
with a root injector and an additional injector for each component.
|
||
|
||
有一组[树状多层注入器](../guide/hierarchical-dependency-injection.html),有一个根注入器,每个组件也有一个额外的注入器。
|
||
|
||
:marked
|
||
Even accounting for these differences we can still have dependency injection
|
||
interoperability. The `UpgradeAdapter` resolves the differences and makes
|
||
everything work seamlessly:
|
||
|
||
就算有这么多不同点,也并不妨碍我们在依赖注入时进行互操作。`UpgradeAdapter`解决了这些差异,并让它们无缝的对接:
|
||
|
||
* We can make Angular 1 services available for injection to Angular 2 code
|
||
by *upgrading* them. The same singleton instance of each service is shared
|
||
between the frameworks. In Angular 2 these services will always be in the
|
||
*root injector* and available to all components. They will always have
|
||
*string tokens* - the same tokens that they have in Angular 1.
|
||
|
||
* 通过升级它们,我们就能让那些在Angular 1中能被注入的服务在Angular 2的代码中可用。
|
||
在框架之间共享的是服务的同一个单例对象。在Angular 2中,这些外来服务总是被放在*根注入器*中,并可用于所有组件。
|
||
它们总是具有*字符串令牌* —— 跟它们在Angular 1中的令牌相同。
|
||
|
||
* We can also make Angular 2 services available for injection to Angular 1 code
|
||
by *downgrading* them. Only services from the Angular 2 root injector can
|
||
be downgraded. Again, the same singleton instances are shared between the frameworks.
|
||
When we register a downgrade, we explicitly specify a *string token* that we want to
|
||
use in Angular 1.
|
||
|
||
* 通过降级它们,我们也能让那些在Angular 2中能被注入的服务在Angular 1的代码中可用。
|
||
只有那些来自Angular 2根注入器的服务才能被降级。同样的,在框架之间共享的是同一个单例对象。
|
||
当我们注册一个要降级的服务时,要明确指定一个打算在Angular 1中使用的*字符串令牌*。
|
||
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/upgrade/injectors.png" alt="The two injectors in a hybrid application" width="700")
|
||
|
||
:marked
|
||
### Components and the DOM
|
||
### 组件与DOM
|
||
|
||
What we'll find in the DOM of a hybrid application are components and
|
||
directives from both Angular 1 and Angular 2. These components
|
||
communicate with each other by using the input and output bindings
|
||
of their respective frameworks, which the `UpgradeAdapter` bridges
|
||
together. They may also communicate through shared injected dependencies,
|
||
as described above.
|
||
|
||
在混合应用中,我们能同时发现那些来自Angular 1和Angular 2中组件和指令的DOM。
|
||
这些组件通过它们各自框架中的输入和输出绑定来互相通讯,它们由`UpgradeAdapter`桥接在一起。
|
||
它们也能通过共享被注入的依赖彼此通讯,就像前面所说的那样。
|
||
|
||
There are two key things to understand about what happens in the DOM
|
||
of a hybrid application:
|
||
|
||
要弄明白在一个混合应用的DOM中发生了什么,有两点很关键:
|
||
|
||
1. Every element in the DOM is owned by exactly one of the two
|
||
frameworks. The other framework ignores it. If an element is
|
||
owned by Angular 1, Angular 2 treats it as if it didn't exist,
|
||
and vice versa.
|
||
|
||
1. DOM中的每个元素都只能被两个框架之一拥有。另一个框架会忽略它。
|
||
如果一个元素被Angular 1拥有,Angular 2就会当它不存在。反之亦然。
|
||
|
||
2. The root of the application *is always an Angular 1 template*.
|
||
|
||
2. 应用的根节点*总是来自Angular 1中的模板*。
|
||
|
||
So a hybrid application begins life as an Angular 1 application,
|
||
and it is Angular 1 that processes its root template. Angular 2 then steps
|
||
into the picture when an Angular 2 component is used somewhere in
|
||
the application templates. That component's view will then be managed
|
||
by Angular 2, and it may use any number of Angular 2 components and
|
||
directives.
|
||
|
||
所以,混合应用总是像Angular 1程序那样启动,处理根模板的也是Angular 1.
|
||
然后,当这个应用的模板中使用到了Angular 2的组件时,Angular 2才开始参与。
|
||
这个组件的视图由Angular 2进行管理,而且它还可以使用一系列的Angular 2组件和指令。
|
||
|
||
Beyond that, we may interleave the two frameworks as much as we need to.
|
||
We always cross the boundary between the two frameworks by one of two
|
||
ways:
|
||
|
||
更进一步说,我们可以按照需要,任意穿插使用这两个框架。
|
||
使用下面的两种方式之一,我们可以自由穿梭于这两个框架的边界:
|
||
|
||
1. By using a component from the other framework: An Angular 1 template
|
||
using an Angular 2 component, or an Angular 2 template using an
|
||
Angular 1 component.
|
||
|
||
1. 通过使用来自另一个框架的组件:Angular 1的模板中用到了Angular 2的组件,或者Angular 2的模板中使用了Angular 1的组件。
|
||
|
||
2. By transcluding or projecting content from the other framework. The
|
||
`UpgradeAdapter` bridges the related concepts of Angular 1 transclusion
|
||
and Angular 2 content projection together.
|
||
|
||
2. 通过透传(transclude)或投影(project)来自另一个框架的内容。`UpgradeAdapter`牵线搭桥,把Angular 1的透传概念和Angular 2的内容投影概念关联起来。
|
||
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/upgrade/dom.png" alt="DOM element ownership in a hybrid application" width="500")
|
||
|
||
:marked
|
||
Whenever we use a component that belongs to the other framework, a
|
||
switch between framework boundaries occurs. However, that switch only
|
||
happens to the *children* of the component element. Consider a situation
|
||
where we use an Angular 2 component from Angular 1 like this:
|
||
|
||
无论什么时候,只要我们用到了来自另一个框架的组件,就会发生框架边界的切换。然而,这种切换只会发生在组件元素的*子节点*上。
|
||
考虑一个场景,我们从Angular 1中像这样使用Angular 2的组件:
|
||
|
||
```
|
||
<ng2-component></ng2-component>
|
||
```
|
||
|
||
The DOM element `<ng2-component>` will remain to be an Angular 1 managed
|
||
element, because it's defined in an Angular 1 template. That also
|
||
means you can apply additional Angular 1 directives to it, but *not*
|
||
Angular 2 directives. It is only in the template of the `Ng2Component`
|
||
component where Angular 2 steps in. This same rule also applies when you
|
||
use Angular 1 component directives from Angular 2.
|
||
|
||
此时,`<ng2-component>`这个DOM元素仍然由Angular 1管理,因为它是在Angular 1的模板中定义的。
|
||
这也意味着你可以往它上面添加额外的Angular 1指令,却*不能*添加Angular 2的指令。
|
||
只有在`Ng2Component`组件的模板中才是Angular 2的天下。同样的规则也适用于在Angular 2中使用Angular 1组件型指令的情况。
|
||
|
||
:marked
|
||
### Change Detection
|
||
### 变更检测
|
||
|
||
Change detection in Angular 1 is all about `scope.$apply()`. After every
|
||
event that occurs, `scope.$apply()` gets called. This is done either
|
||
automatically by the framework, or in some cases manually by our own
|
||
code. It is the point in time when change detection occurs and data
|
||
bindings get updated.
|
||
|
||
Angular 1中的变更检测全是关于`scope.$apply()`的。在每个事件发生之后,`scope.$apply()`就会被调用。
|
||
这或者由框架自动调用,或者在某些情况下由我们自己的代码手动调用。它是发生变更检测以及更新数据绑定的时间点。
|
||
|
||
In Angular 2 things are different. While change detection still
|
||
occurs after every event, no one needs to call `scope.$apply()` for
|
||
that to happen. This is because all Angular 2 code runs inside something
|
||
called the [Angular zone](../api/core/NgZone-class.html). Angular always
|
||
knows when the code finishes, so it also knows when it should kick off
|
||
change detection. The code itself doesn't have to call `scope.$apply()`
|
||
or anything like it.
|
||
|
||
在Angular 2中,事情有点不一样。虽然变更检测仍然会在每一个事件之后发生,却不再需要每次调用`scope.$apply()`了。
|
||
这是因为所有Angular 2代码都运行在一个叫做[Angular zone](../api/core/NgZone-class.html)的地方。
|
||
Angular总是知道什么时候代码执行完了,也就知道了它什么时候应该触发变更检测。代码本身并不需要调用`scope.$apply()`或其它类似的东西。
|
||
|
||
In the case of hybrid applications, the `UpgradeAdapter` bridges the
|
||
Angular 1 and Angular 2 approaches. Here's what happens:
|
||
|
||
在这种混合应用的案例中,`UpgradeAdapter`在Angular 1的方法和Angular 2的方法之间建立了桥梁。发生了什么呢?
|
||
|
||
* Everything that happens in the application runs inside the Angular 2 zone.
|
||
This is true whether the event originated in Angular 1 or Angular 2 code.
|
||
The zone triggers Angular 2 change detection after every event.
|
||
|
||
* 应用中发生的每件事都运行在Angular 2的zone里。
|
||
无论事件发生在Angular 1还是Angular 2的代码中,都是如此。
|
||
|
||
* The `UpgradeAdapter` will invoke the Angular 1 `$rootScope.$apply()` after
|
||
every turn of the Angular zone. This also triggers Angular 1 change
|
||
detection after every event.
|
||
|
||
* `UpgradeAdapter`将在每一次离开Angular zone时调用Angular 1的`$rootScope.$apply()`。这样也就同样会在每个事件之后触发Angular 1的变更检测。
|
||
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/upgrade/change_detection.png" alt="Change detection in a hybrid application" width="600")
|
||
|
||
:marked
|
||
What this means in practice is that we do not need to call `$apply()` in
|
||
our code, regardless of whether it is in Angular 1 on Angular 2. The
|
||
`UpgradeAdapter` does it for us. We *can* still call `$apply()` so there
|
||
is no need to remove such calls from existing code. Those calls just don't
|
||
have any effect in a hybrid application.
|
||
|
||
在实践中,这意味着我们不用在自己的代码中调用`$apply()`,而不用管这段代码是在Angular 1还是Angular 2中。
|
||
`UpgradeAdapter`都替我们做了。我们仍然*可以*调用`$apply()`,以便可以不必从现有代码中移除此调用。
|
||
在混合应用中,那些调用只是没有任何效果而已。
|
||
|
||
:marked
|
||
When we downgrade an Angular 2 component and then use it from Angular 1,
|
||
the component's inputs will be watched using Angular 1 change detection.
|
||
When those inputs change, the corresponding properties in the component
|
||
are set. We can also hook into the changes by implementing the
|
||
[OnChanges](../api/core/OnChanges-interface.html) interface in the component,
|
||
just like we could if it hadn't been downgraded.
|
||
|
||
当我们降级一个Angular 2组件,然后把它用于Angular 1中时,组件的输入属性就会被Angular 1的变更检测体系监视起来。
|
||
当那些输入属性发生变化时,组件中相应的属性就会被设置。我们也能通过实现[OnChanges](../api/core/OnChanges-interface.html)
|
||
接口来挂钩到这些更改,就像它未被降级时一样。
|
||
|
||
Correspondingly, when we upgrade an Angular 1 component and use it from Angular 2,
|
||
all the bindings defined for the component directive's `scope` (or `bindToController`)
|
||
will be hooked into Angular 2 change detection. They will be treated
|
||
as regular Angular 2 inputs and set onto the scope (or controller) when
|
||
they change.
|
||
|
||
相应的,当我们把Angular 1的组件升级给Angular 2使用时,在这个组件型指令的`scope`(或`bindToController`)中定义的所有绑定
|
||
都将被挂钩到Angular 2的变更检测体系中。它们将和标准的Angular 2输入属性被同等对待,并当它们发生变化时设置回scope(或控制器)上。
|
||
|
||
## Bootstrapping Hybrid Angular 1+2 Applications
|
||
## 引导Angular 1+2的混合应用程序
|
||
|
||
The first step to upgrading an application using the `UpgradeAdapter` is
|
||
always to bootstrap it as a hybrid that supports both Angular 1 and
|
||
Angular 2.
|
||
|
||
使用`UpgradeAdapter`升级应用的第一步总是把它引导成一个同时支持Angular 1和Angular 2的混合应用。
|
||
|
||
Pure Angular 1 applications can be bootstrapped in two ways: By using an `ng-app`
|
||
directive somewhere on the HTML page, or by calling
|
||
[angular.bootstrap](https://docs.angularjs.org/api/ng/function/angular.bootstrap)
|
||
from JavaScript. In Angular 2, only the second method is possible - there is
|
||
no `ng-app` in Angular 2. This is also the case for hybrid applications.
|
||
Therefore, it is a good preliminary step to switch Angular 1 applications to use the
|
||
JavaScript bootstrap method even before switching them to hybrid mode.
|
||
|
||
纯粹的Angular 1应用可以用两种方式引导:在HTML页面中的某处使用`ng-app`指令,或者从JavaScript中调用
|
||
[angular.bootstrap](https://docs.angularjs.org/api/ng/function/angular.bootstrap)。
|
||
在Angular 2中,只有第二种方法是可行的,因为它没有`ng-app`指令。在混合应用中也同样只能用第二种方法。
|
||
所以,即使在把Angular 1应用切换到混合模式之前,把它改为用JavaScript引导的方式也是一个不错的起点。
|
||
|
||
Say we have an `ng-app` driven bootstrap such as this one:
|
||
|
||
比如说我们有个由`ng-app`驱动的引导过程,就像这个:
|
||
|
||
+makeExample('upgrade-adapter/ts/index-ng-app.html', null, null, {otl: /(ng-app.*ng-strict-di)/})
|
||
|
||
:marked
|
||
We can remove the `ng-app` and `ng-strict-di` directives from the HTML
|
||
and instead switch to calling `angular.bootstrap` from JavaScript, which
|
||
will result in the same thing:
|
||
|
||
我们可以从HTML中移除`ng-app`和`ng-strict-di`指令,改为从JavaScript中调用`angular.bootstrap`,它能达到同样效果:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/1-bootstrap/app.module.ts', 'bootstrap')
|
||
|
||
:marked
|
||
To then switch the application into hybrid mode, we must first
|
||
install Angular 2 to the project. Follow the instructions in
|
||
[the QuickStart](../quickstart.html) for some pointers on this.
|
||
When we have Angular 2 installed, we can import and instantiate
|
||
the `UpgradeAdapter`, and then call its `bootstrap` method. It
|
||
is designed to take the exact same arguments as
|
||
[angular.bootstrap](https://docs.angularjs.org/api/ng/function/angular.bootstrap)
|
||
so that it is easy to make the switch:
|
||
|
||
要把这个应用切换到混合模式,我们得先把Angular 2安装到项目中。遵循[“快速起步”](../quickstart.html)中给出的步骤完成它。
|
||
安装完Angular 2之后,我们可以导入和实例化`UpgradeAdapter`类,然后调用它的`bootstrap`方法。
|
||
它被设计成接受与[angular.bootstrap](https://docs.angularjs.org/api/ng/function/angular.bootstrap)完全相同的参数。
|
||
所以,做这种切换很简单:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/1-2-hybrid-bootstrap/app.module.ts', 'bootstrap')
|
||
|
||
:marked
|
||
At this point we'll be running a hybrid Angular 1+2 application! All the
|
||
existing Angular 1 code will work as it always did, but we are now ready
|
||
to run Angular 2 code as well.
|
||
|
||
这时,我们就要开始运行Angular 1+2的混合应用程序了!所有现存的Angular 1代码会像以前一样正常工作,但是我们现在也同样可以运行Angular 2代码了。
|
||
|
||
.alert.is-helpful
|
||
:marked
|
||
One notable difference between `angular.bootstrap` and
|
||
`upgradeAdapter.bootstrap` is that the latter works *asynchronously*.
|
||
This means that we cannot assume that the application has been instantiated
|
||
immediately after the bootstrap call returns.
|
||
|
||
在`angular.bootstrap`和`upgradeAdapter.bootstrap`之间一个显著的不同点是:后者是*异步*工作的。
|
||
这意味着当这次`bootstrap`调用刚刚返回时,我们不能假设应用程序已经被初始化过了。
|
||
|
||
:marked
|
||
As we begin to migrate components to Angular 2, we'll be using the
|
||
`UpgradeAdapter` for more than just bootstrapping. It'll be important
|
||
to use the **same** instance of the adapter across the whole application,
|
||
because it stores internal information about what's going on in the application.
|
||
It'll be useful to have a module for a shared `UpgradeAdapter` instance in
|
||
the project:
|
||
|
||
当我们开始把组件移植到Angular 2时,我们还将使用`UpgradeAdapter` —— 不止是进行引导。
|
||
在整个应用程序中使用此适配器的**同一个**实例是非常重要的,因为它保存了关于该应用程序当前状态的内部信息:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/1-2-hybrid-shared-adapter-bootstrap/upgrade_adapter.ts', null, 'upgrade_adapter.ts')
|
||
|
||
:marked
|
||
This shared instance can then be pulled in to all the modules that need it:
|
||
|
||
然后这个共享的实例就能被所有需要它的模块获取到:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/1-2-hybrid-shared-adapter-bootstrap/app.module.ts', 'bootstrap')
|
||
|
||
:marked
|
||
## Using Angular 2 Components from Angular 1 Code
|
||
## 在Angular 1的代码中使用Angular 2的组件
|
||
figure
|
||
img(src="/resources/images/devguide/upgrade/a1-to-a2.png" alt="Using an Angular 2 component from Angular 1 code" align="left" style="width:250px; margin-left:-40px;margin-right:10px" )
|
||
:marked
|
||
Once we're running a hybrid app, we can start the gradual process of upgrading
|
||
code. One of the more common patterns for doing that is to use an Angular 2 component
|
||
in an Angular 1 context. This could be a completely new component or one that was
|
||
previously Angular 1 but has been rewritten for Angular 2.
|
||
|
||
一旦我们开始运行混合应用,我们就可以开始逐渐升级代码了。做这件事的一种更常见的模式就是在Angular 1的上下文中使用Angular 2的组件。
|
||
该组件可能是全新的,也可能是把原本Angular 1的组件用Angular 2重写而成的。
|
||
|
||
Say we have a simple Angular 2 component that shows information about a hero:
|
||
|
||
假设我们有一个简单的用来显示英雄信息的Angular 2组件:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/downgrade-static/hero-detail.component.ts', null, 'hero-detail.component.ts')
|
||
|
||
:marked
|
||
If we want to use this component from Angular 1, we need to *downgrade* it
|
||
using the upgrade adapter. What we get when we do that is an Angular 1
|
||
*directive*, which we can then register into our Angular 1 module:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/downgrade-static/app.module.ts', 'downgradecomponent')
|
||
|
||
:marked
|
||
What we have here is an Angular 1 directive called `heroDetail`, which we can
|
||
use like any other directive in our Angular 1 templates.
|
||
|
||
+makeExample('upgrade-adapter/ts/index-downgrade-static.html', 'usecomponent')
|
||
|
||
.alert.is-helpful
|
||
:marked
|
||
Note that since Angular 1 directives are matched based on their name,
|
||
*the selector metadata of the Angular 2 component is not used in Angular 1*.
|
||
It is matched as an element directive (`restrict: 'E'`) called `heroDetail`.
|
||
|
||
:marked
|
||
Most components are not quite this simple, of course. Many of them
|
||
have *inputs and outputs* that connect them to the outside world. An
|
||
Angular 2 hero detail component with inputs and outputs might look
|
||
like this:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/downgrade-io/hero-detail.component.ts', null, 'hero-detail.component.ts')
|
||
|
||
:marked
|
||
These inputs and outputs can be supplied from the Angular 1 template, and the
|
||
`UpgradeAdapter` takes care of bridging them over:
|
||
|
||
+makeExample('upgrade-adapter/ts/index-downgrade-io.html', 'usecomponent')
|
||
|
||
:marked
|
||
Note that even though we are in an Angular 1 template, **we're using Angular 2
|
||
attribute syntax to bind the inputs and outputs**. This is a requirement for downgraded
|
||
components. The expressions themselves are still regular Angular 1 expressions.
|
||
|
||
The `$event` variable can be used in outputs to gain access to the
|
||
object that was emitted. In this case it will be the `Hero` object, because
|
||
that is what was passed to `this.deleted.emit()`.
|
||
|
||
Since this is an Angular 1 template, we can still use other Angular 1
|
||
directives on the element, even though it has Angular 2 binding attributes on it.
|
||
For example, we can easily make multiple copies of the component using `ng-repeat`:
|
||
|
||
+makeExample('upgrade-adapter/ts/index-downgrade-io.html', 'userepeatedcomponent')
|
||
|
||
:marked
|
||
## Using Angular 1 Component Directives from Angular 2 Code
|
||
figure
|
||
img(src="/resources/images/devguide/upgrade/a2-to-a1.png" alt="Using an Angular 1 component from Angular 2 code" align="left" style="width:250px; margin-left:-40px;margin-right:10px" )
|
||
:marked
|
||
So, we can write an Angular 2 component and then use it from Angular 1
|
||
code. This is very useful when we start our migration from lower-level
|
||
components and work our way up. But in some cases it is more convenient
|
||
to do things in the opposite order: To start with higher-level components
|
||
and work our way down. This too can be done using the `UpgradeAdapter`.
|
||
We can *upgrade* Angular 1 component directives and then use them from
|
||
Angular 2.
|
||
|
||
Not all kinds of Angular 1 directives can be upgraded. The directive
|
||
really has to be a *component directive*, with the characteristics
|
||
[described in the preparation guide above](#using-component-directives).
|
||
Our safest bet for ensuring compatibility is using the
|
||
[component API](https://docs.angularjs.org/api/ng/type/angular.Module)
|
||
introduced in Angular 1.5.
|
||
|
||
A simple example of an upgradable component is one that just has a template
|
||
and a controller:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/upgrade-static/hero-detail.component.ts', null, 'hero-detail.component.ts')
|
||
|
||
:marked
|
||
We can *upgrade* this component to Angular 2 using the `UpgradeAdapter`'s
|
||
`upgradeNg1Component` method. It takes the name of an Angular 1 component
|
||
directive and returns an Angular 2 **component class**. When we then
|
||
want to use it from an Angular 2 component, we list it the in the `directives`
|
||
metadata of the component and then just use it in the Angular 2 template:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/upgrade-static/container.component.ts', null, 'container.component.ts')
|
||
|
||
.alert.is-helpful
|
||
:marked
|
||
Upgraded components always have an element selector, which is based
|
||
on the original name of the original Angular 1 component directive.
|
||
|
||
|
||
:marked
|
||
An upgraded component may also have inputs and outputs, as defined by
|
||
the scope/controller bindings of the original Angular 1 component
|
||
directive. When we use the component from an Angular 2 template,
|
||
we provide the inputs and outputs using **Angular 2 template syntax**,
|
||
with the following rules:
|
||
|
||
table
|
||
tr
|
||
th
|
||
th Binding definition
|
||
th Template syntax
|
||
tr
|
||
th Attribute binding
|
||
td
|
||
:marked
|
||
`myAttribute: '@myAttribute'`
|
||
td
|
||
:marked
|
||
`<my-component myAttribute="value">`
|
||
tr
|
||
th Expression binding
|
||
td
|
||
:marked
|
||
`myOutput: '&myOutput'`
|
||
td
|
||
:marked
|
||
`<my-component (myOutput)="action()">`
|
||
tr
|
||
th Two-way binding
|
||
td
|
||
:marked
|
||
`myValue: '=myValue'`
|
||
td
|
||
:marked
|
||
As input: `<my-component [myValue]="anExpression">` or
|
||
as two-way binding: `<my-component [(myValue)]="anExpression"`
|
||
|
||
:marked
|
||
As an example, say we have a hero detail Angular 1 component directive
|
||
with one input and one output:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/upgrade-io/hero-detail.component.ts', null, 'hero-detail.component.ts')
|
||
|
||
:marked
|
||
We can upgrade this component to Angular 2, and then provide the input
|
||
and output using Angular 2 template syntax:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/upgrade-io/container.component.ts', null, 'container.component.ts')
|
||
|
||
|
||
:marked
|
||
## Projecting Angular 1 Content into Angular 2 Components
|
||
figure
|
||
img(src="/resources/images/devguide/upgrade/a1-to-a2-with-projection.png" alt="Projecting Angular 1 content into Angular 2" align="left" style="width:250px; margin-left:-40px;margin-right:10px" )
|
||
:marked
|
||
When we are using a downgraded Angular 2 component from an Angular 1
|
||
template, the need may arise to *transclude* some content into it. This
|
||
is also possible. While there is no such thing as transclusion in Angular 2,
|
||
there is a very similar concept called *content projection*. The `UpgradeAdapter`
|
||
is able to make these two features interoperate.
|
||
|
||
Angular 2 components that support content projection make use of an `<ng-content>`
|
||
tag within them. Here's an example of such a component:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/1-to-2-projection/hero-detail.component.ts', null, 'hero-detail.component.ts')
|
||
|
||
:marked
|
||
When using the component from Angular 1, we can supply contents for it. Just
|
||
like they would be transcluded in Angular 1, they get projected to the location
|
||
of the `<ng-content>` tag in Angular 2:
|
||
|
||
+makeExample('upgrade-adapter/ts/index-1-to-2-projection.html', 'usecomponent')
|
||
|
||
.alert.is-helpful
|
||
:marked
|
||
When Angular 1 content gets projected inside an Angular 2 component, it still
|
||
remains in "Angular 1 land" and is managed by the Angular 1 framework.
|
||
|
||
:marked
|
||
## Transcluding Angular 2 Content into Angular 1 Component Directives
|
||
figure
|
||
img(src="/resources/images/devguide/upgrade/a2-to-a1-with-transclusion.png" alt="Projecting Angular 2 content into Angular 1" align="left" style="width:250px; margin-left:-40px;margin-right:10px" )
|
||
:marked
|
||
Just like we can project Angular 1 content into Angular 2 components,
|
||
we can *transclude* Angular 2 content into Angular 1 components, whenever
|
||
we are using upgraded versions from them.
|
||
|
||
When an Angular 1 component directive supports transclusion, it may use
|
||
the `ng-transclude` directive in its template to mark the transclusion
|
||
point:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/2-to-1-transclusion/hero-detail.component.ts', null, 'hero-detail.component.ts')
|
||
|
||
.alert.is-helpful
|
||
:marked
|
||
The directive also needs to have the `transclude: true` option enabled.
|
||
It is on by default for component directives defined with the
|
||
1.5 component API.
|
||
|
||
:marked
|
||
If we upgrade this component and use it from Angular 2, we can populate
|
||
the component tag with contents that will then get transcluded:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/2-to-1-transclusion/container.component.ts', null, 'container.component.ts')
|
||
|
||
:marked
|
||
## Making Angular 1 Dependencies Injectable to Angular 2
|
||
|
||
When running a hybrid app, we may bump into situations where we need to have
|
||
some Angular 1 dependencies to be injected to Angular 2 code. This may be
|
||
because we have some business logic still in Angular 1 services, or because
|
||
we need some of Angular 1's built-in services like `$location` or `$timeout`.
|
||
|
||
In these situations, it is possible to *upgrade* an Angular 1 provider to
|
||
Angular 2. This makes it possible to then inject it somewhere in Angular 2
|
||
code. For example, we might have a service called `HeroesService` in Angular 1:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/1-to-2-providers/heroes.service.ts', null, 'heroes.service.ts')
|
||
|
||
:marked
|
||
We can upgrade the service using the `UpgradeAdapter`'s `upgradeNg1Provider` method
|
||
by giving it the name of the service. This adds the service into Angular 2's root injector.
|
||
|
||
+makeExample('upgrade-adapter/ts/app/1-to-2-providers/app.module.ts', 'register', 'app.module.ts')
|
||
|
||
:marked
|
||
We can then inject it in Angular 2 using a string token that matches
|
||
its original name in Angular 1:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/1-to-2-providers/hero-detail.component.ts', null, 'hero-detail.component.ts')
|
||
|
||
.alert.is-helpful
|
||
:marked
|
||
In this example we upgraded a service class, which has the added benefit that
|
||
we can use a TypeScript type annotation when we inject it. While it doesn't
|
||
affect how the dependency is handled, it enables the benefits of static type
|
||
checking. This is not required though, and any Angular 1 service, factory, or
|
||
provider can be upgraded.
|
||
|
||
:marked
|
||
## Making Angular 2 Dependencies Injectable to Angular 1
|
||
|
||
In addition to upgrading Angular 1 dependencies, we can also *downgrade*
|
||
Angular 2 dependencies, so that we can use them from Angular 1. This can be
|
||
useful when we start migrating services to Angular 2 or creating new services
|
||
in Angular 2 while we still have components written in Angular 1.
|
||
|
||
For example, we might have an Angular 2 service called `Heroes`:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/2-to-1-providers/heroes.ts', null, 'heroes.ts')
|
||
|
||
:marked
|
||
We can again use the `UpgradeAdapter` for this, but first we need to register `Heroes`
|
||
to the Angular 2 injector itself. In a pure Angular 2 application we would do this
|
||
when we bootstrap the app, as described in the [dependency injection guide](dependency-injection.html#!#providers).
|
||
But since hybrid applications are bootstrapped using the `UpgradeAdapter`, we also
|
||
need to register our Angular 2 providers using `UpgradeAdapter`. It has a method
|
||
called `addProvider` for this purpose.
|
||
|
||
Once we've registered the Angular 2 provider, we can turn `Heroes` into an *Angular 1
|
||
factory function* using `upgradeAdapter.downgradeNg2Provider()`. We can
|
||
then plug the factory into an Angular 1 module, at which point we also choose what the
|
||
name of the dependency will be in Angular 1:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/2-to-1-providers/app.module.ts', 'register', 'app.module.ts')
|
||
|
||
:marked
|
||
After this, the service is injectable anywhere in our Angular 1 code:
|
||
|
||
+makeExample('upgrade-adapter/ts/app/2-to-1-providers/hero-detail.component.ts', null, 'hero-detail.component.ts')
|
||
|
||
.l-main-section
|
||
:marked
|
||
# PhoneCat Preparation Tutorial
|
||
|
||
In this section and the one following it 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 apply the steps outlined in the
|
||
[preparation guide](#preparation) in practice: We'll align the application
|
||
with Angular 2 and also take both the SystemJS module loader and TypeScript
|
||
into use.
|
||
|
||
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).
|
||
|
||
:marked
|
||
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](#following-the-angular-style-guide) 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
|
||
## Switching to 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.
|
||
While we're at it, let's also install the
|
||
[Typings type definition manager](https://github.com/typings/typings).
|
||
It will allow us to install type definitions for libraries that don't come with
|
||
prepackaged types.
|
||
|
||
code-example(format="").
|
||
npm i systemjs --save
|
||
npm i typescript typings --save-dev
|
||
|
||
:marked
|
||
Let's also add run scripts for the `tsc` TypeScript compiler and the `typings`
|
||
tool to `package.json`:
|
||
|
||
+makeJson('upgrade-phonecat/ts/typescript-conversion/package.1.json', {paths: 'scripts'}, 'package.json', {otl: /(\"tsc.*|\"typings.*)/g})
|
||
|
||
|
||
:marked
|
||
We can now use Typings 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. It will also create a `typings.json` file to the
|
||
project, which contains metadata about the type definitions we've installed:
|
||
|
||
code-example(format="").
|
||
npm run typings install jquery -- --save --global
|
||
npm run typings install angular -- --save --global
|
||
npm run typings install angular-route -- --save --global
|
||
npm run typings install angular-resource -- --save --global
|
||
npm run typings install angular-mocks -- --save --global
|
||
npm run typings install jasmine -- --save --global
|
||
|
||
:marked
|
||
In `index.html`, let's now enable SystemJS. Add a couple of `<script>` tags that
|
||
load the SystemJS library and then one that loads a configuration file for
|
||
it. Then add one more `<script>` tag that uses SystemJS to load our application.
|
||
These will *replace* the various `<script>` tags we had earlier for loading the
|
||
application components:
|
||
|
||
+makeExample('upgrade-phonecat/ts/typescript-conversion/app/index.html', 'scripts', 'app/index.html')
|
||
|
||
+makeExample('upgrade-phonecat/ts/typescript-conversion/app/systemjs.config.js', ull, 'app/systemjs.config.js')
|
||
|
||
:marked
|
||
This 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-phonecat/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.
|
||
|
||
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.
|
||
|
||
code-example(format="").
|
||
npm run tsc
|
||
|
||
:marked
|
||
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 renaming `app.module.js` to `app.module.ts`. The TypeScript
|
||
compiler should be able to compile it successfully right away. This means we can
|
||
move right along and 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-phonecat/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-phonecat/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-phonecat/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-phonecat/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-phonecat/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-phonecat/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-phonecat/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`. We'll
|
||
import each of the three submodule files and register them as dependencies
|
||
of the main application module:
|
||
|
||
+makeExample('upgrade-phonecat/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-phonecat/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.
|
||
Of these two, E2E tests can be dealt with much quicker: 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.
|
||
|
||
For unit tests, on the other hand, we'll do a bit of conversion work.
|
||
What we'll do is convert our existing unit tests to TypeScript and 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. For this we'll use a shim file that will tweak the way
|
||
files get loaded:
|
||
|
||
+makeExample('upgrade-phonecat/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-phonecat/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-phonecat/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-phonecat/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-phonecat/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-phonecat/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 Typings.
|
||
|
||
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-phonecat/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-phonecat/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 with Typings 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. That's what
|
||
ES2015/TypeScript classes really are, so 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-phonecat/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-phonecat/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.
|
||
We'll instead turn it directly into an Angular 2 service in the
|
||
next section.
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
.l-main-section
|
||
:marked
|
||
# PhoneCat Upgrade Tutorial
|
||
|
||
Having completed our preparation work, let's get going with the Angular 2
|
||
upgrade of PhoneCat. We'll do this incrementally with the help of the
|
||
[upgrade module](#upgrading-with-the-upgrade-adapter) that comes with Angular 2.
|
||
By the time we're done, we'll be able to remove Angular 1 from the project
|
||
completely, but the key is to do this 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` and `typings.json` as described in the [Quickstart](../quickstart.html).
|
||
Then run:
|
||
|
||
code-example(format="").
|
||
npm install
|
||
npm run typings install
|
||
|
||
:marked
|
||
We should then load some Angular 2 dependencies into the application by adding
|
||
some `<script>` tags to `index.html`. They should go before the `<script>` tag
|
||
that loads `systemjs.config.js`:
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_initial/app/index.html', 'ng2')
|
||
|
||
:marked
|
||
We can then load Angular 2 itself by adding its packages into the `systemjs.config.js`
|
||
file:
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_initial/app/systemjs.config.js', null, 'app/systemjs.config.js')
|
||
|
||
:marked
|
||
While we're at it, let's also make these files available to unit tests by
|
||
updating the Karma config:
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_initial/test/karma.conf.1.js', 'ng2', 'test/karma.conf.js')
|
||
|
||
:marked
|
||
## Bootstrapping A Hybrid 1+2 PhoneCat
|
||
|
||
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
|
||
these type definitions with Typings 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-phonecat/ts/ng2_initial/tsconfig.1.json', null, 'tsconfig.json', {otl: /(\"moduleResolution.*)/})
|
||
|
||
:marked
|
||
To boostrap a hybrid application, we first need to initialize an `UpgradeAdapter`,
|
||
which [provides the glue](#upgrading-with-the-upgrade-adapter) that joins the two
|
||
versions of the framework together. Let's import the `UpgradeAdapter` class into
|
||
`app.module.ts`:
|
||
|
||
+makeExample('upgrade-phonecat/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-phonecat/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-phonecat/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 the Phone factory
|
||
|
||
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 `Http` service from Angular 2 instead of ngResource.
|
||
|
||
Before the `Http` service is available for injection, we need to register
|
||
it into our application's dependency injector. We should import the `HTTP_PROVIDERS`
|
||
constant in `app.module.ts`:
|
||
|
||
+makeExample('upgrade-phonecat/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-phonecat/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.service.ts` in the core module. It will be a TypeScript
|
||
class decorated as `@Injectable`:
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_initial/app/js/core/phones.service.ts', 'class', 'app/js/core/phones.service.ts')
|
||
|
||
:marked
|
||
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-phonecat/ts/ng2_initial/app/js/core/phones.service.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-phonecat/ts/ng2_initial/app/js/core/phones.service.ts', 'phone-interface', 'app/js/core/phones.service.ts')
|
||
|
||
:marked
|
||
Here's the full, final code for the service:
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_initial/app/js/core/phones.service.ts', 'full', 'app/js/core/phones.service.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-phonecat/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-phonecat/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-phonecat/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-phonecat/ts/ng2_initial/app/js/phone_detail/phone_detail.controller.ts', null, 'app/js/phone_detail/phone_detail.controller.ts')
|
||
|
||
+makeExample('upgrade-phonecat/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.
|
||
|
||
.alert.is-helpful
|
||
:marked
|
||
You could use the `toPromise` method of `Observable` to turn those Observables
|
||
into Promises in the service to further reduce the amount of changes
|
||
needed in controller code.
|
||
|
||
:marked
|
||
To bring our test suite up to speed with the changes, we should update the Karma
|
||
test shim. It'll make some of Angular 2 providers available before starting
|
||
to load any of the spec files:
|
||
|
||
+makeExample('upgrade-phonecat/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.service.spec.ts` and
|
||
set the contents as follows:
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_initial/test/unit/phones.service.spec.ts', null, 'test/unit/phones.service.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-phonecat/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-phonecat/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 `phone_list.component.ts`. Then rename the controller class
|
||
inside to just `PhoneList` and decorate it as a `@Component`:
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_components/app/js/phone_list/phone_list_without_pipes.component.ts', 'top', 'app/js/phone_list/phone_list.component.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-phonecat/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, which is [described in our
|
||
Template Syntax guide](../guide/template-syntax.html#directives).
|
||
|
||
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-phonecat/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-phonecat/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-phonecat/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-phonecat/ts/ng2_components/app/js/phone_list/phone_filter.pipe.ts', null, 'app/js/phone_list/phone_filter.pipe.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-phonecat/ts/ng2_components/test/unit/phone_filter.pipe.spec.ts', null, 'test/unit/phone_filter.pipe.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-phonecat/ts/ng2_components/app/js/phone_list/order_by.pipe.ts', null, 'app/js/phone_list/order_by.pipe.ts')
|
||
|
||
:marked
|
||
Here's a unit test for `OrderByPipe` as well:
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_components/test/unit/order_by.pipe.spec.ts', null, 'test/unit/order_by.pipe.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-phonecat/ts/ng2_components/app/js/phone_list/phone_list.component.ts', 'top', 'app/js/phone_list/phone_list.component.ts')
|
||
|
||
:marked
|
||
In the template we need to use the `phoneFilter` pipe instead of `filter`.
|
||
No changes are needed for the `orderBy`
|
||
|
||
+makeExample('upgrade-phonecat/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-phonecat/ts/ng2_components/app/js/phone_list/phone_list.component.ts', 'full', 'app/js/phone_list/phone_list.component.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-phonecat/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-phonecat/ts/ng2_components/test/unit/phone_list.component.spec.ts', null, 'test/unit/phone_list.component.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-phonecat/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 `phone_detail.component.ts`,
|
||
and set the contents as follows:
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_components/app/js/phone_detail/phone_detail_without_pipes.component.ts', null, 'app/js/phone_detail/phone_detail.component.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-phonecat/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-phonecat/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`](../guide/template-syntax.html#directives)
|
||
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 safe navigation 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-phonecat/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-phonecat/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 turn 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 `checkmark.pipe.ts`:
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_components/app/js/core/checkmark.pipe.ts', null, 'app/js/core/checkmark.pipe.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-phonecat/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-phonecat/ts/ng2_components/test/unit/checkmark.pipe.spec.ts', null, 'test/unit/checkmark.pipe.spec.ts')
|
||
|
||
:marked
|
||
In the component we should now import and declare our newly created pipe:
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_components/app/js/phone_detail/phone_detail.component.ts', 'top', 'app/js/phone_detail/phone_detail.component.ts')
|
||
|
||
:marked
|
||
With the phone detail component now migrated as well, we can go and migrate
|
||
its unit tests too.
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_components/test/unit/phone_detail.component.spec.ts', null, 'test/unit/phone_detail.component.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.js`:
|
||
|
||
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](router.html)
|
||
that we can use for this.
|
||
|
||
Angular 2 applications all have 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 into a new file
|
||
`app.component.ts`:
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_final/app/js/app.component.ts', null, 'app/js/app.component.ts')
|
||
|
||
: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-phonecat/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-phonecat/ts/ng2_final/app/js/phone_detail/phone_detail.component.ts', null, 'app/js/phone_detail/phone_detail.component.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-phonecat/ts/ng2_final/test/unit/phone_detail.component.spec.ts', 'routeparams', 'test/unit/phone_detail.component.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 the router and the new app component in `app.module.ts`
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_final/app/js/main.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-phonecat/ts/ng2_final/app/js/main.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-phonecat/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-phonecat/ts/ng2_final/app/js/phone_list/phone_list.component.ts', 'top')
|
||
|
||
:marked
|
||
Also, the unit tests for `PhoneList` now need to set up some router providers
|
||
so that the `RouterLink` directive can be injected in the tests:
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_final/test/unit/phone_list.component.spec.ts', null)
|
||
|
||
: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-phonecat/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-phonecat/ts/ng2_final/test/e2e/scenarios.js', 'redirect')
|
||
|
||
:marked
|
||
And the second is the phone links spec:
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_final/test/e2e/scenarios.js', '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 `main.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 `main.ts`. Also remove
|
||
the Angular 1 bootstrap code 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 `main.ts` should look like:
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_final/app/js/main.ts', null, 'app/js/main.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`
|
||
* `app/js/core/upgrade_adapter.ts`
|
||
* `app/js/phone_detail/phone_detail.module.ts`
|
||
* `app/js/phone_list/phone_list.module.ts`
|
||
|
||
The external typings for Angular 1 may be uninstalled as well. The only ones
|
||
we still need are for Jasmine.
|
||
|
||
code-example(format="").
|
||
npm run typings uninstall jquery -- --save --global
|
||
npm run typings uninstall angular -- --save --global
|
||
npm run typings uninstall angular-route -- --save --global
|
||
npm run typings uninstall angular-resource -- --save --global
|
||
npm run typings uninstall angular-mocks -- --save --global
|
||
|
||
:marked
|
||
Finally, from `index.html` and `karma.conf.js`, remove all references to
|
||
Angular 1 scripts as well as jQuery. Instead of importing `js/app.module`
|
||
with SystemJS, import `js/main`. When you're done, this is what `index.html`
|
||
should look like:
|
||
|
||
+makeExample('upgrade-phonecat/ts/ng2_final/app/index.html', null, 'app/index.html')
|
||
|
||
:marked
|
||
And this is what `karma.conf.js` should look like:
|
||
|
||
+makeExample('upgrade-phonecat/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.
|