Compare commits

..

61 Commits

Author SHA1 Message Date
268e9772d5 release: cut the v7.0.0-beta.5 release 2018-09-05 21:22:46 -07:00
e2c67ba155 docs: release notes for the v6.1.7 release 2018-09-05 21:18:53 -07:00
dcad0544d7 build: update ngcontainer to node 10.9.0 (#25812)
PR Close #25812
2018-09-05 11:37:26 -07:00
1bc6f64eb5 docs: update event page (#25799)
docs: change reactiveconf event location
PR Close #25799
2018-09-05 11:37:02 -07:00
397b047eff docs: fix showcase address truly-ui (#25757)
PR Close #25757
2018-09-05 11:36:39 -07:00
d96e962da3 test(ivy): mock the filesystem in ngcc main() integration tests (#25557)
Bazel does not like the filesystem being modified.
This commit a temporary mock filesystem that can be modified as needed.

PR Close #25557
2018-09-05 11:35:47 -07:00
b0cb134815 feat(ivy): implement ngcc build marker (#25557)
`ngcc` adds marker files to each folder that has been
compiled, containing the version of the ngcc used.

When compiling, it will ignore folders that contain these
marker files, as long as the version matches.

PR Close #25557
2018-09-05 11:35:47 -07:00
2a672a97ab fix(upgrade): trigger $destroy event on upgraded component element (#25357)
Fixes #25334

PR Close #25357
2018-09-05 11:35:14 -07:00
71007ef9b2 refactor(upgrade): share code for destroying upgraded components between dynamic and static (#25357)
PR Close #25357
2018-09-05 11:35:14 -07:00
51c26b8afb test: remove deprecated Buffer usage in sourcemap test (#25805)
PR Close #25805
2018-09-05 09:38:48 -07:00
1e7a873cf4 build(bazel): remove unused angular.tsconfig.json integration/bazel test (#25778)
PR Close #25778
2018-09-05 09:38:19 -07:00
13b8399d0c test(docs-infra): fix double-slash in URL of aio_monitoring test (#25641)
As part of the tests run in the CircleCI `aio_monitoring` job, we need
to retrieve `sitemap.xml` from the site under test. Previously, the URL
used to retrieve that contained a double-slash (`//`). At some point,
Firebase (which is used for hosting the site) stopped normalizing
double-slashes to a single slash, causing the test to fail.

This commit fixes the problem by ensuring that the constructed URLs do
not contain double-slashes.

PR Close #25641
2018-09-05 09:37:55 -07:00
010e35d995 feat(router): warn if navigation triggered outside Angular zone (#24959)
closes #15770, closes #15946, closes #24728

PR Close #24959
2018-09-05 09:35:14 -07:00
234661b3d6 feat(docs-infra): disable "status" selector in API list when displaying only packages (#25718)
Closes #25708

PR Close #25718
2018-09-05 09:28:28 -07:00
4a04ab8823 build(docs-infra): do not render internals in package API pages (#25723)
Closes #24493

PR Close #25723
2018-09-05 09:28:05 -07:00
a417b2b419 fix(ivy): detect frozen flyweight objects in definitions and unfreeze (#25755)
defineComponent() and friends can return a flyweight EMPTY object for
specific fields when they contain no data. InheritDefinitionFeature
was attempting to write into these flyweight objects, which have been
protected with Object.freeze().

This commit adds detection to InheritDefinitionFeature to identify the
frozen objects.

PR Close #25755
2018-09-05 09:27:41 -07:00
2c66523222 docs(changelog): fix version 6.1.5 typo (#25760)
PR Close #25760
2018-09-05 09:27:15 -07:00
25c1f331d6 refactor(docs-infra): bump polyfills payload limit (#25806)
PR Close #25806
2018-09-05 09:23:10 -07:00
59aab14394 refactor(docs-infra): simplify custom-element polyfill setup (#25806)
PR Close #25806
2018-09-05 09:23:10 -07:00
c1ae3c16e8 build(docs-infra): ensure root node_modules exists (#25811)
Now that the doc-gen parses the imports of TS source
files we need to ensure that the root node_modules
exists. Otherwise running `yarn docs` produces an
obscure error:

```
Error: No SourceFile found with path node_modules/@types/jasmine/index.d.ts
```

Closes #25759

PR Close #25811
2018-09-05 09:22:46 -07:00
51c0d9cae9 style(ivy): remove unused ivy code (#25780)
PR Close #25780
2018-09-04 12:12:04 -07:00
08dfbc5475 fix(ivy): reexport __POST_NGCC__ symbols as private to prevent DCE in FESM (#25780)
While creating FESM files, rollup usually drops all unused symbols.
All *__POST_NGCC__ are unused unless ngcc rewires stuff. To prevent this DCE
we reexport them as private symbols. If ngcc is not used, these symbols will
be dropped when we optimize an application bundle.

We don't have an infrastructure to test this fix, so I just manually inspected
the bundles before and after to verify that the fix works.

PR Close #25780
2018-09-04 12:12:04 -07:00
2f1bc1aa1a docs: add pwa keyword to service worker page (#25725)
PR Close #25725
2018-09-04 12:09:54 -07:00
cc29b9cf93 fix(ivy): use globally unique names for i18n constants (#25689)
Closure compiler requires that the i18n message constants of the form

const MSG_XYZ = goog.getMessage('...');

have names that are unique across an entire compilation, even if the
variables themselves are local to a given module. This means that in
practice these names must be unique in a codebase.

The best way to guarantee this requirement is met is to encode the
relative file name of the file into which the constant is being written
into the constant name itself. This commit implements that solution.

PR Close #25689
2018-09-04 12:09:29 -07:00
bd0eb0d1d4 docs: correct misspellings and add missing punctuation in tutorial (#25676)
PR Close #25676
2018-09-04 12:08:52 -07:00
e84da1981d ci: ensure build-packages-dist works on OS/X bash (#25591)
PR Close #25591
2018-09-04 12:08:24 -07:00
abd29f5049 docs(forms): update API reference for reactive and template-driven forms modules (#25687)
PR Close #25687
2018-08-31 13:37:40 -07:00
6def18a95e fix(ivy): support directive outputs on ng-template (#25717)
Compiler part of #25698
Fixes #25697

PR Close #25717
2018-08-31 13:37:16 -07:00
34be51898d fix(ivy): support host bindings on dynamically created components (#25765)
PR Close #25765
2018-08-31 13:36:53 -07:00
1e3460be0b refactor(ivy): remove obsolete types (#25767)
In the past factories could return an array with content queries
but we no longer manage queries in factory functions.

PR Close #25767
2018-08-31 13:36:22 -07:00
31349fde90 build(bazel): make resolveTypeReferenceDirectives override work with both ts 2.9 & ts 3.0 (#25581)
PR Close #25581
2018-08-31 11:12:03 -07:00
910381ddbd build(bazel): fix bazel types reference directive resolves (#25581)
PR Close #25581
2018-08-31 11:12:03 -07:00
20b9c61d4c perf(ivy): speed up ngcc ivy switch processing (#25534)
Only parse the AST for ngcc ivy switch constants
if the marker is not found in the module text.

PR Close #25534
2018-08-31 09:47:50 -07:00
e964319fe9 test(ivy): test Esm5Renderer.getSwitchableDeclarations (#25534)
Also incorporates a refactoring of the tests to make them less fragile.

PR Close #25534
2018-08-31 09:47:50 -07:00
26cd9f5433 feat(ivy): implement Renderer.getSwitchableDeclarations (#25534)
This supports the "ngcc ivy switch" specified in #25238.

PR Close #25534
2018-08-31 09:47:50 -07:00
e73e864f87 test(ivy): refactor Esm5Renderer tests to make them less fragile (#25534)
PR Close #25534
2018-08-31 09:47:50 -07:00
73047483a1 test(ivy): refactor Esm2015Renderer tests to make them less fragile (#25534)
PR Close #25534
2018-08-31 09:47:50 -07:00
6f168b7a0f feat(ivy): implement NgccReflectionHost.getSwitchableDeclarations() (#25534)
This method will be used to find all the places where the "ivy switch"
will occur. See #25238

PR Close #25534
2018-08-31 09:47:49 -07:00
a469c2c412 feat(ivy): produce contextual diagnostics in ngtsc mode (#25647)
TypeScript has a more modern diagnostic emit function which produces
contextually annotated error information, using colors in the console
to indicate where in the code the error occurs.

This commit swiches ngtsc to use this format for diagnostics when
emitting them after a failed compilation.

PR Close #25647
2018-08-31 09:43:31 -07:00
38f624d7e3 feat(ivy): output diagnostics for many errors in ngtsc (#25647)
This commit takes the first steps towards ngtsc producing real
TypeScript diagnostics instead of simply throwing errors when
encountering incorrect code.

A new class is introduced, FatalDiagnosticError, which can be thrown by
handlers whenever a condition in the code is encountered which by
necessity prevents the class from being compiled. This error type is
convertable to a ts.Diagnostic which represents the type and source of
the error.

Error codes are introduced for Angular errors, and are prefixed with -99
(so error code 1001 becomes -991001) to distinguish them from other TS
errors.

A function is provided which will read TS diagnostic output and convert
the TS errors to NG errors if they match this negative error code
format.

PR Close #25647
2018-08-31 09:43:30 -07:00
b424b3187e fix(compiler): add hostVars and support pure functions in host bindings (#25626)
PR Close #25626
2018-08-31 09:42:58 -07:00
00f13110be feat(ivy): support injecting Renderer2 (#25523)
PR Close #25523
2018-08-31 09:42:36 -07:00
ccb4a396f0 test(docs-infra): test that the "suggest edit" buttons are visible where expected (#24378)
PR Close #24378
2018-08-31 09:42:10 -07:00
d539122466 refactor(docs-infra): refactor templates (#24378)
PR Close #24378
2018-08-31 09:42:10 -07:00
b89a7dd4a2 fix(docs-infra): show "suggest edits" only for /guide and /tutorial dirs (#24378)
PR Close #24378
2018-08-31 09:42:10 -07:00
9533cc9809 feat(docs-infra): add "suggest edits" feature to all docs (#24378)
PR Close #24378
2018-08-31 09:42:10 -07:00
06d04002fd fix(benchpress): Use performance.mark() instead of console.time() (#24114)
Previously, benchpress would use `console.time()` and
`console.timeEnd()` to measure the start and end of a test in the
performance log. This used to work over navigations - if you called
`console.time(id)` then navigated to a different page, calling
`console.timeEnd(id)` would still insert an event in the performance
log.

As of Chrome 65, this is no longer the case. `console.timeEnd(id)` will
simply not insert an event in the performance log unless
`console.time(id)` was called on the same page. Likewise, using
`performance.measure()` does not work if the starting mark was on a
different page.

This simple workaround uses `performance.mark()` to insert events in the
performance log at the start and end of the test. Benchpress looks for
'-bpstart' and '-bpend' in the name of the performance mark, and
normalizes that to the start and end events expected by PerflogMetric

PR Close #24114
2018-08-30 21:33:40 -07:00
6143da66b2 fix(elements): add compiler dependency (#24861)
PR Close #24861
2018-08-30 21:33:14 -07:00
a080ffc743 fix(elements): add compiler to integration (#24861)
PR Close #24861
2018-08-30 21:33:14 -07:00
a8210d010b fix(elements): strict null checks (#24861)
PR Close #24861
2018-08-30 21:33:14 -07:00
c9844a2f01 feat(elements): enable Shadow DOM v1 and slots (#24861)
When using ViewEncapsulation.ShadowDom, Angular will not remove the child nodes of the DOM node a root Component is bootstrapped into. This enables developers building Angular Elements to use the `<slot>` element to do native content projection.

PR Close #24861
2018-08-30 21:33:14 -07:00
4815b92495 test(elements): add basic integration test for angular elements (#24861)
PR Close #24861
2018-08-30 21:33:14 -07:00
d76a7d6f7c test(core): update Web Platform feature detection (#24861)
PR Close #24861
2018-08-30 21:33:14 -07:00
6e6489a408 ci: update firefox version to 61 (#24861)
PR Close #24861
2018-08-30 21:33:14 -07:00
1d8e821276 ci: update chrome version to 67 (#24861)
PR Close #24861
2018-08-30 21:33:14 -07:00
6e828bba88 fix(core): do not clear element content when using shadow dom (#24861)
PR Close #24861
2018-08-30 21:33:13 -07:00
1f59f2f04d fix(core): size regression with closure compiler (#25531)
By pulling in `compiler` into `core` the `compiler` was not
100% tree-shakable and about  8KB of code was retained
when tree-shaken with closure.

PR Close #25531
2018-08-30 21:22:40 -07:00
371df35624 fix(ivy): register to directive outputs on ng-template / ng-container (#25698)
Runtime part of #25697

PR Close #25698
2018-08-30 21:22:01 -07:00
3809e0fcae fix(bazel): protractor rule should include *.e2e-spec.js (#25701)
PR Close #25701
2018-08-30 21:21:27 -07:00
b06f1c0087 refactor(ivy): remove duplicate global (#25756)
PR Close #25756
2018-08-30 21:20:15 -07:00
2379ad1a4b docs: edit and organize di guide (#21915)
PR Close #21915
2018-08-30 13:15:47 -04:00
180 changed files with 6430 additions and 2488 deletions

View File

@ -13,7 +13,7 @@
# If you change the `docker_image` version, also change the `cache_key` suffix and the version of # If you change the `docker_image` version, also change the `cache_key` suffix and the version of
# `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file. # `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file.
var_1: &docker_image angular/ngcontainer:0.4.0 var_1: &docker_image angular/ngcontainer:0.4.0
var_2: &cache_key v2-angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.4.0 var_2: &cache_key v2-angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-bust1-0.4.0
# Define common ENV vars # Define common ENV vars
var_3: &define_env_vars var_3: &define_env_vars

View File

@ -1,3 +1,47 @@
<a name="7.0.0-beta.5"></a>
# [7.0.0-beta.5](https://github.com/angular/angular/compare/7.0.0-beta.4...7.0.0-beta.5) (2018-09-06)
### Bug Fixes
* **bazel:** protractor rule should include *.e2e-spec.js ([#25701](https://github.com/angular/angular/issues/25701)) ([3809e0f](https://github.com/angular/angular/commit/3809e0f))
* **benchpress:** Use performance.mark() instead of console.time() ([#24114](https://github.com/angular/angular/issues/24114)) ([06d0400](https://github.com/angular/angular/commit/06d0400))
* **compiler:** add hostVars and support pure functions in host bindings ([#25626](https://github.com/angular/angular/issues/25626)) ([b424b31](https://github.com/angular/angular/commit/b424b31))
* **core:** do not clear element content when using shadow dom ([#24861](https://github.com/angular/angular/issues/24861)) ([6e828bb](https://github.com/angular/angular/commit/6e828bb))
* **core:** size regression with closure compiler ([#25531](https://github.com/angular/angular/issues/25531)) ([1f59f2f](https://github.com/angular/angular/commit/1f59f2f))
* **elements:** add compiler dependency ([#24861](https://github.com/angular/angular/issues/24861)) ([6143da6](https://github.com/angular/angular/commit/6143da6))
* **elements:** add compiler to integration ([#24861](https://github.com/angular/angular/issues/24861)) ([a080ffc](https://github.com/angular/angular/commit/a080ffc))
* **elements:** strict null checks ([#24861](https://github.com/angular/angular/issues/24861)) ([a8210d0](https://github.com/angular/angular/commit/a8210d0))
* **upgrade:** trigger `$destroy` event on upgraded component element ([#25357](https://github.com/angular/angular/issues/25357)) ([2a672a9](https://github.com/angular/angular/commit/2a672a9)), closes [#25334](https://github.com/angular/angular/issues/25334)
### Features
* **elements:** enable Shadow DOM v1 and slots ([#24861](https://github.com/angular/angular/issues/24861)) ([c9844a2](https://github.com/angular/angular/commit/c9844a2))
* **router:** warn if navigation triggered outside Angular zone ([#24959](https://github.com/angular/angular/issues/24959)) ([010e35d](https://github.com/angular/angular/commit/010e35d)), closes [#15770](https://github.com/angular/angular/issues/15770) [#15946](https://github.com/angular/angular/issues/15946) [#24728](https://github.com/angular/angular/issues/24728)
<a name="6.1.7"></a>
## [6.1.7](https://github.com/angular/angular/compare/6.1.6...6.1.7) (2018-09-06)
### Bug Fixes
* **bazel:** protractor rule should include *.e2e-spec.js ([#25701](https://github.com/angular/angular/issues/25701)) ([ed6b68b](https://github.com/angular/angular/commit/ed6b68b))
* **core:** size regression with closure compiler ([#25531](https://github.com/angular/angular/issues/25531)) ([ebcf762](https://github.com/angular/angular/commit/ebcf762))
* **docs-infra:** show "suggest edits" only for /guide and /tutorial dirs ([#24378](https://github.com/angular/angular/issues/24378)) ([66b7870](https://github.com/angular/angular/commit/66b7870))
* **upgrade:** trigger `$destroy` event on upgraded component element ([#25357](https://github.com/angular/angular/issues/25357)) ([82e0676](https://github.com/angular/angular/commit/82e0676)), closes [#25334](https://github.com/angular/angular/issues/25334)
### Features
* **docs-infra:** add "suggest edits" feature to all docs ([#24378](https://github.com/angular/angular/issues/24378)) ([82088a8](https://github.com/angular/angular/commit/82088a8))
* **docs-infra:** disable "status" selector in API list when displaying only packages ([#25718](https://github.com/angular/angular/issues/25718)) ([6f7df8a](https://github.com/angular/angular/commit/6f7df8a)), closes [#25708](https://github.com/angular/angular/issues/25708)
* **router:** warn if navigation triggered outside Angular zone ([#24959](https://github.com/angular/angular/issues/24959)) ([23a96dc](https://github.com/angular/angular/commit/23a96dc)), closes [#15770](https://github.com/angular/angular/issues/15770) [#15946](https://github.com/angular/angular/issues/15946) [#24728](https://github.com/angular/angular/issues/24728)
<a name="7.0.0-beta.4"></a> <a name="7.0.0-beta.4"></a>
# [7.0.0-beta.4](https://github.com/angular/angular/compare/7.0.0-beta.3...7.0.0-beta.4) (2018-08-29) # [7.0.0-beta.4](https://github.com/angular/angular/compare/7.0.0-beta.3...7.0.0-beta.4) (2018-08-29)
@ -21,7 +65,7 @@
* **bazel:** only lookup amd module-name tags in .d.ts files ([#25710](https://github.com/angular/angular/issues/25710)) ([7aff364](https://github.com/angular/angular/commit/7aff364)) * **bazel:** only lookup amd module-name tags in .d.ts files ([#25710](https://github.com/angular/angular/issues/25710)) ([7aff364](https://github.com/angular/angular/commit/7aff364))
Note: the 1.6.5 release on npm accidentally glitched-out midway, so we cut 6.1.6 instead. sorry! :-) Note: the 6.1.5 release on npm accidentally glitched-out midway, so we cut 6.1.6 instead. sorry! :-)
<a name="7.0.0-beta.3"></a> <a name="7.0.0-beta.3"></a>
# [7.0.0-beta.3](https://github.com/angular/angular/compare/7.0.0-beta.2...7.0.0-beta.3) (2018-08-22) # [7.0.0-beta.3](https://github.com/angular/angular/compare/7.0.0-beta.2...7.0.0-beta.3) (2018-08-22)

View File

@ -12,9 +12,9 @@ http_archive(
http_archive( http_archive(
name = "build_bazel_rules_typescript", name = "build_bazel_rules_typescript",
url = "https://github.com/bazelbuild/rules_typescript/archive/0.16.1.zip", url = "https://github.com/bazelbuild/rules_typescript/archive/0.16.2.zip",
strip_prefix = "rules_typescript-0.16.1", strip_prefix = "rules_typescript-0.16.2",
sha256 = "5b2b0bc63cfcffe7bf97cad2dad3b26a73362f806de66207051f66c87956a995", sha256 = "31601b777840fbf600dbd1893ade0d1de37166e7ba52b90735b107cfb67e38c7",
) )
load("@build_bazel_rules_typescript//:package.bzl", "rules_typescript_dependencies") load("@build_bazel_rules_typescript//:package.bzl", "rules_typescript_dependencies")
rules_typescript_dependencies() rules_typescript_dependencies()

View File

@ -36,3 +36,7 @@
<div class="di-component"> <div class="di-component">
<app-parent-finder></app-parent-finder> <app-parent-finder></app-parent-finder>
</div> </div>
<div class="di-component">
<app-storage></app-storage>
</div>

View File

@ -9,9 +9,6 @@ import { UserService } from './user.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
// #docregion providers
providers: [ LoggerService, UserContextService, UserService ]
// #enddocregion providers
}) })
export class AppComponent { export class AppComponent {
// #enddocregion import-services // #enddocregion import-services

View File

@ -31,6 +31,7 @@ import { ParentFinderComponent,
BarryComponent, BarryComponent,
BethComponent, BethComponent,
BobComponent } from './parent-finder.component'; BobComponent } from './parent-finder.component';
import { StorageComponent } from './storage.component';
const declarations = [ const declarations = [
AppComponent, AppComponent,
@ -63,6 +64,7 @@ const c_components = [
a_components, a_components,
b_components, b_components,
c_components, c_components,
StorageComponent,
], ],
bootstrap: [ AppComponent ], bootstrap: [ AppComponent ],
// #docregion providers // #docregion providers

View File

@ -5,7 +5,9 @@ import { Injectable } from '@angular/core';
import { LoggerService } from './logger.service'; import { LoggerService } from './logger.service';
// #docregion date-logger-service // #docregion date-logger-service
@Injectable() @Injectable({
providedIn: 'root'
})
// #docregion date-logger-service-signature // #docregion date-logger-service-signature
export class DateLoggerService extends LoggerService export class DateLoggerService extends LoggerService
// #enddocregion date-logger-service-signature // #enddocregion date-logger-service-signature

View File

@ -2,7 +2,9 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Hero } from './hero'; import { Hero } from './hero';
@Injectable() @Injectable({
providedIn: 'root'
})
export class HeroService { export class HeroService {
// TODO: move to database // TODO: move to database

View File

@ -1,7 +1,9 @@
// #docregion // #docregion
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
@Injectable() @Injectable({
providedIn: 'root'
})
export class LoggerService { export class LoggerService {
logs: string[] = []; logs: string[] = [];

View File

@ -0,0 +1,38 @@
// #docregion
import { Component, OnInit, Self, SkipSelf } from '@angular/core';
import { BROWSER_STORAGE, BrowserStorageService } from './storage.service';
@Component({
selector: 'app-storage',
template: `
Open the inspector to see the local/session storage keys:
<h3>Session Storage</h3>
<button (click)="setSession()">Set Session Storage</button>
<h3>Local Storage</h3>
<button (click)="setLocal()">Set Local Storage</button>
`,
providers: [
BrowserStorageService,
{ provide: BROWSER_STORAGE, useFactory: () => sessionStorage }
]
})
export class StorageComponent implements OnInit {
constructor(
@Self() private sessionStorageService: BrowserStorageService,
@SkipSelf() private localStorageService: BrowserStorageService,
) { }
ngOnInit() {
}
setSession() {
this.sessionStorageService.set('hero', 'Mr. Nice - Session');
}
setLocal() {
this.localStorageService.set('hero', 'Mr. Nice - Local');
}
}

View File

@ -0,0 +1,34 @@
// #docregion
import { Inject, Injectable, InjectionToken } from '@angular/core';
// #docregion storage-token
export const BROWSER_STORAGE = new InjectionToken<Storage>('Browser Storage', {
providedIn: 'root',
factory: () => localStorage
});
// #enddocregion storage-token
// #docregion inject-storage-token
@Injectable({
providedIn: 'root'
})
export class BrowserStorageService {
constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {}
get(key: string) {
this.storage.getItem(key);
}
set(key: string, value: string) {
this.storage.setItem(key, value);
}
remove(key: string) {
this.storage.removeItem(key);
}
clear() {
this.storage.clear();
}
}
// #enddocregion inject-storage-token

View File

@ -6,7 +6,9 @@ import { LoggerService } from './logger.service';
import { UserService } from './user.service'; import { UserService } from './user.service';
// #docregion injectables, injectable // #docregion injectables, injectable
@Injectable() @Injectable({
providedIn: 'root'
})
export class UserContextService { export class UserContextService {
// #enddocregion injectables, injectable // #enddocregion injectables, injectable
name: string; name: string;

View File

@ -1,7 +1,9 @@
// #docregion // #docregion
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
@Injectable() @Injectable({
providedIn: 'root'
})
export class UserService { export class UserService {
getUserById(userId: number): any { getUserById(userId: number): any {

View File

@ -1,11 +1,10 @@
// #docregion // #docregion
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HEROES } from './mock-heroes'; import { HEROES } from './mock-heroes';
@Injectable({ @Injectable({
// we declare that this service should be created // we declare that this service should be created
// by the root application injector. // by the root application injector.
providedIn: 'root', providedIn: 'root',
}) })
export class HeroService { export class HeroService {

View File

@ -1,12 +1,11 @@
// #docregion // #docregion
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HeroModule } from './hero.module'; import { HeroModule } from './hero.module';
import { HEROES } from './mock-heroes'; import { HEROES } from './mock-heroes';
@Injectable({ @Injectable({
// we declare that this service should be created // we declare that this service should be created
// by any injector that includes HeroModule. // by any injector that includes HeroModule.
providedIn: HeroModule, providedIn: HeroModule,
}) })
export class HeroService { export class HeroService {

View File

@ -1,6 +1,7 @@
// #docplaster // #docplaster
// #docregion, v1 // #docregion, v1
import { Component } from '@angular/core'; import { Component } from '@angular/core';
// #enddocregion v1 // #enddocregion v1
import { HeroService } from './hero.service'; import { HeroService } from './hero.service';

View File

@ -1,7 +1,9 @@
// #docregion // #docregion
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
@Injectable() @Injectable({
providedIn: 'root'
})
export class Logger { export class Logger {
logs: string[] = []; // capture logs for testing logs: string[] = []; // capture logs for testing

View File

@ -11,7 +11,9 @@ export class User {
let alice = new User('Alice', true); let alice = new User('Alice', true);
let bob = new User('Bob', false); let bob = new User('Bob', false);
@Injectable() @Injectable({
providedIn: 'root'
})
export class UserService { export class UserService {
user = bob; // initial user is Bob user = bob; // initial user is Bob

View File

@ -3,23 +3,18 @@ import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { HeroTaxReturnComponent } from './hero-tax-return.component'; import { HeroTaxReturnComponent } from './hero-tax-return.component';
import { HeroesListComponent } from './heroes-list.component'; import { HeroesListComponent } from './heroes-list.component';
import { HeroesService } from './heroes.service'; import { VillainsListComponent } from './villains-list.component';
import { VillainsListComponent } from './villains-list.component';
import { carComponents, carServices } from './car.components'; import { carComponents } from './car.components';
@NgModule({ @NgModule({
imports: [ imports: [
BrowserModule, BrowserModule,
FormsModule FormsModule
], ],
providers: [
carServices,
HeroesService
],
declarations: [ declarations: [
AppComponent, AppComponent,
carComponents, carComponents,

View File

@ -21,13 +21,17 @@ export class Tires {
} }
//// Engine services /// //// Engine services ///
@Injectable() @Injectable({
providedIn: 'root'
})
export class EngineService { export class EngineService {
id = 'E1'; id = 'E1';
getEngine() { return new Engine(); } getEngine() { return new Engine(); }
} }
@Injectable() @Injectable({
providedIn: 'root'
})
export class EngineService2 { export class EngineService2 {
id = 'E2'; id = 'E2';
getEngine() { getEngine() {
@ -38,14 +42,18 @@ export class EngineService2 {
} }
//// Tire services /// //// Tire services ///
@Injectable() @Injectable({
providedIn: 'root'
})
export class TiresService { export class TiresService {
id = 'T1'; id = 'T1';
getTires() { return new Tires(); } getTires() { return new Tires(); }
} }
/// Car Services /// /// Car Services ///
@Injectable() @Injectable({
providedIn: 'root'
})
export class CarService { export class CarService {
id = 'C1'; id = 'C1';
constructor( constructor(
@ -63,7 +71,9 @@ export class CarService {
} }
} }
@Injectable() @Injectable({
providedIn: 'root'
})
export class CarService2 extends CarService { export class CarService2 extends CarService {
id = 'C2'; id = 'C2';
constructor( constructor(
@ -78,7 +88,9 @@ export class CarService2 extends CarService {
} }
} }
@Injectable() @Injectable({
providedIn: 'root'
})
export class CarService3 extends CarService2 { export class CarService3 extends CarService2 {
id = 'C3'; id = 'C3';
constructor( constructor(

View File

@ -13,17 +13,19 @@ import { HeroTaxReturnService } from './hero-tax-return.service';
}) })
export class HeroTaxReturnComponent { export class HeroTaxReturnComponent {
message = ''; message = '';
@Output() close = new EventEmitter<void>(); @Output() close = new EventEmitter<void>();
get taxReturn(): HeroTaxReturn { get taxReturn(): HeroTaxReturn {
return this.heroTaxReturnService.taxReturn; return this.heroTaxReturnService.taxReturn;
} }
@Input() @Input()
set taxReturn (htr: HeroTaxReturn) { set taxReturn (htr: HeroTaxReturn) {
this.heroTaxReturnService.taxReturn = htr; this.heroTaxReturnService.taxReturn = htr;
} }
constructor(private heroTaxReturnService: HeroTaxReturnService ) { } constructor(private heroTaxReturnService: HeroTaxReturnService) { }
onCanceled() { onCanceled() {
this.flashMessage('Canceled'); this.flashMessage('Canceled');

View File

@ -4,7 +4,9 @@ import { Observable, Observer } from 'rxjs';
import { Hero, HeroTaxReturn } from './hero'; import { Hero, HeroTaxReturn } from './hero';
@Injectable() @Injectable({
providedIn: 'root'
})
export class HeroesService { export class HeroesService {
heroes: Hero[] = [ heroes: Hero[] = [
{ id: 1, name: 'RubberMan', tid: '082-27-5678'}, { id: 1, name: 'RubberMan', tid: '082-27-5678'},

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,312 @@
# Navigate the component tree with DI
Application components often need to share information.
You can often use loosely coupled techniques for sharing information,
such as data binding and service sharing,
but sometimes it makes sense for one component to have a direct reference to another component.
You need a direct reference, for instance, to access values or call methods on that component.
Obtaining a component reference is a bit tricky in Angular.
Angular components themselves do not have a tree that you can
inspect or navigate programmatically. The parent-child relationship is indirect,
established through the components' [view objects](guide/glossary#view).
Each component has a *host view*, and can have additional *embedded views*.
An embedded view in component A is the
host view of component B, which can in turn have embedded view.
This means that there is a [view hierarchy](guide/glossary#view-hierarchy) for each component,
of which that component's host view is the root.
There is an API for navigating *down* the view hierarchy.
Check out `Query`, `QueryList`, `ViewChildren`, and `ContentChildren`
in the [API Reference](api/).
There is no public API for acquiring a parent reference.
However, because every component instance is added to an injector's container,
you can use Angular dependency injection to reach a parent component.
This section describes some techniques for doing that.
{@a find-parent}
{@a known-parent}
### Find a parent component of known type
You use standard class injection to acquire a parent component whose type you know.
In the following example, the parent `AlexComponent` has several children including a `CathyComponent`:
{@a alex}
<code-example path="dependency-injection-in-action/src/app/parent-finder.component.ts" region="alex-1" title="parent-finder.component.ts (AlexComponent v.1)" linenums="false">
</code-example>
*Cathy* reports whether or not she has access to *Alex*
after injecting an `AlexComponent` into her constructor:
<code-example path="dependency-injection-in-action/src/app/parent-finder.component.ts" region="cathy" title="parent-finder.component.ts (CathyComponent)" linenums="false">
</code-example>
Notice that even though the [@Optional](guide/dependency-injection-in-action#optional) qualifier
is there for safety,
the <live-example name="dependency-injection-in-action"></live-example>
confirms that the `alex` parameter is set.
{@a base-parent}
### Unable to find a parent by its base class
What if you *don't* know the concrete parent component class?
A re-usable component might be a child of multiple components.
Imagine a component for rendering breaking news about a financial instrument.
For business reasons, this news component makes frequent calls
directly into its parent instrument as changing market data streams by.
The app probably defines more than a dozen financial instrument components.
If you're lucky, they all implement the same base class
whose API your `NewsComponent` understands.
<div class="alert is-helpful">
Looking for components that implement an interface would be better.
That's not possible because TypeScript interfaces disappear
from the transpiled JavaScript, which doesn't support interfaces.
There's no artifact to look for.
</div>
This isn't necessarily good design.
This example is examining *whether a component can
inject its parent via the parent's base class*.
The sample's `CraigComponent` explores this question. [Looking back](#alex),
you see that the `Alex` component *extends* (*inherits*) from a class named `Base`.
<code-example path="dependency-injection-in-action/src/app/parent-finder.component.ts" region="alex-class-signature" title="parent-finder.component.ts (Alex class signature)" linenums="false">
</code-example>
The `CraigComponent` tries to inject `Base` into its `alex` constructor parameter and reports if it succeeded.
<code-example path="dependency-injection-in-action/src/app/parent-finder.component.ts" region="craig" title="parent-finder.component.ts (CraigComponent)" linenums="false">
</code-example>
Unfortunately, this does'nt work.
The <live-example name="dependency-injection-in-action"></live-example>
confirms that the `alex` parameter is null.
*You cannot inject a parent by its base class.*
{@a class-interface-parent}
### Find a parent by its class interface
You can find a parent component with a [class interface](guide/dependency-injection-in-action#class-interface).
The parent must cooperate by providing an *alias* to itself in the name of a class interface token.
Recall that Angular always adds a component instance to its own injector;
that's why you could inject *Alex* into *Cathy* [earlier](#known-parent).
Write an [*alias provider*](guide/dependency-injection-in-action#useexisting)&mdash;a `provide` object literal with a `useExisting`
definition&mdash;that creates an *alternative* way to inject the same component instance
and add that provider to the `providers` array of the `@Component()` metadata for the `AlexComponent`.
{@a alex-providers}
<code-example path="dependency-injection-in-action/src/app/parent-finder.component.ts" region="alex-providers" title="parent-finder.component.ts (AlexComponent providers)" linenums="false">
</code-example>
[Parent](#parent-token) is the provider's class interface token.
The [*forwardRef*](guide/dependency-injection-in-action#forwardref) breaks the circular reference you just created by having the `AlexComponent` refer to itself.
*Carol*, the third of *Alex*'s child components, injects the parent into its `parent` parameter,
the same way you've done it before.
<code-example path="dependency-injection-in-action/src/app/parent-finder.component.ts" region="carol-class" title="parent-finder.component.ts (CarolComponent class)" linenums="false">
</code-example>
Here's *Alex* and family in action.
<figure>
<img src="generated/images/guide/dependency-injection-in-action/alex.png" alt="Alex in action">
</figure>
{@a parent-tree}
### Find a parent in a tree with _@SkipSelf()_
Imagine one branch of a component hierarchy: *Alice* -> *Barry* -> *Carol*.
Both *Alice* and *Barry* implement the `Parent' class interface.
*Barry* is the problem. He needs to reach his parent, *Alice*, and also be a parent to *Carol*.
That means he must both *inject* the `Parent` class interface to get *Alice* and
*provide* a `Parent` to satisfy *Carol*.
Here's *Barry*.
<code-example path="dependency-injection-in-action/src/app/parent-finder.component.ts" region="barry" title="parent-finder.component.ts (BarryComponent)" linenums="false">
</code-example>
*Barry*'s `providers` array looks just like [*Alex*'s](#alex-providers).
If you're going to keep writing [*alias providers*](guide/dependency-injection-in-action#useexisting) like this you should create a [helper function](#provideparent).
For now, focus on *Barry*'s constructor.
<code-tabs>
<code-pane title="Barry's constructor" path="dependency-injection-in-action/src/app/parent-finder.component.ts" region="barry-ctor">
</code-pane>
<code-pane title="Carol's constructor" path="dependency-injection-in-action/src/app/parent-finder.component.ts" region="carol-ctor">
</code-pane>
</code-tabs>
It's identical to *Carol*'s constructor except for the additional `@SkipSelf` decorator.
`@SkipSelf` is essential for two reasons:
1. It tells the injector to start its search for a `Parent` dependency in a component *above* itself,
which *is* what parent means.
2. Angular throws a cyclic dependency error if you omit the `@SkipSelf` decorator.
`Cannot instantiate cyclic dependency! (BethComponent -> Parent -> BethComponent)`
Here's *Alice*, *Barry*, and family in action.
<figure>
<img src="generated/images/guide/dependency-injection-in-action/alice.png" alt="Alice in action">
</figure>
{@a parent-token}
### Parent class interface
You [learned earlier](guide/dependency-injection-in-action#class-interface) that a class interface is an abstract class used as an interface rather than as a base class.
The example defines a `Parent` class interface.
<code-example path="dependency-injection-in-action/src/app/parent-finder.component.ts" region="parent" title="parent-finder.component.ts (Parent class-interface)" linenums="false">
</code-example>
The `Parent` class interface defines a `name` property with a type declaration but *no implementation*.
The `name` property is the only member of a parent component that a child component can call.
Such a narrow interface helps decouple the child component class from its parent components.
A component that could serve as a parent *should* implement the class interface as the `AliceComponent` does.
<code-example path="dependency-injection-in-action/src/app/parent-finder.component.ts" region="alice-class-signature" title="parent-finder.component.ts (AliceComponent class signature)" linenums="false">
</code-example>
Doing so adds clarity to the code. But it's not technically necessary.
Although `AlexComponent` has a `name` property, as required by its `Base` class,
its class signature doesn't mention `Parent`.
<code-example path="dependency-injection-in-action/src/app/parent-finder.component.ts" region="alex-class-signature" title="parent-finder.component.ts (AlexComponent class signature)" linenums="false">
</code-example>
<div class="alert is-helpful">
`AlexComponent` *should* implement `Parent` as a matter of proper style.
It doesn't in this example *only* to demonstrate that the code will compile and run without the interface.
</div>
{@a provideparent}
### `provideParent()` helper function
Writing variations of the same parent *alias provider* gets old quickly,
especially this awful mouthful with a [*forwardRef*](guide/dependency-injection-in-action#forwardref).
<code-example path="dependency-injection-in-action/src/app/parent-finder.component.ts" region="alex-providers" title="dependency-injection-in-action/src/app/parent-finder.component.ts" linenums="false">
</code-example>
You can extract that logic into a helper function like the following.
<code-example path="dependency-injection-in-action/src/app/parent-finder.component.ts" region="provide-the-parent" title="dependency-injection-in-action/src/app/parent-finder.component.ts" linenums="false">
</code-example>
Now you can add a simpler, more meaningful parent provider to your components.
<code-example path="dependency-injection-in-action/src/app/parent-finder.component.ts" region="alice-providers" title="dependency-injection-in-action/src/app/parent-finder.component.ts" linenums="false">
</code-example>
You can do better. The current version of the helper function can only alias the `Parent` class interface.
The application might have a variety of parent types, each with its own class interface token.
Here's a revised version that defaults to `parent` but also accepts an optional second parameter for a different parent class interface.
<code-example path="dependency-injection-in-action/src/app/parent-finder.component.ts" region="provide-parent" title="dependency-injection-in-action/src/app/parent-finder.component.ts" linenums="false">
</code-example>
And here's how you could use it with a different parent type.
<code-example path="dependency-injection-in-action/src/app/parent-finder.component.ts" region="beth-providers" title="dependency-injection-in-action/src/app/parent-finder.component.ts" linenums="false">
</code-example>

View File

@ -1,167 +0,0 @@
# The Dependency Injection pattern
**Dependency injection** is an important application design pattern.
It's used so widely that almost everyone just calls it _DI_.
Angular has its own dependency injection framework, and
you really can't build an Angular application without it.
This page covers what DI is and why it's useful.
When you've learned the general pattern, you're ready to turn to
the [Angular Dependency Injection](guide/dependency-injection) guide to see how it works in an Angular app.
{@a why-di }
## Why dependency injection?
To understand why dependency injection is so important, consider an example without it.
Imagine writing the following code:
<code-example path="dependency-injection/src/app/car/car-no-di.ts" region="car" title="src/app/car/car.ts (without DI)">
</code-example>
The `Car` class creates everything it needs inside its constructor.
What's the problem?
The problem is that the `Car` class is brittle, inflexible, and hard to test.
This `Car` needs an engine and tires. Instead of asking for them,
the `Car` constructor instantiates its own copies from
the very specific classes `Engine` and `Tires`.
What if the `Engine` class evolves and its constructor requires a parameter?
That would break the `Car` class and it would stay broken until you rewrote it along the lines of
`this.engine = new Engine(theNewParameter)`.
The `Engine` constructor parameters weren't even a consideration when you first wrote `Car`.
You may not anticipate them even now.
But you'll *have* to start caring because
when the definition of `Engine` changes, the `Car` class must change.
That makes `Car` brittle.
What if you want to put a different brand of tires on your `Car`? Too bad.
You're locked into whatever brand the `Tires` class creates. That makes the
`Car` class inflexible.
Right now each new car gets its own `engine`. It can't share an `engine` with other cars.
While that makes sense for an automobile engine,
surely you can think of other dependencies that should be shared, such as the onboard
wireless connection to the manufacturer's service center. This `Car` lacks the flexibility
to share services that have been created previously for other consumers.
When you write tests for `Car` you're at the mercy of its hidden dependencies.
Is it even possible to create a new `Engine` in a test environment?
What does `Engine` depend upon? What does that dependency depend on?
Will a new instance of `Engine` make an asynchronous call to the server?
You certainly don't want that going on during tests.
What if the `Car` should flash a warning signal when tire pressure is low?
How do you confirm that it actually does flash a warning
if you can't swap in low-pressure tires during the test?
You have no control over the car's hidden dependencies.
When you can't control the dependencies, a class becomes difficult to test.
How can you make `Car` more robust, flexible, and testable?
{@a ctor-injection}
That's super easy. Change the `Car` constructor to a version with DI:
<code-tabs>
<code-pane title="src/app/car/car.ts (excerpt with DI)" path="dependency-injection/src/app/car/car.ts" region="car-ctor">
</code-pane>
<code-pane title="src/app/car/car.ts (excerpt without DI)" path="dependency-injection/src/app/car/car-no-di.ts" region="car-ctor">
</code-pane>
</code-tabs>
See what happened? The definition of the dependencies are
now in the constructor.
The `Car` class no longer creates an `engine` or `tires`.
It just consumes them.
<div class="alert is-helpful">
This example leverages TypeScript's constructor syntax for declaring
parameters and properties simultaneously.
</div>
Now you can create a car by passing the engine and tires to the constructor.
<code-example path="dependency-injection/src/app/car/car-creations.ts" region="car-ctor-instantiation" linenums="false">
</code-example>
How cool is that?
The definition of the `engine` and `tire` dependencies are
decoupled from the `Car` class.
You can pass in any kind of `engine` or `tires` you like, as long as they
conform to the general API requirements of an `engine` or `tires`.
Now, if someone extends the `Engine` class, that is not `Car`'s problem.
<div class="alert is-helpful">
The _consumer_ of `Car` has the problem. The consumer must update the car creation code to
something like this:
<code-example path="dependency-injection/src/app/car/car-creations.ts" region="car-ctor-instantiation-with-param" linenums="false">
</code-example>
The critical point is this: the `Car` class did not have to change.
You'll take care of the consumer's problem shortly.
</div>
The `Car` class is much easier to test now because you are in complete control
of its dependencies.
You can pass mocks to the constructor that do exactly what you want them to do
during each test:
<code-example path="dependency-injection/src/app/car/car-creations.ts" region="car-ctor-instantiation-with-mocks" linenums="false">
</code-example>
**You just learned what dependency injection is**.
It's a coding pattern in which a class receives its dependencies from external
sources rather than creating them itself.
Cool! But what about that poor consumer?
Anyone who wants a `Car` must now
create all three parts: the `Car`, `Engine`, and `Tires`.
The `Car` class shed its problems at the consumer's expense.
You need something that takes care of assembling these parts.
You _could_ write a giant class to do that:
<code-example path="dependency-injection/src/app/car/car-factory.ts" title="src/app/car/car-factory.ts">
</code-example>
It's not so bad now with only three creation methods.
But maintaining it will be hairy as the application grows.
This factory is going to become a huge spiderweb of
interdependent factory methods!
Wouldn't it be nice if you could simply list the things you want to build without
having to define which dependency gets injected into what?
This is where the dependency injection framework comes into play.
Imagine the framework had something called an _injector_.
You register some classes with this injector, and it figures out how to create them.
When you need a `Car`, you simply ask the injector to get it for you and you're good to go.
<code-example path="dependency-injection/src/app/car/car-injector.ts" region="injector-call" title="src/app/car/car-injector.ts" linenums="false">
</code-example>
Everyone wins. The `Car` knows nothing about creating an `Engine` or `Tires`.
The consumer knows nothing about creating a `Car`.
You don't have a gigantic factory class to maintain.
Both `Car` and consumer simply ask for what they need and the injector delivers.
This is what a **dependency injection framework** is all about.
Now that you know what dependency injection is and appreciate its benefits,
turn to the [Angular Dependency Injection](guide/dependency-injection) guide to see how it is implemented in Angular.

View File

@ -0,0 +1,353 @@
# Dependency Providers
A dependency [provider](guide/glossary#provider) configures an injector
with a [DI token](guide/glossary#di-token),
which that injector uses to provide the concrete, runtime version of a dependency value.
The injector relies on the provider configuration to create instances of the dependencies
that it injects into components, directives, pipes, and other services.
You must configure an injector with a provider, or it won't know how to create the dependency.
The most obvious way for an injector to create an instance of a service class is with the class itself.
If you specify the service class itself as the provider token, the default behavior is for the injector to instantiate that class with `new`.
In the following typical example, the `Logger` class itself provides a `Logger` instance.
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-logger">
</code-example>
You can, however, configure an injector with an alternative provider,
in order to deliver some other object that provides the needed logging functionality.
For instance:
* You can provide a substitute class.
* You can provide a logger-like object.
* Your provider can call a logger factory function.
{@a provide}
## The `Provider` object literal
The class-provider syntax is a shorthand expression that expands
into a provider configuration, defined by the [`Provider` interface](api/core/Provider).
The following code snippets shows how a class that is given as the `providers` value is expanded into a full provider object.
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-logger">
</code-example>
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-3" >
</code-example>
The expanded provider configuration is an object literal with two properties.
* The `provide` property holds the [token](guide/dependency-injection#token)
that serves as the key for both locating a dependency value and configuring the injector.
* The second property is a provider definition object, which tells the injector how to create the dependency value.
The provider-definition key can be `useClass`, as in the example.
It can also be `useExisting`, `useValue`, or `useFactory`.
Each of these keys provides a different type of dependency, as discussed below.
{@a class-provider}
## Alternative class providers
Different classes can provide the same service.
For example, the following code tells the injector
to return a `BetterLogger` instance when the component asks for a logger
using the `Logger` token.
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-4" >
</code-example>
{@a class-provider-dependencies}
### Class providers with dependencies
Another class, `EvenBetterLogger`, might display the user name in the log message.
This logger gets the user from an injected `UserService` instance.
<code-example path="dependency-injection/src/app/providers.component.ts" region="EvenBetterLogger" linenums="false">
</code-example>
The injector needs providers for both this new logging service and its dependent `UserService`. Configure this alternative logger with the `useClass` provider-definition key, like `BetterLogger`. The following array specifies both providers in the `providers` metadata option of the parent module or component.
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-5" linenums="false">
</code-example>
{@a aliased-class-providers}
### Aliased class providers
Suppose an old component depends upon the `OldLogger` class.
`OldLogger` has the same interface as `NewLogger`, but for some reason
you can't update the old component to use it.
When the old component logs a message with `OldLogger`,
you want the singleton instance of `NewLogger` to handle it instead.
In this case, the dependency injector should inject that singleton instance
when a component asks for either the new or the old logger.
`OldLogger` should be an *alias* for `NewLogger`.
If you try to alias `OldLogger` to `NewLogger` with `useClass`, you end up with two different `NewLogger` instances in your app.
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-6a" linenums="false">
</code-example>
To make sure there is only one instance of `NewLogger`, alias `OldLogger` with the `useExisting` option.
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-6b" linenums="false">
</code-example>
{@a value-provider}
## Value providers
Sometimes it's easier to provide a ready-made object rather than ask the injector to create it from a class.
To inject an object you have already created,
configure the injector with the `useValue` option
The following code defines a variable that creates such an object to play the logger role.
<code-example path="dependency-injection/src/app/providers.component.ts" region="silent-logger" linenums="false">
</code-example>
The following provider object uses the `useValue` key to associate the variable with the `Logger` token.
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-7" linenums="false">
</code-example>
{@a non-class-dependencies}
### Non-class dependencies
Not all dependencies are classes.
Sometimes you want to inject a string, function, or object.
Apps often define configuration objects with lots of small facts,
like the title of the application or the address of a web API endpoint.
These configuration objects aren't always instances of a class.
They can be object literals, as shown in the following example.
<code-example path="dependency-injection/src/app/app.config.ts" region="config" title="src/app/app.config.ts (excerpt)" linenums="false">
</code-example>
{@a interface-not-valid-token}
**TypeScript interfaces are not valid tokens**
The `HERO_DI_CONFIG` constant conforms to the `AppConfig` interface.
Unfortunately, you cannot use a TypeScript interface as a token.
In TypeScript, an interface is a design-time artifact, and doesn't have a runtime representation (token) that the DI framework can use.
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-9-interface" linenums="false">
</code-example>
<code-example path="dependency-injection/src/app/providers.component.ts" region="provider-9-ctor-interface" linenums="false">
</code-example>
<div class="alert-is-helpful">
This might seem strange if you're used to dependency injection in strongly typed languages where an interface is the preferred dependency lookup key.
However, JavaScript, doesn't have interfaces, so when TypeScript is transpiled to JavaScript, the interface disappears.
There is no interface type information left for Angular to find at runtime.
</div>
One alternative is to provide and inject the configuration object in an NgModule like `AppModule`.
<code-example path="dependency-injection/src/app/app.module.ts" region="providers" title="src/app/app.module.ts (providers)"></code-example>
Another solution to choosing a provider token for non-class dependencies is
to define and use an `InjectionToken` object.
The following example shows how to define such a token.
<code-example path="dependency-injection/src/app/app.config.ts" region="token" title="src/app/app.config.ts" linenums="false">
</code-example>
The type parameter, while optional, conveys the dependency's type to developers and tooling.
The token description is another developer aid.
Register the dependency provider using the `InjectionToken` object:
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-9" linenums="false">
</code-example>
Now you can inject the configuration object into any constructor that needs it, with
the help of an `@Inject()` parameter decorator.
<code-example path="dependency-injection/src/app/app.component.2.ts" region="ctor" title="src/app/app.component.ts" linenums="false">
</code-example>
<div class="alert-is-helpful">
Although the `AppConfig` interface plays no role in dependency injection,
it supports typing of the configuration object within the class.
</div>
{@a factory-provider}
{@a factory-providers}
## Factory providers
Sometimes you need to create a dependent value dynamically,
based on information you won't have until run time.
For example, you might need information that changes repeatedly in the course of the browser session.
Also, your injectable service might not have independent access to the source of the information.
In cases like this you can use a *factory provider*.
Factory providers can also be useful when creating an instance of a dependency from
a third-party library that wasn't designed to work with DI.
For example, suppose `HeroService` must hide *secret* heroes from normal users.
Only authorized users should see secret heroes.
Like `EvenBetterLogger`, `HeroService` needs to know if the user is authorized to see secret heroes.
That authorization can change during the course of a single application session,
as when you log in a different user.
Let's say you don't want to inject `UserService` directly into `HeroService`, because you don't want to complicate that service with security-sensitive information.
`HeroService` won't have direct access to the user information to decide
who is authorized and who isn't.
To resolve this, we give the `HeroService` constructor a boolean flag to control display of secret heroes.
<code-example path="dependency-injection/src/app/heroes/hero.service.ts" region="internals" title="src/app/heroes/hero.service.ts (excerpt)" linenums="false">
</code-example>
You can inject `Logger`, but you can't inject the `isAuthorized` flag. Instead, you can use a factory provider to create a new logger instance for `HeroService`.
A factory provider needs a factory function.
<code-example path="dependency-injection/src/app/heroes/hero.service.provider.ts" region="factory" title="src/app/heroes/hero.service.provider.ts (excerpt)" linenums="false">
</code-example>
Although `HeroService` has no access to `UserService`, the factory function does.
You inject both `Logger` and `UserService` into the factory provider
and let the injector pass them along to the factory function.
<code-example path="dependency-injection/src/app/heroes/hero.service.provider.ts" region="provider" title="src/app/heroes/hero.service.provider.ts (excerpt)" linenums="false">
</code-example>
* The `useFactory` field tells Angular that the provider is a factory function whose implementation is `heroServiceFactory`.
* The `deps` property is an array of [provider tokens](guide/dependency-injection#token).
The `Logger` and `UserService` classes serve as tokens for their own class providers.
The injector resolves these tokens and injects the corresponding services into the matching factory function parameters.
Notice that you captured the factory provider in an exported variable, `heroServiceProvider`.
This extra step makes the factory provider reusable.
You can configure a provider of `HeroService` with this variable wherever you need it.
In this sample, you need it only in `HeroesComponent`,
where `heroServiceProvider` replaces `HeroService` in the metadata `providers` array.
The following shows the new and the old implementations side-by-side.
<code-tabs>
<code-pane title="src/app/heroes/heroes.component (v3)" path="dependency-injection/src/app/heroes/heroes.component.ts">
</code-pane>
<code-pane title="src/app/heroes/heroes.component (v2)" path="dependency-injection/src/app/heroes/heroes.component.1.ts">
</code-pane>
</code-tabs>
## Predefined tokens and multiple providers
Angular provides a number of built-in injection-token constants that you can use to customize the behavior of
various systems.
For example, you can use the following built-in tokens as hooks into the frameworks bootstrapping and initialization process.
A provider object can associate any of these injection tokens with one or more callback functions that take app-specific initialization actions.
* [PLATFORM_INITIALIZER](api/core/PLATFORM_INITIALIZER): Callback is invoked when a platform is initialized.
* [APP_BOOTSTRAP_LISTENER](api/core/APP_BOOTSTRAP_LISTENER): Callback is invoked for each component that is bootstrapped. The handler function receives the ComponentRef instance of the bootstrapped component.
* [APP_INITIALIZER](api/core/APP_INITIALIZER): Callback is invoked before an app is initialized. All registered initializers can optionally return a Promise. All initializer functions that return Promises must be resolved before the application is bootstrapped. If one of the initializers fails to resolves, the application is not bootstrapped.
The provider object can have a third option, `multi: true`, which you can use with `APP_INITIALIZER`
to register multiple handlers for the provide event.
For example, when bootstrapping an application, you can register many initializers using the same token.
```
export const APP_TOKENS = [
{ provide: PLATFORM_INITIALIZER, useFactory: platformInitialized, multi: true },
{ provide: APP_INITIALIZER, useFactory: delayBootstrapping, multi: true },
{ provide: APP_BOOTSTRAP_LISTENER, useFactory: appBootstrapped, multi: true },
];
```
Multiple providers can be associated with a single token in other areas as well.
For example, you can register a custom form validator using the built-in [NG_VALIDATORS](api/forms/NG_VALIDATORS) token,
and provide multiple instances of a given validator provider by using the `multi: true` property in the provider object.
Angular adds your custom validators to the existing collection.
The Router also makes use of multiple providers associated with a single token.
When you provide multiple sets of routes using [RouterModule.forRoot](api/router/RouterModule#forroot)
and [RouterModule.forChild](api/router/RouterModule#forchild) in a single module,
the [ROUTES](api/router/ROUTES) token combines all the different provided sets of routes into a single value.
<div class="alert-is-helpful>
Search for [Constants in API documentation](api?type=const) to find more built-in tokens.
</div>
{@a tree-shakable-provider}
{@a tree-shakable-providers}
## Tree-shakable providers
Tree shaking refers to a compiler option that removes code from the final bundle if that code not referenced in an application.
When providers are tree-shakable, the Angular compiler removes the associated
services from the final output when it determines that they are not used in your application.
This significantly reduces the size of your bundles.
<div class="alert-is-helpful">
Ideally, if an application isn't injecting a service, it shouldn't be included in the final output.
However, Angular has to be able to identify at build time whether the service will be required or not.
Because it's always possible to inject a service directly using `injector.get(Service)`,
Angular can't identify all of the places in your code where this injection could happen,
so it has no choice but to include the service in the injector.
Thus, services provided at the NgModule or component level are not tree-shakable.
</div>
The following example of non-tree-shakable providers in Angular configures a service provider for the injector of an NgModule.
<code-example path="dependency-injection/src/app/tree-shaking/service-and-module.ts" title="src/app/tree-shaking/service-and-modules.ts" linenums="false"> </code-example>
This module can then be imported into your application module
to make the service available for injection in your app,
as shown in the following example.
<code-example path="dependency-injection/src/app/tree-shaking/app.module.ts" title="src/app/tree-shaking/app.modules.ts" linenums="false"> </code-example>
When `ngc` runs, it compiles `AppModule` into a module factory, which contains definitions for all the providers declared in all the modules it includes. At runtime, this factory becomes an injector that instantiates these services.
Tree-shaking doesn't work here because Angular can't decide to exclude one chunk of code (the provider definition for the service within the module factory) based on whether another chunk of code (the service class) is used. To make services tree-shakable, the information about how to construct an instance of the service (the provider definition) needs to be a part of the service class itself.
### Creating tree-shakable providers
You can make a provider tree-shakable by specifying it in the `@Injectable()` decorator on the service itself, rather than in the metadata for the NgModule or component that depends on the service.
The following example shows the tree-shakable equivalent to the `ServiceModule` example above.
<code-example path="dependency-injection/src/app/tree-shaking/service.ts" title="src/app/tree-shaking/service.ts" linenums="false"> </code-example>
The service can be instantiated by configuring a factory function, as in the following example.
<code-example path="dependency-injection/src/app/tree-shaking/service.0.ts" title="src/app/tree-shaking/service.0.ts" linenums="false"> </code-example>
<div class="alert-is-helpful">
To override a tree-shakable provider, configure the injector of a specific NgModule or component with another provider, using the `providers: []` array syntax of the `@NgModule()` or `@Component()` decorator.
</div>

View File

@ -1,23 +1,25 @@
# Angular Dependency Injection # Dependency Injection in Angular
**Dependency Injection (DI)** is a way to create objects that depend upon other objects. Dependency injection (DI), is an important application design pattern.
A Dependency Injection system supplies the dependent objects (called the _dependencies_) Angular has its own DI framework, which is typically
when it creates an instance of an object. used in the design of Angular applications to increase their efficiency and modularity.
The [Dependency Injection pattern](guide/dependency-injection-pattern) page describes this general approach. Dependencies are services or objects that a class needs to perform its function.
_The guide you're reading now_ explains how Angular's own Dependency Injection system works. DI is a coding pattern in which a class asks for dependencies from external sources rather than creating them itself.
## DI by example In Angular, the DI framework provides declared dependencies to a class when that class is instantiated. This guide explains how DI works in Angular, and how you use it to make your apps flexible, efficient, and robust, as well as testable and maintainable.
You'll learn Angular Dependency Injection through a discussion of the sample app that accompanies this guide. <div class="alert-is-helpful">
Run the <live-example></live-example> anytime.
You can run the <live-example></live-example> of the sample app that accompanies this guide.
</div>
Start by reviewing this simplified version of the _heroes_ feature Start by reviewing this simplified version of the _heroes_ feature
from the [The Tour of Heroes](tutorial/). from the [The Tour of Heroes](tutorial/). This simple version doesn't use DI; we'll walk through converting it to do so.
<code-tabs> <code-tabs>
<code-pane title="src/app/heroes/heroes.component.ts" path="dependency-injection/src/app/heroes/heroes.component.1.ts" <code-pane title="src/app/heroes/heroes.component.ts" path="dependency-injection/src/app/heroes/heroes.component.1.ts" region="v1">
region="v1">
</code-pane> </code-pane>
<code-pane title="src/app/heroes/hero-list.component.ts" path="dependency-injection/src/app/heroes/hero-list.component.1.ts"> <code-pane title="src/app/heroes/hero-list.component.ts" path="dependency-injection/src/app/heroes/hero-list.component.1.ts">
@ -31,26 +33,40 @@ from the [The Tour of Heroes](tutorial/).
</code-tabs> </code-tabs>
The `HeroesComponent` is the top-level heroes component. `HeroesComponent` is the top-level heroes component.
Its only purpose is to display the `HeroListComponent` Its only purpose is to display `HeroListComponent`, which displays a list of hero names.
which displays a list of hero names.
This version of the `HeroListComponent` gets its `heroes` from the `HEROES` array, an in-memory collection This version of the `HeroListComponent` gets heroes from the `HEROES` array, an in-memory collection
defined in a separate `mock-heroes` file. defined in a separate `mock-heroes` file.
<code-example title="src/app/heroes/hero-list.component.ts (class)" path="dependency-injection/src/app/heroes/hero-list.component.1.ts" <code-example title="src/app/heroes/hero-list.component.ts (class)" path="dependency-injection/src/app/heroes/hero-list.component.1.ts" region="class">
region="class">
</code-example> </code-example>
That may suffice in the early stages of development, but it's far from ideal. This approach works for prototyping, but is not robust or maintainable.
As soon as you try to test this component or get heroes from a remote server, As soon as you try to test this component or get heroes from a remote server,
you'll have to change the implementation of `HerosListComponent` and you have to change the implementation of `HeroesListComponent` and
replace every other use of the `HEROES` mock data. replace every use of the `HEROES` mock data.
It's better to hide these details inside a _service_ class,
[defined in its own file](#one-class-per-file).
## Create an injectable _HeroService_ ## Create and register an injectable service
The DI framework lets you supply data to a component from an injectable _service_ class, defined in its own file. To demonstrate, we'll create an injectable service class that provides a list of heroes, and register that class as a provider of that service.
<div class="alert-is-helpful">
Having multiple classes in the same file can be confusing. We generally recommend that you define components and services in separate files.
If you do combine a component and service in the same file,
it is important to define the service first, and then the component. If you define the component before the service, you get a run-time null reference error.
It is possible to define the component first with the help of the `forwardRef()` method as explained in this [blog post](http://blog.thoughtram.io/angular/2015/09/03/forward-references-in-angular-2.html).
You can also use forward references to break circular dependencies.
See an example in the [DI Cookbook](guide/dependency-injection-in-action#forwardref).
</div>
### Create an injectable service class
The [**Angular CLI**](https://cli.angular.io/) can generate a new `HeroService` class in the `src/app/heroes` folder with this command. The [**Angular CLI**](https://cli.angular.io/) can generate a new `HeroService` class in the `src/app/heroes` folder with this command.
@ -58,412 +74,84 @@ The [**Angular CLI**](https://cli.angular.io/) can generate a new `HeroService`
ng generate service heroes/hero ng generate service heroes/hero
</code-example> </code-example>
The command above creates the following `HeroService` skeleton. The command creates the following `HeroService` skeleton.
<code-example path="dependency-injection/src/app/heroes/hero.service.0.ts" title="src/app/heroes/hero.service.ts (CLI-generated)"> <code-example path="dependency-injection/src/app/heroes/hero.service.0.ts" title="src/app/heroes/hero.service.ts (CLI-generated)">
</code-example> </code-example>
The `@Injectable` decorator is an essential ingredient in every Angular service definition. The `@Injectable()` is an essential ingredient in every Angular service definition. The rest of the class has been written to expose a `getHeroes` method that returns the same mock data as before. (A real app would probably get its data asynchronously from a remote server, but we'll ignore that to focus on the mechanics of injecting the service.)
The rest of the class has been rewritten to expose a `getHeroes` method
that returns the same mock data as before.
<code-example path="dependency-injection/src/app/heroes/hero.service.3.ts" title="src/app/heroes/hero.service.3.ts"> <code-example path="dependency-injection/src/app/heroes/hero.service.3.ts" title="src/app/heroes/hero.service.ts">
</code-example> </code-example>
Of course, this isn't a real data service.
If the app were actually getting data from a remote server,
the `getHeroes` method signature would have to be asynchronous.
That's a defect we can safely ignore in this guide where our focus is on
_injecting the service_ into the `HeroList` component.
{@a injector-config} {@a injector-config}
{@a bootstrap} {@a bootstrap}
## Injectors ### Configure an injector with a service provider
A _service_ like `HeroService` is just a class in Angular until you register it with an Angular dependency injector. The class we have created provides a service. The `@Injectable()` decorator marks it as a service
that can be injected, but Angular can't actually inject it anywhere until you configure
an Angular [dependency injector](guide/glossary#injector) with a [provider](guide/glossary#provider) of that service.
An Angular injector is responsible for creating service instances and injecting them into classes like the `HeroListComponent`. The injector is responsible for creating service instances and injecting them into classes like `HeroListComponent`.
You rarely create an Angular injector yourself. Angular creates injectors for you as it executes the app, starting with the _root injector_ that it creates during the [bootstrap process](guide/bootstrapping).
You rarely create an Angular injector yourself. A provider tells an injector _how to create the service_.
Angular creates injectors for you as it executes the app, You must configure an injector with a provider before that injector can create a service (or provide any other kind of dependency).
starting with the _root injector_ that it creates during the [bootstrap process](guide/bootstrapping).
Angular doesn't automatically know how you want to create instances of your services or the injector to create your service. You must configure it by specifying providers for every service. A provider can be the service class itself, so that the injector can use `new` to create an instance.
You might also define more than one class to provide the same service in different ways,
**Providers** tell the injector _how to create the service_. and configure different injectors with different providers.
Without a provider, the injector would not know
that it is responsible for injecting the service
nor be able to create the service.
<div class="alert is-helpful"> <div class="alert is-helpful">
You'll learn much more about _providers_ [below](#providers). Injectors are inherited, which means that if a given injector can't resolve a dependency,
For now, it is sufficient to know that they configure where and how services are created. it asks the parent injector to resolve it.
A component can get services from its own injector,
from the injectors of its component ancestors,
from the injector of its parent NgModule, or from the `root` injector.
* Learn more about the [different kinds of providers](guide/dependency-injection-providers).
* Learn more about how the [injector hierarchy](guide/hierarchical-dependency-injection) works.
</div> </div>
There are many ways to register a service provider with an injector. This section shows the most common ways You can configure injectors with providers at different levels of your app, by setting a metadata value in one of three places:
of configuring a provider for your services.
{@a register-providers-injectable} * In the `@Injectable()` decorator for the service itself.
## @Injectable providers * In the `@NgModule()` decorator for an NgModule.
The `@Injectable` decorator identifies services and other classes that are intended to be injected. It can also be used to configure a provider for those services. * In the `@Component()` decorator for a component.
Here we configure a provider for `HeroService` using the `@Injectable` decorator on the class. The `@Injectable()` decorator has the `providedIn` metadata option, where you can specify the provider of the decorated service class with the `root` injector, or with the injector for a specific NgModule.
<code-example path="dependency-injection/src/app/heroes/hero.service.0.ts" title="src/app/heroes/heroes.service.ts" linenums="false"> </code-example> The `@NgModule()` and `@Component()` decorators have the `providers` metadata option, where you can configure providers for NgModule-level or component-level injectors.
`providedIn` tells Angular that the root injector is responsible for creating an instance of the `HeroService` (by invoking its constructor) and making it available across the application. The CLI sets up this kind of a provider automatically for you when generating a new service.
Sometimes it's not desirable to have a service always be provided in the application root injector. Perhaps users should explicitly opt-in to using the service, or the service should be provided in a lazily-loaded context. In this case, the provider should be associated with a specific `@NgModule` class, and will be used by whichever injector includes that module.
In the following excerpt, the `@Injectable` decorator is used to configure a provider that will be available in any injector that includes the HeroModule.
<code-example path="dependency-injection/src/app/heroes/hero.service.4.ts" title="src/app/heroes/hero.service.ts" linenums="false"> </code-example>
{@a register-providers-ngmodule}
### _@NgModule_ providers
In the following excerpt, the root `AppModule` registers two providers in its `providers` array.
<code-example path="dependency-injection/src/app/app.module.ts" linenums="false" title="src/app/app.module.ts (providers)" region="providers">
</code-example>
The first entry registers the `UserService` class (_not shown_) under the `UserService` _injection token_.
The second registers a value (`HERO_DI_CONFIG`) under the `APP_CONFIG` _injection token_.
With the above registrations, Angular can inject the `UserService` or the `HERO_DI_CONFIG` value
into any class that it creates.
<div class="alert is-helpful"> <div class="alert is-helpful">
You'll learn about _injection tokens_ and _provider_ syntax [below](#providers). Components are directives, and the `providers` option is inherited from `@Directive()`. You can also configure providers for directives and pipes at the same level as the component.
</div>
{@a register-providers-component} Learn more about [where to configure providers](guide/hierarchical-dependency-injection#where-to-register).
### _@Component_ providers
In addition to providing the service application-wide or within a particular `@NgModule`, services can also be provided in specific components. Services provided in component-level is only available within that component injector or in any of its child components.
The example below shows a revised `HeroesComponent` that registers the `HeroService` in its `providers` array.
<code-example path="dependency-injection/src/app/heroes/heroes.component.1.ts" title="src/app/heroes/heroes.component.ts" linenums="false">
</code-example>
{@a ngmodule-vs-comp}
### @Injectable, _@NgModule_ or _@Component_?
Should you provide a service with an `@Injectable` decorator, in an `@NgModule`, or within an `@Component`?
The choices lead to differences in the final bundle size, service _scope_, and service _lifetime_.
When you register providers in the **@Injectable** decorator of the service itself, optimization tools such as those used by the CLI's production builds can perform tree shaking, which removes services that aren't used by your app. Tree shaking results in smaller bundle sizes.
**Angular module providers** (`@NgModule.providers`) are registered with the application's root injector.
Angular can inject the corresponding services in any class it creates.
Once created, a service instance lives for the life of the app and Angular injects this one service instance in every class that needs it.
You're likely to inject the `UserService` in many places throughout the app
and will want to inject the same service instance every time.
Providing the `UserService` with an Angular module is a good choice if an `@Injectable` provider is not an option..
<div class="alert is-helpful">
To be precise, Angular module providers are registered with the root injector
_unless the module is_ [lazy loaded](guide/lazy-loading-ngmodules).
In this sample, all modules are _eagerly loaded_ when the application starts,
so all module providers are registered with the app's root injector.
</div><br>
<hr>
**A component's providers** (`@Component.providers`) are registered with each component instance's own injector.
Angular can only inject the corresponding services in that component instance or one of its descendant component instances.
Angular cannot inject the same service instance anywhere else.
Note that a component-provided service may have a limited lifetime. Each new instance of the component gets its own instance of the service
and, when the component instance is destroyed, so is that service instance.
In this sample app, the `HeroComponent` is created when the application starts
and is never destroyed so the `HeroService` created for the `HeroComponent` also live for the life of the app.
If you want to restrict `HeroService` access to the `HeroComponent` and its nested `HeroListComponent`,
providing the `HeroService` in the `HeroComponent` may be a good choice.
<div class="alert is-helpful">
The scope and lifetime of component-provided services is a consequence of [the way Angular creates component instances](#component-child-injectors).
</div>
{@a providers}
## Providers
A service provider *provides* the concrete, runtime version of a dependency value.
The injector relies on **providers** to create instances of the services
that the injector injects into components, directives, pipes, and other services.
You must register a service *provider* with an injector, or it won't know how to create the service.
The next few sections explain the many ways you can specify a provider.
### The class as its own provider
There are many ways to *provide* something that looks and behaves like a `Logger`.
The `Logger` class itself is an obvious and natural provider.
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-logger">
</code-example>
But it's not the only way.
You can configure the injector with alternative providers that can deliver an object that behaves like a `Logger`.
You could provide a substitute class. You could provide a logger-like object.
You could give it a provider that calls a logger factory function.
Any of these approaches might be a good choice under the right circumstances.
What matters is that the injector has a provider to go to when it needs a `Logger`.
{@a provide}
### The _provide_ object literal
Here's the class-provider syntax again.
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-logger">
</code-example>
This is actually a shorthand expression for a provider registration
using a _provider_ object literal with two properties:
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-3" >
</code-example>
The `provide` property holds the [token](guide/dependency-injection#token) that serves as the key for both locating a dependency value
and registering the provider.
The second property is always a provider definition object,
which you can think of as a *recipe* for creating the dependency value.
There are many ways to create dependency values just as there are many ways to write a recipe.
{@a class-provider}
### Alternative class providers
Occasionally you'll ask a different class to provide the service.
The following code tells the injector
to return a `BetterLogger` when something asks for the `Logger`.
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-4" >
</code-example>
{@a class-provider-dependencies}
### Class provider with dependencies
Maybe an `EvenBetterLogger` could display the user name in the log message.
This logger gets the user from the injected `UserService`,
which is also injected at the application level.
<code-example path="dependency-injection/src/app/providers.component.ts" region="EvenBetterLogger" linenums="false">
</code-example>
Configure it like `BetterLogger`.
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-5" linenums="false">
</code-example>
{@a aliased-class-providers}
### Aliased class providers
Suppose an old component depends upon an `OldLogger` class.
`OldLogger` has the same interface as the `NewLogger`, but for some reason
you can't update the old component to use it.
When the *old* component logs a message with `OldLogger`,
you'd like the singleton instance of `NewLogger` to handle it instead.
The dependency injector should inject that singleton instance
when a component asks for either the new or the old logger.
The `OldLogger` should be an alias for `NewLogger`.
You certainly do not want two different `NewLogger` instances in your app.
Unfortunately, that's what you get if you try to alias `OldLogger` to `NewLogger` with `useClass`.
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-6a" linenums="false">
</code-example>
The solution: alias with the `useExisting` option.
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-6b" linenums="false">
</code-example>
{@a value-provider}
### Value providers
Sometimes it's easier to provide a ready-made object rather than ask the injector to create it from a class.
<code-example path="dependency-injection/src/app/providers.component.ts" region="silent-logger" linenums="false">
</code-example>
Then you register a provider with the `useValue` option,
which makes this object play the logger role.
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-7" linenums="false">
</code-example>
See more `useValue` examples in the
[Non-class dependencies](guide/dependency-injection#non-class-dependencies) and
[InjectionToken](guide/dependency-injection#injection-token) sections.
{@a factory-provider}
### Factory providers
Sometimes you need to create the dependent value dynamically,
based on information you won't have until the last possible moment.
Maybe the information changes repeatedly in the course of the browser session.
Suppose also that the injectable service has no independent access to the source of this information.
This situation calls for a **factory provider**.
To illustrate the point, add a new business requirement:
the `HeroService` must hide *secret* heroes from normal users.
Only authorized users should see secret heroes.
Like the `EvenBetterLogger`, the `HeroService` needs a fact about the user.
It needs to know if the user is authorized to see secret heroes.
That authorization can change during the course of a single application session,
as when you log in a different user.
Unlike `EvenBetterLogger`, you can't inject the `UserService` into the `HeroService`.
The `HeroService` won't have direct access to the user information to decide
who is authorized and who is not.
Instead, the `HeroService` constructor takes a boolean flag to control display of secret heroes.
<code-example path="dependency-injection/src/app/heroes/hero.service.ts" region="internals" title="src/app/heroes/hero.service.ts (excerpt)" linenums="false">
</code-example>
You can inject the `Logger`, but you can't inject the boolean `isAuthorized`.
You'll have to take over the creation of new instances of this `HeroService` with a factory provider.
A factory provider needs a factory function:
<code-example path="dependency-injection/src/app/heroes/hero.service.provider.ts" region="factory" title="src/app/heroes/hero.service.provider.ts (excerpt)" linenums="false">
</code-example>
Although the `HeroService` has no access to the `UserService`, the factory function does.
You inject both the `Logger` and the `UserService` into the factory provider
and let the injector pass them along to the factory function:
<code-example path="dependency-injection/src/app/heroes/hero.service.provider.ts" region="provider" title="src/app/heroes/hero.service.provider.ts (excerpt)" linenums="false">
</code-example>
<div class="alert is-helpful">
The `useFactory` field tells Angular that the provider is a factory function
whose implementation is the `heroServiceFactory`.
The `deps` property is an array of [provider tokens](guide/dependency-injection#token).
The `Logger` and `UserService` classes serve as tokens for their own class providers.
The injector resolves these tokens and injects the corresponding services into the matching factory function parameters.
</div>
Notice that you captured the factory provider in an exported variable, `heroServiceProvider`.
This extra step makes the factory provider reusable.
You can register the `HeroService` with this variable wherever you need it.
In this sample, you need it only in the `HeroesComponent`,
where it replaces the previous `HeroService` registration in the metadata `providers` array.
Here you see the new and the old implementation side-by-side:
<code-tabs>
<code-pane title="src/app/heroes/heroes.component (v3)" path="dependency-injection/src/app/heroes/heroes.component.ts">
</code-pane>
<code-pane title="src/app/heroes/heroes.component (v2)" path="dependency-injection/src/app/heroes/heroes.component.1.ts">
</code-pane>
</code-tabs>
{@a tree-shakable-provider}
### Tree-shakable providers
Tree shaking is the ability to remove code that is not referenced in an application from the final bundle. Tree-shakable providers give Angular the ability to remove services that are not used in your application from the final output. This significantly reduces the size of your bundles.
Ideally, if an application is not injecting a service, it should not be included in the final output. However, it turns out that the Angular compiler cannot identify at build time if the service will be required or not. Because it's always possible to inject a service directly using `injector.get(Service)`, Angular cannot identify all of the places in your code where this injection could happen, so it has no choice but to include the service in the injector regardless. Thus, services provided in modules are not tree-shakable.
Let us consider an example of non-tree-shakable providers in Angular.
In this example, to provide services in Angular, you include them in an `@NgModule`:
<code-example path="dependency-injection/src/app/tree-shaking/service-and-module.ts" title="src/app/tree-shaking/service-and-modules.ts" linenums="false"> </code-example>
This module can then be imported into your application module, to make the service available for injection in your app:
<code-example path="dependency-injection/src/app/tree-shaking/app.module.ts" title="src/app/tree-shaking/app.modules.ts" linenums="false"> </code-example>
When `ngc` runs, it compiles AppModule into a module factory, which contains definitions for all the providers declared in all the modules it includes. At runtime, this factory becomes an injector that instantiates these services.
Tree-shaking doesn't work in the method above because Angular cannot decide to exclude one chunk of code (the provider definition for the service within the module factory) based on whether another chunk of code (the service class) is used. To make services tree-shakable, the information about how to construct an instance of the service (the provider definition) needs to be a part of the service class itself.
#### Creating tree-shakable providers
To create providers that are tree-shakable, the information that used to be specified in the module should be specified in the `@Injectable` decorator on the service itself.
The following example shows the tree-shakable equivalent to the `ServiceModule` example above:
<code-example path="dependency-injection/src/app/tree-shaking/service.ts" title="src/app/tree-shaking/service.ts" linenums="false"> </code-example>
In the example above, `providedIn` allows you to declare the injector which injects this service. Unless there is a special case, the value should always be root. Setting the value to root ensures that the service is scoped to the root injector, without naming a particular module that is present in that injector.
The service can be instantiated by configuring a factory function as shown below:
<code-example path="dependency-injection/src/app/tree-shaking/service.0.ts" title="src/app/tree-shaking/service.0.ts" linenums="false"> </code-example>
<div class="alert is-helpful">
To override tree-shakable providers, register the provider using the `providers: []` array syntax of any Angular decorator that supports it.
</div> </div>
{@a injector-config} {@a injector-config}
{@a bootstrap} {@a bootstrap}
## Inject a service ## Injecting services
The `HeroListComponent` should get heroes from the `HeroService`. In order for `HeroListComponent` to get heroes from `HeroService`, it needs to ask for `HeroService` to be injected, rather than creating it's own `HeroService` instance with `new`.
The component shouldn't create the `HeroService` with `new`. You can tell Angular to inject a dependency in a component's constructor by specifying a **constructor parameter with the dependency type**. Here's the `HeroListComponent` constructor, asking for the `HeroService` to be injected.
It should ask for the `HeroService` to be injected.
You can tell Angular to inject a dependency in the component's constructor by specifying a **constructor parameter with the dependency type**.
Here's the `HeroListComponent` constructor, asking for the `HeroService` to be injected.
<code-example title="src/app/heroes/hero-list.component (constructor signature)" path="dependency-injection/src/app/heroes/hero-list.component.ts" <code-example title="src/app/heroes/hero-list.component (constructor signature)" path="dependency-injection/src/app/heroes/hero-list.component.ts"
region="ctor-signature"> region="ctor-signature">
</code-example> </code-example>
Of course, the `HeroListComponent` should do something with the injected `HeroService`. Of course, `HeroListComponent` should do something with the injected `HeroService`.
Here's the revised component, making use of the injected service, side-by-side with the previous version for comparison. Here's the revised component, making use of the injected service, side-by-side with the previous version for comparison.
<code-tabs> <code-tabs>
@ -474,60 +162,40 @@ Here's the revised component, making use of the injected service, side-by-side w
</code-pane> </code-pane>
</code-tabs> </code-tabs>
Notice that the `HeroListComponent` doesn't know where the `HeroService` comes from. `HeroService` must provided in some parent injector. The code in `HeroListComponent` doesn't depend on where `HeroService` comes from.
_You_ know that it comes from the parent `HeroesComponent`. If you decided to provide `HeroService` in `AppModule`, `HeroListComponent` wouldn't change.
If you decided instead to provide the `HeroService` in the `AppModule`,
the `HeroListComponent` wouldn't change at all.
The _only thing that matters_ is that the `HeroService` is provided in some parent injector.
{@a singleton-services} {@a singleton-services}
## Singleton services
Services are singletons _within the scope of an injector_.
There is at most one instance of a service in a given injector.
There is only one root injector, and the `UserService` is registered with that injector.
Therefore, there can be just one `UserService` instance in the entire app,
and every class that injects `UserService` get this service instance.
However, Angular DI is a
[hierarchical injection system](guide/hierarchical-dependency-injection),
which means that nested injectors can create their own service instances.
Angular creates nested injectors all the time.
{@a component-child-injectors} {@a component-child-injectors}
## Component child injectors ### Injector hierarchy and service instances
Component injectors are independent of each other and Services are singletons _within the scope of an injector_. That is, there is at most one instance of a service in a given injector.
each of them creates its own instances of the component-provided services.
For example, when Angular creates a new instance of a component that has `@Component.providers`, There is only one root injector for an app. Providing `UserService` at the `root` or `AppModule` level means it is registered with the root injector. There is just one `UserService` instance in the entire app and every class that injects `UserService` gets this service instance _unless_ you configure another provider with a _child injector_.
it also creates a new _child injector_ for that instance.
When Angular destroys one of these component instances, it also destroys the Angular DI has a [hierarchical injection system](guide/hierarchical-dependency-injection), which means that nested injectors can create their own service instances.
component's injector and that injector's service instances. Angular regularly creates nested injectors. Whenever Angular creates a new instance of a component that has `providers` specified in `@Component()`, it also creates a new _child injector_ for that instance.
Similarly, when a new NgModule is lazy-loaded at run time, Angular can create an injector for it with its own providers.
Because of [injector inheritance](guide/hierarchical-dependency-injection), Child modules and component injectors are independent of each other, and create their own separate instances of the provided services. When Angular destroys an NgModule or component instance, it also destroys that injector and that injector's service instances.
Thanks to [injector inheritance](guide/hierarchical-dependency-injection),
you can still inject application-wide services into these components. you can still inject application-wide services into these components.
A component's injector is a child of its parent component's injector, A component's injector is a child of its parent component's injector,
and a descendent of its parent's parent's injector, and so on all the way back to the application's _root_ injector. and a descendent of its parent's parent's injector, and so on all the way back to the application's _root_ injector. Angular can inject a service provided by any injector in that lineage.
Angular can inject a service provided by any injector in that lineage.
For example, Angular could inject a `HeroListComponent` For example, Angular can inject `HeroListComponent` with both the `HeroService` provided in `HeroComponent` and the `UserService` provided in `AppModule`.
with both the `HeroService` provided in `HeroComponent`
and the `UserService` provided in `AppModule`.
{@a testing-the-component} {@a testing-the-component}
## Testing the component ## Testing components with dependencies
Earlier you saw that designing a class for dependency injection makes the class easier to test. Designing a class with dependency injection makes the class easier to test.
Listing dependencies as constructor parameters may be all you need to test application parts effectively. Listing dependencies as constructor parameters may be all you need to test application parts effectively.
For example, you can create a new `HeroListComponent` with a mock service that you can manipulate For example, you can create a new `HeroListComponent` with a mock service that you can manipulate
under test: under test.
<code-example path="dependency-injection/src/app/test.component.ts" region="spec" title="src/app/test.component.ts" linenums="false"> <code-example path="dependency-injection/src/app/test.component.ts" region="spec" title="src/app/test.component.ts" linenums="false">
</code-example> </code-example>
@ -540,15 +208,12 @@ Learn more in the [Testing](guide/testing) guide.
{@a service-needs-service} {@a service-needs-service}
## When the service needs a service ## Services that need other services
The `HeroService` is very simple. It doesn't have any dependencies of its own. Service can have their own dependencies. `HeroService` is very simple and doesn't have any dependencies of its own. Suppose, however, that you want it to report its activities through a logging service. You can apply the same *constructor injection* pattern,
What if it had a dependency? What if it reported its activities through a logging service?
You'd apply the same *constructor injection* pattern,
adding a constructor that takes a `Logger` parameter. adding a constructor that takes a `Logger` parameter.
Here is the revised `HeroService` that injects the `Logger`, side-by-side with the previous service for comparison. Here is the revised `HeroService` that injects `Logger`, side by side with the previous service for comparison.
<code-tabs> <code-tabs>
@ -558,154 +223,85 @@ Here is the revised `HeroService` that injects the `Logger`, side-by-side with t
<code-pane title="src/app/heroes/hero.service (v1)" path="dependency-injection/src/app/heroes/hero.service.1.ts"> <code-pane title="src/app/heroes/hero.service (v1)" path="dependency-injection/src/app/heroes/hero.service.1.ts">
</code-pane> </code-pane>
<code-pane title="src/app/logger.service"
path="dependency-injection/src/app/logger.service.ts">
</code-pane>
</code-tabs> </code-tabs>
The constructor asks for an injected instance of a `Logger` and stores it in a private field called `logger`. The constructor asks for an injected instance of `Logger` and stores it in a private field called `logger`. The `getHeroes()` method logs a message when asked to fetch heroes.
The `getHeroes()` method logs a message when asked to fetch heroes.
Notice that the `Logger` service also has the `@Injectable()` decorator, even though it might not need its own dependencies. In fact, the `@Injectable()` decorator is **required for all services**.
{@a logger-service} When Angular creates a class whose constructor has parameters, it looks for type and injection metadata about those parameters so that it can inject the correct service.
If Angular can't find that parameter information, it throws an error.
Angular can only find the parameter information _if the class has a decorator of some kind_.
The `@Injectable()` decorator is the standard decorator for service classes.
#### The dependent _Logger_ service <div class="alert-is-helpful">
The sample app's `Logger` service is quite simple:
<code-example path="dependency-injection/src/app/logger.service.ts" title="src/app/logger.service.ts">
</code-example>
If the app didn't provide this `Logger`,
Angular would throw an exception when it looked for a `Logger` to inject
into the `HeroService`.
<code-example language="sh" class="code-shell">
ERROR Error: No provider for Logger!
</code-example>
Because a singleton logger service is useful everywhere,
it's provided in the root `AppModule`.
<code-example path="dependency-injection/src/app/app.module.ts" linenums="false" title="src/app/app.module.ts (providers)" region="providers-2">
</code-example>
The decorator requirement is imposed by TypeScript. TypeScript normally discards parameter type information when it [transpiles]((guide/glossary#transpile) the code to JavaScript. TypeScript preserves this information if the class has a decorator and the `emitDecoratorMetadata` compiler option is set `true` in TypeScript's `tsconfig.json` configuration file. The CLI configures `tsconfig.json` with `emitDecoratorMetadata: true`.
This means you're responsible for putting `@Injectable()` on your service classes.
</div>
{@a token} {@a token}
## Dependency injection tokens {@a injection-token}
When you register a provider with an injector, you associate that provider with a dependency injection token. ### Dependency injection tokens
When you configure an injector with a provider, you associate that provider with a [DI token](guide/glossary#di-token).
The injector maintains an internal *token-provider* map that it references when The injector maintains an internal *token-provider* map that it references when
asked for a dependency. The token is the key to the map. asked for a dependency. The token is the key to the map.
In all previous examples, the dependency value has been a class *instance*, and In simple examples, the dependency value is an *instance*, and
the class *type* served as its own lookup key. the class *type* serves as its own lookup key.
Here you get a `HeroService` directly from the injector by supplying the `HeroService` type as the token: Here you get a `HeroService` directly from the injector by supplying the `HeroService` type as the token:
<code-example path="dependency-injection/src/app/injector.component.ts" region="get-hero-service" title="src/app/injector.component.ts" linenums="false"> <code-example path="dependency-injection/src/app/injector.component.ts" region="get-hero-service" title="src/app/injector.component.ts" linenums="false">
</code-example> </code-example>
You have similar good fortune when you write a constructor that requires an injected class-based dependency. The behavior is similar when you write a constructor that requires an injected class-based dependency.
When you define a constructor parameter with the `HeroService` class type, When you define a constructor parameter with the `HeroService` class type,
Angular knows to inject the Angular knows to inject the service associated with that `HeroService` class token:
service associated with that `HeroService` class token:
<code-example path="dependency-injection/src/app/heroes/hero-list.component.ts" region="ctor-signature" title="src/app/heroes/hero-list.component.ts"> <code-example path="dependency-injection/src/app/heroes/hero-list.component.ts" region="ctor-signature" title="src/app/heroes/hero-list.component.ts">
</code-example> </code-example>
This is especially convenient when you consider that most dependency values are provided by classes. Many dependency values are provided by classes, but not all. The expanded *provide* object lets you associate different kinds of providers with a DI token.
{@a non-class-dependencies} * Learn more about [different kinds of providers](guide/dependency-injection-providers).
### Non-class dependencies
What if the dependency value isn't a class? Sometimes the thing you want to inject is a
string, function, or object.
Applications often define configuration objects with lots of small facts
(like the title of the application or the address of a web API endpoint)
but these configuration objects aren't always instances of a class.
They can be object literals such as this one:
<code-example path="dependency-injection/src/app/app.config.ts" region="config" title="src/app/app.config.ts (excerpt)" linenums="false">
</code-example>
What if you'd like to make this configuration object available for injection?
You know you can register an object with a [value provider](guide/dependency-injection#value-provider).
But what should you use as the token?
You don't have a class to serve as a token.
There is no `AppConfig` class.
<div class="alert is-helpful">
### TypeScript interfaces aren't valid tokens
The `HERO_DI_CONFIG` constant conforms to the `AppConfig` interface.
Unfortunately, you cannot use a TypeScript interface as a token:
<code-example path="dependency-injection/src/app/providers.component.ts" region="providers-9-interface" linenums="false">
</code-example>
<code-example path="dependency-injection/src/app/providers.component.ts" region="provider-9-ctor-interface" linenums="false">
</code-example>
That seems strange if you're used to dependency injection in strongly typed languages, where
an interface is the preferred dependency lookup key.
It's not Angular's doing. An interface is a TypeScript design-time artifact. JavaScript doesn't have interfaces.
The TypeScript interface disappears from the generated JavaScript.
There is no interface type information left for Angular to find at runtime.
</div>
{@a injection-token}
### _InjectionToken_
One solution to choosing a provider token for non-class dependencies is
to define and use an [*InjectionToken*](api/core/InjectionToken).
The definition of such a token looks like this:
<code-example>
import { InjectionToken } from '@angular/core';
export const TOKEN = new InjectionToken('desc');
</code-example>
You can directly configure a provider when creating an `InjectionToken`. The provider configuration determines which injector provides the token and how the value will be created. This is similar to using `@Injectable`, except that you cannot define standard providers (such as `useClass` or `useFactory`) with `InjectionToken`. Instead, you specify a factory function which returns the value to be provided directly.
<code-example>
export const TOKEN =
new InjectionToken('desc', { providedIn: 'root', factory: () => new AppConfig(), })
</code-example>
Now you can inject the configuration object into any constructor that needs it, with
the help of an `@Inject` decorator:
<code-example>
constructor(@Inject(TOKEN));
</code-example>
If the factory function needs access to other DI tokens, it can use the inject function from `@angular/core` to request dependencies.
<code-example>
const TOKEN =
new InjectionToken('tree-shakable token',
{ providedIn: 'root', factory: () =>
new AppConfig(inject(Parameter1), inject(Parameter2)), });
</code-example>
{@a optional} {@a optional}
## Optional dependencies ### Optional dependencies
You can tell Angular that the dependency is optional by annotating the constructor argument with null: `HeroService` *requires* a logger, but what if it could get by without
one?
<code-example> When a component or service declares a dependency, the class constructor takes that dependency as a parameter.
constructor(@Inject(Token, null)); You can tell Angular that the dependency is optional by annotating the
constructor parameter with `@Optional()`.
<code-example path="dependency-injection/src/app/providers.component.ts" region="import-optional">
</code-example> </code-example>
When using optional dependencies, your code must be prepared for a null value. <code-example path="dependency-injection/src/app/providers.component.ts" region="provider-10-ctor" linenums="false">
</code-example>
When using `@Optional()`, your code must be prepared for a null value. If you
don't register a logger provider anywhere, the injector sets the
value of `logger` to null.
<div class="alert-is-helpful">
`@Inject()` and `@Optional()` are _parameter decorators_. They alter the way the DI framework provides a dependency, by annotating the dependency parameter on the constructor of the class that requires the dependency.
Learn more about parameter decorators in [Hierarchical Dependency Injectors](guide/hierarchical-dependency-injection).
</div>
## Summary ## Summary
@ -714,74 +310,13 @@ You can register various kinds of providers,
and you know how to ask for an injected object (such as a service) by and you know how to ask for an injected object (such as a service) by
adding a parameter to a constructor. adding a parameter to a constructor.
Angular dependency injection is more capable than this guide has described. Dive deeper into the capabilities and advanced feature of the Angular DI system in the following pages:
You can learn more about its advanced features, beginning with its support for
nested injectors, in * Learn more about nested injectors in
[Hierarchical Dependency Injection](guide/hierarchical-dependency-injection). [Hierarchical Dependency Injection](guide/hierarchical-dependency-injection).
{@a explicit-injector} * Learn more about [DI tokens and providers](guide/dependency-injection-providers).
## Appendix: Working with injectors directly * [Dependency Injection in Action](guide/dependency-injection-in-action) is a cookbook for some of the interesting things you can do with DI.
Developers rarely work directly with an injector, but
here's an `InjectorComponent` that does.
<code-example path="dependency-injection/src/app/injector.component.ts" region="injector" title="src/app/injector.component.ts">
</code-example>
An `Injector` is itself an injectable service.
In this example, Angular injects the component's own `Injector` into the component's constructor.
The component then asks the injected injector for the services it wants in `ngOnInit()`.
Note that the services themselves are not injected into the component.
They are retrieved by calling `injector.get()`.
The `get()` method throws an error if it can't resolve the requested service.
You can call `get()` with a second parameter, which is the value to return if the service
is not found. Angular can't find the service if it's not registered with this or any ancestor injector.
<div class="alert is-helpful">
The technique is an example of the
[service locator pattern](https://en.wikipedia.org/wiki/Service_locator_pattern).
**Avoid** this technique unless you genuinely need it.
It encourages a careless grab-bag approach such as you see here.
It's difficult to explain, understand, and test.
You can't know by inspecting the constructor what this class requires or what it will do.
It could acquire services from any ancestor component, not just its own.
You're forced to spelunk the implementation to discover what it does.
Framework developers may take this approach when they
must acquire services generically and dynamically.
</div>
{@a one-class-per-file}
## Appendix: one class per file
Having multiple classes in the same file is confusing and best avoided.
Developers expect one class per file. Keep them happy.
If you combine the `HeroService` class with
the `HeroesComponent` in the same file,
**define the component last**.
If you define the component before the service,
you'll get a runtime null reference error.
<div class="alert is-helpful">
You actually can define the component first with the help of the `forwardRef()` method as explained
in this [blog post](http://blog.thoughtram.io/angular/2015/09/03/forward-references-in-angular-2.html).
But it's best to avoid the problem altogether by defining components and services in separate files.
</div>

View File

@ -1,120 +1,216 @@
# Hierarchical Dependency Injectors # Hierarchical Dependency Injectors
You learned the basics of Angular Dependency injection in the The Angular dependency injection system is _hierarchical_.
[Dependency Injection](guide/dependency-injection) guide. There is a tree of injectors that parallel an app's component tree.
Angular has a _Hierarchical Dependency Injection_ system.
There is actually a tree of injectors that parallel an application's component tree.
You can reconfigure the injectors at any level of that component tree. You can reconfigure the injectors at any level of that component tree.
This guide explores this system and how to use it to your advantage. This guide explores this system and how to use it to your advantage.
It uses examples based on this <live-example></live-example>.
Try the <live-example></live-example>. {@a ngmodule-vs-comp}
{@a where-to-register}
## Where to configure providers
You can configure providers for different injectors in the injector hierarchy.
An internal platform-level injector is shared by all running apps.
The `AppModule` injector is the root of an app-wide injector hierarchy, and within
an NgModule, directive-level injectors follow the structure of the component hierarchy.
## The injector tree The choices you make about where to configure providers lead to differences in the final bundle size, service _scope_, and service _lifetime_.
In the [Dependency Injection](guide/dependency-injection) guide, When you specify providers in the `@Injectable()` decorator of the service itself (typically at the app root level), optimization tools such as those used by the CLI's production builds can perform *tree shaking*, which removes services that aren't used by your app. Tree shaking results in smaller bundle sizes.
you learned how to configure a dependency injector and how to retrieve dependencies where you need them.
In fact, there is no such thing as ***the*** injector. * Learn more about [tree-shakable providers](guide/dependency-injection-providers#tree-shakable-providers).
An application may have multiple injectors.
An Angular application is a tree of components. Each component instance has its own injector.
The tree of components parallels the tree of injectors.
You're likely to inject `UserService` in many places throughout the app and will want to inject the same service instance every time. Providing `UserService` through the `root` injector is a good choice, and is the default that the CLI uses when you generate a service for your app.
<div class="alert is-helpful"> <div class="alert-is-helpful">
<header>Platform injector</header>
When you use `providedIn:'root'`, you are configuring the root injector for the _app_, which is the injector for `AppModule`.
The actual root of the entire injector hierarchy is a _platform injector_ that is the parent of app-root injectors.
This allows multiple apps to share a platform configuration. For example, a browser has only one URL bar, no matter how many apps you have running.
The platform injector is used internally during bootstrap, to configure platform-specific dependencies. You can configure additional platform-specific providers at the platform level by supplying `extraProviders` using the `platformBrowser()` function.
The component's injector may be a _proxy_ for an ancestor injector higher in the component tree. Learn more about dependency resolution through the injector hierarchy:
That's an implementation detail that improves efficiency. [What you always wanted to know about Angular Dependency Injection tree](https://blog.angularindepth.com/angular-dependency-injection-and-tree-shakeable-tokens-4588a8f70d5d)
You won't notice the difference and
your mental model should be that every component has its own injector.
</div> </div>
*NgModule-level* providers can be specified with `@NgModule()` `providers` metadata option, or in the `@Injectable()` `providedIn` option (with some module other than the root `AppModule`).
Use the `@NgModule()` `provides` option if a module is [lazy loaded](guide/lazy-loading-ngmodules). The module's own injector is configured with the provider when that module is loaded, and Angular can inject the corresponding services in any class it creates in that module. If you use the `@Injectable()` option `providedIn: MyLazyloadModule`, the provider could be shaken out at compile time, if it is not used anywhere else in the app.
* Learn more about [tree-shakable providers](guide/dependency-injection-providers#tree-shakable-providers).
For both root-level and module-level injectors, a service instance lives for the life of the app or module, and Angular injects this one service instance in every class that needs it.
*Component-level* providers configure each component instance's own injector.
Angular can only inject the corresponding services in that component instance or one of its descendant component instances.
Angular can't inject the same service instance anywhere else.
A component-provided service may have a limited lifetime.
Each new instance of the component gets its own instance of the service.
When the component instance is destroyed, so is that service instance.
In our sample app, `HeroComponent` is created when the application starts
and is never destroyed,
so the `HeroService` instance created for `HeroComponent` lives for the life of the app.
If you want to restrict `HeroService` access to `HeroComponent` and its nested
`HeroListComponent`, provide `HeroService` at the component level, in `HeroComponent` metadata.
* See more [examples of component-level injection](#component-injectors) below.
{@a register-providers-injectable}
### @Injectable-level configuration
The `@Injectable()` decorator identifies every service class. The `providedIn` metadata option for a service class configures a specific injector (typically `root`)
to use the decorated class as a provider of the service.
When an injectable class provides its own service to the `root` injector, the service is available anywhere the class is imported.
The following example configures a provider for `HeroService` using the `@Injectable()` decorator on the class.
<code-example path="dependency-injection/src/app/heroes/hero.service.0.ts" title="src/app/heroes/heroes.service.ts" linenums="false"> </code-example>
This configuration tells Angular that the app's root injector is responsible for creating an
instance of `HeroService` by invoking its constructor,
and for making that instance available across the application.
Providing a service with the app's root injector is a typical case,
and the CLI sets up this kind of a provider automatically for you
when generating a new service.
However, you might not always want to provide your service at the root level.
You might, for instance, want users to explicitly opt-in to using the service.
Instead of specifying the `root` injector, you can set `providedIn` to a specific NgModule.
For example, in the following excerpt, the `@Injectable()` decorator configures a provider
that is available in any injector that includes the `HeroModule`.
<code-example path="dependency-injection/src/app/heroes/hero.service.4.ts" title="src/app/heroes/hero.service.ts" linenums="false"> </code-example>
This is generally no different from configuring the injector of the NgModule itself,
except that the service is tree-shakable if the NgModule doesn't use it.
It can be useful for a library that offers a particular service that some
components *might* want to inject optionally,
and leave it up to the app whether to provide the service.
* Learn more about [tree-shakable providers](guide/dependency-injection-providers#tree-shakable-providers).
### @NgModule-level injectors
You can configure a provider at the module level using the `providedIn` metadata option for a non-root NgModule, in order to limit the scope of the provider to that module.
This is the equivalent of specifying the non-root module in the `@Injectable()` metadata, except that the service provided this way is not tree-shakable.
You generally don't need to specify `AppModule` with `providedIn`, because the app's `root` injector is the `AppModule` injector.
However, if you configure a app-wide provider in the`@NgModule()` metadata for `AppModule`,
it overrides one configured for `root` in the `@Injectable()` metadata.
You can do this to configure a non-default provider of a service that is shared with multiple apps.
Here is an example of the case where the component router configuration includes
a non-default [location strategy](guide/router#location-strategy) by listing its provider
in the `providers` list of the `AppModule`.
<code-example path="dependency-injection-in-action/src/app/app.module.ts" region="providers" title="src/app/app.module.ts (providers)" linenums="false">
</code-example>
{@a register-providers-component}
### @Component-level injectors
Individual components within an NgModule have their own injectors.
You can limit the scope of a provider to a component and its children
by configuring the provider at the component level using the `@Component` metadata.
The following example is a revised `HeroesComponent` that specifies `HeroService` in its `providers` array. `HeroService` can provide heroes to instances of this component, or to any child component instances.
<code-example path="dependency-injection/src/app/heroes/heroes.component.1.ts" title="src/app/heroes/heroes.component.ts" linenums="false">
</code-example>
### Element injectors
An injector does not actually belong to a component, but rather to the component instance's anchor element in the DOM. A different component instance on a different DOM element uses a different injector.
Components are a special type of directive, and the `providers` property of
`@Component()` is inherited from `@Directive()`.
Directives can also have dependencies, and you can configure providers
in their `@Directive()` metadata.
When you configure a provider for a component or directive using the `providers` property, that provider belongs to the injector for the anchor DOM element. Components and directives on the same element share an injector.
<!--- TBD with examples
* For an example of how this works, see [Element-level providers](guide/dependency-injection-in-action#directive-level-providers).
--->
* Learn more about [Element Injectors in Angular](https://blog.angularindepth.com/a-curios-case-of-the-host-decorator-and-element-injectors-in-angular-582562abcf0a).
## Injector bubbling
Consider this guide's variation on the Tour of Heroes application. Consider this guide's variation on the Tour of Heroes application.
At the top is the `AppComponent` which has some sub-components. At the top is the `AppComponent` which has some subcomponents, such as the `HeroesListComponent`.
One of them is the `HeroesListComponent`.
The `HeroesListComponent` holds and manages multiple instances of the `HeroTaxReturnComponent`. The `HeroesListComponent` holds and manages multiple instances of the `HeroTaxReturnComponent`.
The following diagram represents the state of the this guide's three-level component tree when there are three instances of `HeroTaxReturnComponent` The following diagram represents the state of this three-level component tree when there are three instances of `HeroTaxReturnComponent` open simultaneously.
open simultaneously.
<figure> <figure>
<img src="generated/images/guide/dependency-injection/component-hierarchy.png" alt="injector tree"> <img src="generated/images/guide/dependency-injection/component-hierarchy.png" alt="injector tree">
</figure> </figure>
### Injector bubbling
When a component requests a dependency, Angular tries to satisfy that dependency with a provider registered in that component's own injector. When a component requests a dependency, Angular tries to satisfy that dependency with a provider registered in that component's own injector.
If the component's injector lacks the provider, it passes the request up to its parent component's injector. If the component's injector lacks the provider, it passes the request up to its parent component's injector.
If that injector can't satisfy the request, it passes it along to *its* parent injector. If that injector can't satisfy the request, it passes the request along to the next parent injector up the tree.
The requests keep bubbling up until Angular finds an injector that can handle the request or runs out of ancestor injectors. The requests keep bubbling up until Angular finds an injector that can handle the request or runs out of ancestor injectors.
If it runs out of ancestors, Angular throws an error. If it runs out of ancestors, Angular throws an error.
If you have registered a provider for the same DI token at different levels, the first one Angular encounters is the one it uses to provide the dependency. If, for example, a provider is registered locally in the component that needs a service, Angular doesn't look for another provider of the same service.
<div class="alert is-helpful"> <div class="alert is-helpful">
You can cap the bubbling by adding the `@Host()` parameter decorator on the dependant-service parameter
in a component's constructor.
The hunt for providers stops at the injector for the host element of the component.
* See an [example](guide/dependency-injection-in-action#qualify-dependency-lookup) of using `@Host` together with `@Optional`, another parameter decorator that lets you handle the null case if no provider is found.
You can cap the bubbling. An intermediate component can declare that it is the "host" component. * Learn more about the [`@Host` decorator and Element Injectors](https://blog.angularindepth.com/a-curios-case-of-the-host-decorator-and-element-injectors-in-angular-582562abcf0a).
The hunt for providers will climb no higher than the injector for that host component.
This is a topic for another day.
</div> </div>
If you only register providers with the root injector at the top level (typically the root `AppModule`), the tree of injectors appears to be flat.
All requests bubble up to the root injector, whether you configured it with the `bootstrapModule` method, or registered all providers with `root` in their own services.
{@a component-injectors}
### Re-providing a service at different levels
You can re-register a provider for a particular dependency token at multiple levels of the injector tree.
You don't *have* to re-register providers. You shouldn't do so unless you have a good reason.
But you *can*.
As the resolution logic works upwards, the first provider encountered wins.
Thus, a provider in an intermediate injector intercepts a request for a service from something lower in the tree.
It effectively "reconfigures" and "shadows" a provider at a higher level in the tree.
If you only specify providers at the top level (typically the root `AppModule`), the tree of injectors appears to be flat.
All requests bubble up to the root <code>NgModule</code> injector that you configured with the `bootstrapModule` method.
## Component injectors ## Component injectors
The ability to configure one or more providers at different levels opens up interesting and useful possibilities. The ability to configure one or more providers at different levels opens up interesting and useful possibilities.
The guide sample offers some scenarios where you might want to do so.
### Scenario: service isolation ### Scenario: service isolation
Architectural reasons may lead you to restrict access to a service to the application domain where it belongs. Architectural reasons may lead you to restrict access to a service to the application domain where it belongs.
For example, the guide sample includes a `VillainsListComponent` that displays a list of villains.
The guide sample includes a `VillainsListComponent` that displays a list of villains.
It gets those villains from a `VillainsService`. It gets those villains from a `VillainsService`.
While you _could_ provide `VillainsService` in the root `AppModule` (that's where you'll find the `HeroesService`), If you provide `VillainsService` in the root `AppModule` (where you registered the `HeroesService`),
that would make the `VillainsService` available everywhere in the application, including the _Hero_ workflows. that would make the `VillainsService` available everywhere in the application, including the _Hero_ workflows. If you later modified the `VillainsService`, you could break something in a hero component somewhere. Providing the service in the root `AppModule` creates that risk.
If you later modified the `VillainsService`, you could break something in a hero component somewhere. Instead, you can provide the `VillainsService` in the `providers` metadata of the `VillainsListComponent` like this:
That's not supposed to happen but providing the service in the root `AppModule` creates that risk.
Instead, provide the `VillainsService` in the `providers` metadata of the `VillainsListComponent` like this:
<code-example path="hierarchical-dependency-injection/src/app/villains-list.component.ts" linenums="false" title="src/app/villains-list.component.ts (metadata)" region="metadata"> <code-example path="hierarchical-dependency-injection/src/app/villains-list.component.ts" linenums="false" title="src/app/villains-list.component.ts (metadata)" region="metadata">
</code-example> </code-example>
By providing `VillainsService` in the `VillainsListComponent` metadata and nowhere else, By providing `VillainsService` in the `VillainsListComponent` metadata and nowhere else,
the service becomes available only in the `VillainsListComponent` and its sub-component tree. the service becomes available only in the `VillainsListComponent` and its sub-component tree.
It's still a singleton, but it's a singleton that exist solely in the _villain_ domain. It's still a singleton, but it's a singleton that exist solely in the _villain_ domain.
@ -144,12 +240,10 @@ Each tax return component has the following characteristics:
<img src="generated/images/guide/dependency-injection/hid-heroes-anim.gif" alt="Heroes in action"> <img src="generated/images/guide/dependency-injection/hid-heroes-anim.gif" alt="Heroes in action">
</figure> </figure>
Suppose that the `HeroTaxReturnComponent` has logic to manage and restore changes.
One might suppose that the `HeroTaxReturnComponent` has logic to manage and restore changes.
That would be a pretty easy task for a simple hero tax return. That would be a pretty easy task for a simple hero tax return.
In the real world, with a rich tax return data model, the change management would be tricky. In the real world, with a rich tax return data model, the change management would be tricky.
You might delegate that management to a helper service, as this example does. You could delegate that management to a helper service, as this example does.
Here is the `HeroTaxReturnService`. Here is the `HeroTaxReturnService`.
It caches a single `HeroTaxReturn`, tracks changes to that return, and can save or restore it. It caches a single `HeroTaxReturn`, tracks changes to that return, and can save or restore it.
@ -160,8 +254,6 @@ It also delegates to the application-wide singleton `HeroService`, which it gets
</code-example> </code-example>
Here is the `HeroTaxReturnComponent` that makes use of it. Here is the `HeroTaxReturnComponent` that makes use of it.
@ -170,36 +262,29 @@ Here is the `HeroTaxReturnComponent` that makes use of it.
</code-example> </code-example>
The _tax-return-to-edit_ arrives via the input property which is implemented with getters and setters. The _tax-return-to-edit_ arrives via the input property which is implemented with getters and setters.
The setter initializes the component's own instance of the `HeroTaxReturnService` with the incoming return. The setter initializes the component's own instance of the `HeroTaxReturnService` with the incoming return.
The getter always returns what that service says is the current state of the hero. The getter always returns what that service says is the current state of the hero.
The component also asks the service to save and restore this tax return. The component also asks the service to save and restore this tax return.
There'd be big trouble if _this_ service were an application-wide singleton. This won't work if the service is an application-wide singleton.
Every component would share the same service instance. Every component would share the same service instance, and each component would overwrite the tax return that belonged to another hero.
Each component would overwrite the tax return that belonged to another hero.
What a mess!
Look closely at the metadata for the `HeroTaxReturnComponent`. Notice the `providers` property. To prevent this, we configure the component-level injector of `HeroTaxReturnComponent` to provide the service, using the `providers` property in the component metadata.
<code-example path="hierarchical-dependency-injection/src/app/hero-tax-return.component.ts" linenums="false" title="src/app/hero-tax-return.component.ts (providers)" region="providers"> <code-example path="hierarchical-dependency-injection/src/app/hero-tax-return.component.ts" linenums="false" title="src/app/hero-tax-return.component.ts (providers)" region="providers">
</code-example> </code-example>
The `HeroTaxReturnComponent` has its own provider of the `HeroTaxReturnService`. The `HeroTaxReturnComponent` has its own provider of the `HeroTaxReturnService`.
Recall that every component _instance_ has its own injector. Recall that every component _instance_ has its own injector.
Providing the service at the component level ensures that _every_ instance of the component gets its own, private instance of the service. Providing the service at the component level ensures that _every_ instance of the component gets its own, private instance of the service, and no tax return gets overwritten.
No tax return overwriting. No mess.
<div class="alert is-helpful"> <div class="alert is-helpful">
The rest of the scenario code relies on other Angular features and techniques that you can learn about elsewhere in the documentation. The rest of the scenario code relies on other Angular features and techniques that you can learn about elsewhere in the documentation.
You can review it and download it from the <live-example></live-example>. You can review it and download it from the <live-example></live-example>.
@ -210,10 +295,9 @@ You can review it and download it from the <live-example></live-example>.
### Scenario: specialized providers ### Scenario: specialized providers
Another reason to re-provide a service is to substitute a _more specialized_ implementation of that service, Another reason to re-provide a service at another level is to substitute a _more specialized_ implementation of that service, deeper in the component tree.
deeper in the component tree.
Consider again the Car example from the [Dependency Injection](guide/dependency-injection) guide. Consider a Car component that depends on several services.
Suppose you configured the root injector (marked as A) with _generic_ providers for Suppose you configured the root injector (marked as A) with _generic_ providers for
`CarService`, `EngineService` and `TiresService`. `CarService`, `EngineService` and `TiresService`.
@ -229,8 +313,6 @@ Component (B) is the parent of another component (C) that defines its own, even
<img src="generated/images/guide/dependency-injection/car-components.png" alt="car components"> <img src="generated/images/guide/dependency-injection/car-components.png" alt="car components">
</figure> </figure>
Behind the scenes, each component sets up its own injector with zero, one, or more providers defined for that component itself. Behind the scenes, each component sets up its own injector with zero, one, or more providers defined for that component itself.
When you resolve an instance of `Car` at the deepest component (C), When you resolve an instance of `Car` at the deepest component (C),
@ -247,9 +329,7 @@ its injector produces an instance of `Car` resolved by injector (C) with an `Eng
<div class="alert is-helpful"> <div class="alert is-helpful">
The code for this _cars_ scenario is in the `car.components.ts` and `car.services.ts` files of the sample The code for this _cars_ scenario is in the `car.components.ts` and `car.services.ts` files of the sample
which you can review and download from the <live-example></live-example>. which you can review and download from the <live-example></live-example>.
</div> </div>

View File

@ -88,5 +88,5 @@ Register a provider with a component when you must limit a service instance to a
You may also be interested in: You may also be interested in:
* [Singleton Services](guide/singleton-services), which elaborates on the concepts covered on this page. * [Singleton Services](guide/singleton-services), which elaborates on the concepts covered on this page.
* [Lazy Loading Modules](guide/lazy-loading-ngmodules). * [Lazy Loading Modules](guide/lazy-loading-ngmodules).
* [Tree-shakable Providers](guide/dependency-injection#tree-shakable-providers). * [Tree-shakable Providers](guide/dependency-injection-providers#tree-shakable-providers).
* [NgModule FAQ](guide/ngmodule-faq). * [NgModule FAQ](guide/ngmodule-faq).

View File

@ -1,6 +1,6 @@
# Angular service worker introduction # Angular service worker introduction
Service workers augment the traditional web deployment model and empower applications to deliver a user experience with the reliability and performance on par with natively-installed code. Service workers augment the traditional web deployment model and empower applications to deliver a user experience with the reliability and performance on par with natively-installed code. Adding a service worker to an Angular application is one of the steps for turning an application into a [Progressive Web App](https://developers.google.com/web/progressive-web-apps/) (also known as a PWA).
At its simplest, a service worker is a script that runs in the web browser and manages caching for an application. At its simplest, a service worker is a script that runs in the web browser and manages caching for an application.

View File

@ -768,7 +768,7 @@ code. For example, you might have a service called `HeroesService` in AngularJS:
<code-example path="upgrade-module/src/app/ajs-to-a-providers/heroes.service.ts" title="heroes.service.ts"> <code-example path="upgrade-module/src/app/ajs-to-a-providers/heroes.service.ts" title="heroes.service.ts">
</code-example> </code-example>
You can upgrade the service using a Angular [factory provider](guide/dependency-injection#factory-providers) You can upgrade the service using a Angular [factory provider](guide/dependency-injection-providers#factory-providers)
that requests the service from the AngularJS `$injector`. that requests the service from the AngularJS `$injector`.
Many developers prefer to declare the factory provider in a separate `ajs-upgraded-providers.ts` file Many developers prefer to declare the factory provider in a separate `ajs-upgraded-providers.ts` file

View File

@ -4,6 +4,31 @@
<article> <article>
<p>Where we'll be presenting:</p> <p>Where we'll be presenting:</p>
<table class="is-full-width">
<thead>
<tr>
<th>Event</th>
<th>Location</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<!-- ReactiveConf -->
<tr>
<th><a href="https://reactiveconf.com/" title="ReactiveConf">ReactiveConf</a></th>
<td>Prague, Czech Republic</td>
<td>October 29-31, 2018</td>
</tr>
<!-- AngularConnect-->
<tr>
<th><a href="http://angularconnect.com" title="AngularConnect">AngularConnect</a></th>
<td>London, United Kingdom</td>
<td>November 5-7, 2018</td>
</tr>
</tbody>
</table>
<p>Where we already presented:</p>
<table class="is-full-width"> <table class="is-full-width">
<thead> <thead>
<tr> <tr>
@ -55,18 +80,6 @@
<td>Melbourne, Australia</td> <td>Melbourne, Australia</td>
<td>Jun 22, 2018</td> <td>Jun 22, 2018</td>
</tr> </tr>
<!-- ReactiveConf -->
<tr>
<th><a href="https://reactiveconf.com/" title="ReactiveConf">ReactiveConf</a></th>
<td>Bratislava, Slovakia</td>
<td>October 29-31, 2018</td>
</tr>
<!-- AngularConnect-->
<tr>
<th><a href="http://angularconnect.com" title="AngularConnect">AngularConnect</a></th>
<td>London, United Kingdom</td>
<td>November 5-7, 2018</td>
</tr>
</tbody> </tbody>
</table> </table>
</article> </article>

View File

@ -413,7 +413,7 @@
"desc": "TrulyUI is an Angular UI Framework especially developed for Desktop Applications based on Web Components using the greatest technologies of the world.", "desc": "TrulyUI is an Angular UI Framework especially developed for Desktop Applications based on Web Components using the greatest technologies of the world.",
"rev": true, "rev": true,
"title": "Truly UI", "title": "Truly UI",
"url": "http://truly-ui.tk" "url": "http://truly-ui.com"
} }
} }
} }

View File

@ -349,11 +349,6 @@
"title": "Dependency Injection", "title": "Dependency Injection",
"tooltip": "Dependency Injection: creating and injecting services", "tooltip": "Dependency Injection: creating and injecting services",
"children": [ "children": [
{
"url": "guide/dependency-injection-pattern",
"title": "The Dependency Injection pattern",
"tooltip": "Learn about the dependency injection pattern behind the Angular DI system."
},
{ {
"url": "guide/dependency-injection", "url": "guide/dependency-injection",
"title": "Angular Dependency Injection", "title": "Angular Dependency Injection",
@ -362,12 +357,22 @@
{ {
"url": "guide/hierarchical-dependency-injection", "url": "guide/hierarchical-dependency-injection",
"title": "Hierarchical Injectors", "title": "Hierarchical Injectors",
"tooltip": "Angular's hierarchical dependency injection system supports nested injectors in parallel with the component tree." "tooltip": "An injector tree parallels the component tree and supports nested dependencies."
},
{
"url": "guide/dependency-injection-providers",
"title": "DI Providers",
"tooltip": "More about the different kinds of providers."
}, },
{ {
"url": "guide/dependency-injection-in-action", "url": "guide/dependency-injection-in-action",
"title": "DI in Action", "title": "DI in Action",
"tooltip": "Techniques for Dependency Injection." "tooltip": "Techniques for dependency injection."
},
{
"url": "guide/dependency-injection-navtree",
"title": "Navigate the Component Tree",
"tooltip": "Use the injection tree to find parent components."
} }
] ]
}, },

View File

@ -12,7 +12,7 @@ Using the Angular CLI, generate a new component named `heroes`.
ng generate component heroes ng generate component heroes
</code-example> </code-example>
The CLI creates a new folder, `src/app/heroes/` and generates The CLI creates a new folder, `src/app/heroes/`, and generates
the three files of the `HeroesComponent`. the three files of the `HeroesComponent`.
The `HeroesComponent` class file is as follows: The `HeroesComponent` class file is as follows:
@ -38,7 +38,7 @@ The CLI generated three metadata properties:
The [CSS element selector](https://developer.mozilla.org/en-US/docs/Web/CSS/Type_selectors), The [CSS element selector](https://developer.mozilla.org/en-US/docs/Web/CSS/Type_selectors),
`'app-heroes'`, matches the name of the HTML element that identifies this component within a parent component's template. `'app-heroes'`, matches the name of the HTML element that identifies this component within a parent component's template.
The `ngOnInit` is a [lifecycle hook](guide/lifecycle-hooks#oninit) The `ngOnInit` is a [lifecycle hook](guide/lifecycle-hooks#oninit).
Angular calls `ngOnInit` shortly after creating a component. Angular calls `ngOnInit` shortly after creating a component.
It's a good place to put initialization logic. It's a good place to put initialization logic.
@ -108,7 +108,7 @@ and show both `id` and `name` in a details layout like this:
title="heroes.component.html (HeroesComponent's template)" linenums="false"> title="heroes.component.html (HeroesComponent's template)" linenums="false">
</code-example> </code-example>
The browser refreshes and display's the hero's information. The browser refreshes and displays the hero's information.
## Format with the _UppercasePipe_ ## Format with the _UppercasePipe_

View File

@ -22,7 +22,7 @@
"test": "yarn check-env && ng test", "test": "yarn check-env && ng test",
"pree2e": "yarn check-env && yarn update-webdriver", "pree2e": "yarn check-env && yarn update-webdriver",
"e2e": "ng e2e --no-webdriver-update", "e2e": "ng e2e --no-webdriver-update",
"presetup": "yarn install --frozen-lockfile && yarn ~~check-env && yarn boilerplate:remove", "presetup": "yarn --cwd .. install && yarn install --frozen-lockfile && yarn ~~check-env && yarn boilerplate:remove",
"setup": "yarn aio-use-npm && yarn example-use-npm", "setup": "yarn aio-use-npm && yarn example-use-npm",
"postsetup": "yarn boilerplate:add && yarn build-ie-polyfills && yarn docs", "postsetup": "yarn boilerplate:add && yarn build-ie-polyfills && yarn docs",
"presetup-local": "yarn presetup", "presetup-local": "yarn presetup",
@ -79,7 +79,7 @@
"@angular/platform-browser-dynamic": "6.1.0-rc.3", "@angular/platform-browser-dynamic": "6.1.0-rc.3",
"@angular/router": "6.1.0-rc.3", "@angular/router": "6.1.0-rc.3",
"@angular/service-worker": "6.1.0-rc.3", "@angular/service-worker": "6.1.0-rc.3",
"@webcomponents/custom-elements": "^1.0.8", "@webcomponents/custom-elements": "^1.2.0",
"classlist.js": "^1.1.20150312", "classlist.js": "^1.1.20150312",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"rxjs": "6.2.2", "rxjs": "6.2.2",

View File

@ -4,7 +4,7 @@
"uncompressed": { "uncompressed": {
"runtime": 2768, "runtime": 2768,
"main": 476338, "main": 476338,
"polyfills": 38453, "polyfills": 53922,
"prettify": 14913 "prettify": 14913
} }
} }

View File

@ -10,6 +10,7 @@
<aio-select (change)="setStatus($event.option)" <aio-select (change)="setStatus($event.option)"
[options]="statuses" [options]="statuses"
[selected]="status" [selected]="status"
[disabled]="type.value === 'package'"
label="Status:"> label="Status:">
</aio-select> </aio-select>

View File

@ -1,5 +1,5 @@
<div class="form-select-menu"> <div class="form-select-menu">
<button class="form-select-button" (click)="toggleOptions()"> <button class="form-select-button" (click)="toggleOptions()" [disabled]="disabled">
<strong>{{label}}</strong><span *ngIf="showSymbol" class="symbol {{selected?.value}}"></span>{{selected?.title}} <strong>{{label}}</strong><span *ngIf="showSymbol" class="symbol {{selected?.value}}"></span>{{selected?.title}}
</button> </button>
<ul class="form-select-dropdown" *ngIf="showOptions"> <ul class="form-select-dropdown" *ngIf="showOptions">

View File

@ -67,6 +67,28 @@ describe('SelectComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
expect(getOptionContainer()).toEqual(null); expect(getOptionContainer()).toEqual(null);
}); });
it('should be disabled if the component is disabled', () => {
host.options = options;
fixture.detectChanges();
expect(getButton().disabled).toBe(false);
expect(getButton().getAttribute('disabled')).toBe(null);
host.disabled = true;
fixture.detectChanges();
expect(getButton().disabled).toBe(true);
expect(getButton().getAttribute('disabled')).toBeDefined();
});
it('should not toggle the visibility of the options list if disabled', () => {
host.options = options;
host.disabled = true;
fixture.detectChanges();
getButton().click();
fixture.detectChanges();
expect(getOptionContainer()).toEqual(null);
});
}); });
describe('options list', () => { describe('options list', () => {
@ -138,7 +160,8 @@ describe('SelectComponent', () => {
[options]="options" [options]="options"
[selected]="selected" [selected]="selected"
[label]="label" [label]="label"
[showSymbol]="showSymbol"> [showSymbol]="showSymbol"
[disabled]="disabled">
</aio-select>` </aio-select>`
}) })
class HostComponent { class HostComponent {
@ -147,6 +170,7 @@ class HostComponent {
selected: Option; selected: Option;
label: string; label: string;
showSymbol: boolean; showSymbol: boolean;
disabled: boolean;
} }
function getButton(): HTMLButtonElement { function getButton(): HTMLButtonElement {

View File

@ -25,6 +25,9 @@ export class SelectComponent implements OnInit {
@Input() @Input()
label: string; label: string;
@Input()
disabled: boolean;
showOptions = false; showOptions = false;
constructor(private hostElement: ElementRef) {} constructor(private hostElement: ElementRef) {}

View File

@ -83,28 +83,6 @@
<script nomodule src="generated/ie-polyfills.min.js"></script> <script nomodule src="generated/ie-polyfills.min.js"></script>
<script>
//load CE polyfill
//HACK: webpack's html plugin mangles the document.write calls if we don't trick it.
//load the ES5 shim for browsers with native CE support
function loadCustomElementsShim(){
document.write('<scri' + 'pt src="assets/js/native-shim.js"><' + '/script>');
}
//load the full custom elements polyfill for browsers without support
function loadCustomElementsPolyfill(){
document.write('<scri' + 'pt src="assets/js/custom-elements.min.js"><' + '/script>');
}
//detect if we have native CE support
if(!window.customElements){
loadCustomElementsPolyfill();
}
else {
loadCustomElementsShim();
}
</script>
</head> </head>
<body> <body>

View File

@ -36,7 +36,8 @@
* Zone JS is required by Angular itself. * Zone JS is required by Angular itself.
*/ */
import 'zone.js/dist/zone'; // Included with Angular CLI. import 'zone.js/dist/zone'; // Included with Angular CLI.
import '@webcomponents/custom-elements'; // Custom Elements Polyfill
import '@webcomponents/custom-elements/src/native-shim';
/*************************************************************************************************** /***************************************************************************************************

View File

@ -30,6 +30,11 @@
border: 1px solid $blue-400; border: 1px solid $blue-400;
box-shadow: 0 2px 2px rgba($blue-400, 0.24), 0 0 2px rgba($blue-400, 0.12); box-shadow: 0 2px 2px rgba($blue-400, 0.24), 0 0 2px rgba($blue-400, 0.12);
} }
&[disabled] {
color: lightgrey;
cursor: not-allowed;
}
} }
.form-select-dropdown { .form-select-dropdown {

View File

@ -3,8 +3,9 @@ import { SitePage } from './site.po';
describe(browser.baseUrl, () => { describe(browser.baseUrl, () => {
const page = new SitePage(); const page = new SitePage();
const getCurrentUrl = async () => (await browser.getCurrentUrl()).replace(/\?.*$/, ''); const getCurrentUrl = () => browser.getCurrentUrl().then(stripQuery).then(stripTrailingSlash);
const prependBaseUrl = (url: string) => browser.baseUrl.replace(/\/$/, '') + url; const stripQuery = (url: string) => url.replace(/\?.*$/, '');
const stripTrailingSlash = (url: string) => url.replace(/\/$/, '');
beforeAll(done => page.init().then(done)); beforeAll(done => page.init().then(done));
@ -16,7 +17,7 @@ describe(browser.baseUrl, () => {
it(`should not redirect '${url}' (${i + 1}/${page.sitemapUrls.length})`, async () => { it(`should not redirect '${url}' (${i + 1}/${page.sitemapUrls.length})`, async () => {
await page.goTo(url); await page.goTo(url);
const expectedUrl = prependBaseUrl(url); const expectedUrl = stripTrailingSlash(page.baseUrl + url);
const actualUrl = await getCurrentUrl(); const actualUrl = await getCurrentUrl();
expect(actualUrl).toBe(expectedUrl); expect(actualUrl).toBe(expectedUrl);
@ -29,7 +30,7 @@ describe(browser.baseUrl, () => {
it(`should redirect '${fromUrl}' to '${toUrl}' (${i + 1}/${page.legacyUrls.length})`, async () => { it(`should redirect '${fromUrl}' to '${toUrl}' (${i + 1}/${page.legacyUrls.length})`, async () => {
await page.goTo(fromUrl); await page.goTo(fromUrl);
const expectedUrl = /^http/.test(toUrl) ? toUrl : prependBaseUrl(toUrl); const expectedUrl = stripTrailingSlash(/^http/.test(toUrl) ? toUrl : page.baseUrl + toUrl);
const actualUrl = await getCurrentUrl(); const actualUrl = await getCurrentUrl();
expect(actualUrl).toBe(expectedUrl); expect(actualUrl).toBe(expectedUrl);
@ -66,8 +67,8 @@ describe(browser.baseUrl, () => {
expect(homeNavLink.isPresent()).toBe(true); expect(homeNavLink.isPresent()).toBe(true);
await homeNavLink.click(); await homeNavLink.click();
const expectedUrl = browser.baseUrl; const expectedUrl = page.baseUrl;
const actualUrl = await browser.getCurrentUrl(); const actualUrl = await getCurrentUrl();
expect(actualUrl).toBe(expectedUrl); expect(actualUrl).toBe(expectedUrl);
}); });

View File

@ -1,6 +1,9 @@
import { browser, by, element, ExpectedConditions } from 'protractor'; import { browser, by, element, ExpectedConditions } from 'protractor';
export class SitePage { export class SitePage {
/** The base URL with the trailing `/` stripped off (if any). */
baseUrl = browser.baseUrl.replace(/\/$/, '');
/** All URLs found in the app's `sitemap.xml` (i.e. valid URLs tha should not be redirected). */ /** All URLs found in the app's `sitemap.xml` (i.e. valid URLs tha should not be redirected). */
sitemapUrls: string[] = browser.params.sitemapUrls; sitemapUrls: string[] = browser.params.sitemapUrls;
@ -44,7 +47,7 @@ export class SitePage {
.then(regs => Promise.all(regs.map(reg => reg.unregister()))) .then(regs => Promise.all(regs.map(reg => reg.unregister())))
.then(cb); .then(cb);
await browser.get(url || browser.baseUrl); await browser.get(url || this.baseUrl);
await browser.executeScript('document.body.classList.add(\'no-animations\')'); await browser.executeScript('document.body.classList.add(\'no-animations\')');
await browser.executeAsyncScript(unregisterServiceWorker); await browser.executeAsyncScript(unregisterServiceWorker);
await browser.waitForAngular(); await browser.waitForAngular();

View File

@ -49,6 +49,7 @@ export function loadLocalSitemapUrls() {
} }
export async function loadRemoteSitemapUrls(host: string) { export async function loadRemoteSitemapUrls(host: string) {
host = host.replace(/\/$/, '');
const urlToSiteMap = `${host}/generated/sitemap.xml`; const urlToSiteMap = `${host}/generated/sitemap.xml`;
const get = /^https:/.test(host) ? httpsGet : httpGet; const get = /^https:/.test(host) ? httpsGet : httpGet;

View File

@ -179,4 +179,25 @@ describe('site App', function() {
expect(results).toContain('Router'); expect(results).toContain('Router');
}); });
}); });
describe('suggest edit link', () => {
it('should be present on all docs pages', () => {
page.navigateTo('tutorial/toh-pt1');
expect(page.ghLinks.count()).toEqual(1);
/* tslint:disable:max-line-length */
expect(page.ghLinks.get(0).getAttribute('href'))
.toMatch(/https:\/\/github\.com\/angular\/angular\/edit\/master\/aio\/content\/tutorial\/toh-pt1\.md\?message=docs%3A%20describe%20your%20change\.\.\./);
page.navigateTo('guide/http');
expect(page.ghLinks.count()).toEqual(1);
/* tslint:disable:max-line-length */
expect(page.ghLinks.get(0).getAttribute('href'))
.toMatch(/https:\/\/github\.com\/angular\/angular\/edit\/master\/aio\/content\/guide\/http\.md\?message=docs%3A%20describe%20your%20change\.\.\./);
});
it('should not be present on top level pages', () => {
page.navigateTo('features');
expect(page.ghLinks.count()).toEqual(0);
});
});
}); });

View File

@ -43,7 +43,7 @@ module.exports = new Package('angular-content', [basePackage, contentPackage])
readFilesProcessor.sourceFiles = readFilesProcessor.sourceFiles.concat([ readFilesProcessor.sourceFiles = readFilesProcessor.sourceFiles.concat([
{ {
basePath: CONTENTS_PATH, basePath: CONTENTS_PATH,
include: CONTENTS_PATH + '/{cookbook,guide,tutorial}/**/*.md', include: CONTENTS_PATH + '/{guide,tutorial}/**/*.md',
fileReader: 'contentFileReader' fileReader: 'contentFileReader'
}, },
{ {

View File

@ -1,4 +1,4 @@
{% import "lib/githubLinks.html" as github -%} {% import "../lib/githubLinks.html" as github -%}
{% set comma = joiner(',') %} {% set comma = joiner(',') %}
{% set breadcrumbDelimiter = joiner('&gt;') %} {% set breadcrumbDelimiter = joiner('&gt;') %}
<article> <article>

View File

@ -1,4 +1,4 @@
{% import "lib/githubLinks.html" as github -%} {% import "../../lib/githubLinks.html" as github -%}
<!-- INFO BAR --> <!-- INFO BAR -->
<section class="info-bar"> <section class="info-bar">

View File

@ -1,19 +0,0 @@
{% macro githubViewHref(doc, versionInfo) -%}
https://github.com/{$ versionInfo.gitRepoInfo.owner $}/{$ versionInfo.gitRepoInfo.repo $}/tree/{$ versionInfo.currentVersion.isSnapshot and versionInfo.currentVersion.SHA or versionInfo.currentVersion.raw $}/packages/{$ doc.fileInfo.realProjectRelativePath or doc.fileInfo.relativePath $}#L{$ doc.startingLine + 1 $}-L{$ doc.endingLine + 1 $}
{%- endmacro -%}
{% macro githubEditHref(doc, versionInfo) -%}
https://github.com/{$ versionInfo.gitRepoInfo.owner $}/{$ versionInfo.gitRepoInfo.repo $}/edit/master/packages/{$ doc.fileInfo.realProjectRelativePath or doc.fileInfo.relativePath $}?message=docs(
{%- if doc.moduleDoc %}{$ doc.moduleDoc.id.split('/')[0] $}
{%- elseif doc.docType === 'package' %}{$ doc.id.split('/')[0] $}
{%- else %}...{%- endif -%}
)%3A%20describe%20your%20change...#L{$ doc.startingLine + 1 $}-L{$ doc.endingLine + 1 $}
{%- endmacro -%}
{% macro githubLinks(doc, versionInfo) -%}
<div class="github-links">
<a href="{$ githubEditHref(doc, versionInfo) $}" aria-label="Suggest Edits" title="Suggest Edits"><i class="material-icons" aria-hidden="true" role="img">mode_edit</i></a>
<a href="{$ githubViewHref(doc, versionInfo) $}" aria-label="View Source" title="View Source"><i class="material-icons" aria-hidden="true" role="img">code</i></a>
</div>
{%- endmacro -%}

View File

@ -1,11 +1,12 @@
{% extends 'base.template.html' -%} {% extends 'base.template.html' -%}
{% macro listItems(items, title, overridePath) %} {% macro listItems(items, title, overridePath) %}
{% if items.length %} {% set filteredItems = items | filterByPropertyValue('internal', undefined) %}
{% if filteredItems.length %}
<section class="export-list"> <section class="export-list">
<h3>{$ title $}</h3> <h3>{$ title $}</h3>
<table class="is-full-width list-table"> <table class="is-full-width list-table">
{% for item in items %} {% for item in filteredItems %}
<tr> <tr>
<td><code class="code-anchor"> <td><code class="code-anchor">
<a href="{$ overridePath or item.path $}">{$ item.name $}</a></code></td> <a href="{$ overridePath or item.path $}">{$ item.name $}</a></code></td>

View File

@ -1,4 +1,12 @@
{% import "lib/githubLinks.html" as github -%}
{% set relativePath = doc.fileInfo.relativePath %}
{% if doc.title %}{$ ('# ' + doc.title.trim()) | marked $}{% endif %} {% if doc.title %}{$ ('# ' + doc.title.trim()) | marked $}{% endif %}
{% if 'guide/' in relativePath or 'tutorial/' in relativePath or 'docs.md' in relativePath %}
<div class="github-links">
{$ github.githubEditLink(doc, versionInfo) $}
</div>
{% endif %}
<div class="content"> <div class="content">
{$ doc.description | marked $} {$ doc.description | marked $}
</div> </div>

View File

@ -0,0 +1,31 @@
{% macro projectRelativePath(fileInfo) -%}
{$ fileInfo.realProjectRelativePath if fileInfo.realProjectRelativePath else fileInfo.projectRelativePath $}
{%- endmacro %}
{% macro githubViewHref(doc, versionInfo) -%}
https://github.com/{$ versionInfo.gitRepoInfo.owner $}/{$ versionInfo.gitRepoInfo.repo $}/tree/{$ versionInfo.currentVersion.isSnapshot and versionInfo.currentVersion.SHA or versionInfo.currentVersion.raw $}/packages/{$ doc.fileInfo.realProjectRelativePath $}#L{$ doc.startingLine + 1 $}-L{$ doc.endingLine + 1 $}
{%- endmacro %}
{% macro githubEditHref(doc, versionInfo) -%}
https://github.com/{$ versionInfo.gitRepoInfo.owner $}/{$ versionInfo.gitRepoInfo.repo $}/edit/master{$ '/packages' if doc.docType !== 'content' $}/{$ projectRelativePath(doc.fileInfo) $}?message=docs
{%- if doc.moduleDoc %}({$ doc.moduleDoc.id.split('/')[0] $})
{%- elseif doc.docType === 'module' %}({$ doc.id.split('/')[0] $})
{%- elseif doc.docType === 'content' %}
{%- else %}(...){%- endif -%}
%3A%20describe%20your%20change...{% if doc.docType !== 'content' %}#L{$ doc.startingLine + 1 $}-L{$ doc.endingLine + 1 $}{% endif %}
{%- endmacro %}
{% macro githubEditLink(doc, versionInfo) -%}
<a href="{$ githubEditHref(doc, versionInfo) $}" aria-label="Suggest Edits" title="Suggest Edits"><i class="material-icons" aria-hidden="true" role="img">mode_edit</i></a>
{%- endmacro %}
{% macro githubViewLink(doc, versionInfo) -%}
<a href="{$ githubViewHref(doc, versionInfo) $}" aria-label="View Source" title="View Source"><i class="material-icons" aria-hidden="true" role="img">code</i></a>
{%- endmacro %}
{% macro githubLinks(doc, versionInfo) -%}
<div class="github-links">
{$ githubEditLink(doc, versionInfo) $}
{$ githubViewLink(doc, versionInfo) $}
</div>
{%- endmacro -%}

View File

@ -1,4 +1,4 @@
{% import "api/lib/githubLinks.html" as github -%} {% import "lib/githubLinks.html" as github -%}
{% import "api/lib/memberHelpers.html" as members -%} {% import "api/lib/memberHelpers.html" as members -%}
{% macro goToCode(doc) %}<a href="{$ github.githubViewHref(doc, versionInfo) $}" class="go-to-code" title="Go to source code"><i class="material-icons" aria-hidden="true" role="img">code</i></a>{% endmacro %} {% macro goToCode(doc) %}<a href="{$ github.githubViewHref(doc, versionInfo) $}" class="go-to-code" title="Go to source code"><i class="material-icons" aria-hidden="true" role="img">code</i></a>{% endmacro %}
{% macro label(test, class, text) %}{% if test %}<label class="{$ class $}">{$ text $}</label>{% endif %}{% endmacro %} {% macro label(test, class, text) %}{% if test %}<label class="{$ class $}">{$ text $}</label>{% endif %}{% endmacro %}

View File

@ -468,9 +468,9 @@
"@webassemblyjs/wast-parser" "1.4.3" "@webassemblyjs/wast-parser" "1.4.3"
long "^3.2.0" long "^3.2.0"
"@webcomponents/custom-elements@^1.0.8": "@webcomponents/custom-elements@^1.2.0":
version "1.0.8" version "1.2.0"
resolved "https://registry.yarnpkg.com/@webcomponents/custom-elements/-/custom-elements-1.0.8.tgz#b7b8ef7248f7681d1ad4286a0ada5fe3c2bc7228" resolved "https://registry.yarnpkg.com/@webcomponents/custom-elements/-/custom-elements-1.2.0.tgz#7fc8da6c8243b15b4f69c74c65dfe2a4996c694c"
JSONStream@^1.2.1: JSONStream@^1.2.1:
version "1.3.1" version "1.3.1"

View File

@ -43,10 +43,10 @@ var customLaunchers = {
'DartiumWithWebPlatform': 'DartiumWithWebPlatform':
{base: 'Dartium', flags: ['--enable-experimental-web-platform-features']}, {base: 'Dartium', flags: ['--enable-experimental-web-platform-features']},
'ChromeNoSandbox': {base: 'Chrome', flags: ['--no-sandbox']}, 'ChromeNoSandbox': {base: 'Chrome', flags: ['--no-sandbox']},
'SL_CHROME': {base: 'SauceLabs', browserName: 'chrome', version: '60'}, 'SL_CHROME': {base: 'SauceLabs', browserName: 'chrome', version: '67'},
'SL_CHROMEBETA': {base: 'SauceLabs', browserName: 'chrome', version: 'beta'}, 'SL_CHROMEBETA': {base: 'SauceLabs', browserName: 'chrome', version: 'beta'},
'SL_CHROMEDEV': {base: 'SauceLabs', browserName: 'chrome', version: 'dev'}, 'SL_CHROMEDEV': {base: 'SauceLabs', browserName: 'chrome', version: 'dev'},
'SL_FIREFOX': {base: 'SauceLabs', browserName: 'firefox', version: '54'}, 'SL_FIREFOX': {base: 'SauceLabs', browserName: 'firefox', version: '60'},
'SL_FIREFOXBETA': 'SL_FIREFOXBETA':
{base: 'SauceLabs', platform: 'Windows 10', browserName: 'firefox', version: 'beta'}, {base: 'SauceLabs', platform: 'Windows 10', browserName: 'firefox', version: 'beta'},
'SL_FIREFOXDEV': 'SL_FIREFOXDEV':

View File

@ -1,22 +0,0 @@
// WORKAROUND https://github.com/angular/angular/issues/18810
// This file is required to run ngc on angular libraries, to write files like
// node_modules/@angular/core/core.ngsummary.json
{
"compilerOptions": {
"lib": [
"dom",
"es2015"
],
"experimentalDecorators": true,
"types": []
},
"include": [
"node_modules/@angular/**/*"
],
"exclude": [
"node_modules/@angular/bazel/**",
"node_modules/@angular/compiler-cli/**",
"node_modules/@angular/common/locales/**",
"node_modules/@angular/*/testing/**"
]
}

View File

@ -10,7 +10,7 @@
"zone.js": "file:../../node_modules/zone.js" "zone.js": "file:../../node_modules/zone.js"
}, },
"devDependencies": { "devDependencies": {
"@bazel/typescript": "0.16.1", "@bazel/typescript": "0.16.2",
"@types/jasmine": "file:../../node_modules/@types/jasmine", "@types/jasmine": "file:../../node_modules/@types/jasmine",
"@types/source-map": "0.5.1", "@types/source-map": "0.5.1",
"http-server": "0.11.1", "http-server": "0.11.1",

View File

@ -2,12 +2,11 @@
# yarn lockfile v1 # yarn lockfile v1
"@bazel/typescript@0.16.1": "@bazel/typescript@0.16.2":
version "0.16.1" version "0.16.2"
resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-0.16.1.tgz#5a5d145a17898bfadae8268cf942815424119412" resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-0.16.2.tgz#42474e029b0c6fa42a058fc86e2f593b7a367d56"
dependencies: dependencies:
protobufjs "5.0.0" protobufjs "5.0.0"
tsickle "0.32.1"
tsutils "2.20.0" tsutils "2.20.0"
"@types/jasmine@file:../../node_modules/@types/jasmine": "@types/jasmine@file:../../node_modules/@types/jasmine":
@ -853,16 +852,6 @@ tough-cookie@~2.4.3:
psl "^1.1.24" psl "^1.1.24"
punycode "^1.4.1" punycode "^1.4.1"
tsickle@0.32.1:
version "0.32.1"
resolved "https://registry.yarnpkg.com/tsickle/-/tsickle-0.32.1.tgz#f16e94ba80b32fc9ebe320dc94fbc2ca7f3521a5"
dependencies:
jasmine-diff "^0.1.3"
minimist "^1.2.0"
mkdirp "^0.5.1"
source-map "^0.6.0"
source-map-support "^0.5.0"
"tsickle@file:../../node_modules/tsickle": "tsickle@file:../../node_modules/tsickle":
version "0.32.0" version "0.32.0"
dependencies: dependencies:

View File

@ -0,0 +1,44 @@
--compilation_level=ADVANCED_OPTIMIZATIONS
--language_out=ES6
--language_in=ES6
--js_output_file=dist/bundle.js
--output_manifest=dist/manifest.MF
--variable_renaming_report=dist/variable_renaming_report
--property_renaming_report=dist/property_renaming_report
--create_source_map=%outname%.map
--warning_level=QUIET
--dependency_mode=STRICT
--rewrite_polyfills=false
--jscomp_off=checkVars
node_modules/zone.js/dist/zone_externs.js
--js node_modules/rxjs/package.json
--js node_modules/rxjs/_esm2015/index.js
--js node_modules/rxjs/_esm2015/internal/**.js
--js node_modules/rxjs/operators/package.json
--js node_modules/rxjs/_esm2015/operators/index.js
--js node_modules/@angular/core/package.json
--js node_modules/@angular/core/fesm2015/core.js
--js node_modules/@angular/core/src/testability/testability.externs.js
--js node_modules/@angular/compiler/package.json
--js node_modules/@angular/compiler/fesm2015/compiler.js
--js node_modules/@angular/common/package.json
--js node_modules/@angular/common/fesm2015/common.js
--js node_modules/@angular/platform-browser/package.json
--js node_modules/@angular/platform-browser/fesm2015/platform-browser.js
--js node_modules/@angular/elements/package.json
--js node_modules/@angular/elements/fesm2015/elements.js
--module_resolution=node
--package_json_entry_names es2015
--process_common_js_modules
--js built/**.js
--entry_point=built/src/main

View File

@ -0,0 +1,20 @@
import { browser, element, by } from 'protractor';
browser.waitForAngularEnabled(false);
describe('Element E2E Tests', function () {
describe('Hello World Elements', () => {
it('should display: Hello world!', function () {
browser.get('hello-world.html');
const helloWorldEl = element(by.css('hello-world-el'));
expect(helloWorldEl.getText()).toEqual('Hello World!');
});
it('should display: Hello Foo! via name attribute', function () {
browser.get('hello-world.html');
const helloWorldEl = element(by.css('hello-world-el'));
const input = element(by.css('input[type=text]'));
input.sendKeys('F', 'o', 'o');
expect(helloWorldEl.getText()).toEqual('Hello Foo!');
});
});
});

View File

@ -0,0 +1,15 @@
{
"open": false,
"logLevel": "silent",
"port": 8080,
"server": {
"baseDir": "src",
"routes": {
"/dist": "dist",
"/node_modules": "node_modules"
},
"middleware": {
"0": null
}
}
}

View File

@ -0,0 +1,16 @@
exports.config = {
specs: [
'../built/e2e/*.e2e-spec.js'
],
capabilities: {
browserName: 'chrome',
chromeOptions: {
args: ['--no-sandbox'],
binary: process.env.CHROME_BIN,
}
},
directConnect: true,
baseUrl: 'http://localhost:8080/',
framework: 'jasmine',
useAllAngular2AppRoots: true
};

View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"outDir": "../built/e2e",
"types": ["jasmine"],
// TODO(alexeagle): was required for Protractor 4.0.11
"skipLibCheck": true
}
}

View File

@ -0,0 +1,34 @@
{
"name": "angular-integration",
"version": "0.0.0",
"license": "MIT",
"dependencies": {
"@angular/animations": "file:../../dist/packages-dist/animations",
"@angular/common": "file:../../dist/packages-dist/common",
"@angular/compiler": "file:../../dist/packages-dist/compiler",
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
"@angular/core": "file:../../dist/packages-dist/core",
"@angular/elements": "file:../../dist/packages-dist/elements",
"@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
"@angular/platform-server": "file:../../dist/packages-dist/platform-server",
"google-closure-compiler": "20180319.0.0",
"rxjs": "file:../../node_modules/rxjs",
"typescript": "file:../../node_modules/typescript",
"zone.js": "file:../../node_modules/zone.js"
},
"devDependencies": {
"@types/jasmine": "2.5.41",
"concurrently": "3.4.0",
"lite-server": "2.2.2",
"protractor": "file:../../node_modules/protractor"
},
"scripts": {
"postinstall": "webdriver-manager update --gecko false --standalone false $CHROMEDRIVER_VERSION_ARG",
"closure": "java -jar node_modules/google-closure-compiler/compiler.jar --flagfile closure.conf",
"test": "ngc && yarn run closure && concurrently \"yarn run serve\" \"yarn run protractor\" --kill-others --success first",
"serve": "lite-server -c e2e/browser.config.json",
"preprotractor": "tsc -p e2e",
"protractor": "protractor e2e/protractor.config.js"
},
"private": true
}

View File

@ -0,0 +1,23 @@
import {Injector, NgModule} from '@angular/core';
import {createCustomElement} from '@angular/elements';
import {BrowserModule} from '@angular/platform-browser';
import {HelloWorldComponent, HelloWorldShadowComponent, TestCardComponent} from './elements';
@NgModule({
declarations: [HelloWorldComponent, HelloWorldShadowComponent, TestCardComponent],
entryComponents: [HelloWorldComponent, HelloWorldShadowComponent, TestCardComponent],
imports: [BrowserModule],
})
export class AppModule {
constructor(private injector: Injector) {
customElements.define('hello-world-el', createCustomElement(HelloWorldComponent, {injector}));
customElements.define(
'hello-world-shadow-el', createCustomElement(HelloWorldShadowComponent, {injector}));
customElements.define('test-card', createCustomElement(TestCardComponent, {injector}));
}
ngDoBootstrap() {}
}
export {HelloWorldComponent};

View File

@ -0,0 +1,35 @@
import {Component, Input, ViewEncapsulation} from '@angular/core';
@Component({
selector: 'hello-world-el',
template: `Hello {{name}}!`,
})
export class HelloWorldComponent {
@Input() name: string = 'World';
}
@Component({
selector: 'hello-world-shadow-el',
template: `Hello {{name}}!`,
encapsulation: ViewEncapsulation.ShadowDom
})
export class HelloWorldShadowComponent {
@Input() name: string = 'World';
}
@Component({
selector: 'test-card',
template: `
<header>
<slot name="card-header"></slot>
</header>
<slot></slot>
<footer>
<slot name="card-footer"></slot>
</footer>`,
encapsulation: ViewEncapsulation.ShadowDom,
styles: []
})
export class TestCardComponent {
}

View File

@ -0,0 +1,17 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Hello World</title>
<base href="/">
</head>
<body>
<input type="text">
<hello-world-el></hello-world-el>
<script src="dist/bundle.js"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
import {platformBrowser} from '@angular/platform-browser';
import {AppModuleNgFactory} from './app.ngfactory';
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory, {ngZone: 'noop'});
const helloWorld = document.querySelector('hello-world-el');
const input = document.querySelector('input[type=text]');
if(input && helloWorld){
input.addEventListener('input', e => {
const newText = (e.target as any).value;
helloWorld.setAttribute('name', newText);
});
}

View File

@ -0,0 +1,20 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Slot Test</title>
<base href="/">
</head>
<body>
<test-card>
<span slot="card-header">TestCardTitle</span>
<p>TestCardContent</p>
<span slot="card-footer">TestCardFooter</span>
</test-card>
<script src="dist/bundle.js"></script>
</body>
</html>

View File

@ -0,0 +1,28 @@
{
"angularCompilerOptions": {
"annotationsAs": "static fields",
"annotateForClosureCompiler": true,
"alwaysCompileGeneratedCode": true
},
"compilerOptions": {
"module": "es2015",
"moduleResolution": "node",
"strictNullChecks": true,
"target": "es2015",
"sourceMap": false,
"experimentalDecorators": true,
"outDir": "built",
"rootDir": ".",
"declaration": true,
"types": []
},
"exclude": [
"vendor",
"node_modules",
"built",
"dist",
"e2e"
]
}

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,14 @@ set -e -x
PATH=$PATH:$(npm bin) PATH=$PATH:$(npm bin)
ivy-ngcc fesm2015,esm2015 ivy-ngcc fesm2015,esm2015
# Did it add the appropriate build markers?
ls node_modules/@angular/common | grep __modified_by_ngcc_for_fesm2015
if [[ $? != 0 ]]; then exit 1; fi
ls node_modules/@angular/common | grep __modified_by_ngcc_for_esm2015
if [[ $? != 0 ]]; then exit 1; fi
ngc -p tsconfig-app.json ngc -p tsconfig-app.json
# Look for correct output # Did it compile the main.ts correctly?
grep "directives: \[\S*\.NgIf\]" dist/src/main.js > /dev/null grep "directives: \[\S*\.NgIf\]" dist/src/main.js
if [[ $? != 0 ]]; then exit 1; fi

View File

@ -43,7 +43,7 @@ describe('sourcemaps', function() {
const marker = '//# sourceMappingURL=data:application/json;base64,'; const marker = '//# sourceMappingURL=data:application/json;base64,';
const index = content.indexOf(marker); const index = content.indexOf(marker);
const sourceMapData = const sourceMapData =
new Buffer(content.substring(index + marker.length), 'base64').toString('utf8'); Buffer.from(content.substring(index + marker.length), 'base64').toString('utf8');
const decoder = new sourceMap.SourceMapConsumer(JSON.parse(sourceMapData)); const decoder = new sourceMap.SourceMapConsumer(JSON.parse(sourceMapData));

View File

@ -1,6 +1,6 @@
{ {
"name": "angular-srcs", "name": "angular-srcs",
"version": "7.0.0-beta.4", "version": "7.0.0-beta.5",
"private": true, "private": true,
"branchPattern": "2.0.*", "branchPattern": "2.0.*",
"description": "Angular - a web framework for modern web apps", "description": "Angular - a web framework for modern web apps",
@ -21,9 +21,7 @@
"prebuildifier": "bazel build --noshow_progress @com_github_bazelbuild_buildtools//buildifier", "prebuildifier": "bazel build --noshow_progress @com_github_bazelbuild_buildtools//buildifier",
"buildifier": "find . -type f \\( -name \"*.bzl\" -or -name BUILD -or -name BUILD.bazel \\) ! -path \"*/node_modules/*\" | xargs $(bazel info bazel-bin)/external/com_github_bazelbuild_buildtools/buildifier/*/buildifier", "buildifier": "find . -type f \\( -name \"*.bzl\" -or -name BUILD -or -name BUILD.bazel \\) ! -path \"*/node_modules/*\" | xargs $(bazel info bazel-bin)/external/com_github_bazelbuild_buildtools/buildifier/*/buildifier",
"preinstall": "node tools/yarn/check-yarn.js", "preinstall": "node tools/yarn/check-yarn.js",
"postinstall": "yarn update-webdriver && node ./tools/postinstall-patches.js && yarn patch-types", "postinstall": "yarn update-webdriver && node ./tools/postinstall-patches.js",
"//patch-types": "work-around for issue https://github.com/angular/angular/issues/25051",
"patch-types": "node -e \"var sh = require('shelljs'); sh.set('-e'); sh.mkdir('-p', 'node_modules/@types/zone.js'); sh.cp('-f', 'node_modules/zone.js/dist/zone.js.d.ts', 'node_modules/@types/zone.js/index.d.ts')\"",
"update-webdriver": "webdriver-manager update --gecko false $CHROMEDRIVER_VERSION_ARG", "update-webdriver": "webdriver-manager update --gecko false $CHROMEDRIVER_VERSION_ARG",
"check-env": "gulp check-env", "check-env": "gulp check-env",
"commitmsg": "node ./scripts/git/commit-msg.js" "commitmsg": "node ./scripts/git/commit-msg.js"
@ -133,4 +131,4 @@
"xhr2": "0.1.4", "xhr2": "0.1.4",
"yargs": "9.0.1" "yargs": "9.0.1"
} }
} }

View File

@ -21,4 +21,5 @@ def ng_setup_workspace():
) )
# 0.16.0: minimal version required to work with ng_module # 0.16.0: minimal version required to work with ng_module
check_rules_typescript_version("0.16.0") # 0.16.2: bazel type resolution for zone.js types
check_rules_typescript_version("0.16.2")

View File

@ -62,9 +62,7 @@ if (onPreparePath) {
// ts_web_test_suite & rules_webtesting WEB_TEST_METADATA attributes // ts_web_test_suite & rules_webtesting WEB_TEST_METADATA attributes
setConf(conf, 'framework', 'jasmine2', 'is set to jasmine2'); setConf(conf, 'framework', 'jasmine2', 'is set to jasmine2');
const specs = [TMPL_specs] const specs = [TMPL_specs].map(s => require.resolve(s)).filter(s => /\b(spec|test)\.js$/.test(s));
.map(s => require.resolve(s))
.filter(s => s.endsWith('.spec.js') || s.endsWith('.test.js'));
setConf(conf, 'specs', specs, 'are determined by the srcs and deps attribute'); setConf(conf, 'specs', specs, 'are determined by the srcs and deps attribute');

View File

@ -58,13 +58,13 @@ export class ChromeDriverExtension extends WebDriverExtension {
// so that the chrome buffer does not fill up. // so that the chrome buffer does not fill up.
await this._driver.logs('performance'); await this._driver.logs('performance');
} }
return this._driver.executeScript(`console.time('${name}');`); return this._driver.executeScript(`performance.mark('${name}-bpstart');`);
} }
timeEnd(name: string, restartName: string|null = null): Promise<any> { timeEnd(name: string, restartName: string|null = null): Promise<any> {
let script = `console.timeEnd('${name}');`; let script = `performance.mark('${name}-bpend');`;
if (restartName) { if (restartName) {
script += `console.time('${restartName}');`; script += `performance.mark('${restartName}-bpstart');`;
} }
return this._driver.executeScript(script); return this._driver.executeScript(script);
} }
@ -109,6 +109,8 @@ export class ChromeDriverExtension extends WebDriverExtension {
const args = event['args']; const args = event['args'];
if (this._isEvent(categories, name, ['blink.console'])) { if (this._isEvent(categories, name, ['blink.console'])) {
return normalizeEvent(event, {'name': name}); return normalizeEvent(event, {'name': name});
} else if (this._isEvent(categories, name, ['blink.user_timing'])) {
return normalizeEvent(event, {'name': name});
} else if (this._isEvent( } else if (this._isEvent(
categories, name, ['benchmark'], categories, name, ['benchmark'],
'BenchmarkInstrumentation::ImplThreadRenderingStats')) { 'BenchmarkInstrumentation::ImplThreadRenderingStats')) {
@ -201,6 +203,15 @@ function normalizeEvent(chromeEvent: {[key: string]: any}, data: PerfLogEvent):
} else if (ph === 'R') { } else if (ph === 'R') {
// mark events from navigation timing // mark events from navigation timing
ph = 'I'; ph = 'I';
// Chrome 65+ doesn't allow user timing measurements across page loads.
// Instead, we use performance marks with special names.
if (chromeEvent['name'].match(/-bpstart/)) {
data['name'] = chromeEvent['name'].slice(0, -8);
ph = 'B';
} else if (chromeEvent['name'].match(/-bpend$/)) {
data['name'] = chromeEvent['name'].slice(0, -6);
ph = 'E';
}
} }
const result: {[key: string]: any} = const result: {[key: string]: any} =
{'pid': chromeEvent['pid'], 'ph': ph, 'cat': 'timeline', 'ts': chromeEvent['ts'] / 1000}; {'pid': chromeEvent['pid'], 'ph': ph, 'cat': 'timeline', 'ts': chromeEvent['ts'] / 1000};

View File

@ -61,16 +61,17 @@ import {TraceEventFactory} from '../trace_event_factory';
}); });
})); }));
it('should clear the perf logs and mark the timeline via console.time() on the first call', it('should clear the perf logs and mark the timeline via performance.mark() on the first call',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
createExtension().timeBegin('someName').then(() => { createExtension().timeBegin('someName').then(() => {
expect(log).toEqual( expect(log).toEqual([
[['logs', 'performance'], ['executeScript', `console.time('someName');`]]); ['logs', 'performance'], ['executeScript', `performance.mark('someName-bpstart');`]
]);
async.done(); async.done();
}); });
})); }));
it('should mark the timeline via console.time() on the second call', it('should mark the timeline via performance.mark() on the second call',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const ext = createExtension(); const ext = createExtension();
ext.timeBegin('someName') ext.timeBegin('someName')
@ -79,24 +80,25 @@ import {TraceEventFactory} from '../trace_event_factory';
ext.timeBegin('someName'); ext.timeBegin('someName');
}) })
.then(() => { .then(() => {
expect(log).toEqual([['executeScript', `console.time('someName');`]]); expect(log).toEqual([['executeScript', `performance.mark('someName-bpstart');`]]);
async.done(); async.done();
}); });
})); }));
it('should mark the timeline via console.timeEnd()', it('should mark the timeline via performance.mark()',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
createExtension().timeEnd('someName', null).then((_) => { createExtension().timeEnd('someName', null).then((_) => {
expect(log).toEqual([['executeScript', `console.timeEnd('someName');`]]); expect(log).toEqual([['executeScript', `performance.mark('someName-bpend');`]]);
async.done(); async.done();
}); });
})); }));
it('should mark the timeline via console.time() and console.timeEnd()', it('should mark the timeline via performance.mark() with start and end of a test',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
createExtension().timeEnd('name1', 'name2').then((_) => { createExtension().timeEnd('name1', 'name2').then((_) => {
expect(log).toEqual( expect(log).toEqual([[
[['executeScript', `console.timeEnd('name1');console.time('name2');`]]); 'executeScript', `performance.mark('name1-bpend');performance.mark('name2-bpstart');`
]]);
async.done(); async.done();
}); });
})); }));

View File

@ -26,6 +26,7 @@ ts_library(
deps = [ deps = [
"//packages/compiler", "//packages/compiler",
"//packages/compiler-cli/src/ngtsc/annotations", "//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/factories", "//packages/compiler-cli/src/ngtsc/factories",
"//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/transform", "//packages/compiler-cli/src/ngtsc/transform",

View File

@ -12,6 +12,8 @@ import 'reflect-metadata';
import * as ts from 'typescript'; import * as ts from 'typescript';
import * as tsickle from 'tsickle'; import * as tsickle from 'tsickle';
import {replaceTsWithNgInErrors} from './ngtsc/diagnostics';
import * as api from './transformers/api'; import * as api from './transformers/api';
import {GENERATED_FILES} from './transformers/util'; import {GENERATED_FILES} from './transformers/util';
@ -160,7 +162,15 @@ function reportErrorsAndExit(
getCanonicalFileName: fileName => fileName, getCanonicalFileName: fileName => fileName,
getNewLine: () => ts.sys.newLine getNewLine: () => ts.sys.newLine
}; };
consoleError(formatDiagnostics(errorsAndWarnings, formatHost)); if (options && (options.enableIvy === true || options.enableIvy === 'ngtsc')) {
const ngDiagnostics = errorsAndWarnings.filter(api.isNgDiagnostic);
const tsDiagnostics = errorsAndWarnings.filter(api.isTsDiagnostic);
consoleError(replaceTsWithNgInErrors(
ts.formatDiagnosticsWithColorAndContext(tsDiagnostics, formatHost)));
consoleError(formatDiagnostics(ngDiagnostics, formatHost));
} else {
consoleError(formatDiagnostics(errorsAndWarnings, formatHost));
}
} }
return exitCodeFromResult(allDiagnostics); return exitCodeFromResult(allDiagnostics);
} }

View File

@ -49,14 +49,16 @@ export class Analyzer {
handlers: DecoratorHandler<any, any>[] = [ handlers: DecoratorHandler<any, any>[] = [
new BaseDefDecoratorHandler(this.typeChecker, this.host), new BaseDefDecoratorHandler(this.typeChecker, this.host),
new ComponentDecoratorHandler( new ComponentDecoratorHandler(
this.typeChecker, this.host, this.scopeRegistry, false, this.resourceLoader), this.typeChecker, this.host, this.scopeRegistry, false, this.resourceLoader, this.rootDirs),
new DirectiveDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, false), new DirectiveDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, false),
new InjectableDecoratorHandler(this.host, false), new InjectableDecoratorHandler(this.host, false),
new NgModuleDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, false), new NgModuleDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, false),
new PipeDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, false), new PipeDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, false),
]; ];
constructor(private typeChecker: ts.TypeChecker, private host: NgccReflectionHost) {} constructor(
private typeChecker: ts.TypeChecker, private host: NgccReflectionHost,
private rootDirs: string[]) {}
/** /**
* Analyize a parsed file to generate the information about decorated classes that * Analyize a parsed file to generate the information about decorated classes that

View File

@ -10,9 +10,9 @@ import * as ts from 'typescript';
import {ClassMember, ClassMemberKind, CtorParameter, Decorator} from '../../../ngtsc/host'; import {ClassMember, ClassMemberKind, CtorParameter, Decorator} from '../../../ngtsc/host';
import {TypeScriptReflectionHost, reflectObjectLiteral} from '../../../ngtsc/metadata'; import {TypeScriptReflectionHost, reflectObjectLiteral} from '../../../ngtsc/metadata';
import {getNameText} from '../utils'; import {findAll, getNameText} from '../utils';
import {NgccReflectionHost} from './ngcc_host'; import {NgccReflectionHost, PRE_NGCC_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host';
export const DECORATORS = 'decorators' as ts.__String; export const DECORATORS = 'decorators' as ts.__String;
export const PROP_DECORATORS = 'propDecorators' as ts.__String; export const PROP_DECORATORS = 'propDecorators' as ts.__String;
@ -198,6 +198,19 @@ export class Fesm2015ReflectionHost extends TypeScriptReflectionHost implements
undefined; undefined;
} }
/**
* Search the given module for variable declarations in which the initializer
* is an identifier marked with the `PRE_NGCC_MARKER`.
* @param module The module in which to search for switchable declarations.
* @returns An array of variable declarations that match.
*/
getSwitchableDeclarations(module: ts.Node): SwitchableVariableDeclaration[] {
// Don't bother to walk the AST if the marker is not found in the text
return module.getText().indexOf(PRE_NGCC_MARKER) >= 0 ?
findAll(module, isSwitchableVariableDeclaration) :
[];
}
/** /**
* Member decorators are declared as static properties of the class in ES2015: * Member decorators are declared as static properties of the class in ES2015:
* *

View File

@ -8,9 +8,33 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ReflectionHost} from '../../../ngtsc/host'; import {ReflectionHost} from '../../../ngtsc/host';
export const PRE_NGCC_MARKER = '__PRE_NGCC__';
export const POST_NGCC_MARKER = '__POST_NGCC__';
export type SwitchableVariableDeclaration = ts.VariableDeclaration & {initializer: ts.Identifier};
export function isSwitchableVariableDeclaration(node: ts.Node):
node is SwitchableVariableDeclaration {
return ts.isVariableDeclaration(node) && !!node.initializer &&
ts.isIdentifier(node.initializer) && node.initializer.text.endsWith(PRE_NGCC_MARKER);
}
/** /**
* A reflection host that has extra methods for looking at non-Typescript package formats * A reflection host that has extra methods for looking at non-Typescript package formats
*/ */
export interface NgccReflectionHost extends ReflectionHost { export interface NgccReflectionHost extends ReflectionHost {
/**
* Find a symbol for a declaration that we think is a class.
* @param declaration The declaration whose symbol we are finding
* @returns the symbol for the declaration or `undefined` if it is not
* a "class" or has no symbol.
*/
getClassSymbol(node: ts.Node): ts.Symbol|undefined; getClassSymbol(node: ts.Node): ts.Symbol|undefined;
/**
* Search the given module for variable declarations in which the initializer
* is an identifier marked with the `PRE_NGCC_MARKER`.
* @param module The module in which to search for switchable declarations.
* @returns An array of variable declarations that match.
*/
getSwitchableDeclarations(module: ts.Node): SwitchableVariableDeclaration[];
} }

View File

@ -7,6 +7,7 @@
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import {POST_NGCC_MARKER, PRE_NGCC_MARKER} from '../host/ngcc_host';
import {AnalyzedClass} from '../analyzer'; import {AnalyzedClass} from '../analyzer';
import {Renderer} from './renderer'; import {Renderer} from './renderer';
@ -70,4 +71,14 @@ export class Esm2015Renderer extends Renderer {
} }
}); });
} }
rewriteSwitchableDeclarations(outputText: MagicString, sourceFile: ts.SourceFile): void {
const declarations = this.host.getSwitchableDeclarations(sourceFile);
declarations.forEach(declaration => {
const start = declaration.initializer.getStart();
const end = declaration.initializer.getEnd();
const replacement = declaration.initializer.text.replace(PRE_NGCC_MARKER, POST_NGCC_MARKER);
outputText.overwrite(start, end, replacement);
});
}
} }

View File

@ -89,9 +89,13 @@ export abstract class Renderer {
file.sourceFile); file.sourceFile);
this.addImports(outputText, importManager.getAllImports(file.sourceFile.fileName, null)); this.addImports(outputText, importManager.getAllImports(file.sourceFile.fileName, null));
// QUESTION: do we need to remove contructor param metadata and property decorators?
// TODO: remove contructor param metadata and property decorators (we need info from the
// handlers to do this)
this.removeDecorators(outputText, decoratorsToRemove); this.removeDecorators(outputText, decoratorsToRemove);
this.rewriteSwitchableDeclarations(outputText, file.sourceFile);
return this.renderSourceAndMap(file, input, outputText, targetPath); return this.renderSourceAndMap(file, input, outputText, targetPath);
} }
@ -102,6 +106,8 @@ export abstract class Renderer {
output: MagicString, analyzedClass: AnalyzedClass, definitions: string): void; output: MagicString, analyzedClass: AnalyzedClass, definitions: string): void;
protected abstract removeDecorators( protected abstract removeDecorators(
output: MagicString, decoratorsToRemove: Map<ts.Node, ts.Node[]>): void; output: MagicString, decoratorsToRemove: Map<ts.Node, ts.Node[]>): void;
protected abstract rewriteSwitchableDeclarations(
outputText: MagicString, sourceFile: ts.SourceFile): void;
/** /**
* Add the decorator nodes that are to be removed to a map * Add the decorator nodes that are to be removed to a map

View File

@ -25,9 +25,7 @@ import {Esm2015Renderer} from '../rendering/esm2015_renderer';
import {Esm5Renderer} from '../rendering/esm5_renderer'; import {Esm5Renderer} from '../rendering/esm5_renderer';
import {FileInfo, Renderer} from '../rendering/renderer'; import {FileInfo, Renderer} from '../rendering/renderer';
import {getEntryPoints} from './utils'; import {checkMarkerFile, findAllPackageJsonFiles, getEntryPoints, writeMarkerFile} from './utils';
/** /**
* A Package is stored in a directory on disk and that directory can contain one or more package * A Package is stored in a directory on disk and that directory can contain one or more package
@ -52,7 +50,11 @@ export class PackageTransformer {
transform(packagePath: string, format: string, targetPath: string = 'node_modules'): void { transform(packagePath: string, format: string, targetPath: string = 'node_modules'): void {
const sourceNodeModules = this.findNodeModulesPath(packagePath); const sourceNodeModules = this.findNodeModulesPath(packagePath);
const targetNodeModules = resolve(sourceNodeModules, '..', targetPath); const targetNodeModules = resolve(sourceNodeModules, '..', targetPath);
const entryPoints = getEntryPoints(packagePath, format); const packageJsonPaths =
findAllPackageJsonFiles(packagePath)
// Ignore paths that have been built already
.filter(packageJsonPath => !checkMarkerFile(packageJsonPath, format));
const entryPoints = getEntryPoints(packageJsonPaths, format);
entryPoints.forEach(entryPoint => { entryPoints.forEach(entryPoint => {
const outputFiles: FileInfo[] = []; const outputFiles: FileInfo[] = [];
@ -65,13 +67,21 @@ export class PackageTransformer {
// Create the TS program and necessary helpers. // Create the TS program and necessary helpers.
// TODO : create a custom compiler host that reads from .bak files if available. // TODO : create a custom compiler host that reads from .bak files if available.
const host = ts.createCompilerHost(options); const host = ts.createCompilerHost(options);
let rootDirs: string[]|undefined = undefined;
if (options.rootDirs !== undefined) {
rootDirs = options.rootDirs;
} else if (options.rootDir !== undefined) {
rootDirs = [options.rootDir];
} else {
rootDirs = [host.getCurrentDirectory()];
}
const packageProgram = ts.createProgram([entryPoint.entryFileName], options, host); const packageProgram = ts.createProgram([entryPoint.entryFileName], options, host);
const typeChecker = packageProgram.getTypeChecker(); const typeChecker = packageProgram.getTypeChecker();
const dtsMapper = new DtsMapper(entryPoint.entryRoot, entryPoint.dtsEntryRoot); const dtsMapper = new DtsMapper(entryPoint.entryRoot, entryPoint.dtsEntryRoot);
const reflectionHost = this.getHost(format, packageProgram, dtsMapper); const reflectionHost = this.getHost(format, packageProgram, dtsMapper);
const parser = this.getFileParser(format, packageProgram, reflectionHost); const parser = this.getFileParser(format, packageProgram, reflectionHost);
const analyzer = new Analyzer(typeChecker, reflectionHost); const analyzer = new Analyzer(typeChecker, reflectionHost, rootDirs);
const renderer = this.getRenderer(format, packageProgram, reflectionHost); const renderer = this.getRenderer(format, packageProgram, reflectionHost);
// Parse and analyze the files. // Parse and analyze the files.
@ -94,6 +104,9 @@ export class PackageTransformer {
// Write out all the transformed files. // Write out all the transformed files.
outputFiles.forEach(file => this.writeFile(file)); outputFiles.forEach(file => this.writeFile(file));
}); });
// Write the built-with-ngcc markers
packageJsonPaths.forEach(packageJsonPath => { writeMarkerFile(packageJsonPath, format); });
} }
getHost(format: string, program: ts.Program, dtsMapper: DtsMapper): NgccReflectionHost { getHost(format: string, program: ts.Program, dtsMapper: DtsMapper): NgccReflectionHost {

View File

@ -6,12 +6,14 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {readFileSync} from 'fs'; import {existsSync, readFileSync, writeFileSync} from 'fs';
import {dirname, relative, resolve} from 'path'; import {dirname, resolve} from 'path';
import {find} from 'shelljs'; import {find} from 'shelljs';
import {isDefined} from '../utils'; import {isDefined} from '../utils';
export const NGCC_VERSION = '0.0.0-PLACEHOLDER';
/** /**
* Represents an entry point to a package or sub-package. * Represents an entry point to a package or sub-package.
* *
@ -29,7 +31,7 @@ export class EntryPoint {
* @param relativeEntryPath The relative path to the entry point file. * @param relativeEntryPath The relative path to the entry point file.
* @param relativeDtsEntryPath The relative path to the `.d.ts` entry point file. * @param relativeDtsEntryPath The relative path to the `.d.ts` entry point file.
*/ */
constructor(packageRoot: string, relativeEntryPath: string, relativeDtsEntryPath: string) { constructor(public packageRoot: string, relativeEntryPath: string, relativeDtsEntryPath: string) {
this.entryFileName = resolve(packageRoot, relativeEntryPath); this.entryFileName = resolve(packageRoot, relativeEntryPath);
this.entryRoot = dirname(this.entryFileName); this.entryRoot = dirname(this.entryFileName);
const dtsEntryFileName = resolve(packageRoot, relativeDtsEntryPath); const dtsEntryFileName = resolve(packageRoot, relativeDtsEntryPath);
@ -63,15 +65,14 @@ export function findAllPackageJsonFiles(rootDirectory: string): string[] {
} }
/** /**
* Identify the entry points of a package. * Identify the entry points of a collection of package.json files.
* *
* @param packageDirectory The absolute path to the root directory that contains the package. * @param packageJsonPaths A collection of absolute paths to the package.json files.
* @param format The format of the entry points to look for within the package. * @param format The format of the entry points to look for within the package.
* *
* @returns A collection of `EntryPoint`s that correspond to entry points for the package. * @returns A collection of `EntryPoint`s that correspond to entry points for the package.
*/ */
export function getEntryPoints(packageDirectory: string, format: string): EntryPoint[] { export function getEntryPoints(packageJsonPaths: string[], format: string): EntryPoint[] {
const packageJsonPaths = findAllPackageJsonFiles(packageDirectory);
const entryPoints = const entryPoints =
packageJsonPaths packageJsonPaths
.map(packageJsonPath => { .map(packageJsonPath => {
@ -86,3 +87,26 @@ export function getEntryPoints(packageDirectory: string, format: string): EntryP
.filter(isDefined); .filter(isDefined);
return entryPoints; return entryPoints;
} }
function getMarkerPath(packageJsonPath: string, format: string) {
return resolve(dirname(packageJsonPath), `__modified_by_ngcc_for_${format}__`);
}
export function checkMarkerFile(packageJsonPath: string, format: string) {
const markerPath = getMarkerPath(packageJsonPath, format);
const markerExists = existsSync(markerPath);
if (markerExists) {
const previousVersion = readFileSync(markerPath, 'utf8');
if (previousVersion !== NGCC_VERSION) {
throw new Error(
'The ngcc compiler has changed since the last ngcc build.\n' +
'Please completely remove `node_modules` and try again.');
}
}
return markerExists;
}
export function writeMarkerFile(packageJsonPath: string, format: string) {
const markerPath = getMarkerPath(packageJsonPath, format);
writeFileSync(markerPath, NGCC_VERSION, 'utf8');
}

View File

@ -20,3 +20,23 @@ export function isDefined<T>(value: T | undefined | null): value is T {
export function getNameText(name: ts.PropertyName | ts.BindingName): string { export function getNameText(name: ts.PropertyName | ts.BindingName): string {
return ts.isIdentifier(name) || ts.isLiteralExpression(name) ? name.text : name.getText(); return ts.isIdentifier(name) || ts.isLiteralExpression(name) ? name.text : name.getText();
} }
/**
* Parse down the AST and capture all the nodes that satisfy the test.
* @param node The start node.
* @param test The function that tests whether a node should be included.
* @returns a collection of nodes that satisfy the test.
*/
export function findAll<T>(node: ts.Node, test: (node: ts.Node) => node is ts.Node & T): T[] {
const nodes: T[] = [];
findAllVisitor(node);
return nodes;
function findAllVisitor(n: ts.Node) {
if (test(n)) {
nodes.push(n);
} else {
n.forEachChild(child => findAllVisitor(child));
}
}
}

View File

@ -80,7 +80,7 @@ describe('Analyzer', () => {
program = makeProgram(TEST_PROGRAM); program = makeProgram(TEST_PROGRAM);
const file = createParsedFile(program); const file = createParsedFile(program);
const analyzer = new Analyzer( const analyzer = new Analyzer(
program.getTypeChecker(), new Fesm2015ReflectionHost(program.getTypeChecker())); program.getTypeChecker(), new Fesm2015ReflectionHost(program.getTypeChecker()), ['']);
testHandler = createTestHandler(); testHandler = createTestHandler();
analyzer.handlers = [testHandler]; analyzer.handlers = [testHandler];
result = analyzer.analyzeFile(file); result = analyzer.analyzeFile(file);

View File

@ -32,6 +32,24 @@ const CLASSES = [
}, },
]; ];
const MARKER_FILE = {
name: '/marker.js',
contents: `
let compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__;
function compileNgModuleFactory__PRE_NGCC__(injector, options, moduleType) {
const compilerFactory = injector.get(CompilerFactory);
const compiler = compilerFactory.createCompiler([options]);
return compiler.compileModuleAsync(moduleType);
}
function compileNgModuleFactory__POST_NGCC__(injector, options, moduleType) {
ngDevMode && assertNgModuleType(moduleType);
return Promise.resolve(new R3NgModuleFactory(moduleType));
}
`
};
describe('Esm2015ReflectionHost', () => { describe('Esm2015ReflectionHost', () => {
describe('getGenericArityOfClass()', () => { describe('getGenericArityOfClass()', () => {
it('should properly count type parameters', () => { it('should properly count type parameters', () => {
@ -52,4 +70,18 @@ describe('Esm2015ReflectionHost', () => {
expect(host.getGenericArityOfClass(twoTypeParamsClass)).toBe(2); expect(host.getGenericArityOfClass(twoTypeParamsClass)).toBe(2);
}); });
}); });
describe('getSwitchableDeclarations()', () => {
it('should return a collection of all the switchable variable declarations in the given module',
() => {
const program = makeProgram(MARKER_FILE);
const dtsMapper = new DtsMapper('/src', '/typings');
const host = new Esm2015ReflectionHost(program.getTypeChecker(), dtsMapper);
const file = program.getSourceFile(MARKER_FILE.name) !;
const declarations = host.getSwitchableDeclarations(file);
expect(declarations.map(d => [d.name.getText(), d.initializer !.getText()])).toEqual([
['compileNgModuleFactory', 'compileNgModuleFactory__PRE_NGCC__']
]);
});
});
}); });

View File

@ -385,6 +385,24 @@ const FUNCTION_BODY_FILE = {
` `
}; };
const MARKER_FILE = {
name: '/marker.js',
contents: `
var compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__;
function compileNgModuleFactory__PRE_NGCC__(injector, options, moduleType) {
var compilerFactory = injector.get(CompilerFactory);
var compiler = compilerFactory.createCompiler([options]);
return compiler.compileModuleAsync(moduleType);
}
function compileNgModuleFactory__POST_NGCC__(injector, options, moduleType) {
ngDevMode && assertNgModuleType(moduleType);
return Promise.resolve(new R3NgModuleFactory(moduleType));
}
`
};
describe('Fesm2015ReflectionHost', () => { describe('Fesm2015ReflectionHost', () => {
describe('getDecoratorsOfDeclaration()', () => { describe('getDecoratorsOfDeclaration()', () => {
@ -1120,4 +1138,17 @@ describe('Fesm2015ReflectionHost', () => {
expect(host.getGenericArityOfClass(node)).toBe(0); expect(host.getGenericArityOfClass(node)).toBe(0);
}); });
}); });
describe('getSwitchableDeclarations()', () => {
it('should return a collection of all the switchable variable declarations in the given module',
() => {
const program = makeProgram(MARKER_FILE);
const host = new Fesm2015ReflectionHost(program.getTypeChecker());
const file = program.getSourceFile(MARKER_FILE.name) !;
const declarations = host.getSwitchableDeclarations(file);
expect(declarations.map(d => [d.name.getText(), d.initializer !.getText()])).toEqual([
['compileNgModuleFactory', 'compileNgModuleFactory__PRE_NGCC__']
]);
});
});
}); });

View File

@ -17,7 +17,7 @@ function setup(file: {name: string, contents: string}) {
const program = makeProgram(file); const program = makeProgram(file);
const host = new Fesm2015ReflectionHost(program.getTypeChecker()); const host = new Fesm2015ReflectionHost(program.getTypeChecker());
const parser = new Esm2015FileParser(program, host); const parser = new Esm2015FileParser(program, host);
const analyzer = new Analyzer(program.getTypeChecker(), host); const analyzer = new Analyzer(program.getTypeChecker(), host, ['']);
const renderer = new Esm2015Renderer(host); const renderer = new Esm2015Renderer(host);
return {analyzer, host, parser, program, renderer}; return {analyzer, host, parser, program, renderer};
} }
@ -27,49 +27,59 @@ function analyze(parser: Esm2015FileParser, analyzer: Analyzer, file: ts.SourceF
return parsedFiles.map(file => analyzer.analyzeFile(file))[0]; return parsedFiles.map(file => analyzer.analyzeFile(file))[0];
} }
const PROGRAM = {
describe('Esm2015Renderer', () => { name: 'some/file.js',
contents: `
describe('addImports', () => {
it('should insert the given imports at the start of the source file', () => {
const PROGRAM = {
name: 'some/file.js',
contents: `
/* A copyright notice */ /* A copyright notice */
import {Directive} from '@angular/core'; import {Directive} from '@angular/core';
export class A {} export class A {}
A.decorators = [ A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] }, { type: Directive, args: [{ selector: '[a]' }] },
{ type: Other } { type: OtherA }
]; ];
export class B {}
B.decorators = [
{ type: OtherB },
{ type: Directive, args: [{ selector: '[b]' }] }
];
export class C {}
C.decorators = [
{ type: Directive, args: [{ selector: '[c]' }] },
];
let compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__;
let badlyFormattedVariable = __PRE_NGCC__badlyFormattedVariable;
function compileNgModuleFactory__PRE_NGCC__(injector, options, moduleType) {
const compilerFactory = injector.get(CompilerFactory);
const compiler = compilerFactory.createCompiler([options]);
return compiler.compileModuleAsync(moduleType);
}
function compileNgModuleFactory__POST_NGCC__(injector, options, moduleType) {
ngDevMode && assertNgModuleType(moduleType);
return Promise.resolve(new R3NgModuleFactory(moduleType));
}
// Some other content` // Some other content`
}; };
describe('Esm2015Renderer', () => {
describe('addImports', () => {
it('should insert the given imports at the start of the source file', () => {
const {renderer} = setup(PROGRAM); const {renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addImports( renderer.addImports(
output, [{name: '@angular/core', as: 'i0'}, {name: '@angular/common', as: 'i1'}]); output, [{name: '@angular/core', as: 'i0'}, {name: '@angular/common', as: 'i1'}]);
expect(output.toString()) expect(output.toString()).toContain(`import * as i0 from '@angular/core';
.toEqual( import * as i1 from '@angular/common';
`import * as i0 from '@angular/core';\n` +
`import * as i1 from '@angular/common';\n` + PROGRAM.contents); /* A copyright notice */`);
}); });
}); });
describe('addConstants', () => { describe('addConstants', () => {
it('should insert the given constants after imports in the source file', () => { it('should insert the given constants after imports in the source file', () => {
const PROGRAM = {
name: 'some/file.js',
contents: `
/* A copyright notice */
import {Directive} from '@angular/core';
export class A {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] },
{ type: Other }
];
// Some other content`
};
const {renderer, program} = setup(PROGRAM); const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js'); const file = program.getSourceFile('some/file.js');
if (file === undefined) { if (file === undefined) {
@ -85,35 +95,41 @@ export class A {}`);
}); });
}); });
describe('rewriteSwitchableDeclarations', () => {
it('should switch marked declaration initializers', () => {
const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js');
if (file === undefined) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents);
renderer.rewriteSwitchableDeclarations(output, file);
expect(output.toString())
.not.toContain(`let compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__;`);
expect(output.toString())
.toContain(`let badlyFormattedVariable = __PRE_NGCC__badlyFormattedVariable;`);
expect(output.toString())
.toContain(`let compileNgModuleFactory = compileNgModuleFactory__POST_NGCC__;`);
expect(output.toString())
.toContain(
`function compileNgModuleFactory__PRE_NGCC__(injector, options, moduleType) {`);
expect(output.toString())
.toContain(
`function compileNgModuleFactory__POST_NGCC__(injector, options, moduleType) {`);
});
});
describe('addDefinitions', () => { describe('addDefinitions', () => {
it('should insert the definitions directly after the class declaration', () => { it('should insert the definitions directly after the class declaration', () => {
const PROGRAM = {
name: 'some/file.js',
contents: `
/* A copyright notice */
import {Directive} from '@angular/core';
export class A {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] },
{ type: Other }
];
// Some other content`
};
const {analyzer, parser, program, renderer} = setup(PROGRAM); const {analyzer, parser, program, renderer} = setup(PROGRAM);
const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addDefinitions(output, analyzedFile.analyzedClasses[0], 'SOME DEFINITION TEXT'); renderer.addDefinitions(output, analyzedFile.analyzedClasses[0], 'SOME DEFINITION TEXT');
expect(output.toString()).toEqual(` expect(output.toString()).toContain(`
/* A copyright notice */
import {Directive} from '@angular/core';
export class A {} export class A {}
SOME DEFINITION TEXT SOME DEFINITION TEXT
A.decorators = [ A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] }, `);
{ type: Other }
];
// Some other content`);
}); });
}); });
@ -122,96 +138,58 @@ A.decorators = [
describe('removeDecorators', () => { describe('removeDecorators', () => {
it('should delete the decorator (and following comma) that was matched in the analysis', () => { it('should delete the decorator (and following comma) that was matched in the analysis', () => {
const PROGRAM = {
name: 'some/file.js',
contents: `
/* A copyright notice */
import {Directive} from '@angular/core';
export class A {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] },
{ type: Other }
];
// Some other content`
};
const {analyzer, parser, program, renderer} = setup(PROGRAM); const {analyzer, parser, program, renderer} = setup(PROGRAM);
const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const analyzedClass = analyzedFile.analyzedClasses[0]; const analyzedClass = analyzedFile.analyzedClasses[0];
const decorator = analyzedClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set( decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
analyzedClass.decorators[0].node.parent !, [analyzedClass.decorators[0].node]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toEqual(` expect(output.toString()).not.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
/* A copyright notice */ expect(output.toString()).toContain(`{ type: OtherA }`);
import {Directive} from '@angular/core'; expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`);
export class A {} expect(output.toString()).toContain(`{ type: OtherB }`);
A.decorators = [ expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`);
{ type: Other }
];
// Some other content`);
}); });
it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis',
() => { () => {
const PROGRAM = {
name: 'some/file.js',
contents: `
/* A copyright notice */
import {Directive} from '@angular/core';
export class A {}
A.decorators = [
{ type: Other },
{ type: Directive, args: [{ selector: '[a]' }] }
];
// Some other content`
};
const {analyzer, parser, program, renderer} = setup(PROGRAM); const {analyzer, parser, program, renderer} = setup(PROGRAM);
const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const analyzedClass = analyzedFile.analyzedClasses[0]; const analyzedClass = analyzedFile.analyzedClasses[1];
const decorator = analyzedClass.decorators[1];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set( decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
analyzedClass.decorators[0].node.parent !, [analyzedClass.decorators[1].node]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toEqual(` expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
/* A copyright notice */ expect(output.toString()).toContain(`{ type: OtherA }`);
import {Directive} from '@angular/core'; expect(output.toString())
export class A {} .not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`);
A.decorators = [ expect(output.toString()).toContain(`{ type: OtherB }`);
{ type: Other }, expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`);
];
// Some other content`);
}); });
it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis', it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis',
() => { () => {
const PROGRAM = {
name: 'some/file.js',
contents: `
/* A copyright notice */
import {Directive} from '@angular/core';
export class A {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] }
];
// Some other content`
};
const {analyzer, parser, program, renderer} = setup(PROGRAM); const {analyzer, parser, program, renderer} = setup(PROGRAM);
const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const analyzedClass = analyzedFile.analyzedClasses[0]; const analyzedClass = analyzedFile.analyzedClasses[2];
const decorator = analyzedClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set( decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
analyzedClass.decorators[0].node.parent !, [analyzedClass.decorators[0].node]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toEqual(` expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
/* A copyright notice */ expect(output.toString()).toContain(`{ type: OtherA }`);
import {Directive} from '@angular/core'; expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`);
export class A {} expect(output.toString()).toContain(`{ type: OtherB }`);
// Some other content`); expect(output.toString()).not.toContain(`C.decorators = [
{ type: Directive, args: [{ selector: '[c]' }] },
];`);
}); });
}); });

View File

@ -17,7 +17,7 @@ function setup(file: {name: string, contents: string}) {
const program = makeProgram(file); const program = makeProgram(file);
const host = new Esm5ReflectionHost(program.getTypeChecker()); const host = new Esm5ReflectionHost(program.getTypeChecker());
const parser = new Esm5FileParser(program, host); const parser = new Esm5FileParser(program, host);
const analyzer = new Analyzer(program.getTypeChecker(), host); const analyzer = new Analyzer(program.getTypeChecker(), host, ['']);
const renderer = new Esm5Renderer(host); const renderer = new Esm5Renderer(host);
return {analyzer, host, parser, program, renderer}; return {analyzer, host, parser, program, renderer};
} }
@ -27,74 +27,121 @@ function analyze(parser: Esm5FileParser, analyzer: Analyzer, file: ts.SourceFile
return parsedFiles.map(file => analyzer.analyzeFile(file))[0]; return parsedFiles.map(file => analyzer.analyzeFile(file))[0];
} }
const PROGRAM = {
name: 'some/file.js',
contents: `
/* A copyright notice */
import {Directive} from '@angular/core';
var A = (function() {
function A() {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] },
{ type: OtherA }
];
return A;
}());
var B = (function() {
function B() {}
B.decorators = [
{ type: OtherB },
{ type: Directive, args: [{ selector: '[b]' }] }
];
return B;
}());
var C = (function() {
function C() {}
C.decorators = [
{ type: Directive, args: [{ selector: '[c]' }] },
];
return C;
}());
var compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__;
var badlyFormattedVariable = __PRE_NGCC__badlyFormattedVariable;
function compileNgModuleFactory__PRE_NGCC__(injector, options, moduleType) {
const compilerFactory = injector.get(CompilerFactory);
const compiler = compilerFactory.createCompiler([options]);
return compiler.compileModuleAsync(moduleType);
}
function compileNgModuleFactory__POST_NGCC__(injector, options, moduleType) {
ngDevMode && assertNgModuleType(moduleType);
return Promise.resolve(new R3NgModuleFactory(moduleType));
}
// Some other content
export {A, B, C};`
};
describe('Esm5Renderer', () => { describe('Esm5Renderer', () => {
describe('addImports', () => { describe('addImports', () => {
it('should insert the given imports at the start of the source file', () => { it('should insert the given imports at the start of the source file', () => {
const PROGRAM = {
name: 'some/file.js',
contents: `
/* A copyright notice */
import {Directive} from '@angular/core';
var A = (function() {
function A() {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] },
{ type: Other }
];
return A;
}());
// Some other content
export {A};`
};
const {renderer} = setup(PROGRAM); const {renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addImports( renderer.addImports(
output, [{name: '@angular/core', as: 'i0'}, {name: '@angular/common', as: 'i1'}]); output, [{name: '@angular/core', as: 'i0'}, {name: '@angular/common', as: 'i1'}]);
expect(output.toString()) expect(output.toString()).toContain(`import * as i0 from '@angular/core';
.toEqual( import * as i1 from '@angular/common';
`import * as i0 from '@angular/core';\n` +
`import * as i1 from '@angular/common';\n` + PROGRAM.contents); /* A copyright notice */`);
}); });
}); });
describe('addConstants', () => {
it('should insert the given constants after imports in the source file', () => {
const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js');
if (file === undefined) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents);
renderer.addConstants(output, 'const x = 3;', file);
expect(output.toString()).toContain(`
import {Directive} from '@angular/core';
const x = 3;
var A = (function() {`);
});
});
describe('rewriteSwitchableDeclarations', () => {
it('should switch marked declaration initializers', () => {
const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js');
if (file === undefined) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents);
renderer.rewriteSwitchableDeclarations(output, file);
expect(output.toString())
.not.toContain(`var compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__;`);
expect(output.toString())
.toContain(`var badlyFormattedVariable = __PRE_NGCC__badlyFormattedVariable;`);
expect(output.toString())
.toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_NGCC__;`);
expect(output.toString())
.toContain(
`function compileNgModuleFactory__PRE_NGCC__(injector, options, moduleType) {`);
expect(output.toString())
.toContain(
`function compileNgModuleFactory__POST_NGCC__(injector, options, moduleType) {`);
});
});
describe('addDefinitions', () => { describe('addDefinitions', () => {
it('should insert the definitions directly after the class declaration', () => { it('should insert the definitions directly after the class declaration', () => {
const PROGRAM = {
name: 'some/file.js',
contents: `
/* A copyright notice */
import {Directive} from '@angular/core';
var A = (function() {
function A() {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] },
{ type: Other }
];
return A;
}());
// Some other content
export {A};`
};
const {analyzer, parser, program, renderer} = setup(PROGRAM); const {analyzer, parser, program, renderer} = setup(PROGRAM);
const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addDefinitions(output, analyzedFile.analyzedClasses[0], 'SOME DEFINITION TEXT'); renderer.addDefinitions(output, analyzedFile.analyzedClasses[0], 'SOME DEFINITION TEXT');
expect(output.toString()).toEqual(` expect(output.toString()).toContain(`
/* A copyright notice */
import {Directive} from '@angular/core';
var A = (function() {
function A() {} function A() {}
SOME DEFINITION TEXT SOME DEFINITION TEXT
A.decorators = [ A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] }, `);
{ type: Other }
];
return A;
}());
// Some other content
export {A};`);
}); });
}); });
@ -103,120 +150,58 @@ export {A};`);
describe('removeDecorators', () => { describe('removeDecorators', () => {
it('should delete the decorator (and following comma) that was matched in the analysis', () => { it('should delete the decorator (and following comma) that was matched in the analysis', () => {
const PROGRAM = {
name: 'some/file.js',
contents: `
/* A copyright notice */
import {Directive} from '@angular/core';
var A = (function() {
function A() {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] },
{ type: Other }
];
return A;
}());
// Some other content
export {A};`
};
const {analyzer, parser, program, renderer} = setup(PROGRAM); const {analyzer, parser, program, renderer} = setup(PROGRAM);
const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const analyzedClass = analyzedFile.analyzedClasses[0]; const analyzedClass = analyzedFile.analyzedClasses[0];
const decorator = analyzedClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set( decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
analyzedClass.decorators[0].node.parent !, [analyzedClass.decorators[0].node]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toEqual(` expect(output.toString()).not.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
/* A copyright notice */ expect(output.toString()).toContain(`{ type: OtherA }`);
import {Directive} from '@angular/core'; expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`);
var A = (function() { expect(output.toString()).toContain(`{ type: OtherB }`);
function A() {} expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`);
A.decorators = [
{ type: Other }
];
return A;
}());
// Some other content
export {A};`);
}); });
it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis',
() => { () => {
const PROGRAM = {
name: 'some/file.js',
contents: `
/* A copyright notice */
import {Directive} from '@angular/core';
var A = (function() {
function A() {}
A.decorators = [
{ type: Other },
{ type: Directive, args: [{ selector: '[a]' }] }
];
return A;
}());
// Some other content
export {A};`
};
const {analyzer, parser, program, renderer} = setup(PROGRAM); const {analyzer, parser, program, renderer} = setup(PROGRAM);
const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const analyzedClass = analyzedFile.analyzedClasses[0]; const analyzedClass = analyzedFile.analyzedClasses[1];
const decorator = analyzedClass.decorators[1];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set( decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
analyzedClass.decorators[0].node.parent !, [analyzedClass.decorators[1].node]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toEqual(` expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
/* A copyright notice */ expect(output.toString()).toContain(`{ type: OtherA }`);
import {Directive} from '@angular/core'; expect(output.toString())
var A = (function() { .not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`);
function A() {} expect(output.toString()).toContain(`{ type: OtherB }`);
A.decorators = [ expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`);
{ type: Other },
];
return A;
}());
// Some other content
export {A};`);
}); });
it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis', it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis',
() => { () => {
const PROGRAM = {
name: 'some/file.js',
contents: `
/* A copyright notice */
import {Directive} from '@angular/core';
var A = (function() {
function A() {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] }
];
return A;
}());
// Some other content
export {A};`
};
const {analyzer, parser, program, renderer} = setup(PROGRAM); const {analyzer, parser, program, renderer} = setup(PROGRAM);
const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const analyzedClass = analyzedFile.analyzedClasses[0]; const analyzedClass = analyzedFile.analyzedClasses[2];
const decorator = analyzedClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set( decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
analyzedClass.decorators[0].node.parent !, [analyzedClass.decorators[0].node]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toEqual(` expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
/* A copyright notice */ expect(output.toString()).toContain(`{ type: OtherA }`);
import {Directive} from '@angular/core'; expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`);
var A = (function() { expect(output.toString()).toContain(`{ type: OtherB }`);
function A() {} expect(output.toString()).not.toContain(`C.decorators = [
return A; { type: Directive, args: [{ selector: '[c]' }] },
}()); ];`);
// Some other content
export {A};`);
}); });
}); });

View File

@ -29,6 +29,9 @@ class TestRenderer extends Renderer {
removeDecorators(output: MagicString, decoratorsToRemove: Map<ts.Node, ts.Node[]>) { removeDecorators(output: MagicString, decoratorsToRemove: Map<ts.Node, ts.Node[]>) {
output.prepend('\n// REMOVE DECORATORS\n'); output.prepend('\n// REMOVE DECORATORS\n');
} }
rewriteSwitchableDeclarations(output: MagicString, sourceFile: ts.SourceFile): void {
output.prepend('\n// REWRITTEN DECLARATIONS\n');
}
} }
function createTestRenderer() { function createTestRenderer() {
@ -43,7 +46,7 @@ function analyze(file: {name: string, contents: string}) {
const program = makeProgram(file); const program = makeProgram(file);
const host = new Fesm2015ReflectionHost(program.getTypeChecker()); const host = new Fesm2015ReflectionHost(program.getTypeChecker());
const parser = new Esm2015FileParser(program, host); const parser = new Esm2015FileParser(program, host);
const analyzer = new Analyzer(program.getTypeChecker(), host); const analyzer = new Analyzer(program.getTypeChecker(), host, ['']);
const parsedFiles = parser.parseFile(program.getSourceFile(file.name) !); const parsedFiles = parser.parseFile(program.getSourceFile(file.name) !);
return parsedFiles.map(file => analyzer.analyzeFile(file)); return parsedFiles.map(file => analyzer.analyzeFile(file));
@ -68,7 +71,7 @@ describe('Renderer', () => {
] ]
}); });
const RENDERED_CONTENTS = const RENDERED_CONTENTS =
`\n// REMOVE DECORATORS\n\n// ADD IMPORTS\n\n// ADD CONSTANTS\n\n// ADD DEFINITIONS\n` + `\n// REWRITTEN DECLARATIONS\n\n// REMOVE DECORATORS\n\n// ADD IMPORTS\n\n// ADD CONSTANTS\n\n// ADD DEFINITIONS\n` +
INPUT_PROGRAM.contents; INPUT_PROGRAM.contents;
const OUTPUT_PROGRAM_MAP = fromObject({ const OUTPUT_PROGRAM_MAP = fromObject({
'version': 3, 'version': 3,
@ -78,14 +81,14 @@ describe('Renderer', () => {
'import { Directive } from \'@angular/core\';\nexport class A {\n foo(x) {\n return x;\n }\n}\nA.decorators = [\n { type: Directive, args: [{ selector: \'[a]\' }] }\n];\n' 'import { Directive } from \'@angular/core\';\nexport class A {\n foo(x) {\n return x;\n }\n}\nA.decorators = [\n { type: Directive, args: [{ selector: \'[a]\' }] }\n];\n'
], ],
'names': [], 'names': [],
'mappings': ';;;;;;;;AAAA;;;;;;;;;' 'mappings': ';;;;;;;;;;AAAA;;;;;;;;;'
}); });
const MERGED_OUTPUT_PROGRAM_MAP = fromObject({ const MERGED_OUTPUT_PROGRAM_MAP = fromObject({
'version': 3, 'version': 3,
'sources': ['/file.ts'], 'sources': ['/file.ts'],
'names': [], 'names': [],
'mappings': ';;;;;;;;AAAA', 'mappings': ';;;;;;;;;;AAAA',
'file': '/output_file.js', 'file': '/output_file.js',
'sourcesContent': [ 'sourcesContent': [
'import { Directive } from \'@angular/core\';\nexport class A {\n foo(x: string): string {\n return x;\n }\n static decorators = [\n { type: Directive, args: [{ selector: \'[a]\' }] }\n ];\n}' 'import { Directive } from \'@angular/core\';\nexport class A {\n foo(x: string): string {\n return x;\n }\n static decorators = [\n { type: Directive, args: [{ selector: \'[a]\' }] }\n ];\n}'

View File

@ -6,8 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {existsSync, readFileSync, writeFileSync} from 'fs';
import * as mockFs from 'mock-fs'; import * as mockFs from 'mock-fs';
import {EntryPoint, findAllPackageJsonFiles, getEntryPoints} from '../../src/transform/utils';
import {EntryPoint, checkMarkerFile, findAllPackageJsonFiles, getEntryPoints, writeMarkerFile} from '../../src/transform/utils';
function createMockFileSystem() { function createMockFileSystem() {
mockFs({ mockFs({
@ -95,6 +97,11 @@ describe('EntryPoint', () => {
const entryPoint = new EntryPoint('/foo/bar', '../baz/qux/../quux.js', '/typings/foo/bar.d.ts'); const entryPoint = new EntryPoint('/foo/bar', '../baz/qux/../quux.js', '/typings/foo/bar.d.ts');
expect(entryPoint.entryFileName).toBe('/foo/baz/quux.js'); expect(entryPoint.entryFileName).toBe('/foo/baz/quux.js');
}); });
it('should expose the package root for the entry point file', () => {
const entryPoint = new EntryPoint('/foo/bar', '../baz/qux/../quux.js', '/typings/foo/bar.d.ts');
expect(entryPoint.packageRoot).toBe('/foo/bar');
});
}); });
describe('findAllPackageJsonFiles()', () => { describe('findAllPackageJsonFiles()', () => {
@ -138,7 +145,14 @@ describe('getEntryPoints()', () => {
afterEach(restoreRealFileSystem); afterEach(restoreRealFileSystem);
it('should return the entry points for the specified format from each `package.json`', () => { it('should return the entry points for the specified format from each `package.json`', () => {
const entryPoints = getEntryPoints('/node_modules/@angular/common', 'fesm2015'); const entryPoints = getEntryPoints(
[
'/node_modules/@angular/common/package.json',
'/node_modules/@angular/common/http/package.json',
'/node_modules/@angular/common/http/testing/package.json',
'/node_modules/@angular/common/testing/package.json'
],
'fesm2015');
entryPoints.forEach(ep => expect(ep).toEqual(jasmine.any(EntryPoint))); entryPoints.forEach(ep => expect(ep).toEqual(jasmine.any(EntryPoint)));
const sortedPaths = entryPoints.map(x => x.entryFileName).sort(); const sortedPaths = entryPoints.map(x => x.entryFileName).sort();
@ -151,17 +165,18 @@ describe('getEntryPoints()', () => {
}); });
it('should return an empty array if there are no matching `package.json` files', () => { it('should return an empty array if there are no matching `package.json` files', () => {
const entryPoints = getEntryPoints('/node_modules/@angular/other', 'fesm2015'); const entryPoints = getEntryPoints([], 'fesm2015');
expect(entryPoints).toEqual([]); expect(entryPoints).toEqual([]);
}); });
it('should return an empty array if there are no matching formats', () => { it('should return an empty array if there are no matching formats', () => {
const entryPoints = getEntryPoints('/node_modules/@angular/common', 'fesm3000'); const entryPoints = getEntryPoints(['/node_modules/@angular/common/package.json'], 'fesm3000');
expect(entryPoints).toEqual([]); expect(entryPoints).toEqual([]);
}); });
it('should return an entry point even if the typings are not specified', () => { it('should return an entry point even if the typings are not specified', () => {
const entryPoints = getEntryPoints('/node_modules/@angular/no-typings', 'fesm2015'); const entryPoints =
getEntryPoints(['/node_modules/@angular/no-typings/package.json'], 'fesm2015');
expect(entryPoints.length).toEqual(1); expect(entryPoints.length).toEqual(1);
expect(entryPoints[0].entryFileName) expect(entryPoints[0].entryFileName)
.toEqual('/node_modules/@angular/no-typings/fesm2015/index.js'); .toEqual('/node_modules/@angular/no-typings/fesm2015/index.js');
@ -169,3 +184,57 @@ describe('getEntryPoints()', () => {
expect(entryPoints[0].dtsEntryRoot).toEqual(entryPoints[0].entryRoot); expect(entryPoints[0].dtsEntryRoot).toEqual(entryPoints[0].entryRoot);
}); });
}); });
describe('Marker files', () => {
beforeEach(createMockFileSystem);
afterEach(restoreRealFileSystem);
describe('writeMarkerFile', () => {
it('should write a file containing the version placeholder', () => {
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
.toBe(false);
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(false);
writeMarkerFile('/node_modules/@angular/common/package.json', 'fesm2015');
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
.toBe(true);
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(false);
expect(
readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'utf8'))
.toEqual('0.0.0-PLACEHOLDER');
writeMarkerFile('/node_modules/@angular/common/package.json', 'esm5');
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
.toBe(true);
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(true);
expect(
readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'utf8'))
.toEqual('0.0.0-PLACEHOLDER');
expect(readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__', 'utf8'))
.toEqual('0.0.0-PLACEHOLDER');
});
});
describe('checkMarkerFile', () => {
it('should return false if the marker file does not exist', () => {
expect(checkMarkerFile('/node_modules/@angular/common/package.json', 'fesm2015')).toBe(false);
});
it('should return true if the marker file exists and contains the correct version', () => {
writeFileSync(
'/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', '0.0.0-PLACEHOLDER',
'utf8');
expect(checkMarkerFile('/node_modules/@angular/common/package.json', 'fesm2015')).toBe(true);
});
it('should throw if the marker file exists but contains the wrong version', () => {
writeFileSync(
'/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'WRONG_VERSION',
'utf8');
expect(() => checkMarkerFile('/node_modules/@angular/common/package.json', 'fesm2015'))
.toThrowError(
'The ngcc compiler has changed since the last ngcc build.\n' +
'Please completely remove `node_modules` and try again.');
});
});
});

View File

@ -116,7 +116,7 @@ export interface Program {
getTsSemanticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): getTsSemanticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken):
ReadonlyArray<ts.Diagnostic>; ReadonlyArray<ts.Diagnostic>;
getNgSemanticDiagnostics(fileName?: string, cancellationToken?: ts.CancellationToken): getNgSemanticDiagnostics(fileName?: string, cancellationToken?: ts.CancellationToken):
ReadonlyArray<Diagnostic>; ReadonlyArray<ts.Diagnostic|Diagnostic>;
loadNgStructureAsync(): Promise<void>; loadNgStructureAsync(): Promise<void>;
listLazyRoutes(entryRoute?: string): LazyRoute[]; listLazyRoutes(entryRoute?: string): LazyRoute[];
emit({emitFlags, cancellationToken, customTransformers, emitCallback}: { emit({emitFlags, cancellationToken, customTransformers, emitCallback}: {

View File

@ -11,6 +11,7 @@ ts_library(
module_name = "@angular/compiler-cli/src/ngtsc/annotations", module_name = "@angular/compiler-cli/src/ngtsc/annotations",
deps = [ deps = [
"//packages/compiler", "//packages/compiler",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/host", "//packages/compiler-cli/src/ngtsc/host",
"//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/transform", "//packages/compiler-cli/src/ngtsc/transform",

Some files were not shown because too many files have changed in this diff Show More