Compare commits

...

17 Commits
8.0.3 ... 4.1.1

Author SHA1 Message Date
1f5dce2128 docs: add changelog for 4.1.1 2017-05-04 14:22:43 -07:00
14e7e43ad8 release: cut the 4.1.1 release 2017-05-04 14:18:24 -07:00
54d4b893fd test(compiler-cli): add test for missingTranslation parameter 2017-05-04 14:05:17 -07:00
4670cf51cc test: cleanup rxjs custom build
The latest rxjs release works with closure compiler out of the box.
We no longer need to compile our own.

Also put closure options into a file rather than using a shell script.
2017-05-04 14:05:16 -07:00
dd4e501999 fix(upgrade): initialize all inputs in time for ngOnChanges()
Previously, non-bracketed inputs (e.g. `xyz="foo"`) on downgraded components
were initialized using `attrs.$observe()` (which uses `$evalAsync()` under the
hood), while bracketed inputs (e.g. `[xyz]="'foo'"`) were initialized using
`$watch()`. If the downgraded component was created during a `$digest` (e.g. by
an `ng-if` watcher), the non-bracketed inputs were not initialized in time for
the initial call to `ngOnChanges()` and `ngOnInit()`.

This commit fixes it by using `$watch()` to initialize all inputs. `$observe()`
is still used for subsequent updates on non-bracketed inputs, because it is more
performant.

Fixes #16212
2017-05-04 14:05:16 -07:00
9124994849 build: update concurrently to latest version
The regression in `concurrently` v3.2.0 (which made us roll back to v3.1.0
in #14378) has been fixed in v3.3.0 (see kimmobrunfeldt/concurrently#89).
2017-05-04 14:05:16 -07:00
4fbc61469f docs: fix links in api docs 2017-05-04 14:05:16 -07:00
8a883f24f6 refactor(compiler): simplify AOT tests 2017-05-04 14:05:15 -07:00
07cef367ac fix(core): don’t stop change detection because of errors
- prevents unsubscribing from the zone on error
- prevents unsubscribing from directive `EventEmitter`s on error
- prevents detaching views in dev mode if there on error
- ensures that `ngOnInit` is only called 1x (also in prod mode)

Fixes #9531
Fixes #2413
Fixes #15925
2017-05-04 14:05:15 -07:00
c060110695 fix(language-service): remove asserts for non-null expressions (#16422)
Reworked some of the code so asserts are no longer necessary.
Added additional and potentially redundant checks
Added checks where the null checker found real problems.

PR Close #16422
2017-05-04 14:05:15 -07:00
93ff3166ab docs(common): fix API docs for NgComponentOutlet (#16411)
fixes #16373

PR Close #16411
2017-05-04 14:05:14 -07:00
85a1b54c6e fix(core): don’t set ng-version for dynamically created components (#16394)
Angular uses the `ng-version` attribute to indicate which elements
were used to bootstrap an application. However, after 4.0 we also
added this attribute for all dynamically created components.

Fixes #15880

PR Close #16394
2017-05-04 14:05:14 -07:00
acf83b90bc fix(core): allow to detach OnPush components (#16394)
Fixes #9720
2017-05-04 14:05:14 -07:00
f66e59ebe4 fix(core): allow directives to inject the component’s ChangeDetectorRef. (#16394)
When a directive lives on the same element as a component
(e.g. `<my-comp myDir>`), the directive was not able to get hold
of the `ChangeDetectorRef` of the component on that element. However,
as directives are supposed to decorate components, this is incorrect.

This commit enables this use case.

Closes #12816
2017-05-04 14:05:14 -07:00
d932e724ab ci(language-service): update ci tests to official 2.3 build (#16415)
PR Close #16415
2017-05-04 14:05:13 -07:00
dcaa11a88b fix: public API golden files (#16414) 2017-05-04 14:05:13 -07:00
427d63a422 fix: strictNullCheck support. (#16389) (#16389)
Fix #16357

Workaround for https://github.com/Microsoft/TypeScript/issues/10078

Closes #16389

PR Close #16389
2017-05-04 14:05:13 -07:00
82 changed files with 1196 additions and 911 deletions

View File

@ -1,3 +1,19 @@
<a name="4.1.1"></a>
## [4.1.1](https://github.com/angular/angular/compare/4.1.0...4.1.1) (2017-05-04)
### Bug Fixes
* **core**: strictNullCheck support. ([#16389](https://github.com/angular/angular/issues/16389)) ([#16389](https://github.com/angular/angular/issues/16389)) ([427d63a](https://github.com/angular/angular/commit/427d63a)), closes [#16357](https://github.com/angular/angular/issues/16357)
* **core:** allow directives to inject the components `ChangeDetectorRef`. ([#16394](https://github.com/angular/angular/issues/16394)) ([f66e59e](https://github.com/angular/angular/commit/f66e59e)), closes [#12816](https://github.com/angular/angular/issues/12816)
* **core:** allow to detach `OnPush` components ([#16394](https://github.com/angular/angular/issues/16394)) ([acf83b9](https://github.com/angular/angular/commit/acf83b9)), closes [#9720](https://github.com/angular/angular/issues/9720)
* **core:** dont set `ng-version` for dynamically created components ([#16394](https://github.com/angular/angular/issues/16394)) ([85a1b54](https://github.com/angular/angular/commit/85a1b54)), closes [#15880](https://github.com/angular/angular/issues/15880)
* **core:** dont stop change detection because of errors ([07cef36](https://github.com/angular/angular/commit/07cef36)), closes [#9531](https://github.com/angular/angular/issues/9531) [#2413](https://github.com/angular/angular/issues/2413) [#15925](https://github.com/angular/angular/issues/15925)
* **language-service:** remove asserts for non-null expressions ([#16422](https://github.com/angular/angular/issues/16422)) ([c060110](https://github.com/angular/angular/commit/c060110))
* **upgrade:** initialize all inputs in time for `ngOnChanges()` ([dd4e501](https://github.com/angular/angular/commit/dd4e501)), closes [#16212](https://github.com/angular/angular/issues/16212)
<a name="4.1.0"></a> <a name="4.1.0"></a>
# [4.1.0](https://github.com/angular/angular/compare/4.1.0-rc.0...4.1.0) (2017-04-26) # [4.1.0](https://github.com/angular/angular/compare/4.1.0-rc.0...4.1.0) (2017-04-26)

View File

@ -1,4 +1,3 @@
/rxjs/
built/ built/
dist/ dist/
vendor/ vendor/

View File

@ -32,13 +32,7 @@ Angular's `node_modules` is installed.
## Running integration tests ## Running integration tests
The first time you run the tests, you'll need some setup: You can iterate on the tests by keeping the dist folder up-to-date.
```shell
$ ./integration/build_rxjs_es6.sh
```
Now you can iterate on the tests by keeping the dist folder up-to-date.
See the `package.json` of the test(s) you're debugging, to see which dist/ folders they install from. See the `package.json` of the test(s) you're debugging, to see which dist/ folders they install from.
Then run the right `tsc --watch` command to keep those dist folders up-to-date, for example: Then run the right `tsc --watch` command to keep those dist folders up-to-date, for example:

View File

@ -1,19 +0,0 @@
#!/usr/bin/env bash
# This script builds rxjs from source, with an ES6 target, and with tsickle turned on.
# We need to do this until we work with the RxJS team to get a distribution that works
# with Closure Compiler.
# Note that in nodejs, we still run the standard RxJS distribution. This one is only
# used for the bundle that targets a browser runtime.
# TODO(alexeagle): discuss with Jay Phelps once we have a recommendation
set -e -o pipefail
cd `dirname $0`
rm -rf rxjs
git clone https://github.com/ReactiveX/rxjs.git --depth=200
git -C rxjs/ checkout 5.0.3
cp rxjs.tsconfig.json rxjs/
TSC="node --max-old-space-size=3000 ../dist/tools/@angular/tsc-wrapped/src/main"
$TSC -p rxjs/rxjs.tsconfig.json

View File

@ -1,53 +0,0 @@
#!/usr/bin/env bash
# Build a folder using angular ES6 and the closure compiler
set -e -o pipefail
# The ES6 distro we built for rxjs works only in the browser, not in nodejs.
# Since we installed rxjs in node_modules for ngc to use, we have to point
# to the alternate distro when compiling with closure.
rm -rf vendor
mkdir vendor
cp -pr ../rxjs/dist/es6 vendor/rxjs
CLOSURE_ARGS=(
"--language_in=ES6_STRICT"
"--language_out=ES5"
"--compilation_level=ADVANCED_OPTIMIZATIONS"
"--warning_level=QUIET"
"--js_output_file=dist/bundle.js"
"--create_source_map=%outname%.map"
"--variable_renaming_report=dist/variable_renaming_report"
"--property_renaming_report=dist/property_renaming_report"
# Don't include ES6 polyfills
"--rewrite_polyfills=false"
# List of path prefixes to be removed from ES6 & CommonJS modules.
"--js_module_root=node_modules/@angular/core"
"--js_module_root=node_modules/@angular/common"
"--js_module_root=node_modules/@angular/compiler"
"--js_module_root=node_modules/@angular/platform-browser"
"--js_module_root=vendor"
# Uncomment for easier debugging
# "--formatting=PRETTY_PRINT"
node_modules/@angular/core/src/testability/testability.externs.js
node_modules/zone.js/dist/zone.js
$(find -L vendor/rxjs -name *.js)
node_modules/@angular/core/@angular/core.js
node_modules/@angular/common/@angular/common.js
node_modules/@angular/compiler/@angular/compiler.js
node_modules/@angular/platform-browser/@angular/platform-browser.js
"built/src/*.js"
"--entry_point=./built/src/main"
)
java -jar node_modules/google-closure-compiler/compiler.jar $(echo ${CLOSURE_ARGS[*]})
# gzip on Travis doesn't have --keep option so copy the original file first
cp dist/bundle.js dist/bundle.tmp
gzip -f dist/bundle.js
mv dist/bundle.tmp dist/bundle.js
ls -alH dist/bundle*

View File

@ -0,0 +1,30 @@
--compilation_level=ADVANCED_OPTIMIZATIONS
--language_out=ES5
--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
node_modules/zone.js/dist/zone_externs.js
--js node_modules/rxjs/**.js
--process_common_js_modules
--module_resolution=node
node_modules/@angular/core/@angular/core.js
--js_module_root=node_modules/@angular/core
node_modules/@angular/core/src/testability/testability.externs.js
node_modules/@angular/common/@angular/common.js
--js_module_root=node_modules/@angular/common
node_modules/@angular/platform-browser/@angular/platform-browser.js
--js_module_root=node_modules/@angular/platform-browser
--js built/**.js
--entry_point=built/src/main

View File

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

View File

@ -11,21 +11,22 @@
"@angular/platform-browser": "file:../../dist/packages-dist/platform-browser", "@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
"@angular/platform-server": "file:../../dist/packages-dist/platform-server", "@angular/platform-server": "file:../../dist/packages-dist/platform-server",
"@angular/tsc-wrapped": "file:../../dist/tools/@angular/tsc-wrapped", "@angular/tsc-wrapped": "file:../../dist/tools/@angular/tsc-wrapped",
"google-closure-compiler": "20161201.0.0", "google-closure-compiler": "20170409.0.0",
"rxjs": "file:../../node_modules/rxjs", "rxjs": "5.3.1",
"typescript": "2.1.6", "typescript": "2.1.6",
"zone.js": "0.7.6" "zone.js": "0.8.6"
}, },
"devDependencies": { "devDependencies": {
"@types/jasmine": "2.5.41", "@types/jasmine": "2.5.41",
"concurrently": "3.1.0", "concurrently": "3.4.0",
"lite-server": "2.2.2", "lite-server": "2.2.2",
"protractor": "file:../../node_modules/protractor" "protractor": "file:../../node_modules/protractor"
}, },
"scripts": { "scripts": {
"test": "ngc && ./bundle.sh && concurrently \"yarn run serve\" \"yarn run protractor\" --kill-others --success first", "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", "serve": "lite-server -c e2e/browser.config.json",
"preprotractor": "tsc -p e2e", "preprotractor": "tsc -p e2e",
"protractor": "protractor e2e/protractor.config.js" "protractor": "protractor e2e/protractor.config.js"
} }
} }

View File

@ -1,14 +1,18 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Hello World</title> <title>Hello World</title>
<base href="/"> <base href="/">
</head> </head>
<body>
<hello-world-app>Loading...</hello-world-app>
<script src="dist/bundle.js"></script> <body>
<hello-world-app>Loading...</hello-world-app>
<script src="node_modules/zone.js/dist/zone.min.js"></script>
<script src="dist/bundle.js"></script>
</body> </body>
</html>
</html>

View File

@ -24,7 +24,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/jasmine": "2.5.41", "@types/jasmine": "2.5.41",
"concurrently": "3.1.0", "concurrently": "3.4.0",
"lite-server": "2.2.2", "lite-server": "2.2.2",
"protractor": "file:../../node_modules/protractor" "protractor": "file:../../node_modules/protractor"
} }

View File

@ -10,6 +10,6 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"typescript": "2.3.0-dev.20170223" "typescript": "2.3.0"
} }
} }

View File

@ -2,6 +2,6 @@
# yarn lockfile v1 # yarn lockfile v1
typescript@2.3.0-dev.20170223: typescript@2.3.0:
version "2.3.0-dev.20170223" version "2.3.0"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.3.0-dev.20170223.tgz#286494c36625ea2eb26f963ed205cd9ca5c41447" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.3.0.tgz#2e63e09284392bc8158a2444c33e2093795c0418"

View File

@ -4,12 +4,6 @@ set -e -o pipefail
cd `dirname $0` cd `dirname $0`
if [ ! -d "rxjs/dist/es6" ]; then
echo "You must run build the ES2015 version of RxJS for some tests:"
echo "./integration/build_rxjs_es6.sh"
exit 1
fi
# Workaround https://github.com/yarnpkg/yarn/issues/2165 # Workaround https://github.com/yarnpkg/yarn/issues/2165
# Yarn will cache file://dist URIs and not update Angular code # Yarn will cache file://dist URIs and not update Angular code
readonly cache=.yarn_local_cache readonly cache=.yarn_local_cache
@ -20,7 +14,7 @@ rm_cache
mkdir $cache mkdir $cache
trap rm_cache EXIT trap rm_cache EXIT
for testDir in $(ls | grep -v rxjs | grep -v node_modules) ; do for testDir in $(ls | grep -v node_modules) ; do
[[ -d "$testDir" ]] || continue [[ -d "$testDir" ]] || continue
echo "#################################" echo "#################################"
echo "Running integration test $testDir" echo "Running integration test $testDir"

View File

@ -1,42 +0,0 @@
{
"angularCompilerOptions": {
"skipMetadataEmit": true,
"annotationsAs": "static fields",
"annotateForClosureCompiler": true
},
"compilerOptions": {
"types": [],
/**
* Remaining options are copied from
* https://github.com/ReactiveX/rxjs/blob/cba74135810a8e6bbe0b3c7732e8544b0869589e/tsconfig.json
* TODO(alexeagle): use "extends" instead when Angular is on TS 2.1
*/
"removeComments": false,
"preserveConstEnums": true,
"sourceMap": true,
"declaration": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"suppressImplicitAnyIndexErrors": true,
"moduleResolution": "node",
"module": "es2015",
"target": "es6",
"outDir": "dist/es6",
"lib": [
"es5",
"es2015.iterable",
"es2015.collection",
"es2015.promise",
"dom"
]
},
"formatCodeOptions": {
"indentSize": 2,
"tabSize": 2
},
"files": [
"src/Rx.ts"
]
}

View File

@ -9,6 +9,7 @@
import * as compiler from '@angular/compiler'; import * as compiler from '@angular/compiler';
import * as compilerTesting from '@angular/compiler/testing'; import * as compilerTesting from '@angular/compiler/testing';
import * as coreTesting from '@angular/core'; import * as coreTesting from '@angular/core';
import * as forms from '@angular/forms';
import * as core from '@angular/core/testing'; import * as core from '@angular/core/testing';
import * as httpTesting from '@angular/http'; import * as httpTesting from '@angular/http';
import * as http from '@angular/http/testing'; import * as http from '@angular/http/testing';
@ -26,6 +27,7 @@ export default {
compilerTesting, compilerTesting,
core, core,
coreTesting, coreTesting,
forms,
http, http,
httpTesting, httpTesting,
platformBrowser, platformBrowser,

View File

@ -9,6 +9,7 @@
"@angular/compiler": "file:../../dist/packages-dist/compiler", "@angular/compiler": "file:../../dist/packages-dist/compiler",
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli", "@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
"@angular/core": "file:../../dist/packages-dist/core", "@angular/core": "file:../../dist/packages-dist/core",
"@angular/forms": "file:../../dist/packages-dist/forms",
"@angular/http": "file:../../dist/packages-dist/http", "@angular/http": "file:../../dist/packages-dist/http",
"@angular/platform-browser": "file:../../dist/packages-dist/platform-browser", "@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
"@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic", "@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic",

View File

@ -9,8 +9,7 @@
"target": "es5", "target": "es5",
"lib": ["es5", "dom", "es2015.collection", "es2015.iterable", "es2015.promise"], "lib": ["es5", "dom", "es2015.collection", "es2015.iterable", "es2015.promise"],
"types": [], "types": [],
// TODO(i): strictNullChecks should turned on but are temporarily disabled due to #15432 "strictNullChecks": true
"strictNullChecks": false
}, },
"files": [ "files": [
"include-all.ts", "include-all.ts",

View File

@ -10,6 +10,7 @@ import * as compiler from '@angular/compiler';
import * as compilerTesting from '@angular/compiler/testing'; import * as compilerTesting from '@angular/compiler/testing';
import * as coreTesting from '@angular/core'; import * as coreTesting from '@angular/core';
import * as core from '@angular/core/testing'; import * as core from '@angular/core/testing';
import * as forms from '@angular/forms';
import * as httpTesting from '@angular/http'; import * as httpTesting from '@angular/http';
import * as http from '@angular/http/testing'; import * as http from '@angular/http/testing';
import * as platformBrowserTesting from '@angular/platform-browser'; import * as platformBrowserTesting from '@angular/platform-browser';
@ -26,6 +27,7 @@ export default {
compilerTesting, compilerTesting,
core, core,
coreTesting, coreTesting,
forms,
http, http,
httpTesting, httpTesting,
platformBrowser, platformBrowser,

View File

@ -9,6 +9,7 @@
"@angular/compiler": "file:../../dist/packages-dist/compiler", "@angular/compiler": "file:../../dist/packages-dist/compiler",
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli", "@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
"@angular/core": "file:../../dist/packages-dist/core", "@angular/core": "file:../../dist/packages-dist/core",
"@angular/forms": "file:../../dist/packages-dist/forms",
"@angular/http": "file:../../dist/packages-dist/http", "@angular/http": "file:../../dist/packages-dist/http",
"@angular/platform-browser": "file:../../dist/packages-dist/platform-browser", "@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
"@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic", "@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic",

View File

@ -15,11 +15,10 @@
"es2015.promise" "es2015.promise"
], ],
"types": [], "types": [],
// TODO(i): strictNullChecks should turned on but are temporarily disabled due to #15432 "strictNullChecks": true
"strictNullChecks": false
}, },
"files": [ "files": [
"include-all.ts", "include-all.ts",
"node_modules/@types/jasmine/index.d.ts" "node_modules/@types/jasmine/index.d.ts"
] ]
} }

View File

@ -7,7 +7,7 @@
*/ */
import {NgIf} from '@angular/common'; import {NgIf} from '@angular/common';
import {ComponentFactory, ComponentFactoryResolver, ComponentRef, Injector, NgModuleRef, RendererFactory2, RootRenderer, Sanitizer, TemplateRef, ViewContainerRef} from '@angular/core'; import {ComponentFactory, ComponentFactoryResolver, ComponentRef, ErrorHandler, Injector, NgModuleRef, RendererFactory2, RootRenderer, Sanitizer, TemplateRef, ViewContainerRef} from '@angular/core';
import {ArgumentType, BindingFlags, NodeFlags, ViewDefinition, ViewFlags, anchorDef, createComponentFactory, directiveDef, elementDef, initServicesIfNeeded, textDef, viewDef} from '@angular/core/src/view/index'; import {ArgumentType, BindingFlags, NodeFlags, ViewDefinition, ViewFlags, anchorDef, createComponentFactory, directiveDef, elementDef, initServicesIfNeeded, textDef, viewDef} from '@angular/core/src/view/index';
import {DomRendererFactory2} from '@angular/platform-browser/src/dom/dom_renderer'; import {DomRendererFactory2} from '@angular/platform-browser/src/dom/dom_renderer';
import {DomSanitizerImpl, SafeStyle} from '@angular/platform-browser/src/security/dom_sanitization_service'; import {DomSanitizerImpl, SafeStyle} from '@angular/platform-browser/src/security/dom_sanitization_service';
@ -108,6 +108,7 @@ export class AppModule implements Injector, NgModuleRef<any> {
case Sanitizer: case Sanitizer:
return this.sanitizer; return this.sanitizer;
case RootRenderer: case RootRenderer:
case ErrorHandler:
return null; return null;
case NgModuleRef: case NgModuleRef:
return this; return this;

View File

@ -1,6 +1,6 @@
{ {
"name": "angular-srcs", "name": "angular-srcs",
"version": "4.1.0", "version": "4.1.1",
"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",

View File

@ -22,9 +22,6 @@ import {ComponentFactoryResolver, ComponentRef, Directive, Injector, Input, NgMo
* * `ngComponentOutletInjector`: Optional custom {@link Injector} that will be used as parent for * * `ngComponentOutletInjector`: Optional custom {@link Injector} that will be used as parent for
* the Component. Defaults to the injector of the current view container. * the Component. Defaults to the injector of the current view container.
* *
* * `ngComponentOutletProviders`: Optional injectable objects ({@link Provider}) that are visible
* to the component.
*
* * `ngComponentOutletContent`: Optional list of projectable nodes to insert into the content * * `ngComponentOutletContent`: Optional list of projectable nodes to insert into the content
* section of the component, if exists. * section of the component, if exists.
* *

View File

@ -14,22 +14,18 @@ import {InjectionToken} from '@angular/core';
* `PlatformLocation` encapsulates all calls to DOM apis, which allows the Router to be platform * `PlatformLocation` encapsulates all calls to DOM apis, which allows the Router to be platform
* agnostic. * agnostic.
* This means that we can have different implementation of `PlatformLocation` for the different * This means that we can have different implementation of `PlatformLocation` for the different
* platforms * platforms that angular supports. For example, `@angular/platform-browser` provides an
* that angular supports. For example, the default `PlatformLocation` is {@link * implementation specific to the browser environment, while `@angular/platform-webworker` provides
* BrowserPlatformLocation}, * one suitable for use with web workers.
* however when you run your app in a WebWorker you use {@link WebWorkerPlatformLocation}.
* *
* The `PlatformLocation` class is used directly by all implementations of {@link LocationStrategy} * The `PlatformLocation` class is used directly by all implementations of {@link LocationStrategy}
* when * when they need to interact with the DOM apis like pushState, popState, etc...
* they need to interact with the DOM apis like pushState, popState, etc...
* *
* {@link LocationStrategy} in turn is used by the {@link Location} service which is used directly * {@link LocationStrategy} in turn is used by the {@link Location} service which is used directly
* by * by the {@link Router} in order to navigate between routes. Since all interactions between {@link
* the {@link Router} in order to navigate between routes. Since all interactions between {@link
* Router} / * Router} /
* {@link Location} / {@link LocationStrategy} and DOM apis flow through the `PlatformLocation` * {@link Location} / {@link LocationStrategy} and DOM apis flow through the `PlatformLocation`
* class * class they are all platform independent.
* they are all platform independent.
* *
* @stable * @stable
*/ */

View File

@ -29,6 +29,7 @@ function main() {
Promise.resolve() Promise.resolve()
.then(() => codeGenTest()) .then(() => codeGenTest())
.then(() => codeGenTest(true))
.then(() => i18nTest()) .then(() => i18nTest())
.then(() => lazyRoutesTest()) .then(() => lazyRoutesTest())
.then(() => { .then(() => {
@ -42,8 +43,9 @@ function main() {
}); });
} }
function codeGenTest() { function codeGenTest(forceError = false) {
const basePath = path.join(__dirname, '../ngtools_src'); const basePath = path.join(__dirname, '../ngtools_src');
const srcPath = path.join(__dirname, '../src');
const project = path.join(basePath, 'tsconfig-build.json'); const project = path.join(basePath, 'tsconfig-build.json');
const readResources: string[] = []; const readResources: string[] = [];
const wroteFiles: string[] = []; const wroteFiles: string[] = [];
@ -59,6 +61,9 @@ function codeGenTest() {
config.ngOptions.basePath = basePath; config.ngOptions.basePath = basePath;
console.log(`>>> running codegen for ${project}`); console.log(`>>> running codegen for ${project}`);
if (forceError) {
console.log(`>>> asserting that missingTranslation param with error value throws`);
}
return __NGTOOLS_PRIVATE_API_2 return __NGTOOLS_PRIVATE_API_2
.codeGen({ .codeGen({
basePath, basePath,
@ -67,9 +72,10 @@ function codeGenTest() {
angularCompilerOptions: config.ngOptions, angularCompilerOptions: config.ngOptions,
// i18n options. // i18n options.
i18nFormat: undefined, i18nFormat: 'xlf',
i18nFile: undefined, i18nFile: path.join(srcPath, 'messages.fi.xlf'),
locale: undefined, locale: 'fi',
missingTranslation: forceError ? 'error' : 'ignore',
readResource: (fileName: string) => { readResource: (fileName: string) => {
readResources.push(fileName); readResources.push(fileName);
@ -101,10 +107,17 @@ function codeGenTest() {
console.log(`done, no errors.`); console.log(`done, no errors.`);
}) })
.catch((e: any) => { .catch((e: Error) => {
console.error(e.stack); if (forceError) {
console.error('Compilation failed'); assert(
throw e; e.message.match(`Missing translation for message`),
`Expected error message for missing translations`);
console.log(`done, error catched`);
} else {
console.error(e.stack);
console.error('Compilation failed');
throw e;
}
}); });
} }
@ -165,7 +178,7 @@ function i18nTest() {
console.log(`done, no errors.`); console.log(`done, no errors.`);
}) })
.catch((e: any) => { .catch((e: Error) => {
console.error(e.stack); console.error(e.stack);
console.error('Extraction failed'); console.error('Extraction failed');
throw e; throw e;

View File

@ -9,7 +9,7 @@
"ng-xi18n": "./src/extract_i18n.js" "ng-xi18n": "./src/extract_i18n.js"
}, },
"dependencies": { "dependencies": {
"@angular/tsc-wrapped": "4.1.0", "@angular/tsc-wrapped": "4.1.1",
"reflect-metadata": "^0.1.2", "reflect-metadata": "^0.1.2",
"minimist": "^1.2.0" "minimist": "^1.2.0"
}, },

View File

@ -6,8 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AotCompiler, AotCompilerHost, AotCompilerOptions, GeneratedFile, createAotCompiler} from '@angular/compiler'; import {GeneratedFile} from '@angular/compiler';
import {RenderComponentType, ɵReflectionCapabilities as ReflectionCapabilities, ɵreflector as reflector} from '@angular/core';
import {NodeFlags} from '@angular/core/src/view/index'; import {NodeFlags} from '@angular/core/src/view/index';
import {async} from '@angular/core/testing'; import {async} from '@angular/core/testing';
import {MetadataBundler, MetadataCollector, ModuleMetadata, privateEntriesToIndex} from '@angular/tsc-wrapped'; import {MetadataBundler, MetadataCollector, ModuleMetadata, privateEntriesToIndex} from '@angular/tsc-wrapped';
@ -15,60 +14,16 @@ import * as ts from 'typescript';
import {extractSourceMap, originalPositionFor} from '../output/source_map_util'; import {extractSourceMap, originalPositionFor} from '../output/source_map_util';
import {EmittingCompilerHost, MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, MockMetadataBundlerHost, settings} from './test_util'; import {EmittingCompilerHost, MockData, MockDirectory, MockMetadataBundlerHost, arrayToMockDir, arrayToMockMap, compile, settings, setup, toMockFileArray} from './test_util';
const DTS = /\.d\.ts$/;
const minCoreIndex = `
export * from './src/application_module';
export * from './src/change_detection';
export * from './src/metadata';
export * from './src/di/metadata';
export * from './src/di/injector';
export * from './src/di/injection_token';
export * from './src/linker';
export * from './src/render';
export * from './src/codegen_private_exports';
`;
describe('compiler (unbundled Angular)', () => { describe('compiler (unbundled Angular)', () => {
let angularFiles: Map<string, string>; let angularFiles = setup();
beforeAll(() => {
const emittingHost = new EmittingCompilerHost([], {emitMetadata: true});
emittingHost.addScript('@angular/core/index.ts', minCoreIndex);
const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost);
emittingProgram.emit();
angularFiles = emittingHost.written;
});
// Restore reflector since AoT compiler will update it with a new static reflector
afterEach(() => { reflector.updateCapabilities(new ReflectionCapabilities()); });
describe('Quickstart', () => { describe('Quickstart', () => {
let host: MockCompilerHost; it('should compile', async(() => compile([QUICKSTART, angularFiles]).then(({genFiles}) => {
let aotHost: MockAotCompilerHost; expect(genFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl)))
beforeEach(() => {
host = new MockCompilerHost(QUICKSTART, FILES, angularFiles);
aotHost = new MockAotCompilerHost(host);
});
it('should compile',
async(() => compile(host, aotHost, expectNoDiagnostics).then(generatedFiles => {
expect(generatedFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl)))
.toBeDefined();
expect(generatedFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl)))
.toBeDefined();
})));
it('should compile using summaries',
async(() => summaryCompile(host, aotHost).then(generatedFiles => {
expect(generatedFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl)))
.toBeDefined();
expect(generatedFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl)))
.toBeDefined(); .toBeDefined();
expect(genFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl))).toBeDefined();
}))); })));
}); });
@ -97,17 +52,11 @@ describe('compiler (unbundled Angular)', () => {
}); });
function compileApp(): Promise<GeneratedFile> { function compileApp(): Promise<GeneratedFile> {
return new Promise((resolve, reject) => { return compile([rootDir, angularFiles])
const host = new MockCompilerHost(['/app/app.module.ts'], rootDir, angularFiles); .then(
const aotHost = new MockAotCompilerHost(host); ({genFiles}) => {return genFiles.find(
let result: GeneratedFile[]; genFile =>
let error: Error; genFile.srcFileUrl === componentPath && genFile.genFileUrl.endsWith('.ts'))});
resolve(compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics)
.then(
(files) => files.find(
genFile => genFile.srcFileUrl === componentPath &&
genFile.genFileUrl.endsWith('.ts'))));
});
} }
function findLineAndColumn( function findLineAndColumn(
@ -247,7 +196,7 @@ describe('compiler (unbundled Angular)', () => {
describe('errors', () => { describe('errors', () => {
it('should only warn if not all arguments of an @Injectable class can be resolved', it('should only warn if not all arguments of an @Injectable class can be resolved',
async(() => { async(() => {
const FILES: MockData = { const FILES: MockDirectory = {
app: { app: {
'app.ts': ` 'app.ts': `
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
@ -259,10 +208,8 @@ describe('compiler (unbundled Angular)', () => {
` `
} }
}; };
const host = new MockCompilerHost(['/app/app.ts'], FILES, angularFiles);
const aotHost = new MockAotCompilerHost(host);
const warnSpy = spyOn(console, 'warn'); const warnSpy = spyOn(console, 'warn');
compile(host, aotHost, expectNoDiagnostics).then(() => { compile([FILES, angularFiles]).then(() => {
expect(warnSpy).toHaveBeenCalledWith( expect(warnSpy).toHaveBeenCalledWith(
`Warning: Can't resolve all parameters for MyService in /app/app.ts: (?). This will become an error in Angular v5.x`); `Warning: Can't resolve all parameters for MyService in /app/app.ts: (?). This will become an error in Angular v5.x`);
}); });
@ -271,7 +218,7 @@ describe('compiler (unbundled Angular)', () => {
}); });
it('should add the preamble to generated files', async(() => { it('should add the preamble to generated files', async(() => {
const FILES: MockData = { const FILES: MockDirectory = {
app: { app: {
'app.ts': ` 'app.ts': `
import { NgModule, Component } from '@angular/core'; import { NgModule, Component } from '@angular/core';
@ -284,22 +231,19 @@ describe('compiler (unbundled Angular)', () => {
` `
} }
}; };
const host = new MockCompilerHost(['/app/app.ts'], FILES, angularFiles);
const aotHost = new MockAotCompilerHost(host);
const genFilePreamble = '/* Hello world! */'; const genFilePreamble = '/* Hello world! */';
compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics, {genFilePreamble}) compile([FILES, angularFiles], {genFilePreamble}).then(({genFiles}) => {
.then((generatedFiles) => { const genFile =
const genFile = generatedFiles.find( genFiles.find(gf => gf.srcFileUrl === '/app/app.ts' && gf.genFileUrl.endsWith('.ts'));
gf => gf.srcFileUrl === '/app/app.ts' && gf.genFileUrl.endsWith('.ts')); expect(genFile.source.startsWith(genFilePreamble)).toBe(true);
expect(genFile.source.startsWith(genFilePreamble)).toBe(true); });
});
})); }));
describe('ComponentFactories', () => { describe('ComponentFactories', () => {
it('should include inputs, outputs and ng-content selectors in the component factory', it('should include inputs, outputs and ng-content selectors in the component factory',
async(() => { async(() => {
const FILES: MockData = { const FILES: MockDirectory = {
app: { app: {
'app.ts': ` 'app.ts': `
import {Component, NgModule, Input, Output} from '@angular/core'; import {Component, NgModule, Input, Output} from '@angular/core';
@ -323,11 +267,8 @@ describe('compiler (unbundled Angular)', () => {
` `
} }
}; };
const host = new MockCompilerHost(['/app/app.ts'], FILES, angularFiles); compile([FILES, angularFiles]).then(({genFiles}) => {
const aotHost = new MockAotCompilerHost(host); const genFile = genFiles.find(genFile => genFile.srcFileUrl === '/app/app.ts');
let generatedFiles: GeneratedFile[];
compile(host, aotHost, expectNoDiagnostics).then((generatedFiles) => {
const genFile = generatedFiles.find(genFile => genFile.srcFileUrl === '/app/app.ts');
const createComponentFactoryCall = const createComponentFactoryCall =
/ɵccf\([^)]*\)/m.exec(genFile.source) ![0].replace(/\s*/g, ''); /ɵccf\([^)]*\)/m.exec(genFile.source) ![0].replace(/\s*/g, '');
// selector // selector
@ -345,7 +286,7 @@ describe('compiler (unbundled Angular)', () => {
describe('generated templates', () => { describe('generated templates', () => {
it('should not call `check` for directives without bindings nor ngDoCheck/ngOnInit', it('should not call `check` for directives without bindings nor ngDoCheck/ngOnInit',
async(() => { async(() => {
const FILES: MockData = { const FILES: MockDirectory = {
app: { app: {
'app.ts': ` 'app.ts': `
import { NgModule, Component } from '@angular/core'; import { NgModule, Component } from '@angular/core';
@ -358,37 +299,16 @@ describe('compiler (unbundled Angular)', () => {
` `
} }
}; };
const host = new MockCompilerHost(['/app/app.ts'], FILES, angularFiles); compile([FILES, angularFiles]).then(({genFiles}) => {
const aotHost = new MockAotCompilerHost(host); const genFile = genFiles.find(
const genFilePreamble = '/* Hello world! */'; gf => gf.srcFileUrl === '/app/app.ts' && gf.genFileUrl.endsWith('.ts'));
compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics, {genFilePreamble}) expect(genFile.source).not.toContain('check(');
.then((generatedFiles) => { });
const genFile = generatedFiles.find(
gf => gf.srcFileUrl === '/app/app.ts' && gf.genFileUrl.endsWith('.ts'));
expect(genFile.source).not.toContain('check(');
});
})); }));
}); });
describe('inheritance with summaries', () => { describe('inheritance with summaries', () => {
function compileWithSummaries(
libInput: MockData, appInput: MockData): Promise<GeneratedFile[]> {
const libHost = new MockCompilerHost(['/lib/base.ts'], libInput, angularFiles);
const libAotHost = new MockAotCompilerHost(libHost);
libAotHost.tsFilesOnly();
const appHost = new MockCompilerHost(['/app/main.ts'], appInput, angularFiles);
const appAotHost = new MockAotCompilerHost(appHost);
appAotHost.tsFilesOnly();
return compile(libHost, libAotHost, expectNoDiagnostics, expectNoDiagnosticsAndEmit)
.then(() => {
libHost.writtenFiles.forEach((value, key) => appHost.writeFile(key, value, false));
libHost.overrides.forEach((value, key) => appHost.override(key, value));
return compile(appHost, appAotHost, expectNoDiagnostics, expectNoDiagnosticsAndEmit);
});
}
function compileParentAndChild( function compileParentAndChild(
{parentClassDecorator, parentModuleDecorator, childClassDecorator, childModuleDecorator}: { {parentClassDecorator, parentModuleDecorator, childClassDecorator, childModuleDecorator}: {
parentClassDecorator: string, parentClassDecorator: string,
@ -396,7 +316,7 @@ describe('compiler (unbundled Angular)', () => {
childClassDecorator: string, childClassDecorator: string,
childModuleDecorator: string childModuleDecorator: string
}) { }) {
const libInput: MockData = { const libInput: MockDirectory = {
'lib': { 'lib': {
'base.ts': ` 'base.ts': `
import {Injectable, Pipe, Directive, Component, NgModule} from '@angular/core'; import {Injectable, Pipe, Directive, Component, NgModule} from '@angular/core';
@ -409,7 +329,7 @@ describe('compiler (unbundled Angular)', () => {
` `
} }
}; };
const appInput: MockData = { const appInput: MockDirectory = {
'app': { 'app': {
'main.ts': ` 'main.ts': `
import {Injectable, Pipe, Directive, Component, NgModule} from '@angular/core'; import {Injectable, Pipe, Directive, Component, NgModule} from '@angular/core';
@ -424,13 +344,14 @@ describe('compiler (unbundled Angular)', () => {
} }
}; };
return compileWithSummaries(libInput, appInput) return compile([libInput, angularFiles], {useSummaries: true})
.then((generatedFiles) => generatedFiles.find(gf => gf.srcFileUrl === '/app/main.ts')); .then(({outDir}) => compile([outDir, appInput, angularFiles], {useSummaries: true}))
.then(({genFiles}) => genFiles.find(gf => gf.srcFileUrl === '/app/main.ts'));
} }
it('should inherit ctor and lifecycle hooks from classes in other compilation units', it('should inherit ctor and lifecycle hooks from classes in other compilation units',
async(() => { async(() => {
const libInput: MockData = { const libInput: MockDirectory = {
'lib': { 'lib': {
'base.ts': ` 'base.ts': `
export class AParam {} export class AParam {}
@ -442,7 +363,7 @@ describe('compiler (unbundled Angular)', () => {
` `
} }
}; };
const appInput: MockData = { const appInput: MockDirectory = {
'app': { 'app': {
'main.ts': ` 'main.ts': `
import {NgModule, Component} from '@angular/core'; import {NgModule, Component} from '@angular/core';
@ -459,17 +380,19 @@ describe('compiler (unbundled Angular)', () => {
} }
}; };
compileWithSummaries(libInput, appInput).then((generatedFiles) => { compile([libInput, angularFiles], {useSummaries: true})
const mainNgFactory = generatedFiles.find(gf => gf.srcFileUrl === '/app/main.ts'); .then(({outDir}) => compile([outDir, appInput, angularFiles], {useSummaries: true}))
const flags = NodeFlags.TypeDirective | NodeFlags.Component | NodeFlags.OnDestroy; .then(({genFiles}) => {
expect(mainNgFactory.source) const mainNgFactory = genFiles.find(gf => gf.srcFileUrl === '/app/main.ts');
.toContain(`${flags},(null as any),0,import1.Extends,[import2.AParam]`); const flags = NodeFlags.TypeDirective | NodeFlags.Component | NodeFlags.OnDestroy;
}); expect(mainNgFactory.source)
.toContain(`${flags},(null as any),0,import1.Extends,[import2.AParam]`);
});
})); }));
it('should inherit ctor and lifecycle hooks from classes in other compilation units over 2 levels', it('should inherit ctor and lifecycle hooks from classes in other compilation units over 2 levels',
async(() => { async(() => {
const lib1Input: MockData = { const lib1Input: MockDirectory = {
'lib1': { 'lib1': {
'base.ts': ` 'base.ts': `
export class AParam {} export class AParam {}
@ -482,7 +405,7 @@ describe('compiler (unbundled Angular)', () => {
} }
}; };
const lib2Input: MockData = { const lib2Input: MockDirectory = {
'lib2': { 'lib2': {
'middle.ts': ` 'middle.ts': `
import {Base} from '../lib1/base'; import {Base} from '../lib1/base';
@ -492,7 +415,7 @@ describe('compiler (unbundled Angular)', () => {
}; };
const appInput: MockData = { const appInput: MockDirectory = {
'app': { 'app': {
'main.ts': ` 'main.ts': `
import {NgModule, Component} from '@angular/core'; import {NgModule, Component} from '@angular/core';
@ -508,29 +431,11 @@ describe('compiler (unbundled Angular)', () => {
` `
} }
}; };
const lib1Host = new MockCompilerHost(['/lib1/base.ts'], lib1Input, angularFiles); compile([lib1Input, angularFiles], {useSummaries: true})
const lib1AotHost = new MockAotCompilerHost(lib1Host); .then(({outDir}) => compile([outDir, lib2Input, angularFiles], {useSummaries: true}))
lib1AotHost.tsFilesOnly(); .then(({outDir}) => compile([outDir, appInput, angularFiles], {useSummaries: true}))
const lib2Host = new MockCompilerHost(['/lib2/middle.ts'], lib2Input, angularFiles); .then(({genFiles}) => {
const lib2AotHost = new MockAotCompilerHost(lib2Host); const mainNgFactory = genFiles.find(gf => gf.srcFileUrl === '/app/main.ts');
lib2AotHost.tsFilesOnly();
const appHost = new MockCompilerHost(['/app/main.ts'], appInput, angularFiles);
const appAotHost = new MockAotCompilerHost(appHost);
appAotHost.tsFilesOnly();
compile(lib1Host, lib1AotHost, expectNoDiagnostics, expectNoDiagnosticsAndEmit)
.then(() => {
lib1Host.writtenFiles.forEach((value, key) => lib2Host.writeFile(key, value, false));
lib1Host.overrides.forEach((value, key) => lib2Host.override(key, value));
return compile(
lib2Host, lib2AotHost, expectNoDiagnostics, expectNoDiagnosticsAndEmit);
})
.then(() => {
lib2Host.writtenFiles.forEach((value, key) => appHost.writeFile(key, value, false));
lib2Host.overrides.forEach((value, key) => appHost.override(key, value));
return compile(appHost, appAotHost, expectNoDiagnostics, expectNoDiagnosticsAndEmit);
})
.then((generatedFiles) => {
const mainNgFactory = generatedFiles.find(gf => gf.srcFileUrl === '/app/main.ts');
const flags = NodeFlags.TypeDirective | NodeFlags.Component | NodeFlags.OnDestroy; const flags = NodeFlags.TypeDirective | NodeFlags.Component | NodeFlags.OnDestroy;
expect(mainNgFactory.source) expect(mainNgFactory.source)
.toContain(`${flags},(null as any),0,import1.Extends,[import2.AParam_2]`); .toContain(`${flags},(null as any),0,import1.Extends,[import2.AParam_2]`);
@ -660,6 +565,8 @@ describe('compiler (unbundled Angular)', () => {
}); });
describe('compiler (bundled Angular)', () => { describe('compiler (bundled Angular)', () => {
setup({compileAngular: false});
let angularFiles: Map<string, string>; let angularFiles: Map<string, string>;
beforeAll(() => { beforeAll(() => {
@ -681,34 +588,19 @@ describe('compiler (bundled Angular)', () => {
const bundleIndexName = emittingHost.effectiveName('@angular/core/bundle_index.ts'); const bundleIndexName = emittingHost.effectiveName('@angular/core/bundle_index.ts');
const emittingProgram = ts.createProgram([bundleIndexName], settings, emittingHost); const emittingProgram = ts.createProgram([bundleIndexName], settings, emittingHost);
emittingProgram.emit(); emittingProgram.emit();
angularFiles = emittingHost.written; angularFiles = emittingHost.writtenAngularFiles();
}); });
describe('Quickstart', () => { describe('Quickstart', () => {
let host: MockCompilerHost; it('should compile', async(() => compile([QUICKSTART, angularFiles]).then(({genFiles}) => {
let aotHost: MockAotCompilerHost; expect(genFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl)))
beforeEach(() => {
host = new MockCompilerHost(QUICKSTART, FILES, angularFiles);
aotHost = new MockAotCompilerHost(host);
});
// Restore reflector since AoT compiler will update it with a new static reflector
afterEach(() => { reflector.updateCapabilities(new ReflectionCapabilities()); });
it('should compile',
async(() => compile(host, aotHost, expectNoDiagnostics).then(generatedFiles => {
expect(generatedFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl)))
.toBeDefined();
expect(generatedFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl)))
.toBeDefined(); .toBeDefined();
expect(genFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl))).toBeDefined();
}))); })));
}); });
describe('Bundled library', () => { describe('Bundled library', () => {
let host: MockCompilerHost; let libraryFiles: MockDirectory;
let aotHost: MockAotCompilerHost;
let libraryFiles: Map<string, string>;
beforeAll(() => { beforeAll(() => {
// Emit the library bundle // Emit the library bundle
@ -728,135 +620,22 @@ describe('compiler (bundled Angular)', () => {
// Emit the sources // Emit the sources
const emittingProgram = ts.createProgram(['/bolder/index.ts'], settings, emittingHost); const emittingProgram = ts.createProgram(['/bolder/index.ts'], settings, emittingHost);
emittingProgram.emit(); emittingProgram.emit();
libraryFiles = emittingHost.written; const libFiles = emittingHost.written;
// Copy the .html file // Copy the .html file
const htmlFileName = '/bolder/src/bolder.component.html'; const htmlFileName = '/bolder/src/bolder.component.html';
libraryFiles.set(htmlFileName, emittingHost.readFile(htmlFileName)); libFiles.set(htmlFileName, emittingHost.readFile(htmlFileName));
libraryFiles = arrayToMockDir(toMockFileArray(libFiles).map(
({fileName, content}) => ({fileName: `/node_modules${fileName}`, content})));
}); });
beforeEach(() => { it('should compile', async(() => compile([LIBRARY_USING_APP, libraryFiles, angularFiles])));
host = new MockCompilerHost(
LIBRARY_USING_APP_MODULE, LIBRARY_USING_APP, angularFiles, [libraryFiles]);
aotHost = new MockAotCompilerHost(host);
});
it('should compile',
async(() => compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics)));
// Restore reflector since AoT compiler will update it with a new static reflector
afterEach(() => { reflector.updateCapabilities(new ReflectionCapabilities()); });
}); });
}); });
function expectNoDiagnostics(program: ts.Program) {
function fileInfo(diagnostic: ts.Diagnostic): string {
if (diagnostic.file) {
return `${diagnostic.file.fileName}(${diagnostic.start}): `;
}
return '';
}
function chars(len: number, ch: string): string { return new Array(len).fill(ch).join(''); } const QUICKSTART: MockDirectory = {
function lineNoOf(offset: number, text: string): number {
let result = 1;
for (let i = 0; i < offset; i++) {
if (text[i] == '\n') result++;
}
return result;
}
function lineInfo(diagnostic: ts.Diagnostic): string {
if (diagnostic.file) {
const start = diagnostic.start;
let end = diagnostic.start + diagnostic.length;
const source = diagnostic.file.text;
let lineStart = start;
let lineEnd = end;
while (lineStart > 0 && source[lineStart] != '\n') lineStart--;
if (lineStart < start) lineStart++;
while (lineEnd < source.length && source[lineEnd] != '\n') lineEnd++;
let line = source.substring(lineStart, lineEnd);
const lineIndex = line.indexOf('/n');
if (lineIndex > 0) {
line = line.substr(0, lineIndex);
end = start + lineIndex;
}
const lineNo = lineNoOf(start, source) + ': ';
return '\n' + lineNo + line + '\n' + chars(start - lineStart + lineNo.length, ' ') +
chars(end - start, '^');
}
return '';
}
function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) {
if (diagnostics && diagnostics.length) {
throw new Error(
'Errors from TypeScript:\n' +
diagnostics.map(d => `${fileInfo(d)}${d.messageText}${lineInfo(d)}`).join(' \n'));
}
}
expectNoDiagnostics(program.getOptionsDiagnostics());
expectNoDiagnostics(program.getSyntacticDiagnostics());
expectNoDiagnostics(program.getSemanticDiagnostics());
}
function expectNoDiagnosticsAndEmit(program: ts.Program) {
expectNoDiagnostics(program);
program.emit();
}
function isDTS(fileName: string): boolean {
return /\.d\.ts$/.test(fileName);
}
function isSource(fileName: string): boolean {
return /\.ts$/.test(fileName);
}
function isFactory(fileName: string): boolean {
return /\.ngfactory\./.test(fileName);
}
function summaryCompile(
host: MockCompilerHost, aotHost: MockAotCompilerHost,
preCompile?: (program: ts.Program) => void) {
// First compile the program to generate the summary files.
return compile(host, aotHost).then(generatedFiles => {
// Remove generated files that were not generated from a DTS file
host.remove(generatedFiles.filter(f => !isDTS(f.srcFileUrl)).map(f => f.genFileUrl));
// Next compile the program shrowding metadata and only treating .ts files as source.
aotHost.hideMetadata();
aotHost.tsFilesOnly();
return compile(host, aotHost);
});
}
function compile(
host: MockCompilerHost, aotHost: AotCompilerHost, preCompile?: (program: ts.Program) => void,
postCompile: (program: ts.Program) => void = expectNoDiagnostics,
options: AotCompilerOptions = {}): Promise<GeneratedFile[]> {
const scripts = host.scriptNames.slice(0);
const program = ts.createProgram(scripts, settings, host);
if (preCompile) preCompile(program);
const {compiler, reflector} = createAotCompiler(aotHost, options);
return compiler.compileAll(program.getSourceFiles().map(sf => sf.fileName))
.then(generatedFiles => {
generatedFiles.forEach(
file => isSource(file.genFileUrl) ? host.addScript(file.genFileUrl, file.source) :
host.override(file.genFileUrl, file.source));
const scripts = host.scriptNames.slice(0);
const newProgram = ts.createProgram(scripts, settings, host);
if (postCompile) postCompile(newProgram);
return generatedFiles;
});
}
const QUICKSTART = ['/quickstart/app/app.module.ts'];
const FILES: MockData = {
quickstart: { quickstart: {
app: { app: {
'app.component.ts': ` 'app.component.ts': `
@ -891,7 +670,7 @@ const FILES: MockData = {
} }
}; };
const LIBRARY: MockData = { const LIBRARY: MockDirectory = {
bolder: { bolder: {
'public-api.ts': ` 'public-api.ts': `
export * from './src/bolder.component'; export * from './src/bolder.component';
@ -927,7 +706,7 @@ const LIBRARY: MockData = {
}; };
const LIBRARY_USING_APP_MODULE = ['/lib-user/app/app.module.ts']; const LIBRARY_USING_APP_MODULE = ['/lib-user/app/app.module.ts'];
const LIBRARY_USING_APP: MockData = { const LIBRARY_USING_APP: MockDirectory = {
'lib-user': { 'lib-user': {
app: { app: {
'app.component.ts': ` 'app.component.ts': `

View File

@ -6,19 +6,26 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AotCompilerHost} from '@angular/compiler'; import {AotCompilerHost, AotCompilerOptions, GeneratedFile, createAotCompiler} from '@angular/compiler';
import {ɵReflectionCapabilities as ReflectionCapabilities, ɵreflector as reflector} from '@angular/core';
import {MetadataBundlerHost, MetadataCollector, ModuleMetadata} from '@angular/tsc-wrapped'; import {MetadataBundlerHost, MetadataCollector, ModuleMetadata} from '@angular/tsc-wrapped';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as ts from 'typescript'; import * as ts from 'typescript';
export type MockData = string | MockDirectory; let nodeModulesPath: string;
let angularSourcePath: string;
let rootPath: string;
calcPathsOnDisc();
export type MockFileOrDirectory = string | MockDirectory;
export type MockDirectory = { export type MockDirectory = {
[name: string]: MockData | undefined; [name: string]: MockFileOrDirectory | undefined;
}; };
export function isDirectory(data: MockData | undefined): data is MockDirectory { export function isDirectory(data: MockFileOrDirectory | undefined): data is MockDirectory {
return typeof data !== 'string'; return typeof data !== 'string';
} }
@ -43,12 +50,21 @@ export const settings: ts.CompilerOptions = {
export interface EmitterOptions { export interface EmitterOptions {
emitMetadata: boolean; emitMetadata: boolean;
mockData?: MockData; mockData?: MockDirectory;
} }
function calcPathsOnDisc() {
const moduleFilename = module.filename.replace(/\\/g, '/');
const distIndex = moduleFilename.indexOf('/dist/all');
if (distIndex >= 0) {
rootPath = moduleFilename.substr(0, distIndex);
nodeModulesPath = path.join(rootPath, 'node_modules');
angularSourcePath = path.join(rootPath, 'packages');
}
}
export class EmittingCompilerHost implements ts.CompilerHost { export class EmittingCompilerHost implements ts.CompilerHost {
private angularSourcePath: string|undefined;
private nodeModulesPath: string|undefined;
private addedFiles = new Map<string, string>(); private addedFiles = new Map<string, string>();
private writtenFiles = new Map<string, string>(); private writtenFiles = new Map<string, string>();
private scriptNames: string[]; private scriptNames: string[];
@ -56,19 +72,18 @@ export class EmittingCompilerHost implements ts.CompilerHost {
private collector = new MetadataCollector(); private collector = new MetadataCollector();
constructor(scriptNames: string[], private options: EmitterOptions) { constructor(scriptNames: string[], private options: EmitterOptions) {
const moduleFilename = module.filename.replace(/\\/g, '/'); // Rewrite references to scripts with '@angular' to its corresponding location in
const distIndex = moduleFilename.indexOf('/dist/all'); // the source tree.
if (distIndex >= 0) { this.scriptNames = scriptNames.map(f => this.effectiveName(f));
const root = moduleFilename.substr(0, distIndex); this.root = rootPath;
this.nodeModulesPath = path.join(root, 'node_modules'); }
this.angularSourcePath = path.join(root, 'packages');
// Rewrite references to scripts with '@angular' to its corresponding location in public writtenAngularFiles(target = new Map<string, string>()): Map<string, string> {
// the source tree. this.written.forEach((value, key) => {
this.scriptNames = scriptNames.map(f => this.effectiveName(f)); const path = `/node_modules/@angular${key.substring(angularSourcePath.length)}`;
target.set(path, value);
this.root = root; });
} return target;
} }
public addScript(fileName: string, content: string) { public addScript(fileName: string, content: string) {
@ -97,7 +112,7 @@ export class EmittingCompilerHost implements ts.CompilerHost {
public effectiveName(fileName: string): string { public effectiveName(fileName: string): string {
const prefix = '@angular/'; const prefix = '@angular/';
return fileName.startsWith('@angular/') ? return fileName.startsWith('@angular/') ?
path.join(this.angularSourcePath, fileName.substr(prefix.length)) : path.join(angularSourcePath, fileName.substr(prefix.length)) :
fileName; fileName;
} }
@ -171,31 +186,17 @@ export class EmittingCompilerHost implements ts.CompilerHost {
getNewLine(): string { return '\n'; } getNewLine(): string { return '\n'; }
} }
const MOCK_NODEMODULES_PREFIX = '/node_modules/';
export class MockCompilerHost implements ts.CompilerHost { export class MockCompilerHost implements ts.CompilerHost {
scriptNames: string[]; scriptNames: string[];
private angularSourcePath: string|undefined;
private nodeModulesPath: string|undefined;
public overrides = new Map<string, string>(); public overrides = new Map<string, string>();
public writtenFiles = new Map<string, string>(); public writtenFiles = new Map<string, string>();
private sourceFiles = new Map<string, ts.SourceFile>(); private sourceFiles = new Map<string, ts.SourceFile>();
private assumeExists = new Set<string>(); private assumeExists = new Set<string>();
private traces: string[] = []; private traces: string[] = [];
constructor( constructor(scriptNames: string[], private data: MockDirectory) {
scriptNames: string[], private data: MockData, private angular: Map<string, string>,
private libraries?: Map<string, string>[]) {
this.scriptNames = scriptNames.slice(0); this.scriptNames = scriptNames.slice(0);
const moduleFilename = module.filename.replace(/\\/g, '/');
let angularIndex = moduleFilename.indexOf('@angular');
let distIndex = moduleFilename.indexOf('/dist/all');
if (distIndex >= 0) {
const root = moduleFilename.substr(0, distIndex);
this.nodeModulesPath = path.join(root, 'node_modules');
this.angularSourcePath = path.join(root, 'packages');
}
} }
// Test API // Test API
@ -234,22 +235,13 @@ export class MockCompilerHost implements ts.CompilerHost {
const effectiveName = this.getEffectiveName(fileName); const effectiveName = this.getEffectiveName(fileName);
if (effectiveName == fileName) { if (effectiveName == fileName) {
let result = open(fileName, this.data) != null; let result = open(fileName, this.data) != null;
if (!result && fileName.startsWith(MOCK_NODEMODULES_PREFIX)) {
const libraryPath = fileName.substr(MOCK_NODEMODULES_PREFIX.length - 1);
for (const library of this.libraries !) {
if (library.has(libraryPath)) {
return true;
}
}
}
return result; return result;
} else { } else {
if (fileName.match(rxjs)) { if (fileName.match(rxjs)) {
let result = fs.existsSync(effectiveName); let result = fs.existsSync(effectiveName);
return result; return result;
} }
const result = this.angular.has(effectiveName); return false;
return result;
} }
} }
@ -315,12 +307,6 @@ export class MockCompilerHost implements ts.CompilerHost {
let effectiveName = this.getEffectiveName(fileName); let effectiveName = this.getEffectiveName(fileName);
if (effectiveName === fileName) { if (effectiveName === fileName) {
const result = open(fileName, this.data); const result = open(fileName, this.data);
if (!result && fileName.startsWith(MOCK_NODEMODULES_PREFIX)) {
const libraryPath = fileName.substr(MOCK_NODEMODULES_PREFIX.length - 1);
for (const library of this.libraries !) {
if (library.has(libraryPath)) return library.get(libraryPath);
}
}
return result; return result;
} else { } else {
if (fileName.match(rxjs)) { if (fileName.match(rxjs)) {
@ -328,22 +314,16 @@ export class MockCompilerHost implements ts.CompilerHost {
return fs.readFileSync(fileName, 'utf8'); return fs.readFileSync(fileName, 'utf8');
} }
} }
return this.angular.get(effectiveName);
} }
} }
} }
private getEffectiveName(name: string): string { private getEffectiveName(name: string): string {
const node_modules = 'node_modules'; const node_modules = 'node_modules';
const at_angular = '/@angular';
const rxjs = '/rxjs'; const rxjs = '/rxjs';
if (name.startsWith('/' + node_modules)) { if (name.startsWith('/' + node_modules)) {
if (this.angularSourcePath && name.startsWith('/' + node_modules + at_angular)) { if (nodeModulesPath && name.startsWith('/' + node_modules + rxjs)) {
return path.join( return path.join(nodeModulesPath, name.substr(node_modules.length + 1));
this.angularSourcePath, name.substr(node_modules.length + at_angular.length + 1));
}
if (this.nodeModulesPath && name.startsWith('/' + node_modules + rxjs)) {
return path.join(this.nodeModulesPath, name.substr(node_modules.length + 1));
} }
} }
return name; return name;
@ -439,11 +419,12 @@ export class MockMetadataBundlerHost implements MetadataBundlerHost {
} }
} }
function find(fileName: string, data: MockData | undefined): MockData|undefined { function find(fileName: string, data: MockFileOrDirectory | undefined): MockFileOrDirectory|
undefined {
if (!data) return undefined; if (!data) return undefined;
let names = fileName.split('/'); let names = fileName.split('/');
if (names.length && !names[0].length) names.shift(); if (names.length && !names[0].length) names.shift();
let current: MockData|undefined = data; let current: MockFileOrDirectory|undefined = data;
for (let name of names) { for (let name of names) {
if (typeof current === 'string') if (typeof current === 'string')
return undefined; return undefined;
@ -454,7 +435,7 @@ function find(fileName: string, data: MockData | undefined): MockData|undefined
return current; return current;
} }
function open(fileName: string, data: MockData | undefined): string|undefined { function open(fileName: string, data: MockFileOrDirectory | undefined): string|undefined {
let result = find(fileName, data); let result = find(fileName, data);
if (typeof result === 'string') { if (typeof result === 'string') {
return result; return result;
@ -462,7 +443,204 @@ function open(fileName: string, data: MockData | undefined): string|undefined {
return undefined; return undefined;
} }
function directoryExists(dirname: string, data: MockData | undefined): boolean { function directoryExists(dirname: string, data: MockFileOrDirectory | undefined): boolean {
let result = find(dirname, data); let result = find(dirname, data);
return !!result && typeof result !== 'string'; return !!result && typeof result !== 'string';
} }
export type MockFileArray = {
fileName: string,
content: string
}[];
export type MockData = MockDirectory | Map<string, string>| (MockDirectory | Map<string, string>)[];
export function toMockFileArray(data: MockData, target: MockFileArray = []): MockFileArray {
if (data instanceof Map) {
mapToMockFileArray(data, target);
} else if (Array.isArray(data)) {
data.forEach(entry => toMockFileArray(entry, target));
} else {
mockDirToFileArray(data, '', target);
}
return target;
}
function mockDirToFileArray(dir: MockDirectory, path: string, target: MockFileArray) {
Object.keys(dir).forEach((localFileName) => {
const value = dir[localFileName] !;
const fileName = `${path}/${localFileName}`;
if (typeof value === 'string') {
target.push({fileName, content: value});
} else {
mockDirToFileArray(value, fileName, target);
}
});
}
function mapToMockFileArray(files: Map<string, string>, target: MockFileArray) {
files.forEach((content, fileName) => { target.push({fileName, content}); });
}
export function arrayToMockMap(arr: MockFileArray): Map<string, string> {
const map = new Map<string, string>();
arr.forEach(({fileName, content}) => { map.set(fileName, content); });
return map;
}
export function arrayToMockDir(arr: MockFileArray): MockDirectory {
const rootDir: MockDirectory = {};
arr.forEach(({fileName, content}) => {
let pathParts = fileName.split('/');
// trim trailing slash
let startIndex = pathParts[0] ? 0 : 1;
// get/create the directory
let currentDir = rootDir;
for (let i = startIndex; i < pathParts.length - 1; i++) {
const pathPart = pathParts[i];
let localDir = <MockDirectory>currentDir[pathPart];
if (!localDir) {
currentDir[pathPart] = localDir = {};
}
currentDir = localDir;
}
// write the file
currentDir[pathParts[pathParts.length - 1]] = content;
});
return rootDir;
}
const minCoreIndex = `
export * from './src/application_module';
export * from './src/change_detection';
export * from './src/metadata';
export * from './src/di/metadata';
export * from './src/di/injector';
export * from './src/di/injection_token';
export * from './src/linker';
export * from './src/render';
export * from './src/codegen_private_exports';
`;
export function setup(options: {compileAngular: boolean} = {
compileAngular: true
}) {
let angularFiles = new Map<string, string>();
beforeAll(() => {
if (options.compileAngular) {
const emittingHost = new EmittingCompilerHost([], {emitMetadata: true});
emittingHost.addScript('@angular/core/index.ts', minCoreIndex);
const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost);
emittingProgram.emit();
emittingHost.writtenAngularFiles(angularFiles);
}
});
// Restore reflector since AoT compiler will update it with a new static reflector
afterEach(() => { reflector.updateCapabilities(new ReflectionCapabilities()); });
return angularFiles;
}
export function expectNoDiagnostics(program: ts.Program) {
function fileInfo(diagnostic: ts.Diagnostic): string {
if (diagnostic.file) {
return `${diagnostic.file.fileName}(${diagnostic.start}): `;
}
return '';
}
function chars(len: number, ch: string): string { return new Array(len).fill(ch).join(''); }
function lineNoOf(offset: number, text: string): number {
let result = 1;
for (let i = 0; i < offset; i++) {
if (text[i] == '\n') result++;
}
return result;
}
function lineInfo(diagnostic: ts.Diagnostic): string {
if (diagnostic.file) {
const start = diagnostic.start;
let end = diagnostic.start + diagnostic.length;
const source = diagnostic.file.text;
let lineStart = start;
let lineEnd = end;
while (lineStart > 0 && source[lineStart] != '\n') lineStart--;
if (lineStart < start) lineStart++;
while (lineEnd < source.length && source[lineEnd] != '\n') lineEnd++;
let line = source.substring(lineStart, lineEnd);
const lineIndex = line.indexOf('/n');
if (lineIndex > 0) {
line = line.substr(0, lineIndex);
end = start + lineIndex;
}
const lineNo = lineNoOf(start, source) + ': ';
return '\n' + lineNo + line + '\n' + chars(start - lineStart + lineNo.length, ' ') +
chars(end - start, '^');
}
return '';
}
function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) {
if (diagnostics && diagnostics.length) {
throw new Error(
'Errors from TypeScript:\n' +
diagnostics.map(d => `${fileInfo(d)}${d.messageText}${lineInfo(d)}`).join(' \n'));
}
}
expectNoDiagnostics(program.getOptionsDiagnostics());
expectNoDiagnostics(program.getSyntacticDiagnostics());
expectNoDiagnostics(program.getSemanticDiagnostics());
}
function isSource(fileName: string): boolean {
return !/\.d\.ts$/.test(fileName) && /\.ts$/.test(fileName);
}
export function compile(rootDirs: MockData, options: {
emit?: boolean,
useSummaries?: boolean,
preCompile?: (program: ts.Program) => void,
postCompile?: (program: ts.Program) => void,
}& AotCompilerOptions = {}): Promise<{genFiles: GeneratedFile[], outDir: MockDirectory}> {
// Make sure we always return errors via the promise...
return Promise.resolve(null).then(() => {
// when using summaries, always emit so the next step can use the results.
const emit = options.emit || options.useSummaries;
const preCompile = options.preCompile || expectNoDiagnostics;
const postCompile = options.postCompile || expectNoDiagnostics;
const rootDirArr = toMockFileArray(rootDirs);
const scriptNames = rootDirArr.map(entry => entry.fileName).filter(isSource);
const host = new MockCompilerHost(scriptNames, arrayToMockDir(rootDirArr));
const aotHost = new MockAotCompilerHost(host);
if (options.useSummaries) {
aotHost.hideMetadata();
aotHost.tsFilesOnly();
}
const scripts = host.scriptNames.slice(0);
const program = ts.createProgram(scripts, settings, host);
if (preCompile) preCompile(program);
const {compiler, reflector} = createAotCompiler(aotHost, options);
return compiler.compileAll(program.getSourceFiles().map(sf => sf.fileName)).then(genFiles => {
genFiles.forEach(
file => isSource(file.genFileUrl) ? host.addScript(file.genFileUrl, file.source) :
host.override(file.genFileUrl, file.source));
const scripts = host.scriptNames.slice(0);
const newProgram = ts.createProgram(scripts, settings, host);
if (postCompile) postCompile(newProgram);
if (emit) {
newProgram.emit();
}
let outDir: MockDirectory = {};
if (emit) {
outDir = arrayToMockDir(toMockFileArray([
host.writtenFiles, host.overrides
]).filter((entry) => !isSource(entry.fileName)));
}
return {genFiles, outDir};
});
});
}

View File

@ -162,8 +162,8 @@ export function getPlatform(): PlatformRef|null {
* has exactly one platform, and services (such as reflection) which are common * has exactly one platform, and services (such as reflection) which are common
* to every Angular application running on the page are bound in its scope. * to every Angular application running on the page are bound in its scope.
* *
* A page's platform is initialized implicitly when {@link bootstrap}() is called, or * A page's platform is initialized implicitly when a platform is created via a platform factory
* explicitly by calling {@link createPlatform}(). * (e.g. {@link platformBrowser}), or explicitly by calling the {@link createPlatform} function.
* *
* @stable * @stable
*/ */
@ -344,8 +344,6 @@ export class PlatformRef_ extends PlatformRef {
/** /**
* A reference to an Angular application running on a page. * A reference to an Angular application running on a page.
* *
* For more about Angular applications, see the documentation for {@link bootstrap}.
*
* @stable * @stable
*/ */
export abstract class ApplicationRef { export abstract class ApplicationRef {
@ -553,6 +551,9 @@ export class ApplicationRef_ extends ApplicationRef {
if (this._enforceNoNewChanges) { if (this._enforceNoNewChanges) {
this._views.forEach((view) => view.checkNoChanges()); this._views.forEach((view) => view.checkNoChanges());
} }
} catch (e) {
// Attention: Don't rethrow as it could cancel subscriptions to Observables!
this._exceptionHandler.handleError(e);
} finally { } finally {
this._runningTick = false; this._runningTick = false;
wtfLeave(scope); wtfLeave(scope);

View File

@ -50,7 +50,7 @@ export abstract class Injector {
/** /**
* Retrieves an instance from the injector based on the provided token. * Retrieves an instance from the injector based on the provided token.
* If not found: * If not found:
* - Throws {@link NoProviderError} if no `notFoundValue` that is not equal to * - Throws an error if no `notFoundValue` that is not equal to
* Injector.THROW_IF_NOT_FOUND is given * Injector.THROW_IF_NOT_FOUND is given
* - Returns the `notFoundValue` otherwise * - Returns the `notFoundValue` otherwise
*/ */

View File

@ -54,14 +54,14 @@ export class Compiler {
compileModuleAsync<T>(moduleType: Type<T>): Promise<NgModuleFactory<T>> { throw _throwError(); } compileModuleAsync<T>(moduleType: Type<T>): Promise<NgModuleFactory<T>> { throw _throwError(); }
/** /**
* Same as {@link compileModuleSync} but also creates ComponentFactories for all components. * Same as {@link #compileModuleSync} but also creates ComponentFactories for all components.
*/ */
compileModuleAndAllComponentsSync<T>(moduleType: Type<T>): ModuleWithComponentFactories<T> { compileModuleAndAllComponentsSync<T>(moduleType: Type<T>): ModuleWithComponentFactories<T> {
throw _throwError(); throw _throwError();
} }
/** /**
* Same as {@link compileModuleAsync} but also creates ComponentFactories for all components. * Same as {@link #compileModuleAsync} but also creates ComponentFactories for all components.
*/ */
compileModuleAndAllComponentsAsync<T>(moduleType: Type<T>): compileModuleAndAllComponentsAsync<T>(moduleType: Type<T>):
Promise<ModuleWithComponentFactories<T>> { Promise<ModuleWithComponentFactories<T>> {

View File

@ -16,7 +16,8 @@ import {getSymbolIterator} from '../util';
* An unmodifiable list of items that Angular keeps up to date when the state * An unmodifiable list of items that Angular keeps up to date when the state
* of the application changes. * of the application changes.
* *
* The type of object that {@link Query} and {@link ViewQueryMetadata} provide. * The type of object that {@link ViewChildren}, {@link ContentChildren}, and {@link QueryList}
* provide.
* *
* Implements an iterable interface, therefore it can be used in both ES6 * Implements an iterable interface, therefore it can be used in both ES6
* javascript `for (var i of items)` loops as well as in Angular templates with * javascript `for (var i of items)` loops as well as in Angular templates with

View File

@ -287,7 +287,7 @@ export const ContentChild: ContentChildDecorator = makePropDecorator(
/** /**
* Type of the ViewChildren decorator / constructor function. * Type of the ViewChildren decorator / constructor function.
* *
* See {@ViewChildren}. * See {@link ViewChildren}.
* *
* @stable * @stable
*/ */

View File

@ -662,8 +662,8 @@ export interface Component extends Directive {
* encapsulation. * encapsulation.
* *
* When no `encapsulation` is defined for the component, the default value from the * When no `encapsulation` is defined for the component, the default value from the
* {@link CompilerConfig} is used. The default is `ViewEncapsulation.Emulated`}. Provide a new * {@link CompilerOptions} is used. The default is `ViewEncapsulation.Emulated`}. Provide a new
* `CompilerConfig` to override this value. * `CompilerOptions` to override this value.
* *
* If the encapsulation is set to `ViewEncapsulation.Emulated` and the component has no `styles` * If the encapsulation is set to `ViewEncapsulation.Emulated` and the component has no `styles`
* nor `styleUrls` the encapsulation will automatically be switched to `ViewEncapsulation.None`. * nor `styleUrls` the encapsulation will automatically be switched to `ViewEncapsulation.None`.

View File

@ -9,14 +9,14 @@
/** /**
* Defines template and style encapsulation options available for Component's {@link Component}. * Defines template and style encapsulation options available for Component's {@link Component}.
* *
* See {@link ViewMetadata#encapsulation}. * See {@link Component#encapsulation}.
* @stable * @stable
*/ */
export enum ViewEncapsulation { export enum ViewEncapsulation {
/** /**
* Emulate `Native` scoping of styles by adding an attribute containing surrogate id to the Host * Emulate `Native` scoping of styles by adding an attribute containing surrogate id to the Host
* Element and pre-processing the style rules provided via * Element and pre-processing the style rules provided via
* {@link ViewMetadata#styles} or {@link ViewMetadata#stylesUrls}, and adding the new Host Element * {@link Component#styles} or {@link Component#styleUrls}, and adding the new Host Element
* attribute to all selectors. * attribute to all selectors.
* *
* This is the default option. * This is the default option.

View File

@ -189,7 +189,14 @@ export function listenToElementOutputs(view: ViewData, compView: ViewData, def:
} }
function renderEventHandlerClosure(view: ViewData, index: number, eventName: string) { function renderEventHandlerClosure(view: ViewData, index: number, eventName: string) {
return (event: any) => dispatchEvent(view, index, eventName, event); return (event: any) => {
try {
return dispatchEvent(view, index, eventName, event);
} catch (e) {
// Attention: Don't rethrow, to keep in sync with directive events.
view.root.errorHandler.handleError(e);
}
}
} }

View File

@ -148,7 +148,14 @@ export function createDirectiveInstance(view: ViewData, def: NodeDef): any {
} }
function eventHandlerClosure(view: ViewData, index: number, eventName: string) { function eventHandlerClosure(view: ViewData, index: number, eventName: string) {
return (event: any) => dispatchEvent(view, index, eventName, event); return (event: any) => {
try {
return dispatchEvent(view, index, eventName, event);
} catch (e) {
// Attention: Don't rethrow, as it would cancel Observable subscriptions!
view.root.errorHandler.handleError(e);
}
}
} }
export function checkAndUpdateDirectiveInline( export function checkAndUpdateDirectiveInline(
@ -355,6 +362,12 @@ export function resolveDep(
} }
const tokenKey = depDef.tokenKey; const tokenKey = depDef.tokenKey;
if (tokenKey === ChangeDetectorRefTokenKey) {
// directives on the same element as a component should be able to control the change detector
// of that component as well.
allowPrivateServices = !!(elDef && elDef.element !.componentView);
}
if (elDef && (depDef.flags & DepFlags.SkipSelf)) { if (elDef && (depDef.flags & DepFlags.SkipSelf)) {
allowPrivateServices = false; allowPrivateServices = false;
elDef = elDef.parent !; elDef = elDef.parent !;

View File

@ -90,7 +90,9 @@ class ComponentFactory_ extends ComponentFactory<any> {
const view = Services.createRootView( const view = Services.createRootView(
injector, projectableNodes || [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT); injector, projectableNodes || [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT);
const component = asProviderData(view, componentNodeIndex).instance; const component = asProviderData(view, componentNodeIndex).instance;
view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full); if (rootSelectorOrNode) {
view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full);
}
return new ComponentRef_(view, new ViewRef_(view), component); return new ComponentRef_(view, new ViewRef_(view), component);
} }
@ -236,11 +238,11 @@ export class ViewRef_ implements EmbeddedViewRef<any>, InternalViewRef {
get destroyed(): boolean { return (this._view.state & ViewState.Destroyed) !== 0; } get destroyed(): boolean { return (this._view.state & ViewState.Destroyed) !== 0; }
markForCheck(): void { markParentViewsForCheck(this._view); } markForCheck(): void { markParentViewsForCheck(this._view); }
detach(): void { this._view.state &= ~ViewState.ChecksEnabled; } detach(): void { this._view.state &= ~ViewState.Attached; }
detectChanges(): void { Services.checkAndUpdateView(this._view); } detectChanges(): void { Services.checkAndUpdateView(this._view); }
checkNoChanges(): void { Services.checkNoChangesView(this._view); } checkNoChanges(): void { Services.checkNoChangesView(this._view); }
reattach(): void { this._view.state |= ViewState.ChecksEnabled; } reattach(): void { this._view.state |= ViewState.Attached; }
onDestroy(callback: Function) { onDestroy(callback: Function) {
if (!this._view.disposables) { if (!this._view.disposables) {
this._view.disposables = []; this._view.disposables = [];

View File

@ -9,6 +9,7 @@
import {isDevMode} from '../application_ref'; import {isDevMode} from '../application_ref';
import {DebugElement, DebugNode, EventListener, getDebugNode, indexDebugNode, removeDebugNodeFromIndex} from '../debug/debug_node'; import {DebugElement, DebugNode, EventListener, getDebugNode, indexDebugNode, removeDebugNodeFromIndex} from '../debug/debug_node';
import {Injector} from '../di'; import {Injector} from '../di';
import {ErrorHandler} from '../error_handler';
import {NgModuleRef} from '../linker/ng_module_factory'; import {NgModuleRef} from '../linker/ng_module_factory';
import {Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2} from '../render/api'; import {Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2} from '../render/api';
import {Sanitizer} from '../security'; import {Sanitizer} from '../security';
@ -104,11 +105,12 @@ function createRootData(
elInjector: Injector, ngModule: NgModuleRef<any>, rendererFactory: RendererFactory2, elInjector: Injector, ngModule: NgModuleRef<any>, rendererFactory: RendererFactory2,
projectableNodes: any[][], rootSelectorOrNode: any): RootData { projectableNodes: any[][], rootSelectorOrNode: any): RootData {
const sanitizer = ngModule.injector.get(Sanitizer); const sanitizer = ngModule.injector.get(Sanitizer);
const errorHandler = ngModule.injector.get(ErrorHandler);
const renderer = rendererFactory.createRenderer(null, null); const renderer = rendererFactory.createRenderer(null, null);
return { return {
ngModule, ngModule,
injector: elInjector, projectableNodes, injector: elInjector, projectableNodes,
selectorOrNode: rootSelectorOrNode, sanitizer, rendererFactory, renderer selectorOrNode: rootSelectorOrNode, sanitizer, rendererFactory, renderer, errorHandler
}; };
} }
@ -439,7 +441,6 @@ function callWithDebugContext(action: DebugAction, fn: any, self: any, args: any
if (isViewDebugError(e) || !_currentView) { if (isViewDebugError(e) || !_currentView) {
throw e; throw e;
} }
_currentView.state |= ViewState.Errored;
throw viewWrappedDebugError(e, getCurrentDebugContext() !); throw viewWrappedDebugError(e, getCurrentDebugContext() !);
} }
} }

View File

@ -7,6 +7,7 @@
*/ */
import {Injector} from '../di'; import {Injector} from '../di';
import {ErrorHandler} from '../error_handler';
import {NgModuleRef} from '../linker/ng_module_factory'; import {NgModuleRef} from '../linker/ng_module_factory';
import {QueryList} from '../linker/query_list'; import {QueryList} from '../linker/query_list';
import {TemplateRef} from '../linker/template_ref'; import {TemplateRef} from '../linker/template_ref';
@ -323,10 +324,14 @@ export interface ViewData {
* Bitmask of states * Bitmask of states
*/ */
export const enum ViewState { export const enum ViewState {
FirstCheck = 1 << 0, BeforeFirstCheck = 1 << 0,
ChecksEnabled = 1 << 1, FirstCheck = 1 << 1,
Errored = 1 << 2, Attached = 1 << 2,
Destroyed = 1 << 3 ChecksEnabled = 1 << 3,
Destroyed = 1 << 4,
CatDetectChanges = Attached | ChecksEnabled,
CatInit = BeforeFirstCheck | CatDetectChanges
} }
export interface DisposableFn { (): void; } export interface DisposableFn { (): void; }
@ -429,6 +434,7 @@ export interface RootData {
selectorOrNode: any; selectorOrNode: any;
renderer: Renderer2; renderer: Renderer2;
rendererFactory: RendererFactory2; rendererFactory: RendererFactory2;
errorHandler: ErrorHandler;
sanitizer: Sanitizer; sanitizer: Sanitizer;
} }

View File

@ -100,10 +100,10 @@ export function checkAndUpdateBinding(
export function checkBindingNoChanges( export function checkBindingNoChanges(
view: ViewData, def: NodeDef, bindingIdx: number, value: any) { view: ViewData, def: NodeDef, bindingIdx: number, value: any) {
const oldValue = view.oldValues[def.bindingIndex + bindingIdx]; const oldValue = view.oldValues[def.bindingIndex + bindingIdx];
if ((view.state & ViewState.FirstCheck) || !devModeEqual(oldValue, value)) { if ((view.state & ViewState.BeforeFirstCheck) || !devModeEqual(oldValue, value)) {
throw expressionChangedAfterItHasBeenCheckedError( throw expressionChangedAfterItHasBeenCheckedError(
Services.createDebugContext(view, def.index), oldValue, value, Services.createDebugContext(view, def.index), oldValue, value,
(view.state & ViewState.FirstCheck) !== 0); (view.state & ViewState.BeforeFirstCheck) !== 0);
} }
} }

View File

@ -211,7 +211,7 @@ function createView(
viewContainerParent: null, parentNodeDef, viewContainerParent: null, parentNodeDef,
context: null, context: null,
component: null, nodes, component: null, nodes,
state: ViewState.FirstCheck | ViewState.ChecksEnabled, root, renderer, state: ViewState.CatInit, root, renderer,
oldValues: new Array(def.bindingCount), disposables oldValues: new Array(def.bindingCount), disposables
}; };
return view; return view;
@ -323,6 +323,12 @@ export function checkNoChangesView(view: ViewData) {
} }
export function checkAndUpdateView(view: ViewData) { export function checkAndUpdateView(view: ViewData) {
if (view.state & ViewState.BeforeFirstCheck) {
view.state &= ~ViewState.BeforeFirstCheck;
view.state |= ViewState.FirstCheck;
} else {
view.state &= ~ViewState.FirstCheck;
}
Services.updateDirectives(view, CheckType.CheckAndUpdate); Services.updateDirectives(view, CheckType.CheckAndUpdate);
execEmbeddedViewsAction(view, ViewAction.CheckAndUpdate); execEmbeddedViewsAction(view, ViewAction.CheckAndUpdate);
execQueriesAction( execQueriesAction(
@ -345,7 +351,6 @@ export function checkAndUpdateView(view: ViewData) {
if (view.def.flags & ViewFlags.OnPush) { if (view.def.flags & ViewFlags.OnPush) {
view.state &= ~ViewState.ChecksEnabled; view.state &= ~ViewState.ChecksEnabled;
} }
view.state &= ~ViewState.FirstCheck;
} }
export function checkAndUpdateNode( export function checkAndUpdateNode(
@ -453,7 +458,7 @@ function checkNoChangesQuery(view: ViewData, nodeDef: NodeDef) {
if (queryList.dirty) { if (queryList.dirty) {
throw expressionChangedAfterItHasBeenCheckedError( throw expressionChangedAfterItHasBeenCheckedError(
Services.createDebugContext(view, nodeDef.index), `Query ${nodeDef.query!.id} not dirty`, Services.createDebugContext(view, nodeDef.index), `Query ${nodeDef.query!.id} not dirty`,
`Query ${nodeDef.query!.id} dirty`, (view.state & ViewState.FirstCheck) !== 0); `Query ${nodeDef.query!.id} dirty`, (view.state & ViewState.BeforeFirstCheck) !== 0);
} }
} }
@ -542,14 +547,14 @@ function callViewAction(view: ViewData, action: ViewAction) {
const viewState = view.state; const viewState = view.state;
switch (action) { switch (action) {
case ViewAction.CheckNoChanges: case ViewAction.CheckNoChanges:
if ((viewState & ViewState.ChecksEnabled) && if ((viewState & ViewState.CatDetectChanges) === ViewState.CatDetectChanges &&
(viewState & (ViewState.Errored | ViewState.Destroyed)) === 0) { (viewState & ViewState.Destroyed) === 0) {
checkNoChangesView(view); checkNoChangesView(view);
} }
break; break;
case ViewAction.CheckAndUpdate: case ViewAction.CheckAndUpdate:
if ((viewState & ViewState.ChecksEnabled) && if ((viewState & ViewState.CatDetectChanges) === ViewState.CatDetectChanges &&
(viewState & (ViewState.Errored | ViewState.Destroyed)) === 0) { (viewState & ViewState.Destroyed) === 0) {
checkAndUpdateView(view); checkAndUpdateView(view);
} }
break; break;

View File

@ -13,8 +13,8 @@ import {EventEmitter} from '../event_emitter';
* *
* The most common use of this service is to optimize performance when starting a work consisting of * The most common use of this service is to optimize performance when starting a work consisting of
* one or more asynchronous tasks that don't require UI updates or error handling to be handled by * one or more asynchronous tasks that don't require UI updates or error handling to be handled by
* Angular. Such tasks can be kicked off via {@link runOutsideAngular} and if needed, these tasks * Angular. Such tasks can be kicked off via {@link #runOutsideAngular} and if needed, these tasks
* can reenter the Angular zone via {@link run}. * can reenter the Angular zone via {@link #run}.
* *
* <!-- TODO: add/fix links to: * <!-- TODO: add/fix links to:
* - docs explaining zones and the use of zones in Angular and change-detection * - docs explaining zones and the use of zones in Angular and change-detection
@ -132,7 +132,7 @@ export class NgZone {
* the function. * the function.
* *
* Running functions via `run` allows you to reenter Angular zone from a task that was executed * Running functions via `run` allows you to reenter Angular zone from a task that was executed
* outside of the Angular zone (typically started via {@link runOutsideAngular}). * outside of the Angular zone (typically started via {@link #runOutsideAngular}).
* *
* Any future tasks or microtasks scheduled from within this function will continue executing from * Any future tasks or microtasks scheduled from within this function will continue executing from
* within the Angular zone. * within the Angular zone.
@ -151,13 +151,14 @@ export class NgZone {
* Executes the `fn` function synchronously in Angular's parent zone and returns value returned by * Executes the `fn` function synchronously in Angular's parent zone and returns value returned by
* the function. * the function.
* *
* Running functions via `runOutsideAngular` allows you to escape Angular's zone and do work that * Running functions via {@link #runOutsideAngular} allows you to escape Angular's zone and do
* work that
* doesn't trigger Angular change-detection or is subject to Angular's error handling. * doesn't trigger Angular change-detection or is subject to Angular's error handling.
* *
* Any future tasks or microtasks scheduled from within this function will continue executing from * Any future tasks or microtasks scheduled from within this function will continue executing from
* outside of the Angular zone. * outside of the Angular zone.
* *
* Use {@link run} to reenter the Angular zone and do work that updates the application model. * Use {@link #run} to reenter the Angular zone and do work that updates the application model.
*/ */
runOutsideAngular(fn: () => any): any { return this.outer.run(fn); } runOutsideAngular(fn: () => any): any { return this.outer.run(fn); }

View File

@ -103,20 +103,33 @@ export function main() {
describe('ApplicationRef', () => { describe('ApplicationRef', () => {
beforeEach(() => { TestBed.configureTestingModule({imports: [createModule()]}); }); beforeEach(() => { TestBed.configureTestingModule({imports: [createModule()]}); });
it('should throw when reentering tick', inject([ApplicationRef], (ref: ApplicationRef_) => { it('should throw when reentering tick', () => {
const view = jasmine.createSpyObj('view', ['detach', 'attachToAppRef']); @Component({template: '{{reenter()}}'})
const viewRef = jasmine.createSpyObj( class ReenteringComponent {
'viewRef', ['detectChanges', 'detachFromAppRef', 'attachToAppRef']); reenterCount = 1;
viewRef.internalView = view; reenterErr: any;
view.ref = viewRef;
try { constructor(private appRef: ApplicationRef) {}
ref.attachView(viewRef);
viewRef.detectChanges.and.callFake(() => ref.tick()); reenter() {
expect(() => ref.tick()).toThrowError('ApplicationRef.tick is called recursively'); if (this.reenterCount--) {
} finally { try {
ref.detachView(viewRef); this.appRef.tick();
} } catch (e) {
})); this.reenterErr = e;
}
}
}
}
const fixture = TestBed.configureTestingModule({declarations: [ReenteringComponent]})
.createComponent(ReenteringComponent);
const appRef = TestBed.get(ApplicationRef) as ApplicationRef;
appRef.attachView(fixture.componentRef.hostView);
appRef.tick();
expect(fixture.componentInstance.reenterErr.message)
.toBe('ApplicationRef.tick is called recursively');
});
describe('APP_BOOTSTRAP_LISTENER', () => { describe('APP_BOOTSTRAP_LISTENER', () => {
let capturedCompRefs: ComponentRef<any>[]; let capturedCompRefs: ComponentRef<any>[];

View File

@ -765,6 +765,7 @@ export function main() {
try { try {
ctx.detectChanges(false); ctx.detectChanges(false);
} catch (e) { } catch (e) {
expect(e.message).toBe('Boom!');
errored = true; errored = true;
} }
expect(errored).toBe(true); expect(errored).toBe(true);
@ -776,7 +777,8 @@ export function main() {
try { try {
ctx.detectChanges(false); ctx.detectChanges(false);
} catch (e) { } catch (e) {
throw new Error('Second detectChanges() should not have run detection.'); expect(e.message).toBe('Boom!');
throw new Error('Second detectChanges() should not have called ngOnInit.');
} }
expect(directiveLog.filter(['ngOnInit'])).toEqual([]); expect(directiveLog.filter(['ngOnInit'])).toEqual([]);
})); }));
@ -1175,6 +1177,21 @@ export function main() {
expect(renderLog.log).toEqual([]); expect(renderLog.log).toEqual([]);
})); }));
it('Detached should disable OnPush', fakeAsync(() => {
const ctx = createCompFixture('<push-cmp [value]="value"></push-cmp>');
ctx.componentInstance.value = 0;
ctx.detectChanges();
renderLog.clear();
const cmp: CompWithRef = queryDirs(ctx.debugElement, PushComp)[0];
cmp.changeDetectorRef.detach();
ctx.componentInstance.value = 1;
ctx.detectChanges();
expect(renderLog.log).toEqual([]);
}));
it('Detached view can be checked locally', fakeAsync(() => { it('Detached view can be checked locally', fakeAsync(() => {
const ctx = createCompFixture('<wrap-comp-with-ref></wrap-comp-with-ref>'); const ctx = createCompFixture('<wrap-comp-with-ref></wrap-comp-with-ref>');
const cmp: CompWithRef = queryDirs(ctx.debugElement, CompWithRef)[0]; const cmp: CompWithRef = queryDirs(ctx.debugElement, CompWithRef)[0];
@ -1225,7 +1242,6 @@ export function main() {
ctx.detectChanges(); ctx.detectChanges();
expect(cmp.renderCount).toBe(count); expect(cmp.renderCount).toBe(count);
})); }));
}); });

View File

@ -7,7 +7,7 @@
*/ */
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {Compiler, ComponentFactory, EventEmitter, Host, Inject, Injectable, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleRef, OnDestroy, ReflectiveInjector, SkipSelf} from '@angular/core'; import {Compiler, ComponentFactory, ErrorHandler, EventEmitter, Host, Inject, Injectable, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleRef, OnDestroy, ReflectiveInjector, SkipSelf} from '@angular/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, PipeTransform} from '@angular/core/src/change_detection/change_detection'; import {ChangeDetectionStrategy, ChangeDetectorRef, PipeTransform} from '@angular/core/src/change_detection/change_detection';
import {getDebugContext} from '@angular/core/src/errors'; import {getDebugContext} from '@angular/core/src/errors';
import {ComponentFactoryResolver} from '@angular/core/src/linker/component_factory_resolver'; import {ComponentFactoryResolver} from '@angular/core/src/linker/component_factory_resolver';
@ -1480,16 +1480,18 @@ function declareTests({useJit}: {useJit: boolean}) {
const tc = fixture.debugElement.children[0]; const tc = fixture.debugElement.children[0];
try { const errorHandler = tc.injector.get(ErrorHandler);
tc.injector.get(DirectiveEmittingEvent).fireEvent('boom'); let err: any;
} catch (e) { spyOn(errorHandler, 'handleError').and.callFake((e: any) => err = e);
const c = getDebugContext(e); tc.injector.get(DirectiveEmittingEvent).fireEvent('boom');
expect(getDOM().nodeName(c.renderNode).toUpperCase()).toEqual('SPAN');
expect(getDOM().nodeName(c.componentRenderElement).toUpperCase()).toEqual('DIV'); expect(err).toBeTruthy();
expect((<Injector>c.injector).get).toBeTruthy(); const c = getDebugContext(err);
expect(c.context).toBe(fixture.componentInstance); expect(getDOM().nodeName(c.renderNode).toUpperCase()).toEqual('SPAN');
expect(c.references['local']).toBeDefined(); expect(getDOM().nodeName(c.componentRenderElement).toUpperCase()).toEqual('DIV');
} expect((<Injector>c.injector).get).toBeTruthy();
expect(c.context).toBe(fixture.componentInstance);
expect(c.references['local']).toBeDefined();
})); }));
} }
}); });

View File

@ -6,15 +6,19 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ANALYZE_FOR_ENTRY_COMPONENTS, Component, ContentChild, Directive, InjectionToken, Injector, Input, Pipe, PipeTransform, Provider, QueryList, Renderer2, SimpleChanges, TemplateRef, ViewChildren, ViewContainerRef} from '@angular/core'; import {ANALYZE_FOR_ENTRY_COMPONENTS, ApplicationRef, Component, ComponentRef, ContentChild, Directive, ErrorHandler, EventEmitter, HostListener, InjectionToken, Injector, Input, NgModule, NgModuleRef, NgZone, Output, Pipe, PipeTransform, Provider, QueryList, Renderer2, SimpleChanges, TemplateRef, ViewChildren, ViewContainerRef, destroyPlatform} from '@angular/core';
import {TestBed, fakeAsync, tick} from '@angular/core/testing'; import {TestBed, async, fakeAsync, inject, tick} from '@angular/core/testing';
import {By} from '@angular/platform-browser'; import {BrowserModule, By, DOCUMENT} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
export function main() { export function main() {
describe('jit', () => { declareTests({useJit: true}); }); describe('jit', () => { declareTests({useJit: true}); });
describe('no jit', () => { declareTests({useJit: false}); }); describe('no jit', () => { declareTests({useJit: false}); });
declareTestsUsingBootstrap();
} }
function declareTests({useJit}: {useJit: boolean}) { function declareTests({useJit}: {useJit: boolean}) {
@ -365,6 +369,132 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(testDirs[1].tpl).toBeDefined(); expect(testDirs[1].tpl).toBeDefined();
expect(testDirs[2].tpl).toBeDefined(); expect(testDirs[2].tpl).toBeDefined();
}); });
it('should not add ng-version for dynamically created components', () => {
@Component({template: ''})
class App {
}
@NgModule({declarations: [App], entryComponents: [App]})
class MyModule {
}
const modRef = TestBed.configureTestingModule({imports: [MyModule]})
.get(NgModuleRef) as NgModuleRef<MyModule>;
const compRef =
modRef.componentFactoryResolver.resolveComponentFactory(App).create(Injector.NULL);
expect(getDOM().hasAttribute(compRef.location.nativeElement, 'ng-version')).toBe(false);
});
});
}
function declareTestsUsingBootstrap() {
// Place to put reproductions for regressions
describe('regressions using bootstrap', () => {
const COMP_SELECTOR = 'root-comp';
class MockConsole {
errors: any[][] = [];
error(...s: any[]): void { this.errors.push(s); }
}
let logger: MockConsole;
let errorHandler: ErrorHandler;
beforeEach(inject([DOCUMENT], (doc: any) => {
destroyPlatform();
const el = getDOM().createElement(COMP_SELECTOR, doc);
getDOM().appendChild(doc.body, el);
logger = new MockConsole();
errorHandler = new ErrorHandler();
errorHandler._console = logger as any;
}));
afterEach(() => { destroyPlatform(); });
if (getDOM().supportsDOMEvents()) {
// This test needs a real DOM....
it('should keep change detecting if there was an error', (done) => {
@Component({
selector: COMP_SELECTOR,
template:
'<button (click)="next()"></button><button (click)="nextAndThrow()"></button><button (dirClick)="nextAndThrow()"></button><span>Value:{{value}}</span><span>{{throwIfNeeded()}}</span>'
})
class ErrorComp {
value = 0;
thrownValue = 0;
next() { this.value++; }
nextAndThrow() {
this.value++;
this.throwIfNeeded();
}
throwIfNeeded() {
NgZone.assertInAngularZone();
if (this.thrownValue !== this.value) {
this.thrownValue = this.value;
throw new Error(`Error: ${this.value}`);
}
}
}
@Directive({selector: '[dirClick]'})
class EventDir {
@Output()
dirClick = new EventEmitter();
@HostListener('click', ['$event'])
onClick(event: any) { this.dirClick.next(event); }
}
@NgModule({
imports: [BrowserModule],
declarations: [ErrorComp, EventDir],
bootstrap: [ErrorComp],
providers: [{provide: ErrorHandler, useValue: errorHandler}],
})
class TestModule {
}
platformBrowserDynamic().bootstrapModule(TestModule).then((ref) => {
NgZone.assertNotInAngularZone();
const appRef = ref.injector.get(ApplicationRef) as ApplicationRef;
const compRef = appRef.components[0] as ComponentRef<ErrorComp>;
const compEl = compRef.location.nativeElement;
const nextBtn = compEl.children[0];
const nextAndThrowBtn = compEl.children[1];
const nextAndThrowDirBtn = compEl.children[2];
nextBtn.click();
assertValueAndErrors(compEl, 1, 0);
nextBtn.click();
assertValueAndErrors(compEl, 2, 2);
nextAndThrowBtn.click();
assertValueAndErrors(compEl, 3, 4);
nextAndThrowBtn.click();
assertValueAndErrors(compEl, 4, 6);
nextAndThrowDirBtn.click();
assertValueAndErrors(compEl, 5, 8);
nextAndThrowDirBtn.click();
assertValueAndErrors(compEl, 6, 10);
// Assert that there were no more errors
expect(logger.errors.length).toBe(12);
done();
});
function assertValueAndErrors(compEl: any, value: number, errorIndex: number) {
expect(compEl).toHaveText(`Value:${value}`);
expect(logger.errors[errorIndex][0]).toBe('ERROR');
expect(logger.errors[errorIndex][1].message).toBe(`Error: ${value}`);
expect(logger.errors[errorIndex + 1][0]).toBe('ERROR CONTEXT');
}
});
}
}); });
} }

View File

@ -10,7 +10,7 @@ import {ResourceLoader} from '@angular/compiler';
import {SourceMap} from '@angular/compiler/src/output/source_map'; import {SourceMap} from '@angular/compiler/src/output/source_map';
import {extractSourceMap, originalPositionFor} from '@angular/compiler/test/output/source_map_util'; import {extractSourceMap, originalPositionFor} from '@angular/compiler/test/output/source_map_util';
import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock'; import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock';
import {Attribute, Component, Directive, ɵglobal} from '@angular/core'; import {Attribute, Component, Directive, ErrorHandler, ɵglobal} from '@angular/core';
import {getErrorLogger} from '@angular/core/src/errors'; import {getErrorLogger} from '@angular/core/src/errors';
import {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing'; import {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
@ -231,11 +231,10 @@ export function main() {
const comp = compileAndCreateComponent(MyComp); const comp = compileAndCreateComponent(MyComp);
let error: any; let error: any;
try { const errorHandler = TestBed.get(ErrorHandler);
comp.debugElement.children[0].children[0].triggerEventHandler('click', 'EVENT'); spyOn(errorHandler, 'handleError').and.callFake((e: any) => error = e);
} catch (e) { comp.debugElement.children[0].children[0].triggerEventHandler('click', 'EVENT');
error = e; expect(error).toBeTruthy();
}
// the stack should point to the binding // the stack should point to the binding
expect(getSourcePositionForStack(error.stack)).toEqual({ expect(getSourcePositionForStack(error.stack)).toEqual({
line: 2, line: 2,

View File

@ -652,6 +652,46 @@ export function main() {
expect(compEl.nativeElement).toHaveText('1'); expect(compEl.nativeElement).toHaveText('1');
}); });
it('should inject ChangeDetectorRef of a same element component into a directive', () => {
TestBed.configureTestingModule(
{declarations: [PushComponentNeedsChangeDetectorRef, DirectiveNeedsChangeDetectorRef]});
const cf = createComponentFixture(
'<div componentNeedsChangeDetectorRef directiveNeedsChangeDetectorRef></div>');
cf.detectChanges();
const compEl = cf.debugElement.children[0];
const comp = compEl.injector.get(PushComponentNeedsChangeDetectorRef);
const dir = compEl.injector.get(DirectiveNeedsChangeDetectorRef);
comp.counter = 1;
cf.detectChanges();
expect(compEl.nativeElement).toHaveText('0');
dir.changeDetectorRef.markForCheck();
cf.detectChanges();
expect(compEl.nativeElement).toHaveText('1');
});
it(`should not inject ChangeDetectorRef of a parent element's component into a directive`, () => {
TestBed
.configureTestingModule({
declarations: [PushComponentNeedsChangeDetectorRef, DirectiveNeedsChangeDetectorRef]
})
.overrideComponent(
PushComponentNeedsChangeDetectorRef,
{set: {template: '<ng-content></ng-content>{{counter}}'}});
const cf = createComponentFixture(
'<div componentNeedsChangeDetectorRef><div directiveNeedsChangeDetectorRef></div></div>');
cf.detectChanges();
const compEl = cf.debugElement.children[0];
const comp = compEl.injector.get(PushComponentNeedsChangeDetectorRef);
const dirEl = compEl.children[0];
const dir = dirEl.injector.get(DirectiveNeedsChangeDetectorRef);
comp.counter = 1;
cf.detectChanges();
expect(compEl.nativeElement).toHaveText('0');
dir.changeDetectorRef.markForCheck();
cf.detectChanges();
expect(compEl.nativeElement).toHaveText('0');
});
it('should inject ViewContainerRef', () => { it('should inject ViewContainerRef', () => {
TestBed.configureTestingModule({declarations: [NeedsViewContainerRef]}); TestBed.configureTestingModule({declarations: [NeedsViewContainerRef]});
const el = createComponent('<div needsViewContainerRef></div>'); const el = createComponent('<div needsViewContainerRef></div>');

View File

@ -230,7 +230,7 @@ export function main() {
}); });
} }
it('should stop dirty checking views that threw errors in change detection', () => { it('should not stop dirty checking views that threw errors in change detection', () => {
class AComp { class AComp {
a: any; a: any;
} }
@ -255,8 +255,8 @@ export function main() {
expect(update).toHaveBeenCalled(); expect(update).toHaveBeenCalled();
update.calls.reset(); update.calls.reset();
Services.checkAndUpdateView(view); expect(() => Services.checkAndUpdateView(view)).toThrowError('Test');
expect(update).not.toHaveBeenCalled(); expect(update).toHaveBeenCalled();
}); });
}); });

View File

@ -6,9 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Injector, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, WrappedValue, getDebugNode} from '@angular/core'; import {ErrorHandler, Injector, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, WrappedValue, getDebugNode} from '@angular/core';
import {getDebugContext} from '@angular/core/src/errors'; import {getDebugContext} from '@angular/core/src/errors';
import {ArgumentType, BindingFlags, DebugContext, NodeDef, NodeFlags, OutputType, RootData, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; import {ArgumentType, BindingFlags, DebugContext, NodeDef, NodeFlags, OutputType, RootData, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {TestBed} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, isBrowser, removeNodes} from './helper'; import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, isBrowser, removeNodes} from './helper';
@ -282,17 +283,14 @@ export function main() {
}); });
it('should report debug info on event errors', () => { it('should report debug info on event errors', () => {
const handleErrorSpy = spyOn(TestBed.get(ErrorHandler), 'handleError');
const addListenerSpy = spyOn(HTMLElement.prototype, 'addEventListener').and.callThrough(); const addListenerSpy = spyOn(HTMLElement.prototype, 'addEventListener').and.callThrough();
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef( const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
NodeFlags.None, null !, null !, 0, 'button', null !, null !, [[null !, 'click']], NodeFlags.None, null !, null !, 0, 'button', null !, null !, [[null !, 'click']],
() => { throw new Error('Test'); })])); () => { throw new Error('Test'); })]));
let err: any; addListenerSpy.calls.mostRecent().args[1]('SomeEvent');
try { const err = handleErrorSpy.calls.mostRecent().args[0];
addListenerSpy.calls.mostRecent().args[1]('SomeEvent');
} catch (e) {
err = e;
}
expect(err).toBeTruthy(); expect(err).toBeTruthy();
expect(err.message).toBe('Test'); expect(err.message).toBe('Test');
const debugCtx = getDebugContext(err); const debugCtx = getDebugContext(err);

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ChangeDetectorRef, DoCheck, ElementRef, EventEmitter, Injector, OnChanges, OnDestroy, OnInit, RenderComponentType, Renderer, Renderer2, RootRenderer, Sanitizer, SecurityContext, SimpleChange, TemplateRef, ViewContainerRef, ViewEncapsulation, WrappedValue, getDebugNode} from '@angular/core'; import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ChangeDetectorRef, DoCheck, ElementRef, ErrorHandler, EventEmitter, Injector, OnChanges, OnDestroy, OnInit, RenderComponentType, Renderer, Renderer2, RootRenderer, Sanitizer, SecurityContext, SimpleChange, TemplateRef, ViewContainerRef, ViewEncapsulation, WrappedValue, getDebugNode} from '@angular/core';
import {getDebugContext} from '@angular/core/src/errors'; import {getDebugContext} from '@angular/core/src/errors';
import {ArgumentType, BindingFlags, DebugContext, DepFlags, NodeDef, NodeFlags, RootData, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, asProviderData, directiveDef, elementDef, providerDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; import {ArgumentType, BindingFlags, DebugContext, DepFlags, NodeDef, NodeFlags, RootData, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, asProviderData, directiveDef, elementDef, providerDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {TestBed, inject, withModule} from '@angular/core/testing'; import {TestBed, inject, withModule} from '@angular/core/testing';
@ -381,6 +381,7 @@ export function main() {
}); });
it('should report debug info on event errors', () => { it('should report debug info on event errors', () => {
const handleErrorSpy = spyOn(TestBed.get(ErrorHandler), 'handleError');
let emitter = new EventEmitter<any>(); let emitter = new EventEmitter<any>();
class SomeService { class SomeService {
@ -395,12 +396,8 @@ export function main() {
NodeFlags.None, null !, 0, SomeService, [], null !, {emitter: 'someEventName'}) NodeFlags.None, null !, 0, SomeService, [], null !, {emitter: 'someEventName'})
])); ]));
let err: any; emitter.emit('someEventInstance');
try { const err = handleErrorSpy.calls.mostRecent().args[0];
emitter.emit('someEventInstance');
} catch (e) {
err = e;
}
expect(err).toBeTruthy(); expect(err).toBeTruthy();
const debugCtx = getDebugContext(err); const debugCtx = getDebugContext(err);
expect(debugCtx.view).toBe(view); expect(debugCtx.view).toBe(view);

View File

@ -59,7 +59,7 @@ const resolvedPromise = Promise.resolve(null);
* This directive can be used by itself or as part of a larger form. All you need is the * This directive can be used by itself or as part of a larger form. All you need is the
* `ngModel` selector to activate it. * `ngModel` selector to activate it.
* *
* It accepts a domain model as an optional {@link @Input}. If you have a one-way binding * It accepts a domain model as an optional {@link Input}. If you have a one-way binding
* to `ngModel` with `[]` syntax, changing the value of the domain model in the component * to `ngModel` with `[]` syntax, changing the value of the domain model in the component
* class will set the value in the view. If you have a two-way binding with `[()]` syntax * class will set the value in the view. If you have a two-way binding with `[()]` syntax
* (also known as 'banana-box syntax'), the value in the UI will always be synced back to * (also known as 'banana-box syntax'), the value in the UI will always be synced back to

View File

@ -45,12 +45,12 @@ export const formControlBinding: any = {
* {@link AbstractControl}. * {@link AbstractControl}.
* *
* **Set the value**: You can pass in an initial value when instantiating the {@link FormControl}, * **Set the value**: You can pass in an initial value when instantiating the {@link FormControl},
* or you can set it programmatically later using {@link AbstractControl.setValue} or * or you can set it programmatically later using {@link AbstractControl#setValue} or
* {@link AbstractControl.patchValue}. * {@link AbstractControl#patchValue}.
* *
* **Listen to value**: If you want to listen to changes in the value of the control, you can * **Listen to value**: If you want to listen to changes in the value of the control, you can
* subscribe to the {@link AbstractControl.valueChanges} event. You can also listen to * subscribe to the {@link AbstractControl#valueChanges} event. You can also listen to
* {@link AbstractControl.statusChanges} to be notified when the validation status is * {@link AbstractControl#statusChanges} to be notified when the validation status is
* re-calculated. * re-calculated.
* *
* ### Example * ### Example

View File

@ -45,7 +45,7 @@ export const controlNameBinding: any = {
* closest {@link FormGroup} or {@link FormArray} above it. * closest {@link FormGroup} or {@link FormArray} above it.
* *
* **Access the control**: You can access the {@link FormControl} associated with * **Access the control**: You can access the {@link FormControl} associated with
* this directive by using the {@link AbstractControl.get} method. * this directive by using the {@link AbstractControl#get} method.
* Ex: `this.form.get('first');` * Ex: `this.form.get('first');`
* *
* **Get value**: the `value` property is always synced and available on the {@link FormControl}. * **Get value**: the `value` property is always synced and available on the {@link FormControl}.
@ -53,11 +53,11 @@ export const controlNameBinding: any = {
* *
* **Set value**: You can set an initial value for the control when instantiating the * **Set value**: You can set an initial value for the control when instantiating the
* {@link FormControl}, or you can set it programmatically later using * {@link FormControl}, or you can set it programmatically later using
* {@link AbstractControl.setValue} or {@link AbstractControl.patchValue}. * {@link AbstractControl#setValue} or {@link AbstractControl#patchValue}.
* *
* **Listen to value**: If you want to listen to changes in the value of the control, you can * **Listen to value**: If you want to listen to changes in the value of the control, you can
* subscribe to the {@link AbstractControl.valueChanges} event. You can also listen to * subscribe to the {@link AbstractControl#valueChanges} event. You can also listen to
* {@link AbstractControl.statusChanges} to be notified when the validation status is * {@link AbstractControl#statusChanges} to be notified when the validation status is
* re-calculated. * re-calculated.
* *
* ### Example * ### Example

View File

@ -34,11 +34,11 @@ export const formDirectiveProvider: any = {
* *
* **Set value**: You can set the form's initial value when instantiating the * **Set value**: You can set the form's initial value when instantiating the
* {@link FormGroup}, or you can set it programmatically later using the {@link FormGroup}'s * {@link FormGroup}, or you can set it programmatically later using the {@link FormGroup}'s
* {@link AbstractControl.setValue} or {@link AbstractControl.patchValue} methods. * {@link AbstractControl#setValue} or {@link AbstractControl#patchValue} methods.
* *
* **Listen to value**: If you want to listen to changes in the value of the form, you can subscribe * **Listen to value**: If you want to listen to changes in the value of the form, you can subscribe
* to the {@link FormGroup}'s {@link AbstractControl.valueChanges} event. You can also listen to * to the {@link FormGroup}'s {@link AbstractControl#valueChanges} event. You can also listen to
* its {@link AbstractControl.statusChanges} event to be notified when the validation status is * its {@link AbstractControl#statusChanges} event to be notified when the validation status is
* re-calculated. * re-calculated.
* *
* Furthermore, you can listen to the directive's `ngSubmit` event to be notified when the user has * Furthermore, you can listen to the directive's `ngSubmit` event to be notified when the user has

View File

@ -40,7 +40,7 @@ export const formGroupNameProvider: any = {
* controls into their own nested object. * controls into their own nested object.
* *
* **Access the group**: You can access the associated {@link FormGroup} using the * **Access the group**: You can access the associated {@link FormGroup} using the
* {@link AbstractControl.get} method. Ex: `this.form.get('name')`. * {@link AbstractControl#get} method. Ex: `this.form.get('name')`.
* *
* You can also access individual controls within the group using dot syntax. * You can also access individual controls within the group using dot syntax.
* Ex: `this.form.get('name.first')` * Ex: `this.form.get('name.first')`
@ -50,11 +50,11 @@ export const formGroupNameProvider: any = {
* *
* **Set the value**: You can set an initial value for each child control when instantiating * **Set the value**: You can set an initial value for each child control when instantiating
* the {@link FormGroup}, or you can set it programmatically later using * the {@link FormGroup}, or you can set it programmatically later using
* {@link AbstractControl.setValue} or {@link AbstractControl.patchValue}. * {@link AbstractControl#setValue} or {@link AbstractControl#patchValue}.
* *
* **Listen to value**: If you want to listen to changes in the value of the group, you can * **Listen to value**: If you want to listen to changes in the value of the group, you can
* subscribe to the {@link AbstractControl.valueChanges} event. You can also listen to * subscribe to the {@link AbstractControl#valueChanges} event. You can also listen to
* {@link AbstractControl.statusChanges} to be notified when the validation status is * {@link AbstractControl#statusChanges} to be notified when the validation status is
* re-calculated. * re-calculated.
* *
* ### Example * ### Example
@ -111,7 +111,7 @@ export const formArrayNameProvider: any = {
* form controls dynamically. * form controls dynamically.
* *
* **Access the array**: You can access the associated {@link FormArray} using the * **Access the array**: You can access the associated {@link FormArray} using the
* {@link AbstractControl.get} method on the parent {@link FormGroup}. * {@link AbstractControl#get} method on the parent {@link FormGroup}.
* Ex: `this.form.get('cities')`. * Ex: `this.form.get('cities')`.
* *
* **Get the value**: the `value` property is always synced and available on the * **Get the value**: the `value` property is always synced and available on the
@ -119,12 +119,12 @@ export const formArrayNameProvider: any = {
* *
* **Set the value**: You can set an initial value for each child control when instantiating * **Set the value**: You can set an initial value for each child control when instantiating
* the {@link FormArray}, or you can set the value programmatically later using the * the {@link FormArray}, or you can set the value programmatically later using the
* {@link FormArray}'s {@link AbstractControl.setValue} or {@link AbstractControl.patchValue} * {@link FormArray}'s {@link AbstractControl#setValue} or {@link AbstractControl#patchValue}
* methods. * methods.
* *
* **Listen to value**: If you want to listen to changes in the value of the array, you can * **Listen to value**: If you want to listen to changes in the value of the array, you can
* subscribe to the {@link FormArray}'s {@link AbstractControl.valueChanges} event. You can also * subscribe to the {@link FormArray}'s {@link AbstractControl#valueChanges} event. You can also
* listen to its {@link AbstractControl.statusChanges} event to be notified when the validation * listen to its {@link AbstractControl#statusChanges} event to be notified when the validation
* status is re-calculated. * status is re-calculated.
* *
* **Add new controls**: You can add new controls to the {@link FormArray} dynamically by * **Add new controls**: You can add new controls to the {@link FormArray} dynamically by

View File

@ -659,17 +659,18 @@ export class FormControl extends AbstractControl {
* If `emitViewToModelChange` is `true`, an ngModelChange event will be fired to update the * If `emitViewToModelChange` is `true`, an ngModelChange event will be fired to update the
* model. This is the default behavior if `emitViewToModelChange` is not specified. * model. This is the default behavior if `emitViewToModelChange` is not specified.
*/ */
setValue(value: any, {onlySelf, emitEvent, emitModelToViewChange, emitViewToModelChange}: { setValue(value: any, options: {
onlySelf?: boolean, onlySelf?: boolean,
emitEvent?: boolean, emitEvent?: boolean,
emitModelToViewChange?: boolean, emitModelToViewChange?: boolean,
emitViewToModelChange?: boolean emitViewToModelChange?: boolean
} = {}): void { } = {}): void {
this._value = value; this._value = value;
if (this._onChange.length && emitModelToViewChange !== false) { if (this._onChange.length && options.emitModelToViewChange !== false) {
this._onChange.forEach((changeFn) => changeFn(this._value, emitViewToModelChange !== false)); this._onChange.forEach(
(changeFn) => changeFn(this._value, options.emitViewToModelChange !== false));
} }
this.updateValueAndValidity({onlySelf, emitEvent}); this.updateValueAndValidity(options);
} }
/** /**
@ -716,12 +717,11 @@ export class FormControl extends AbstractControl {
* console.log(this.control.status); // 'DISABLED' * console.log(this.control.status); // 'DISABLED'
* ``` * ```
*/ */
reset(formState: any = null, {onlySelf, emitEvent}: {onlySelf?: boolean, reset(formState: any = null, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
emitEvent?: boolean} = {}): void {
this._applyFormState(formState); this._applyFormState(formState);
this.markAsPristine({onlySelf}); this.markAsPristine(options);
this.markAsUntouched({onlySelf}); this.markAsUntouched(options);
this.setValue(this._value, {onlySelf, emitEvent}); this.setValue(this._value, options);
} }
/** /**
@ -886,7 +886,7 @@ export class FormGroup extends AbstractControl {
* Check whether there is an enabled control with the given name in the group. * Check whether there is an enabled control with the given name in the group.
* *
* It will return false for disabled controls. If you'd like to check for * It will return false for disabled controls. If you'd like to check for
* existence in the group only, use {@link AbstractControl.get} instead. * existence in the group only, use {@link AbstractControl#get} instead.
*/ */
contains(controlName: string): boolean { contains(controlName: string): boolean {
return this.controls.hasOwnProperty(controlName) && this.controls[controlName].enabled; return this.controls.hasOwnProperty(controlName) && this.controls[controlName].enabled;
@ -914,15 +914,14 @@ export class FormGroup extends AbstractControl {
* *
* ``` * ```
*/ */
setValue( setValue(value: {[key: string]: any}, options: {onlySelf?: boolean, emitEvent?: boolean} = {}):
value: {[key: string]: any}, void {
{onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
this._checkAllValuesPresent(value); this._checkAllValuesPresent(value);
Object.keys(value).forEach(name => { Object.keys(value).forEach(name => {
this._throwIfControlMissing(name); this._throwIfControlMissing(name);
this.controls[name].setValue(value[name], {onlySelf: true, emitEvent}); this.controls[name].setValue(value[name], {onlySelf: true, emitEvent: options.emitEvent});
}); });
this.updateValueAndValidity({onlySelf, emitEvent}); this.updateValueAndValidity(options);
} }
/** /**
@ -946,15 +945,14 @@ export class FormGroup extends AbstractControl {
* *
* ``` * ```
*/ */
patchValue( patchValue(value: {[key: string]: any}, options: {onlySelf?: boolean, emitEvent?: boolean} = {}):
value: {[key: string]: any}, void {
{onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
Object.keys(value).forEach(name => { Object.keys(value).forEach(name => {
if (this.controls[name]) { if (this.controls[name]) {
this.controls[name].patchValue(value[name], {onlySelf: true, emitEvent}); this.controls[name].patchValue(value[name], {onlySelf: true, emitEvent: options.emitEvent});
} }
}); });
this.updateValueAndValidity({onlySelf, emitEvent}); this.updateValueAndValidity(options);
} }
/** /**
@ -989,14 +987,13 @@ export class FormGroup extends AbstractControl {
* console.log(this.form.get('first').status); // 'DISABLED' * console.log(this.form.get('first').status); // 'DISABLED'
* ``` * ```
*/ */
reset(value: any = {}, {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): reset(value: any = {}, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
void {
this._forEachChild((control: AbstractControl, name: string) => { this._forEachChild((control: AbstractControl, name: string) => {
control.reset(value[name], {onlySelf: true, emitEvent}); control.reset(value[name], {onlySelf: true, emitEvent: options.emitEvent});
}); });
this.updateValueAndValidity({onlySelf, emitEvent}); this.updateValueAndValidity(options);
this._updatePristine({onlySelf}); this._updatePristine(options);
this._updateTouched({onlySelf}); this._updateTouched(options);
} }
/** /**
@ -1222,14 +1219,13 @@ export class FormArray extends AbstractControl {
* console.log(arr.value); // ['Nancy', 'Drew'] * console.log(arr.value); // ['Nancy', 'Drew']
* ``` * ```
*/ */
setValue(value: any[], {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): setValue(value: any[], options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
void {
this._checkAllValuesPresent(value); this._checkAllValuesPresent(value);
value.forEach((newValue: any, index: number) => { value.forEach((newValue: any, index: number) => {
this._throwIfControlMissing(index); this._throwIfControlMissing(index);
this.at(index).setValue(newValue, {onlySelf: true, emitEvent}); this.at(index).setValue(newValue, {onlySelf: true, emitEvent: options.emitEvent});
}); });
this.updateValueAndValidity({onlySelf, emitEvent}); this.updateValueAndValidity(options);
} }
/** /**
@ -1252,14 +1248,13 @@ export class FormArray extends AbstractControl {
* console.log(arr.value); // ['Nancy', null] * console.log(arr.value); // ['Nancy', null]
* ``` * ```
*/ */
patchValue(value: any[], {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): patchValue(value: any[], options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
void {
value.forEach((newValue: any, index: number) => { value.forEach((newValue: any, index: number) => {
if (this.at(index)) { if (this.at(index)) {
this.at(index).patchValue(newValue, {onlySelf: true, emitEvent}); this.at(index).patchValue(newValue, {onlySelf: true, emitEvent: options.emitEvent});
} }
}); });
this.updateValueAndValidity({onlySelf, emitEvent}); this.updateValueAndValidity(options);
} }
/** /**
@ -1293,14 +1288,13 @@ export class FormArray extends AbstractControl {
* console.log(this.arr.get(0).status); // 'DISABLED' * console.log(this.arr.get(0).status); // 'DISABLED'
* ``` * ```
*/ */
reset(value: any = [], {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): reset(value: any = [], options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
void {
this._forEachChild((control: AbstractControl, index: number) => { this._forEachChild((control: AbstractControl, index: number) => {
control.reset(value[index], {onlySelf: true, emitEvent}); control.reset(value[index], {onlySelf: true, emitEvent: options.emitEvent});
}); });
this.updateValueAndValidity({onlySelf, emitEvent}); this.updateValueAndValidity(options);
this._updatePristine({onlySelf}); this._updatePristine(options);
this._updateTouched({onlySelf}); this._updateTouched(options);
} }
/** /**

View File

@ -24,9 +24,6 @@ function isEmptyInputValue(value: any): boolean {
* *
* Provide this using `multi: true` to add validators. * Provide this using `multi: true` to add validators.
* *
* ### Example
*
* {@example core/forms/ts/ng_validators/ng_validators.ts region='ng_validators'}
* @stable * @stable
*/ */
export const NG_VALIDATORS = new InjectionToken<Array<Validator|Function>>('NgValidators'); export const NG_VALIDATORS = new InjectionToken<Array<Validator|Function>>('NgValidators');

View File

@ -13,7 +13,9 @@ export class AstPath<T> {
get head(): T|undefined { return this.path[0]; } get head(): T|undefined { return this.path[0]; }
get tail(): T|undefined { return this.path[this.path.length - 1]; } get tail(): T|undefined { return this.path[this.path.length - 1]; }
parentOf(node: T): T|undefined { return this.path[this.path.indexOf(node) - 1]; } parentOf(node: T|undefined): T|undefined {
return node && this.path[this.path.indexOf(node) - 1];
}
childOf(node: T): T|undefined { return this.path[this.path.indexOf(node) + 1]; } childOf(node: T): T|undefined { return this.path[this.path.indexOf(node) + 1]; }
first<N extends T>(ctor: {new (...args: any[]): N}): N|undefined { first<N extends T>(ctor: {new (...args: any[]): N}): N|undefined {

View File

@ -33,71 +33,73 @@ export function getTemplateCompletions(templateInfo: TemplateInfo): Completions|
let result: Completions|undefined = undefined; let result: Completions|undefined = undefined;
let {htmlAst, templateAst, template} = templateInfo; let {htmlAst, templateAst, template} = templateInfo;
// The templateNode starts at the delimiter character so we add 1 to skip it. // The templateNode starts at the delimiter character so we add 1 to skip it.
let templatePosition = templateInfo.position ! - template.span.start; if (templateInfo.position != null) {
let path = new HtmlAstPath(htmlAst, templatePosition); let templatePosition = templateInfo.position - template.span.start;
let mostSpecific = path.tail; let path = new HtmlAstPath(htmlAst, templatePosition);
if (path.empty) { let mostSpecific = path.tail;
result = elementCompletions(templateInfo, path); if (path.empty || !mostSpecific) {
} else { result = elementCompletions(templateInfo, path);
let astPosition = templatePosition - mostSpecific !.sourceSpan !.start.offset; } else {
mostSpecific !.visit( let astPosition = templatePosition - mostSpecific.sourceSpan.start.offset;
{ mostSpecific.visit(
visitElement(ast) { {
let startTagSpan = spanOf(ast.sourceSpan); visitElement(ast) {
let tagLen = ast.name.length; let startTagSpan = spanOf(ast.sourceSpan);
if (templatePosition <= let tagLen = ast.name.length;
startTagSpan !.start + tagLen + 1 /* 1 for the opening angle bracked */) { if (templatePosition <=
// If we are in the tag then return the element completions. startTagSpan.start + tagLen + 1 /* 1 for the opening angle bracked */) {
result = elementCompletions(templateInfo, path); // If we are in the tag then return the element completions.
} else if (templatePosition < startTagSpan !.end) { result = elementCompletions(templateInfo, path);
// We are in the attribute section of the element (but not in an attribute). } else if (templatePosition < startTagSpan.end) {
// Return the attribute completions. // We are in the attribute section of the element (but not in an attribute).
result = attributeCompletions(templateInfo, path); // Return the attribute completions.
} result = attributeCompletions(templateInfo, path);
}, }
visitAttribute(ast) { },
if (!ast.valueSpan || !inSpan(templatePosition, spanOf(ast.valueSpan))) { visitAttribute(ast) {
// We are in the name of an attribute. Show attribute completions. if (!ast.valueSpan || !inSpan(templatePosition, spanOf(ast.valueSpan))) {
result = attributeCompletions(templateInfo, path); // We are in the name of an attribute. Show attribute completions.
} else if (ast.valueSpan && inSpan(templatePosition, spanOf(ast.valueSpan))) { result = attributeCompletions(templateInfo, path);
result = attributeValueCompletions(templateInfo, templatePosition, ast); } else if (ast.valueSpan && inSpan(templatePosition, spanOf(ast.valueSpan))) {
} result = attributeValueCompletions(templateInfo, templatePosition, ast);
}, }
visitText(ast) { },
// Check if we are in a entity. visitText(ast) {
result = entityCompletions(getSourceText(template, spanOf(ast) !), astPosition); // Check if we are in a entity.
if (result) return result; result = entityCompletions(getSourceText(template, spanOf(ast)), astPosition);
result = interpolationCompletions(templateInfo, templatePosition); if (result) return result;
if (result) return result; result = interpolationCompletions(templateInfo, templatePosition);
let element = path.first(Element); if (result) return result;
if (element) { let element = path.first(Element);
let definition = getHtmlTagDefinition(element.name); if (element) {
if (definition.contentType === TagContentType.PARSABLE_DATA) { let definition = getHtmlTagDefinition(element.name);
if (definition.contentType === TagContentType.PARSABLE_DATA) {
result = voidElementAttributeCompletions(templateInfo, path);
if (!result) {
// If the element can hold content Show element completions.
result = elementCompletions(templateInfo, path);
}
}
} else {
// If no element container, implies parsable data so show elements.
result = voidElementAttributeCompletions(templateInfo, path); result = voidElementAttributeCompletions(templateInfo, path);
if (!result) { if (!result) {
// If the element can hold content Show element completions.
result = elementCompletions(templateInfo, path); result = elementCompletions(templateInfo, path);
} }
} }
} else { },
// If no element container, implies parsable data so show elements. visitComment(ast) {},
result = voidElementAttributeCompletions(templateInfo, path); visitExpansion(ast) {},
if (!result) { visitExpansionCase(ast) {}
result = elementCompletions(templateInfo, path);
}
}
}, },
visitComment(ast) {}, null);
visitExpansion(ast) {}, }
visitExpansionCase(ast) {}
},
null);
} }
return result; return result;
} }
function attributeCompletions(info: TemplateInfo, path: HtmlAstPath): Completions|undefined { function attributeCompletions(info: TemplateInfo, path: HtmlAstPath): Completions|undefined {
let item = path.tail instanceof Element ? path.tail : path.parentOf(path.tail !); let item = path.tail instanceof Element ? path.tail : path.parentOf(path.tail);
if (item instanceof Element) { if (item instanceof Element) {
return attributeCompletionsForElement(info, item.name, item); return attributeCompletionsForElement(info, item.name, item);
} }
@ -213,11 +215,12 @@ function elementCompletions(info: TemplateInfo, path: HtmlAstPath): Completions|
let htmlNames = elementNames().filter(name => !(name in hiddenHtmlElements)); let htmlNames = elementNames().filter(name => !(name in hiddenHtmlElements));
// Collect the elements referenced by the selectors // Collect the elements referenced by the selectors
let directiveElements = let directiveElements = getSelectors(info)
getSelectors(info).selectors.map(selector => selector.element).filter(name => !!name); .selectors.map(selector => selector.element)
.filter(name => !!name) as string[];
let components = let components =
directiveElements.map<Completion>(name => ({kind: 'component', name: name !, sort: name !})); directiveElements.map<Completion>(name => ({kind: 'component', name, sort: name}));
let htmlElements = htmlNames.map<Completion>(name => ({kind: 'element', name: name, sort: name})); let htmlElements = htmlNames.map<Completion>(name => ({kind: 'element', name: name, sort: name}));
// Return components and html elements // Return components and html elements
@ -262,25 +265,25 @@ function voidElementAttributeCompletions(info: TemplateInfo, path: HtmlAstPath):
undefined { undefined {
let tail = path.tail; let tail = path.tail;
if (tail instanceof Text) { if (tail instanceof Text) {
let match = tail.value.match(/<(\w(\w|\d|-)*:)?(\w(\w|\d|-)*)\s/) !; let match = tail.value.match(/<(\w(\w|\d|-)*:)?(\w(\w|\d|-)*)\s/);
// The position must be after the match, otherwise we are still in a place where elements // The position must be after the match, otherwise we are still in a place where elements
// are expected (such as `<|a` or `<a|`; we only want attributes for `<a |` or after). // are expected (such as `<|a` or `<a|`; we only want attributes for `<a |` or after).
if (match && path.position >= match.index ! + match[0].length + tail.sourceSpan.start.offset) { if (match &&
path.position >= (match.index || 0) + match[0].length + tail.sourceSpan.start.offset) {
return attributeCompletionsForElement(info, match[3]); return attributeCompletionsForElement(info, match[3]);
} }
} }
} }
class ExpressionVisitor extends NullTemplateVisitor { class ExpressionVisitor extends NullTemplateVisitor {
private getExpressionScope: () => SymbolTable;
result: Completions; result: Completions;
constructor( constructor(
private info: TemplateInfo, private position: number, private attr?: Attribute, private info: TemplateInfo, private position: number, private attr?: Attribute,
private getExpressionScope?: () => SymbolTable) { getExpressionScope?: () => SymbolTable) {
super(); super();
if (!getExpressionScope) { this.getExpressionScope = getExpressionScope || (() => info.template.members);
this.getExpressionScope = () => info.template.members;
}
} }
visitDirectiveProperty(ast: BoundDirectivePropertyAst): void { visitDirectiveProperty(ast: BoundDirectivePropertyAst): void {
@ -311,7 +314,8 @@ class ExpressionVisitor extends NullTemplateVisitor {
this.info.expressionParser.parseTemplateBindings(key, this.attr.value, null); this.info.expressionParser.parseTemplateBindings(key, this.attr.value, null);
// find the template binding that contains the position // find the template binding that contains the position
const valueRelativePosition = this.position - this.attr.valueSpan !.start.offset - 1; if (!this.attr.valueSpan) return;
const valueRelativePosition = this.position - this.attr.valueSpan.start.offset - 1;
const bindings = templateBindingResult.templateBindings; const bindings = templateBindingResult.templateBindings;
const binding = const binding =
bindings.find( bindings.find(
@ -340,10 +344,12 @@ class ExpressionVisitor extends NullTemplateVisitor {
// We are after the '=' in a let clause. The valid values here are the members of the // We are after the '=' in a let clause. The valid values here are the members of the
// template reference's type parameter. // template reference's type parameter.
const directiveMetadata = selectorInfo.map.get(selector); const directiveMetadata = selectorInfo.map.get(selector);
const contextTable = if (directiveMetadata) {
this.info.template.query.getTemplateContext(directiveMetadata !.type.reference); const contextTable =
if (contextTable) { this.info.template.query.getTemplateContext(directiveMetadata.type.reference);
this.result = this.symbolsToCompletions(contextTable.values()); if (contextTable) {
this.result = this.symbolsToCompletions(contextTable.values());
}
} }
} else if (binding.key && valueRelativePosition <= (binding.key.length - key.length)) { } else if (binding.key && valueRelativePosition <= (binding.key.length - key.length)) {
keyCompletions(); keyCompletions();
@ -371,7 +377,7 @@ class ExpressionVisitor extends NullTemplateVisitor {
const expressionPosition = this.position - ast.sourceSpan.start.offset; const expressionPosition = this.position - ast.sourceSpan.start.offset;
if (inSpan(expressionPosition, ast.value.span)) { if (inSpan(expressionPosition, ast.value.span)) {
const completions = getExpressionCompletions( const completions = getExpressionCompletions(
this.getExpressionScope !(), ast.value, expressionPosition, this.info.template.query); this.getExpressionScope(), ast.value, expressionPosition, this.info.template.query);
if (completions) { if (completions) {
this.result = this.symbolsToCompletions(completions); this.result = this.symbolsToCompletions(completions);
} }
@ -380,8 +386,8 @@ class ExpressionVisitor extends NullTemplateVisitor {
private attributeValueCompletions(value: AST, position?: number) { private attributeValueCompletions(value: AST, position?: number) {
const symbols = getExpressionCompletions( const symbols = getExpressionCompletions(
this.getExpressionScope !(), value, this.getExpressionScope(), value, position == null ? this.attributeValuePosition : position,
position == null ? this.attributeValuePosition : position, this.info.template.query); this.info.template.query);
if (symbols) { if (symbols) {
this.result = this.symbolsToCompletions(symbols); this.result = this.symbolsToCompletions(symbols);
} }
@ -393,12 +399,13 @@ class ExpressionVisitor extends NullTemplateVisitor {
} }
private get attributeValuePosition() { private get attributeValuePosition() {
return this.position - this.attr !.valueSpan !.start.offset - 1; if (this.attr && this.attr.valueSpan) {
return this.position - this.attr.valueSpan.start.offset - 1;
}
return 0;
} }
} }
function getSourceText(template: TemplateSource, span: Span): string { function getSourceText(template: TemplateSource, span: Span): string {
return template.source.substring(span.start, span.end); return template.source.substring(span.start, span.end);
} }

View File

@ -29,7 +29,7 @@ export function getTemplateDiagnostics(
results.push(...ast.parseErrors.map<Diagnostic>( results.push(...ast.parseErrors.map<Diagnostic>(
e => ({ e => ({
kind: DiagnosticKind.Error, kind: DiagnosticKind.Error,
span: offsetSpan(spanOf(e.span) !, template.span.start), span: offsetSpan(spanOf(e.span), template.span.start),
message: e.msg message: e.msg
}))); })));
} else if (ast.templateAst) { } else if (ast.templateAst) {
@ -91,20 +91,24 @@ export function getDeclarationDiagnostics(
function getTemplateExpressionDiagnostics( function getTemplateExpressionDiagnostics(
template: TemplateSource, astResult: AstResult): Diagnostics { template: TemplateSource, astResult: AstResult): Diagnostics {
const info: TemplateInfo = { if (astResult.htmlAst && astResult.directive && astResult.directives && astResult.pipes &&
template, astResult.templateAst && astResult.expressionParser) {
htmlAst: astResult.htmlAst !, const info: TemplateInfo = {
directive: astResult.directive !, template,
directives: astResult.directives !, htmlAst: astResult.htmlAst,
pipes: astResult.pipes !, directive: astResult.directive,
templateAst: astResult.templateAst !, directives: astResult.directives,
expressionParser: astResult.expressionParser ! pipes: astResult.pipes,
}; templateAst: astResult.templateAst,
const visitor = new ExpressionDiagnosticsVisitor( expressionParser: astResult.expressionParser
info, (path: TemplateAstPath, includeEvent: boolean) => };
getExpressionScope(info, path, includeEvent)); const visitor = new ExpressionDiagnosticsVisitor(
templateVisitAll(visitor, astResult.templateAst !); info, (path: TemplateAstPath, includeEvent: boolean) =>
return visitor.diagnostics; getExpressionScope(info, path, includeEvent));
templateVisitAll(visitor, astResult.templateAst !);
return visitor.diagnostics;
}
return [];
} }
class ExpressionDiagnosticsVisitor extends TemplateAstChildVisitor { class ExpressionDiagnosticsVisitor extends TemplateAstChildVisitor {
@ -158,11 +162,11 @@ class ExpressionDiagnosticsVisitor extends TemplateAstChildVisitor {
if (context && !context.has(ast.value)) { if (context && !context.has(ast.value)) {
if (ast.value === '$implicit') { if (ast.value === '$implicit') {
this.reportError( this.reportError(
'The template context does not have an implicit value', spanOf(ast.sourceSpan) !); 'The template context does not have an implicit value', spanOf(ast.sourceSpan));
} else { } else {
this.reportError( this.reportError(
`The template context does not defined a member called '${ast.value}'`, `The template context does not defined a member called '${ast.value}'`,
spanOf(ast.sourceSpan) !); spanOf(ast.sourceSpan));
} }
} }
} }
@ -233,11 +237,13 @@ class ExpressionDiagnosticsVisitor extends TemplateAstChildVisitor {
} }
} }
private reportError(message: string, span: Span) { private reportError(message: string, span: Span|undefined) {
this.diagnostics.push({ if (span) {
span: offsetSpan(span, this.info.template.span.start), this.diagnostics.push({
kind: DiagnosticKind.Error, message span: offsetSpan(span, this.info.template.span.start),
}); kind: DiagnosticKind.Error, message
});
}
} }
private reportWarning(message: string, span: Span) { private reportWarning(message: string, span: Span) {

View File

@ -495,8 +495,9 @@ class AstType implements ExpressionVisitor {
// The type of a method is the selected methods result type. // The type of a method is the selected methods result type.
const method = receiverType.members().get(ast.name); const method = receiverType.members().get(ast.name);
if (!method) return this.reportError(`Unknown method ${ast.name}`, ast); if (!method) return this.reportError(`Unknown method ${ast.name}`, ast);
if (!method.type !.callable) return this.reportError(`Member ${ast.name} is not callable`, ast); if (!method.type) return this.reportError(`Could not find a type for ${ast.name}`, ast);
const signature = method.type !.selectSignature(ast.args.map(arg => this.getType(arg))); if (!method.type.callable) return this.reportError(`Member ${ast.name} is not callable`, ast);
const signature = method.type.selectSignature(ast.args.map(arg => this.getType(arg)));
if (!signature) if (!signature)
return this.reportError(`Unable to resolve signature for call of method ${ast.name}`, ast); return this.reportError(`Unable to resolve signature for call of method ${ast.name}`, ast);
return signature.result; return signature.result;
@ -601,7 +602,9 @@ function visitChildren(ast: AST, visitor: ExpressionVisitor) {
visit(ast.falseExp); visit(ast.falseExp);
}, },
visitFunctionCall(ast) { visitFunctionCall(ast) {
visit(ast.target !); if (ast.target) {
visit(ast.target);
}
visitAll(ast.args); visitAll(ast.args);
}, },
visitImplicitReceiver(ast) {}, visitImplicitReceiver(ast) {},
@ -676,7 +679,7 @@ function getReferences(info: TemplateInfo): SymbolDeclaration[] {
function processReferences(references: ReferenceAst[]) { function processReferences(references: ReferenceAst[]) {
for (const reference of references) { for (const reference of references) {
let type: Symbol = undefined !; let type: Symbol|undefined = undefined;
if (reference.value) { if (reference.value) {
type = info.template.query.getTypeSymbol(tokenReference(reference.value)); type = info.template.query.getTypeSymbol(tokenReference(reference.value));
} }
@ -721,7 +724,7 @@ function getVarDeclarations(info: TemplateInfo, path: TemplateAstPath): SymbolDe
.find(c => !!c); .find(c => !!c);
// Determine the type of the context field referenced by variable.value. // Determine the type of the context field referenced by variable.value.
let type: Symbol = undefined !; let type: Symbol|undefined = undefined;
if (context) { if (context) {
const value = context.get(variable.value); const value = context.get(variable.value);
if (value) { if (value) {
@ -762,7 +765,10 @@ function refinedVariableType(
const bindingType = const bindingType =
new AstType(info.template.members, info.template.query, {}).getType(ngForOfBinding.value); new AstType(info.template.members, info.template.query, {}).getType(ngForOfBinding.value);
if (bindingType) { if (bindingType) {
return info.template.query.getElementType(bindingType) !; const result = info.template.query.getElementType(bindingType);
if (result) {
return result;
}
} }
} }
} }

View File

@ -81,24 +81,25 @@ class LanguageServiceImpl implements LanguageService {
let template = this.host.getTemplateAt(fileName, position); let template = this.host.getTemplateAt(fileName, position);
if (template) { if (template) {
let astResult = this.getTemplateAst(template, fileName); let astResult = this.getTemplateAst(template, fileName);
if (astResult && astResult.htmlAst && astResult.templateAst) if (astResult && astResult.htmlAst && astResult.templateAst && astResult.directive &&
astResult.directives && astResult.pipes && astResult.expressionParser)
return { return {
position, position,
fileName, fileName,
template, template,
htmlAst: astResult.htmlAst, htmlAst: astResult.htmlAst,
directive: astResult.directive !, directive: astResult.directive,
directives: astResult.directives !, directives: astResult.directives,
pipes: astResult.pipes !, pipes: astResult.pipes,
templateAst: astResult.templateAst, templateAst: astResult.templateAst,
expressionParser: astResult.expressionParser ! expressionParser: astResult.expressionParser
}; };
} }
return undefined; return undefined;
} }
getTemplateAst(template: TemplateSource, contextFile: string): AstResult { getTemplateAst(template: TemplateSource, contextFile: string): AstResult {
let result: AstResult = undefined !; let result: AstResult|undefined = undefined;
try { try {
const resolvedMetadata = const resolvedMetadata =
this.metadataResolver.getNonNormalizedDirectiveMetadata(template.type as any); this.metadataResolver.getNonNormalizedDirectiveMetadata(template.type as any);
@ -112,7 +113,7 @@ class LanguageServiceImpl implements LanguageService {
config, expressionParser, new DomElementSchemaRegistry(), htmlParser, null !, []); config, expressionParser, new DomElementSchemaRegistry(), htmlParser, null !, []);
const htmlResult = htmlParser.parse(template.source, '', true); const htmlResult = htmlParser.parse(template.source, '', true);
const analyzedModules = this.host.getAnalyzedModules(); const analyzedModules = this.host.getAnalyzedModules();
let errors: Diagnostic[] = undefined !; let errors: Diagnostic[]|undefined = undefined;
let ngModule = analyzedModules.ngModuleByPipeOrDirective.get(template.type); let ngModule = analyzedModules.ngModuleByPipeOrDirective.get(template.type);
if (!ngModule) { if (!ngModule) {
// Reported by the the declaration diagnostics. // Reported by the the declaration diagnostics.
@ -121,8 +122,7 @@ class LanguageServiceImpl implements LanguageService {
if (ngModule) { if (ngModule) {
const resolvedDirectives = ngModule.transitiveModule.directives.map( const resolvedDirectives = ngModule.transitiveModule.directives.map(
d => this.host.resolver.getNonNormalizedDirectiveMetadata(d.reference)); d => this.host.resolver.getNonNormalizedDirectiveMetadata(d.reference));
const directives = const directives = removeMissing(resolvedDirectives).map(d => d.metadata.toSummary());
resolvedDirectives.filter(d => d !== null).map(d => d !.metadata.toSummary());
const pipes = ngModule.transitiveModule.pipes.map( const pipes = ngModule.transitiveModule.pipes.map(
p => this.host.resolver.getOrLoadPipeMetadata(p.reference).toSummary()); p => this.host.resolver.getOrLoadPipeMetadata(p.reference).toSummary());
const schemas = ngModule.schemas; const schemas = ngModule.schemas;
@ -142,10 +142,14 @@ class LanguageServiceImpl implements LanguageService {
} }
result = {errors: [{kind: DiagnosticKind.Error, message: e.message, span}]}; result = {errors: [{kind: DiagnosticKind.Error, message: e.message, span}]};
} }
return result; return result || {};
} }
} }
function removeMissing<T>(values: (T | null | undefined)[]): T[] {
return values.filter(e => !!e) as T[];
}
function uniqueBySpan < T extends { function uniqueBySpan < T extends {
span: Span; span: Span;
} }
@ -169,8 +173,8 @@ function uniqueBySpan < T extends {
} }
} }
function findSuitableDefaultModule(modules: NgAnalyzedModules): CompileNgModuleMetadata { function findSuitableDefaultModule(modules: NgAnalyzedModules): CompileNgModuleMetadata|undefined {
let result: CompileNgModuleMetadata = undefined !; let result: CompileNgModuleMetadata|undefined = undefined;
let resultSize = 0; let resultSize = 0;
for (const module of modules.ngModules) { for (const module of modules.ngModules) {
const moduleSize = module.transitiveModule.directives.length; const moduleSize = module.transitiveModule.directives.length;

View File

@ -21,22 +21,25 @@ export interface SymbolInfo {
} }
export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined { export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined {
const templatePosition = info.position ! - info.template.span.start; if (!info.position) return undefined;
const templatePosition = info.position - info.template.span.start;
const path = new TemplateAstPath(info.templateAst, templatePosition); const path = new TemplateAstPath(info.templateAst, templatePosition);
if (path.tail) { if (path.tail) {
let symbol: Symbol = undefined !; let symbol: Symbol|undefined = undefined;
let span: Span = undefined !; let span: Span|undefined = undefined;
const attributeValueSymbol = (ast: AST, inEvent: boolean = false): boolean => { const attributeValueSymbol = (ast: AST, inEvent: boolean = false): boolean => {
const attribute = findAttribute(info); const attribute = findAttribute(info);
if (attribute) { if (attribute) {
if (inSpan(templatePosition, spanOf(attribute.valueSpan))) { if (inSpan(templatePosition, spanOf(attribute.valueSpan))) {
const scope = getExpressionScope(info, path, inEvent); const scope = getExpressionScope(info, path, inEvent);
const expressionOffset = attribute.valueSpan !.start.offset + 1; if (attribute.valueSpan) {
const result = getExpressionSymbol( const expressionOffset = attribute.valueSpan.start.offset + 1;
scope, ast, templatePosition - expressionOffset, info.template.query); const result = getExpressionSymbol(
if (result) { scope, ast, templatePosition - expressionOffset, info.template.query);
symbol = result.symbol; if (result) {
span = offsetSpan(result.span, expressionOffset); symbol = result.symbol;
span = offsetSpan(result.span, expressionOffset);
}
} }
return true; return true;
} }
@ -52,28 +55,28 @@ export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined {
if (component) { if (component) {
symbol = info.template.query.getTypeSymbol(component.directive.type.reference); symbol = info.template.query.getTypeSymbol(component.directive.type.reference);
symbol = symbol && new OverrideKindSymbol(symbol, 'component'); symbol = symbol && new OverrideKindSymbol(symbol, 'component');
span = spanOf(ast) !; span = spanOf(ast);
} else { } else {
// Find a directive that matches the element name // Find a directive that matches the element name
const directive = const directive = ast.directives.find(
ast.directives.find(d => d.directive.selector !.indexOf(ast.name) >= 0); d => d.directive.selector != null && d.directive.selector.indexOf(ast.name) >= 0);
if (directive) { if (directive) {
symbol = info.template.query.getTypeSymbol(directive.directive.type.reference); symbol = info.template.query.getTypeSymbol(directive.directive.type.reference);
symbol = symbol && new OverrideKindSymbol(symbol, 'directive'); symbol = symbol && new OverrideKindSymbol(symbol, 'directive');
span = spanOf(ast) !; span = spanOf(ast);
} }
} }
}, },
visitReference(ast) { visitReference(ast) {
symbol = info.template.query.getTypeSymbol(tokenReference(ast.value)); symbol = info.template.query.getTypeSymbol(tokenReference(ast.value));
span = spanOf(ast) !; span = spanOf(ast);
}, },
visitVariable(ast) {}, visitVariable(ast) {},
visitEvent(ast) { visitEvent(ast) {
if (!attributeValueSymbol(ast.handler, /* inEvent */ true)) { if (!attributeValueSymbol(ast.handler, /* inEvent */ true)) {
symbol = findOutputBinding(info, path, ast) !; symbol = findOutputBinding(info, path, ast);
symbol = symbol && new OverrideKindSymbol(symbol, 'event'); symbol = symbol && new OverrideKindSymbol(symbol, 'event');
span = spanOf(ast) !; span = spanOf(ast);
} }
}, },
visitElementProperty(ast) { attributeValueSymbol(ast.value); }, visitElementProperty(ast) { attributeValueSymbol(ast.value); },
@ -93,12 +96,12 @@ export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined {
visitText(ast) {}, visitText(ast) {},
visitDirective(ast) { visitDirective(ast) {
symbol = info.template.query.getTypeSymbol(ast.directive.type.reference); symbol = info.template.query.getTypeSymbol(ast.directive.type.reference);
span = spanOf(ast) !; span = spanOf(ast);
}, },
visitDirectiveProperty(ast) { visitDirectiveProperty(ast) {
if (!attributeValueSymbol(ast.value)) { if (!attributeValueSymbol(ast.value)) {
symbol = findInputBinding(info, path, ast) !; symbol = findInputBinding(info, path, ast);
span = spanOf(ast) !; span = spanOf(ast);
} }
} }
}, },
@ -110,9 +113,11 @@ export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined {
} }
function findAttribute(info: TemplateInfo): Attribute|undefined { function findAttribute(info: TemplateInfo): Attribute|undefined {
const templatePosition = info.position ! - info.template.span.start; if (info.position) {
const path = new HtmlAstPath(info.htmlAst, templatePosition); const templatePosition = info.position - info.template.span.start;
return path.first(Attribute); const path = new HtmlAstPath(info.htmlAst, templatePosition);
return path.first(Attribute);
}
} }
function findInputBinding( function findInputBinding(

View File

@ -22,18 +22,24 @@ class ReflectorModuleModuleResolutionHost implements ts.ModuleResolutionHost {
if (snapshot) { if (snapshot) {
return snapshot.getText(0, snapshot.getLength()); return snapshot.getText(0, snapshot.getLength());
} }
// Typescript readFile() declaration should be `readFile(fileName: string): string | undefined
return undefined !; return undefined !;
} }
directoryExists: (directoryName: string) => boolean; directoryExists: (directoryName: string) => boolean;
} }
// This reflector host's purpose is to first set verboseInvalidExpressions to true so the
// reflector will collect errors instead of throwing, and second to all deferring the creation
// of the program until it is actually needed.
export class ReflectorHost extends CompilerHost { export class ReflectorHost extends CompilerHost {
constructor( constructor(
private getProgram: () => ts.Program, serviceHost: ts.LanguageServiceHost, private getProgram: () => ts.Program, serviceHost: ts.LanguageServiceHost,
options: AngularCompilerOptions) { options: AngularCompilerOptions) {
super( super(
null !, options, // The ancestor value for program is overridden below so passing null here is safe.
/* program */ null !, options,
new ModuleResolutionHostAdapter(new ReflectorModuleModuleResolutionHost(serviceHost)), new ModuleResolutionHostAdapter(new ReflectorModuleModuleResolutionHost(serviceHost)),
{verboseInvalidExpression: true}); {verboseInvalidExpression: true});
} }

View File

@ -104,10 +104,10 @@ class TemplateAstPathBuilder extends TemplateAstChildVisitor {
constructor(private position: number, private allowWidening: boolean) { super(); } constructor(private position: number, private allowWidening: boolean) { super(); }
visit(ast: TemplateAst, context: any): any { visit(ast: TemplateAst, context: any): any {
let span = spanOf(ast) !; let span = spanOf(ast);
if (inSpan(this.position, span)) { if (inSpan(this.position, span)) {
const len = this.path.length; const len = this.path.length;
if (!len || this.allowWidening || isNarrower(span, spanOf(this.path[len - 1]) !)) { if (!len || this.allowWidening || isNarrower(span, spanOf(this.path[len - 1]))) {
this.path.push(ast); this.path.push(ast);
} }
} else { } else {

View File

@ -25,14 +25,16 @@ export function create(info: any /* ts.server.PluginCreateInfo */): ts.LanguageS
} }
function diagnosticToDiagnostic(d: Diagnostic, file: ts.SourceFile): ts.Diagnostic { function diagnosticToDiagnostic(d: Diagnostic, file: ts.SourceFile): ts.Diagnostic {
return { const result = {
file, file,
start: d.span.start, start: d.span.start,
length: d.span.end - d.span.start, length: d.span.end - d.span.start,
messageText: d.message, messageText: d.message,
category: ts.DiagnosticCategory.Error, category: ts.DiagnosticCategory.Error,
code: 0 code: 0,
source: 'ng'
}; };
return result;
} }
function tryOperation(attempting: string, callback: () => void) { function tryOperation(attempting: string, callback: () => void) {
@ -78,7 +80,7 @@ export function create(info: any /* ts.server.PluginCreateInfo */): ts.LanguageS
if (ours) { if (ours) {
const displayParts: typeof base.displayParts = []; const displayParts: typeof base.displayParts = [];
for (const part of ours.text) { for (const part of ours.text) {
displayParts.push({kind: part.language !, text: part.text}); displayParts.push({kind: part.language || 'angular', text: part.text});
} }
base = <any>{ base = <any>{
displayParts, displayParts,

View File

@ -8,8 +8,6 @@
import {CompileDirectiveMetadata, CompileMetadataResolver, NgAnalyzedModules, StaticSymbol} from '@angular/compiler'; import {CompileDirectiveMetadata, CompileMetadataResolver, NgAnalyzedModules, StaticSymbol} from '@angular/compiler';
/** /**
* The range of a span of text in a source file. * The range of a span of text in a source file.
* *
@ -455,7 +453,7 @@ export interface LanguageServiceHost {
* refers to a template file then the `position` should be ignored. If the `position` is not in a * refers to a template file then the `position` should be ignored. If the `position` is not in a
* template literal string then this method should return `undefined`. * template literal string then this method should return `undefined`.
*/ */
getTemplateAt(fileName: string, position: number): TemplateSource /* |undefined */; getTemplateAt(fileName: string, position: number): TemplateSource|undefined;
/** /**
* Return the template source information for all templates in `fileName` or for `fileName` if it * Return the template source information for all templates in `fileName` or for `fileName` if it

View File

@ -74,22 +74,22 @@ export class DummyResourceLoader extends ResourceLoader {
* @experimental * @experimental
*/ */
export class TypeScriptServiceHost implements LanguageServiceHost { export class TypeScriptServiceHost implements LanguageServiceHost {
private _resolver: CompileMetadataResolver; private _resolver: CompileMetadataResolver|null;
private _staticSymbolCache = new StaticSymbolCache(); private _staticSymbolCache = new StaticSymbolCache();
private _summaryResolver: AotSummaryResolver; private _summaryResolver: AotSummaryResolver;
private _staticSymbolResolver: StaticSymbolResolver; private _staticSymbolResolver: StaticSymbolResolver;
private _reflector: StaticReflector; private _reflector: StaticReflector|null;
private _reflectorHost: ReflectorHost; private _reflectorHost: ReflectorHost;
private _checker: ts.TypeChecker; private _checker: ts.TypeChecker|null;
private _typeCache: Symbol[] = []; private _typeCache: Symbol[] = [];
private context: string|undefined; private context: string|undefined;
private lastProgram: ts.Program|undefined; private lastProgram: ts.Program|undefined;
private modulesOutOfDate: boolean = true; private modulesOutOfDate: boolean = true;
private analyzedModules: NgAnalyzedModules; private analyzedModules: NgAnalyzedModules|null;
private service: LanguageService; private service: LanguageService;
private fileToComponent: Map<string, StaticSymbol>; private fileToComponent: Map<string, StaticSymbol>|null;
private templateReferences: string[]; private templateReferences: string[]|null;
private collectedErrors: Map<string, any[]>; private collectedErrors: Map<string, any[]>|null;
private fileVersions = new Map<string, string>(); private fileVersions = new Map<string, string>();
constructor(private host: ts.LanguageServiceHost, private tsService: ts.LanguageService) {} constructor(private host: ts.LanguageServiceHost, private tsService: ts.LanguageService) {}
@ -127,28 +127,28 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
getTemplateReferences(): string[] { getTemplateReferences(): string[] {
this.ensureTemplateMap(); this.ensureTemplateMap();
return this.templateReferences; return this.templateReferences || [];
} }
getTemplateAt(fileName: string, position: number): TemplateSource { getTemplateAt(fileName: string, position: number): TemplateSource|undefined {
let sourceFile = this.getSourceFile(fileName); let sourceFile = this.getSourceFile(fileName);
if (sourceFile) { if (sourceFile) {
this.context = sourceFile.fileName; this.context = sourceFile.fileName;
let node = this.findNode(sourceFile, position); let node = this.findNode(sourceFile, position);
if (node) { if (node) {
return this.getSourceFromNode( return this.getSourceFromNode(
fileName, this.host.getScriptVersion(sourceFile.fileName), node) !; fileName, this.host.getScriptVersion(sourceFile.fileName), node);
} }
} else { } else {
this.ensureTemplateMap(); this.ensureTemplateMap();
// TODO: Cannocalize the file? // TODO: Cannocalize the file?
const componentType = this.fileToComponent.get(fileName); const componentType = this.fileToComponent !.get(fileName);
if (componentType) { if (componentType) {
return this.getSourceFromType( return this.getSourceFromType(
fileName, this.host.getScriptVersion(fileName), componentType) !; fileName, this.host.getScriptVersion(fileName), componentType);
} }
} }
return null !; return undefined;
} }
getAnalyzedModules(): NgAnalyzedModules { getAnalyzedModules(): NgAnalyzedModules {
@ -172,7 +172,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
getTemplates(fileName: string): TemplateSources { getTemplates(fileName: string): TemplateSources {
this.ensureTemplateMap(); this.ensureTemplateMap();
const componentType = this.fileToComponent.get(fileName); const componentType = this.fileToComponent !.get(fileName);
if (componentType) { if (componentType) {
const templateSource = this.getTemplateAt(fileName, 0); const templateSource = this.getTemplateAt(fileName, 0);
if (templateSource) { if (templateSource) {
@ -225,10 +225,10 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
updateAnalyzedModules() { updateAnalyzedModules() {
this.validate(); this.validate();
if (this.modulesOutOfDate) { if (this.modulesOutOfDate) {
this.analyzedModules = null !; this.analyzedModules = null;
this._reflector = null !; this._reflector = null;
this.templateReferences = null !; this.templateReferences = null;
this.fileToComponent = null !; this.fileToComponent = null;
this.ensureAnalyzedModules(); this.ensureAnalyzedModules();
this.modulesOutOfDate = false; this.modulesOutOfDate = false;
} }
@ -273,10 +273,10 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
} }
private clearCaches() { private clearCaches() {
this._checker = null !; this._checker = null;
this._typeCache = []; this._typeCache = [];
this._resolver = null !; this._resolver = null;
this.collectedErrors = null !; this.collectedErrors = null;
this.modulesOutOfDate = true; this.modulesOutOfDate = true;
} }
@ -345,7 +345,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
if (declaration && declaration.name) { if (declaration && declaration.name) {
const sourceFile = this.getSourceFile(fileName); const sourceFile = this.getSourceFile(fileName);
return this.getSourceFromDeclaration( return this.getSourceFromDeclaration(
fileName, version, this.stringOf(node) !, shrink(spanOf(node)), fileName, version, this.stringOf(node) || '', shrink(spanOf(node)),
this.reflector.getStaticSymbol(sourceFile.fileName, declaration.name.text), this.reflector.getStaticSymbol(sourceFile.fileName, declaration.name.text),
declaration, node, sourceFile); declaration, node, sourceFile);
} }
@ -359,11 +359,13 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
let result: TemplateSource|undefined = undefined; let result: TemplateSource|undefined = undefined;
const declaration = this.getTemplateClassFromStaticSymbol(type); const declaration = this.getTemplateClassFromStaticSymbol(type);
if (declaration) { if (declaration) {
const snapshot = this.host.getScriptSnapshot(fileName) !; const snapshot = this.host.getScriptSnapshot(fileName);
const source = snapshot.getText(0, snapshot.getLength()); if (snapshot) {
result = this.getSourceFromDeclaration( const source = snapshot.getText(0, snapshot.getLength());
fileName, version, source, {start: 0, end: source.length}, type, declaration, declaration, result = this.getSourceFromDeclaration(
declaration.getSourceFile()); fileName, version, source, {start: 0, end: source.length}, type, declaration,
declaration, declaration.getSourceFile());
}
} }
return result; return result;
} }
@ -398,17 +400,19 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
return result; return result;
} }
private collectError(error: any, filePath: string) { private collectError(error: any, filePath: string|null) {
let errorMap = this.collectedErrors; if (filePath) {
if (!errorMap) { let errorMap = this.collectedErrors;
errorMap = this.collectedErrors = new Map(); if (!errorMap || !this.collectedErrors) {
errorMap = this.collectedErrors = new Map();
}
let errors = errorMap.get(filePath);
if (!errors) {
errors = [];
this.collectedErrors.set(filePath, errors);
}
errors.push(error);
} }
let errors = errorMap.get(filePath);
if (!errors) {
errors = [];
this.collectedErrors.set(filePath, errors);
}
errors.push(error);
} }
private get staticSymbolResolver(): StaticSymbolResolver { private get staticSymbolResolver(): StaticSymbolResolver {
@ -416,9 +420,9 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
if (!result) { if (!result) {
this._summaryResolver = new AotSummaryResolver( this._summaryResolver = new AotSummaryResolver(
{ {
loadSummary(filePath: string) { return null !; }, loadSummary(filePath: string) { return null; },
isSourceFile(sourceFilePath: string) { return true !; }, isSourceFile(sourceFilePath: string) { return true; },
getOutputFileName(sourceFilePath: string) { return null !; } getOutputFileName(sourceFilePath: string) { return sourceFilePath; }
}, },
this._staticSymbolCache); this._staticSymbolCache);
result = this._staticSymbolResolver = new StaticSymbolResolver( result = this._staticSymbolResolver = new StaticSymbolResolver(
@ -445,7 +449,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
const declarationNode = ts.forEachChild(source, child => { const declarationNode = ts.forEachChild(source, child => {
if (child.kind === ts.SyntaxKind.ClassDeclaration) { if (child.kind === ts.SyntaxKind.ClassDeclaration) {
const classDeclaration = child as ts.ClassDeclaration; const classDeclaration = child as ts.ClassDeclaration;
if (classDeclaration.name !.text === type.name) { if (classDeclaration.name != null && classDeclaration.name.text === type.name) {
return classDeclaration; return classDeclaration;
} }
} }
@ -614,7 +618,7 @@ class TypeScriptSymbolQuery implements SymbolQuery {
private program: ts.Program, private checker: ts.TypeChecker, private source: ts.SourceFile, private program: ts.Program, private checker: ts.TypeChecker, private source: ts.SourceFile,
private fetchPipes: () => SymbolTable) {} private fetchPipes: () => SymbolTable) {}
getTypeKind(symbol: Symbol): BuiltinType { return typeKindOf(this.getTsTypeOf(symbol) !); } getTypeKind(symbol: Symbol): BuiltinType { return typeKindOf(this.getTsTypeOf(symbol)); }
getBuiltinType(kind: BuiltinType): Symbol { getBuiltinType(kind: BuiltinType): Symbol {
// TODO: Replace with typeChecker API when available. // TODO: Replace with typeChecker API when available.
@ -1303,7 +1307,7 @@ function getTypeParameterOf(type: ts.Type, name: string): ts.Type|undefined {
} }
} }
function typeKindOf(type: ts.Type): BuiltinType { function typeKindOf(type: ts.Type | undefined): BuiltinType {
if (type) { if (type) {
if (type.flags & ts.TypeFlags.Any) { if (type.flags & ts.TypeFlags.Any) {
return BuiltinType.Any; return BuiltinType.Any;
@ -1318,17 +1322,19 @@ function typeKindOf(type: ts.Type): BuiltinType {
return BuiltinType.Null; return BuiltinType.Null;
} else if (type.flags & ts.TypeFlags.Union) { } else if (type.flags & ts.TypeFlags.Union) {
// If all the constituent types of a union are the same kind, it is also that kind. // If all the constituent types of a union are the same kind, it is also that kind.
let candidate: BuiltinType = undefined !; let candidate: BuiltinType|null = null;
const unionType = type as ts.UnionType; const unionType = type as ts.UnionType;
if (unionType.types.length > 0) { if (unionType.types.length > 0) {
candidate = typeKindOf(unionType.types[0]) !; candidate = typeKindOf(unionType.types[0]);
for (const subType of unionType.types) { for (const subType of unionType.types) {
if (candidate != typeKindOf(subType)) { if (candidate != typeKindOf(subType)) {
return BuiltinType.Other; return BuiltinType.Other;
} }
} }
} }
return candidate; if (candidate != null) {
return candidate;
}
} else if (type.flags & ts.TypeFlags.TypeParameter) { } else if (type.flags & ts.TypeFlags.TypeParameter) {
return BuiltinType.Unbound; return BuiltinType.Unbound;
} }

View File

@ -21,6 +21,9 @@ export function isParseSourceSpan(value: any): value is ParseSourceSpan {
return value && !!value.start; return value && !!value.start;
} }
export function spanOf(span: SpanHolder): Span;
export function spanOf(span: ParseSourceSpan): Span;
export function spanOf(span: SpanHolder | ParseSourceSpan | undefined): Span|undefined;
export function spanOf(span?: SpanHolder | ParseSourceSpan): Span|undefined { export function spanOf(span?: SpanHolder | ParseSourceSpan): Span|undefined {
if (!span) return undefined; if (!span) return undefined;
if (isParseSourceSpan(span)) { if (isParseSourceSpan(span)) {
@ -39,8 +42,8 @@ export function spanOf(span?: SpanHolder | ParseSourceSpan): Span|undefined {
} }
export function inSpan(position: number, span?: Span, exclusive?: boolean): boolean { export function inSpan(position: number, span?: Span, exclusive?: boolean): boolean {
return span && exclusive ? position >= span.start && position < span.end : return span != null && (exclusive ? position >= span.start && position < span.end :
position >= span !.start && position <= span !.end; position >= span.start && position <= span.end);
} }
export function offsetSpan(span: Span, amount: number): Span { export function offsetSpan(span: Span, amount: number): Span {
@ -54,7 +57,8 @@ export function isNarrower(spanA: Span, spanB: Span): boolean {
export function hasTemplateReference(type: CompileTypeMetadata): boolean { export function hasTemplateReference(type: CompileTypeMetadata): boolean {
if (type.diDeps) { if (type.diDeps) {
for (let diDep of type.diDeps) { for (let diDep of type.diDeps) {
if (diDep.token !.identifier && identifierName(diDep.token !.identifier !) == 'TemplateRef') if (diDep.token && diDep.token.identifier &&
identifierName(diDep.token !.identifier !) == 'TemplateRef')
return true; return true;
} }
} }

View File

@ -13,8 +13,9 @@ import {WebWorkerPlatformLocation} from './platform_location';
/** /**
* Those providers should be added when the router is used in a worker context in addition to the * The {@link PlatformLocation} providers that should be added when the {@link Location} is used in
* {@link ROUTER_PROVIDERS} and after them. * a worker context.
*
* @experimental * @experimental
*/ */
export const WORKER_APP_LOCATION_PROVIDERS = [ export const WORKER_APP_LOCATION_PROVIDERS = [

View File

@ -20,7 +20,6 @@ import {UrlSegment, UrlSegmentGroup} from './url_tree';
* - `path` is a string that uses the route matcher DSL. * - `path` is a string that uses the route matcher DSL.
* - `pathMatch` is a string that specifies the matching strategy. * - `pathMatch` is a string that specifies the matching strategy.
* - `matcher` defines a custom strategy for path matching and supersedes `path` and `pathMatch`. * - `matcher` defines a custom strategy for path matching and supersedes `path` and `pathMatch`.
* See {@link UrlMatcher} for more info.
* - `component` is a component type. * - `component` is a component type.
* - `redirectTo` is the url fragment which will replace the current matched segment. * - `redirectTo` is the url fragment which will replace the current matched segment.
* - `outlet` is the name of the outlet the component should be placed into. * - `outlet` is the name of the outlet the component should be placed into.

View File

@ -92,7 +92,7 @@ export class RouterOutlet implements OnDestroy {
} }
} }
/** @deprecated since v4, use {@link activateWith} */ /** @deprecated since v4, use {@link #activateWith} */
activate( activate(
activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver, injector: Injector, activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver, injector: Injector,
providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void { providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void {

View File

@ -11,7 +11,7 @@ import {ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injecto
import * as angular from './angular1'; import * as angular from './angular1';
import {PropertyBinding} from './component_info'; import {PropertyBinding} from './component_info';
import {$SCOPE} from './constants'; import {$SCOPE} from './constants';
import {getAttributesAsArray, getComponentName, hookupNgModel} from './util'; import {getAttributesAsArray, getComponentName, hookupNgModel, strictEquals} from './util';
const INITIAL_VALUE = { const INITIAL_VALUE = {
__UNINITIALIZED__: true __UNINITIALIZED__: true
@ -75,16 +75,28 @@ export class DowngradeComponentAdapter {
const observeFn = (prop => { const observeFn = (prop => {
let prevValue = INITIAL_VALUE; let prevValue = INITIAL_VALUE;
return (currValue: any) => { return (currValue: any) => {
if (prevValue === INITIAL_VALUE) { // Initially, both `$observe()` and `$watch()` will call this function.
if (!strictEquals(prevValue, currValue)) {
if (prevValue === INITIAL_VALUE) {
prevValue = currValue;
}
this.updateInput(prop, prevValue, currValue);
prevValue = currValue; prevValue = currValue;
} }
this.updateInput(prop, prevValue, currValue);
prevValue = currValue;
}; };
})(input.prop); })(input.prop);
attrs.$observe(input.attr, observeFn); attrs.$observe(input.attr, observeFn);
// Use `$watch()` (in addition to `$observe()`) in order to initialize the input in time
// for `ngOnChanges()`. This is necessary if we are already in a `$digest`, which means that
// `ngOnChanges()` (which is called by a watcher) will run before the `$observe()` callback.
let unwatch: any = this.componentScope.$watch(() => {
unwatch();
unwatch = null;
observeFn((attrs as any)[input.attr]);
});
} else if (attrs.hasOwnProperty(input.bindAttr)) { } else if (attrs.hasOwnProperty(input.bindAttr)) {
expr = (attrs as any /** TODO #9100 */)[input.bindAttr]; expr = (attrs as any /** TODO #9100 */)[input.bindAttr];
} else if (attrs.hasOwnProperty(input.bracketAttr)) { } else if (attrs.hasOwnProperty(input.bracketAttr)) {

View File

@ -75,3 +75,10 @@ export function hookupNgModel(ngModel: angular.INgModelController, component: an
component.registerOnChange(ngModel.$setViewValue.bind(ngModel)); component.registerOnChange(ngModel.$setViewValue.bind(ngModel));
} }
} }
/**
* Test two values for strict equality, accounting for the fact that `NaN !== NaN`.
*/
export function strictEquals(val1: any, val2: any): boolean {
return val1 === val2 || (val1 !== val1 && val2 !== val2);
}

View File

@ -10,7 +10,7 @@ import {Directive, DoCheck, ElementRef, EventEmitter, Inject, OnChanges, OnInit,
import * as angular from '../common/angular1'; import * as angular from '../common/angular1';
import {$COMPILE, $CONTROLLER, $HTTP_BACKEND, $SCOPE, $TEMPLATE_CACHE} from '../common/constants'; import {$COMPILE, $CONTROLLER, $HTTP_BACKEND, $SCOPE, $TEMPLATE_CACHE} from '../common/constants';
import {controllerKey} from '../common/util'; import {controllerKey, strictEquals} from '../common/util';
interface IBindingDestination { interface IBindingDestination {
@ -309,8 +309,7 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
checkProperties.forEach((propName, i) => { checkProperties.forEach((propName, i) => {
const value = destinationObj ![propName]; const value = destinationObj ![propName];
const last = lastValues[i]; const last = lastValues[i];
if (value !== last && if (!strictEquals(last, value)) {
(value === value || last === last)) { // ignore NaN values (NaN !== NaN)
const eventEmitter: EventEmitter<any> = (this as any)[propOuts[i]]; const eventEmitter: EventEmitter<any> = (this as any)[propOuts[i]];
eventEmitter.emit(lastValues[i] = value); eventEmitter.emit(lastValues[i] = value);
} }

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ChangeDetectorRef, Class, Component, EventEmitter, Input, NO_ERRORS_SCHEMA, NgModule, NgZone, SimpleChange, SimpleChanges, Testability, destroyPlatform, forwardRef} from '@angular/core'; import {ChangeDetectorRef, Class, Component, EventEmitter, Input, NO_ERRORS_SCHEMA, NgModule, NgZone, OnChanges, SimpleChange, SimpleChanges, Testability, destroyPlatform, forwardRef} from '@angular/core';
import {async, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; import {async, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
@ -410,6 +410,63 @@ export function main() {
})); }));
it('should initialize inputs in time for `ngOnChanges`', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
@Component({
selector: 'ng2',
template: `
ngOnChangesCount: {{ ngOnChangesCount }} |
firstChangesCount: {{ firstChangesCount }} |
initialValue: {{ initialValue }}`
})
class Ng2Component implements OnChanges {
ngOnChangesCount = 0;
firstChangesCount = 0;
initialValue: string;
@Input() foo: string;
ngOnChanges(changes: SimpleChanges) {
this.ngOnChangesCount++;
if (this.ngOnChangesCount === 1) {
this.initialValue = this.foo;
}
if (changes['foo'] && changes['foo'].isFirstChange()) {
this.firstChangesCount++;
}
}
}
@NgModule({imports: [BrowserModule], declarations: [Ng2Component]})
class Ng2Module {
}
const ng1Module = angular.module('ng1', []).directive(
'ng2', adapter.downgradeNg2Component(Ng2Component));
const element = html(`
<ng2 [foo]="'foo'"></ng2>
<ng2 foo="bar"></ng2>
<ng2 [foo]="'baz'" ng-if="true"></ng2>
<ng2 foo="qux" ng-if="true"></ng2>
`);
adapter.bootstrap(element, ['ng1']).ready(ref => {
const nodes = element.querySelectorAll('ng2');
const expectedTextWith = (value: string) =>
`ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`;
expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo'));
expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar'));
expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz'));
expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux'));
ref.dispose();
});
}));
it('should bind to ng-model', async(() => { it('should bind to ng-model', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module = angular.module('ng1', []); const ng1Module = angular.module('ng1', []);

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Compiler, Component, ComponentFactoryResolver, EventEmitter, Injector, NgModule, NgModuleRef, OnChanges, OnDestroy, SimpleChanges, destroyPlatform} from '@angular/core'; import {Compiler, Component, ComponentFactoryResolver, EventEmitter, Injector, Input, NgModule, NgModuleRef, OnChanges, OnDestroy, SimpleChanges, destroyPlatform} from '@angular/core';
import {async} from '@angular/core/testing'; import {async} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
@ -148,6 +148,64 @@ export function main() {
}); });
})); }));
it('should initialize inputs in time for `ngOnChanges`', async(() => {
@Component({
selector: 'ng2',
template: `
ngOnChangesCount: {{ ngOnChangesCount }} |
firstChangesCount: {{ firstChangesCount }} |
initialValue: {{ initialValue }}`
})
class Ng2Component implements OnChanges {
ngOnChangesCount = 0;
firstChangesCount = 0;
initialValue: string;
@Input() foo: string;
ngOnChanges(changes: SimpleChanges) {
this.ngOnChangesCount++;
if (this.ngOnChangesCount === 1) {
this.initialValue = this.foo;
}
if (changes['foo'] && changes['foo'].isFirstChange()) {
this.firstChangesCount++;
}
}
}
@NgModule({
imports: [BrowserModule, UpgradeModule],
declarations: [Ng2Component],
entryComponents: [Ng2Component]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module = angular.module('ng1', []).directive(
'ng2', downgradeComponent({component: Ng2Component}));
const element = html(`
<ng2 [foo]="'foo'"></ng2>
<ng2 foo="bar"></ng2>
<ng2 [foo]="'baz'" ng-if="true"></ng2>
<ng2 foo="qux" ng-if="true"></ng2>
`);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
const nodes = element.querySelectorAll('ng2');
const expectedTextWith = (value: string) =>
`ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`;
expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo'));
expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar'));
expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz'));
expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux'));
});
}));
it('should bind to ng-model', async(() => { it('should bind to ng-model', async(() => {
const ng1Module = angular.module('ng1', []).run( const ng1Module = angular.module('ng1', []).run(
($rootScope: angular.IScope) => { $rootScope['modelA'] = 'A'; }); ($rootScope: angular.IScope) => { $rootScope['modelA'] = 'A'; });

View File

@ -37,16 +37,6 @@ travisFoldStart "tsc a bunch of useless stuff"
travisFoldEnd "tsc a bunch of useless stuff" travisFoldEnd "tsc a bunch of useless stuff"
# Build integration tests
if [[ ${CI_MODE:-} == "e2e_2" ]]; then
travisFoldStart "build.integration"
cd "`dirname $0`/../../integration"
./build_rxjs_es6.sh
cd -
travisFoldEnd "build.integration"
fi
# Build angular.io # Build angular.io
if [[ ${CI_MODE:-} == "aio" ]]; then if [[ ${CI_MODE:-} == "aio" ]]; then
travisFoldStart "build.aio" travisFoldStart "build.aio"

View File

@ -1,6 +1,6 @@
{ {
"name": "@angular/tsc-wrapped", "name": "@angular/tsc-wrapped",
"version": "4.1.0", "version": "4.1.1",
"description": "Wraps the tsc CLI, allowing extensions.", "description": "Wraps the tsc CLI, allowing extensions.",
"homepage": "https://github.com/angular/angular/tree/master/tools/tsc-wrapped", "homepage": "https://github.com/angular/angular/tree/master/tools/tsc-wrapped",
"bugs": "https://github.com/angular/angular/issues", "bugs": "https://github.com/angular/angular/issues",

View File

@ -179,18 +179,18 @@ export declare class FormArray extends AbstractControl {
at(index: number): AbstractControl; at(index: number): AbstractControl;
getRawValue(): any[]; getRawValue(): any[];
insert(index: number, control: AbstractControl): void; insert(index: number, control: AbstractControl): void;
patchValue(value: any[], {onlySelf, emitEvent}?: { patchValue(value: any[], options?: {
onlySelf?: boolean; onlySelf?: boolean;
emitEvent?: boolean; emitEvent?: boolean;
}): void; }): void;
push(control: AbstractControl): void; push(control: AbstractControl): void;
removeAt(index: number): void; removeAt(index: number): void;
reset(value?: any, {onlySelf, emitEvent}?: { reset(value?: any, options?: {
onlySelf?: boolean; onlySelf?: boolean;
emitEvent?: boolean; emitEvent?: boolean;
}): void; }): void;
setControl(index: number, control: AbstractControl): void; setControl(index: number, control: AbstractControl): void;
setValue(value: any[], {onlySelf, emitEvent}?: { setValue(value: any[], options?: {
onlySelf?: boolean; onlySelf?: boolean;
emitEvent?: boolean; emitEvent?: boolean;
}): void; }): void;
@ -231,11 +231,11 @@ export declare class FormControl extends AbstractControl {
}): void; }): void;
registerOnChange(fn: Function): void; registerOnChange(fn: Function): void;
registerOnDisabledChange(fn: (isDisabled: boolean) => void): void; registerOnDisabledChange(fn: (isDisabled: boolean) => void): void;
reset(formState?: any, {onlySelf, emitEvent}?: { reset(formState?: any, options?: {
onlySelf?: boolean; onlySelf?: boolean;
emitEvent?: boolean; emitEvent?: boolean;
}): void; }): void;
setValue(value: any, {onlySelf, emitEvent, emitModelToViewChange, emitViewToModelChange}?: { setValue(value: any, options?: {
onlySelf?: boolean; onlySelf?: boolean;
emitEvent?: boolean; emitEvent?: boolean;
emitModelToViewChange?: boolean; emitModelToViewChange?: boolean;
@ -289,20 +289,20 @@ export declare class FormGroup extends AbstractControl {
getRawValue(): any; getRawValue(): any;
patchValue(value: { patchValue(value: {
[key: string]: any; [key: string]: any;
}, {onlySelf, emitEvent}?: { }, options?: {
onlySelf?: boolean; onlySelf?: boolean;
emitEvent?: boolean; emitEvent?: boolean;
}): void; }): void;
registerControl(name: string, control: AbstractControl): AbstractControl; registerControl(name: string, control: AbstractControl): AbstractControl;
removeControl(name: string): void; removeControl(name: string): void;
reset(value?: any, {onlySelf, emitEvent}?: { reset(value?: any, options?: {
onlySelf?: boolean; onlySelf?: boolean;
emitEvent?: boolean; emitEvent?: boolean;
}): void; }): void;
setControl(name: string, control: AbstractControl): void; setControl(name: string, control: AbstractControl): void;
setValue(value: { setValue(value: {
[key: string]: any; [key: string]: any;
}, {onlySelf, emitEvent}?: { }, options?: {
onlySelf?: boolean; onlySelf?: boolean;
emitEvent?: boolean; emitEvent?: boolean;
}): void; }): void;
@ -532,8 +532,8 @@ export interface ValidatorFn {
/** @stable */ /** @stable */
export declare class Validators { export declare class Validators {
static compose(validators: null): null;
static compose(validators: (ValidatorFn | null | undefined)[]): ValidatorFn | null; static compose(validators: (ValidatorFn | null | undefined)[]): ValidatorFn | null;
static compose(validators: null): null;
static composeAsync(validators: (AsyncValidatorFn | null)[]): AsyncValidatorFn | null; static composeAsync(validators: (AsyncValidatorFn | null)[]): AsyncValidatorFn | null;
static email(control: AbstractControl): ValidationErrors | null; static email(control: AbstractControl): ValidationErrors | null;
static maxLength(maxLength: number): ValidatorFn; static maxLength(maxLength: number): ValidatorFn;