Compare commits

...

70 Commits
7.2.0 ... 2.3.1

Author SHA1 Message Date
540b1197a6 fix(form): fix merge errors 2016-12-14 18:22:03 -08:00
d30cc8461b docs(changelog): add changelog for 2.3.1 2016-12-14 18:14:34 -08:00
f27954e62c build: bump angular to 2.3.1 & tsc-wrapped to 0.5.0 2016-12-14 18:11:35 -08:00
69b52eb2b3 fix(compiler): fix merge error in compiler_host 2016-12-14 18:08:54 -08:00
b9b557cdb0 fix(compiler): update to metadata version 3 (#13464)
This change retracts support for metadata version 2.

The collector used to produce version 2 metadata was incomplete
and can cause the AOT compiler to fail to resolve symbols or
produce other spurious errors.

All libraries compiled and published with 2.3.0 ngc will need
to be recompiled and updated with this change.
2016-12-14 18:08:48 -08:00
a72a002a8d refactor: format & lint 2016-12-14 18:08:43 -08:00
a0437f8c9d chore(animations/aot): always export NoOpAnimationDriver (#13480) 2016-12-14 18:08:36 -08:00
1c279b3264 fix(compiler): fix simplify a reference without a name
closes #13470
2016-12-14 18:08:32 -08:00
cd03c77364 fix(tsc-wrapped): generate metadata for exports without module specifier
fixes #13327
2016-12-14 18:08:29 -08:00
f6ef7d6e5a fix(compiler): propagate exports when upgrading metadata to v2 2016-12-14 18:08:25 -08:00
6aeaca3fb4 fix(compiler): resolver should merge host bindings and listeners (#13474)
fixes #13327
2016-12-14 18:07:41 -08:00
af62050729 docs(upgrade): fix UpgradeAdapter examples
closes #12675
2016-12-14 18:02:26 -08:00
cb69656b56 docs(upgrade/upgrade_adapter): fix up references to AngularJS and Angular 2 2016-12-14 18:02:14 -08:00
2fc0560988 feat(upgrade): enable Angular 1 unit testing of upgrade module
- New method `UpgradeAdapter.registerForNg1Tests(modules)` declares the
  Angular 1 upgrade module and provides it to the `angular.mock.module()`
  helper.
  This prevents the need to bootstrap the entire hybrid for every test.

Closes #5462, #12675
2016-12-14 18:02:05 -08:00
86c50983d7 fix(upgrade): fix downgrade content projection and injector inheritance
- Full support for content projection in downgraded Angular 2
  components. In particular, this enables multi-slot projection and
  other features on <ng-content>.
- Correctly wire up hierarchical injectors for downgraded Angular 2
  components: downgraded components inherit the injector of the first
  other downgraded Angular 2 component they find up the DOM tree.

Closes #6629, #7727, #8729, #9643, #9649, #12675
2016-12-14 17:56:20 -08:00
21976446e0 refactor(upgrade/upgrade_adapter): use Deferred helper
Making Angular 1's `$compile` asynchronous by chaining injector promises
in linking functions can cause flickering views in applications.
2016-12-14 17:56:16 -08:00
998ce9ad7e refactor(upgrade/util): remove unused stringify() method 2016-12-14 17:56:11 -08:00
111523677c refactor(compiler/template_parser): export createElementCssSelector
This is needed in `ngUpgrade`.
2016-12-14 17:56:06 -08:00
2d74a224d0 refactor(upgrade): add missing Angular 1 type info 2016-12-14 17:55:56 -08:00
4d6ac9d414 fix(core): detectChanges() doesn't work on detached instance
Closes #13426
Closes #13472
2016-12-14 17:55:38 -08:00
6557bc34f6 fix(animations): throw errors and normalize offset beyond the range of [0,1]
Closes #13348
Closes #13440
2016-12-14 17:55:34 -08:00
e2622add07 perf(animations): always run the animation queue outside of zones
Related #12732
Closes #13440
2016-12-14 17:55:27 -08:00
ecfad467a1 fix(compiler): emit quoted object literal keys if the source is quoted
feat(tsc-wrapped): recored when to quote a object literal key

Collecting quoted literals is off by default as it introduces
a breaking change in the .metadata.json file. A follow-up commit
will address this.

Fixes #13249
Closes #13356
2016-12-14 17:55:22 -08:00
5918133784 Revert "fix(compiler): xmb <ph> tags should not self close (#13413)"
This reverts commit 4b3d135193.
closes #13463
2016-12-14 17:55:18 -08:00
700bce9ec1 Revert "test(i18n): fix a typo in the reference xmb (#13441)"
This reverts commit a8d237581d.
2016-12-14 17:55:14 -08:00
a64a35a8c1 refactor(facade): don't expect super() to return a new Error object in BaseError (#12600)
Related to #12575
2016-12-14 17:55:10 -08:00
b3dcff0cc1 fix(forms): ensure select[multiple] retains selections
If you bound an array to select[multiple] via ngModel and subsequently
changed the options to select from, the UI would drop any selections
made since by the user. This was due to
SelectMultipleControlValueAccessor not keeping a reference to the new
model arrays it generated when users interacted with the select control.
Update code to keep the reference.

Closes #12527
Closes #12654
2016-12-14 17:55:02 -08:00
124267c87a fix(forms): introduce checkbox required validator
Closes #11459
Closes #13364
2016-12-14 17:52:53 -08:00
547bfa92ef fix (forms): clear selected options when model is not an array (#12519)
When an invalid model value (eg empty string) was preset ngModel on
select[multiple] would throw an error, which is inconsistent with how it
works on other user input elements. Setting the model value to null or
undefined would also have no effect on what was already selected in the
UI. Fix this by clearing selected options when model set to null,
undefined or a type other than Array.

Closes #11926
2016-12-14 17:52:02 -08:00
d40bbf4d5c fix(core): properly destroy embedded Views attatched to ApplicationRef (#13459)
Fixes #13062
2016-12-14 17:51:56 -08:00
94b7031fe9 refactor: format & lint 2016-12-14 17:51:48 -08:00
df0bf1dd74 chore(internal API): introduce an internal API for ngtools. (#13415) 2016-12-14 17:51:40 -08:00
c8a9b70890 fix(compiler): generated CSS files suffixed with ngstyle. (#13353)
Mirrors factories which ends in `ngfactory`.

Closes #13141.
2016-12-14 17:50:29 -08:00
efa2d80df8 fix(compiler): make sure provider values with name property don’t break.
Fixes #13394
Closes #13445
2016-12-14 17:50:22 -08:00
a58e5efd09 test(i18n): fix a typo in the reference xmb (#13441) 2016-12-14 17:50:12 -08:00
86cf0ef892 refactor: remove intl from facades (#13404)
The existing intl.ts file is not a facade but
rather a set of utils used by i18n-related pipes only.
As such moving it back to common module so those utils
are not used accidently from other places.
2016-12-14 17:50:02 -08:00
5c568fab86 test(upgrade): fix failing test in browsers which do not support RAF
closes #13399
2016-12-14 17:49:52 -08:00
566104504c ci(browser providers): update browsers in SL and BS (#13431) 2016-12-14 17:49:37 -08:00
307d305b2d fix(compiler): narrow the span reported for invalid pipes
fixes #13326
closes #13411
2016-12-14 17:49:05 -08:00
0a7364feea fix(language-service): correctly type undefined
fixes #13412
closes #13414
2016-12-14 17:40:58 -08:00
4544b1d7a6 fix(compiler): xmb <ph> tags should not self close (#13413) 2016-12-14 17:39:51 -08:00
9e0e6b59d1 docs(core): update OnDestroy description (#13369)
Closes #11228
2016-12-14 17:39:45 -08:00
14dd2b367a fix(language-service): treat string unions as strings (#13406)
Fixes #13403
2016-12-14 17:39:36 -08:00
91eb8914dd build: update the package list of the symlinks scripts for Windows (#13408) 2016-12-14 17:39:30 -08:00
77823d721f refactor: format and lint code 2016-12-14 17:38:31 -08:00
2afe2d107f docs(Location): updating Location docs and adding example
closes #11500
2016-12-14 17:38:22 -08:00
17f40fb75f chore: Add @types/systemjs 2016-12-14 17:37:43 -08:00
98936fdf16 chore: convert hash_location_strategy example to a tested spec 2016-12-14 17:37:33 -08:00
7383e4a801 fix(forms): fix Validators.min/maxLength with FormArray (#13095)
Fixes #13089
2016-12-14 17:37:18 -08:00
65c9b5b6aa fix(http): create a copy of headers when merge options (#13365)
Closes #11980
2016-12-14 17:36:48 -08:00
5fab8710cb fix(dom_adapter): remove logError from logGroup (#12925) 2016-12-14 17:35:12 -08:00
f106a18b96 fix(http): check response body text against undefined (#13017) 2016-12-14 17:35:05 -08:00
8db184d349 fix(compiler): support dotted property binding
fixes angular/flex-layout#34
2016-12-14 17:31:08 -08:00
c18eb298eb test(Selector): add a test for dotted attribute names 2016-12-14 17:31:00 -08:00
3f4aa59cfa refactor(Compiler): cleanup 2016-12-14 17:30:50 -08:00
79728b4c41 fix(compiler): fix PR 13322 (#13331) 2016-12-14 17:30:40 -08:00
413167ab1b style: clang-format the code 2016-12-14 17:26:52 -08:00
203cc7e1f1 fix: Better instructions on running examples and their tests 2016-12-14 17:23:11 -08:00
b0cd514709 fix: Better error when directive not listed in NgModule.declarations 2016-12-14 17:22:58 -08:00
392c9ac214 fix(selector): SelectorMatcher match elements with :not selector (#12977) 2016-12-14 17:21:34 -08:00
a26e054857 fix(animations): always cleanup players after they have finished internally (#13334)
Closes #13333
Closes #13334
2016-12-14 17:21:23 -08:00
c0b001a6af refactor(router): misc refactoring (#13330) 2016-12-14 17:21:09 -08:00
c8c1f22f9c refactor(router): simplify regexp
closes #11373
closes #13329
2016-12-14 17:20:47 -08:00
e4d5a5f003 fix(router): add support for query params with mulitple values
closes #11373
2016-12-14 17:20:22 -08:00
03d9de33a1 Revert "fix(compiler): fix transpiled ES5 code (#13322)"
This reverts commit 4398056146.
2016-12-14 17:20:14 -08:00
a8a80cf523 doc: update triage owners for language service and router (#13325) 2016-12-14 17:19:50 -08:00
6c1d7908d5 fix(compiler): fix transpiled ES5 code (#13322)
fixes #13301

The inner class would transpile to a nested function declaration which is not
allowed in ES5.

See http://eslint.org/docs/rules/no-inner-declarations
2016-12-14 17:18:12 -08:00
9aab6d24eb build(tslint): enable no-inner-declarations (#13316) 2016-12-14 17:18:02 -08:00
5ee8155e4e fix(router): Use T type in Resolve interface (#13242) 2016-12-14 17:17:45 -08:00
21de0f239d docs(changelog): fix a typo (#13298) 2016-12-14 17:16:49 -08:00
144 changed files with 10379 additions and 4852 deletions

View File

@ -1,3 +1,52 @@
<a name="2.3.1"></a>
## [2.3.1](https://github.com/angular/angular/compare/2.3.0...2.3.1) (2016-12-15)
### Bug Fixes
* **animations:** always cleanup players after they have finished internally ([#13334](https://github.com/angular/angular/issues/13334)) ([a26e054](https://github.com/angular/angular/commit/a26e054)), closes [#13333](https://github.com/angular/angular/issues/13333)
* **animations:** throw errors and normalize offset beyond the range of [0,1] ([6557bc3](https://github.com/angular/angular/commit/6557bc3)), closes [#13348](https://github.com/angular/angular/issues/13348) [#13440](https://github.com/angular/angular/issues/13440)
* **compiler:** emit quoted object literal keys if the source is quoted ([ecfad46](https://github.com/angular/angular/commit/ecfad46)), closes [#13249](https://github.com/angular/angular/issues/13249) [#13356](https://github.com/angular/angular/issues/13356)
* **compiler:** fix merge error in compiler_host ([69b52eb](https://github.com/angular/angular/commit/69b52eb))
* **compiler:** fix PR 13322 ([#13331](https://github.com/angular/angular/issues/13331)) ([79728b4](https://github.com/angular/angular/commit/79728b4))
* **compiler:** fix simplify a reference without a name ([1c279b3](https://github.com/angular/angular/commit/1c279b3)), closes [#13470](https://github.com/angular/angular/issues/13470)
* **compiler:** fix transpiled ES5 code ([#13322](https://github.com/angular/angular/issues/13322)) ([6c1d790](https://github.com/angular/angular/commit/6c1d790)), closes [#13301](https://github.com/angular/angular/issues/13301)
* **compiler:** generated CSS files suffixed with ngstyle. ([#13353](https://github.com/angular/angular/issues/13353)) ([c8a9b70](https://github.com/angular/angular/commit/c8a9b70)), closes [#13141](https://github.com/angular/angular/issues/13141)
* **compiler:** make sure provider values with `name` property dont break. ([efa2d80](https://github.com/angular/angular/commit/efa2d80)), closes [#13394](https://github.com/angular/angular/issues/13394) [#13445](https://github.com/angular/angular/issues/13445)
* **compiler:** narrow the span reported for invalid pipes ([307d305](https://github.com/angular/angular/commit/307d305)), closes [#13326](https://github.com/angular/angular/issues/13326) [#13411](https://github.com/angular/angular/issues/13411)
* **compiler:** propagate exports when upgrading metadata to v2 ([f6ef7d6](https://github.com/angular/angular/commit/f6ef7d6))
* **compiler:** resolver should merge host bindings and listeners ([#13474](https://github.com/angular/angular/issues/13474)) ([6aeaca3](https://github.com/angular/angular/commit/6aeaca3)), closes [#13327](https://github.com/angular/angular/issues/13327)
* **compiler:** support dotted property binding ([8db184d](https://github.com/angular/angular/commit/8db184d)), closes [angular/flex-layout#34](https://github.com/angular/flex-layout/issues/34)
* **compiler:** update to metadata version 3 ([#13464](https://github.com/angular/angular/issues/13464)) ([b9b557c](https://github.com/angular/angular/commit/b9b557c))
* **core:** detectChanges() doesn't work on detached instance ([4d6ac9d](https://github.com/angular/angular/commit/4d6ac9d)), closes [#13426](https://github.com/angular/angular/issues/13426) [#13472](https://github.com/angular/angular/issues/13472)
* **core:** properly destroy embedded Views attatched to ApplicationRef ([#13459](https://github.com/angular/angular/issues/13459)) ([d40bbf4](https://github.com/angular/angular/commit/d40bbf4)), closes [#13062](https://github.com/angular/angular/issues/13062)
* **dom_adapter:** remove logError from logGroup ([#12925](https://github.com/angular/angular/issues/12925)) ([5fab871](https://github.com/angular/angular/commit/5fab871))
* **forms:** ensure `select[multiple]` retains selections ([b3dcff0](https://github.com/angular/angular/commit/b3dcff0)), closes [#12527](https://github.com/angular/angular/issues/12527) [#12654](https://github.com/angular/angular/issues/12654)
* **forms:** fix Validators.min/maxLength with FormArray ([#13095](https://github.com/angular/angular/issues/13095)) ([7383e4a](https://github.com/angular/angular/commit/7383e4a)), closes [#13089](https://github.com/angular/angular/issues/13089)
* **forms:** introduce checkbox required validator ([124267c](https://github.com/angular/angular/commit/124267c)), closes [#11459](https://github.com/angular/angular/issues/11459) [#13364](https://github.com/angular/angular/issues/13364)
* **http:** check response body text against undefined ([#13017](https://github.com/angular/angular/issues/13017)) ([f106a18](https://github.com/angular/angular/commit/f106a18))
* **http:** create a copy of headers when merge options ([#13365](https://github.com/angular/angular/issues/13365)) ([65c9b5b](https://github.com/angular/angular/commit/65c9b5b)), closes [#11980](https://github.com/angular/angular/issues/11980)
* **language-service:** correctly type `undefined` ([0a7364f](https://github.com/angular/angular/commit/0a7364f)), closes [#13412](https://github.com/angular/angular/issues/13412) [#13414](https://github.com/angular/angular/issues/13414)
* Better error when directive not listed in NgModule.declarations ([b0cd514](https://github.com/angular/angular/commit/b0cd514))
* Better instructions on running examples and their tests ([203cc7e](https://github.com/angular/angular/commit/203cc7e))
* **language-service:** treat string unions as strings ([#13406](https://github.com/angular/angular/issues/13406)) ([14dd2b3](https://github.com/angular/angular/commit/14dd2b3)), closes [#13403](https://github.com/angular/angular/issues/13403)
* **router:** add support for query params with multiple values ([e4d5a5f](https://github.com/angular/angular/commit/e4d5a5f)), closes [#11373](https://github.com/angular/angular/issues/11373)
* **router:** Use T type in Resolve interface ([#13242](https://github.com/angular/angular/issues/13242)) ([5ee8155](https://github.com/angular/angular/commit/5ee8155))
* **selector:** SelectorMatcher match elements with :not selector ([#12977](https://github.com/angular/angular/issues/12977)) ([392c9ac](https://github.com/angular/angular/commit/392c9ac))
* **tsc-wrapped:** generate metadata for exports without module specifier ([cd03c77](https://github.com/angular/angular/commit/cd03c77)), closes [#13327](https://github.com/angular/angular/issues/13327)
* **upgrade:** fix downgrade content projection and injector inheritance ([86c5098](https://github.com/angular/angular/commit/86c5098)), closes [#6629](https://github.com/angular/angular/issues/6629) [#7727](https://github.com/angular/angular/issues/7727) [#8729](https://github.com/angular/angular/issues/8729) [#9643](https://github.com/angular/angular/issues/9643) [#9649](https://github.com/angular/angular/issues/9649) [#12675](https://github.com/angular/angular/issues/12675)
### Features
* **upgrade:** enable Angular 1 unit testing of upgrade module ([2fc0560](https://github.com/angular/angular/commit/2fc0560)), closes [#5462](https://github.com/angular/angular/issues/5462) [#12675](https://github.com/angular/angular/issues/12675)
### Performance Improvements
* **animations:** always run the animation queue outside of zones ([e2622ad](https://github.com/angular/angular/commit/e2622ad)), closes [#13440](https://github.com/angular/angular/issues/13440)
<a name="2.3.0"></a>
# [2.3.0](https://github.com/angular/angular/compare/2.3.0-rc.0...2.3.0) (2016-12-07)
@ -15,7 +64,7 @@
* **language-service:** do not throw for invalid metadata ([#13261](https://github.com/angular/angular/issues/13261)) ([4a09c81](https://github.com/angular/angular/commit/4a09c81)), closes [#13255](https://github.com/angular/angular/issues/13255)
* **language-service:** remove incompletely used parameter from `createLanguageServiceFromTypescript()` ([#13278](https://github.com/angular/angular/issues/13278)) ([25c2141](https://github.com/angular/angular/commit/25c2141)), closes [#13277](https://github.com/angular/angular/issues/13277)
* **language-service:** update to use `CompilerHost` from compiler-cli ([#13189](https://github.com/angular/angular/issues/13189)) ([3ff6554](https://github.com/angular/angular/commit/3ff6554))
* **router:** allow specifying a matcher wihtout specifying a path ([bbb7a39](https://github.com/angular/angular/commit/bbb7a39)), closes [#12972](https://github.com/angular/angular/issues/12972)
* **router:** allow specifying a matcher without specifying a path ([bbb7a39](https://github.com/angular/angular/commit/bbb7a39)), closes [#12972](https://github.com/angular/angular/issues/12972)
* **router:** fix replaceUrl on RouterLink directives ([349ad75](https://github.com/angular/angular/commit/349ad75))
* **router:** fix skipLocationChanges on RouterLink directives ([f562cbf](https://github.com/angular/angular/commit/f562cbf)), closes [#13156](https://github.com/angular/angular/issues/13156)
* **router:** make setUpLocationChangeListener idempotent ([25e5b2f](https://github.com/angular/angular/commit/25e5b2f))

View File

@ -24,8 +24,9 @@ with it.
* `comp: forms`: `@kara`
* `comp: http`: `@jeffbcross`
* `comp: i18n`: `@vicb`
* `comp: language service`: `@chuckjaz`
* `comp: metadata-extractor`: `@chuckjaz`
* `comp: router`: `@vsavkin`
* `comp: router`: `@vicb`
* `comp: testing`: `@juliemr`
* `comp: upgrade`: `@mhevery`
* `comp: web-worker`: `@vicb`

View File

@ -36,7 +36,7 @@ var CIconfiguration = {
'iOS7': {unitTest: {target: 'BS', required: true}, e2e: {target: null, required: true}},
'iOS8': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
'iOS9': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
'iOS10': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
'iOS10': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
'WindowsPhone': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}}
};
@ -44,10 +44,10 @@ var customLaunchers = {
'DartiumWithWebPlatform':
{base: 'Dartium', flags: ['--enable-experimental-web-platform-features']},
'ChromeNoSandbox': {base: 'Chrome', flags: ['--no-sandbox']},
'SL_CHROME': {base: 'SauceLabs', browserName: 'chrome', version: '52'},
'SL_CHROME': {base: 'SauceLabs', browserName: 'chrome', version: '54'},
'SL_CHROMEBETA': {base: 'SauceLabs', browserName: 'chrome', version: 'beta'},
'SL_CHROMEDEV': {base: 'SauceLabs', browserName: 'chrome', version: 'dev'},
'SL_FIREFOX': {base: 'SauceLabs', browserName: 'firefox', version: '46'},
'SL_FIREFOX': {base: 'SauceLabs', browserName: 'firefox', version: '50'},
'SL_FIREFOXBETA': {base: 'SauceLabs', browserName: 'firefox', version: 'beta'},
'SL_FIREFOXDEV': {base: 'SauceLabs', browserName: 'firefox', version: 'dev'},
'SL_SAFARI7': {base: 'SauceLabs', browserName: 'safari', platform: 'OS X 10.9', version: '7.0'},
@ -96,7 +96,7 @@ var customLaunchers = {
'BS_IE10': {
base: 'BrowserStack',
browser: 'ie',
browser_version: '10.0',
browser_version: '10.1',
os: 'Windows',
os_version: '8'
},

View File

@ -48,13 +48,15 @@ module.exports = function(config) {
exclude: [
'dist/all/@angular/**/e2e_test/**',
'dist/all/@angular/router/**',
'dist/all/@angular/**/*node_only_spec.js',
'dist/all/@angular/benchpress/**',
'dist/all/@angular/compiler-cli/**',
'dist/all/@angular/compiler/test/aot/**',
'dist/all/@angular/benchpress/**',
'dist/all/@angular/examples/**/e2e_test/*',
'dist/all/@angular/language-service/**',
'dist/all/angular1_router.js',
'dist/all/@angular/router/**',
'dist/all/@angular/platform-browser/testing/e2e_util.js',
'dist/all/angular1_router.js',
'dist/examples/**/e2e_test/**',
],

View File

@ -17,6 +17,8 @@ import {LocationChangeListener, PlatformLocation} from './platform_location';
/**
* @whatItDoes Use URL hash for storing application location data.
* @description
* `HashLocationStrategy` is a {@link LocationStrategy} used to configure the
* {@link Location} service to represent its state in the
* [hash fragment](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax)
@ -27,18 +29,7 @@ import {LocationChangeListener, PlatformLocation} from './platform_location';
*
* ### Example
*
* ```
* import {Component, NgModule} from '@angular/core';
* import {
* LocationStrategy,
* HashLocationStrategy
* } from '@angular/common';
*
* @NgModule({
* providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}]
* })
* class AppModule {}
* ```
* {@example common/location/ts/hash_location_component.ts region='LocationComponent'}
*
* @stable
*/

View File

@ -12,7 +12,8 @@ import {LocationStrategy} from './location_strategy';
/**
* `Location` is a service that applications can use to interact with a browser's URL.
* @whatItDoes `Location` is a service that applications can use to interact with a browser's URL.
* @description
* Depending on which {@link LocationStrategy} is used, `Location` will either persist
* to the URL's path or the URL's hash segment.
*
@ -28,19 +29,7 @@ import {LocationStrategy} from './location_strategy';
* - `/my/app/user/123/` **is not** normalized
*
* ### Example
*
* ```
* import {Component} from '@angular/core';
* import {Location} from '@angular/common';
*
* @Component({selector: 'app-component'})
* class AppCmp {
* constructor(location: Location) {
* location.go('/foo');
* }
* }
* ```
*
* {@example common/location/ts/path_location_component.ts region='LocationComponent'}
* @stable
*/
@Injectable()

View File

@ -12,7 +12,7 @@ import {LocationChangeListener} from './platform_location';
/**
* `LocationStrategy` is responsible for representing and reading route state
* from the browser's URL. Angular provides two strategies:
* {@link HashLocationStrategy} and {@link PathLocationStrategy} (default).
* {@link HashLocationStrategy} and {@link PathLocationStrategy}.
*
* This is used under the hood of the {@link Location} service.
*

View File

@ -17,14 +17,13 @@ import {LocationChangeListener, PlatformLocation} from './platform_location';
/**
* @whatItDoes Use URL for storing application location data.
* @description
* `PathLocationStrategy` is a {@link LocationStrategy} used to configure the
* {@link Location} service to represent its state in the
* [path](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax) of the
* browser's URL.
*
* `PathLocationStrategy` is the default binding for {@link LocationStrategy}
* provided in {@link ROUTER_PROVIDERS}.
*
* If you're using `PathLocationStrategy`, you must provide a {@link APP_BASE_HREF}
* or add a base element to the document. This URL prefix that will be preserved
* when generating and recognizing URLs.
@ -37,6 +36,10 @@ import {LocationChangeListener, PlatformLocation} from './platform_location';
* `location.go('/foo')`, the browser's URL will become
* `example.com/my/app/foo`.
*
* ### Example
*
* {@example common/location/ts/path_location_component.ts region='LocationComponent'}
*
* @stable
*/
@Injectable()

View File

@ -7,11 +7,14 @@
*/
import {Inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core';
import {DateFormatter} from '../facade/intl';
import {NumberWrapper, isDate} from '../facade/lang';
import {DateFormatter} from './intl';
import {InvalidPipeArgumentError} from './invalid_pipe_argument_error';
/**
* @ngModule CommonModule
* @whatItDoes Formats a date according to locale rules.

View File

@ -8,9 +8,9 @@
import {Inject, LOCALE_ID, Pipe, PipeTransform, Type} from '@angular/core';
import {NumberFormatStyle, NumberFormatter} from '../facade/intl';
import {NumberWrapper, isBlank, isPresent} from '../facade/lang';
import {NumberFormatStyle, NumberFormatter} from './intl';
import {InvalidPipeArgumentError} from './invalid_pipe_argument_error';
const _NUMBER_FORMAT_REGEXP = /^(\d+)?\.((\d+)(-(\d+))?)?$/;

View File

@ -11,3 +11,7 @@ export {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter, NodeComp
export {Extractor} from './src/extractor';
export * from '@angular/tsc-wrapped';
export {VERSION} from './src/version';
// TODO(hansl): moving to Angular 4 need to update this API.
export {NgTools_InternalApi_NG_2 as __NGTOOLS_PRIVATE_API_2} from './src/ngtools_api'

View File

@ -0,0 +1,3 @@
:host {
background-color: blue;
}

View File

@ -0,0 +1,5 @@
<div>
<h1>hello world</h1>
<a [routerLink]="['lazy']">lazy</a>
<router-outlet></router-outlet>
</div>

View File

@ -0,0 +1,19 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, ViewEncapsulation} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.css'],
encapsulation: ViewEncapsulation.None
})
export class AppComponent {
}

View File

@ -0,0 +1,32 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {RouterModule} from '@angular/router';
import {AppComponent} from './app.component';
@Component({selector: 'home-view', template: 'home!'})
export class HomeView {
}
@NgModule({
declarations: [AppComponent, HomeView],
imports: [
BrowserModule, RouterModule.forRoot([
{path: 'lazy', loadChildren: './lazy.module#LazyModule'},
{path: 'feature2', loadChildren: 'feature2/feature2.module#Feature2Module'},
{path: '', component: HomeView}
])
],
bootstrap: [AppComponent]
})
export class AppModule {
}

View File

@ -0,0 +1,21 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
@Component({selector: 'feature-component', template: 'foo.html'})
export class FeatureComponent {
}
@NgModule({
declarations: [FeatureComponent],
imports: [RouterModule.forChild([{path: '', component: FeatureComponent}])]
})
export class FeatureModule {
}

View File

@ -0,0 +1,24 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
@Component({selector: 'lazy-feature-comp', template: 'lazy feature!'})
export class LazyFeatureComponent {
}
@NgModule({
imports: [RouterModule.forChild([
{path: '', component: LazyFeatureComponent, pathMatch: 'full'},
{path: 'feature', loadChildren: './feature.module#FeatureModule'}
])],
declarations: [LazyFeatureComponent]
})
export class LazyFeatureModule {
}

View File

@ -0,0 +1,23 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
@Component({selector: 'feature-component', template: 'foo.html'})
export class FeatureComponent {
}
@NgModule({
declarations: [FeatureComponent],
imports: [RouterModule.forChild([
{path: '', component: FeatureComponent},
])]
})
export default class DefaultModule {
}

View File

@ -0,0 +1,26 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
@Component({selector: 'feature-component', template: 'foo.html'})
export class FeatureComponent {
}
@NgModule({
declarations: [FeatureComponent],
imports: [RouterModule.forChild([
{path: '', component: FeatureComponent}, {path: 'd', loadChildren: './default.module'} {
path: 'e',
loadChildren: 'feature/feature.module#FeatureModule'
}
])]
})
export class Feature2Module {
}

View File

@ -0,0 +1,27 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
@Component({selector: 'lazy-comp', template: 'lazy!'})
export class LazyComponent {
}
@NgModule({
imports: [RouterModule.forChild([
{path: '', component: LazyComponent, pathMatch: 'full'},
{path: 'feature', loadChildren: './feature/feature.module#FeatureModule'},
{path: 'lazy-feature', loadChildren: './feature/lazy-feature.module#LazyFeatureModule'}
])],
declarations: [LazyComponent]
})
export class LazyModule {
}
export class SecondModule {}

View File

@ -0,0 +1,19 @@
{
"angularCompilerOptions": {
// For TypeScript 1.8, we have to lay out generated files
// in the same source directory with your code.
"genDir": ".",
"debug": true
},
"compilerOptions": {
"target": "es5",
"experimentalDecorators": true,
"noImplicitAny": true,
"moduleResolution": "node",
"rootDir": "",
"declaration": true,
"lib": ["es6", "dom"],
"baseUrl": "."
}
}

View File

@ -0,0 +1,158 @@
#!/usr/bin/env node
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/* tslint:disable:no-console */
// Must be imported first, because angular2 decorators throws on load.
import 'reflect-metadata';
import * as path from 'path';
import * as ts from 'typescript';
import * as assert from 'assert';
import {tsc} from '@angular/tsc-wrapped/src/tsc';
import {AngularCompilerOptions, CodeGenerator, CompilerHostContext, NodeCompilerHostContext, __NGTOOLS_PRIVATE_API_2} from '@angular/compiler-cli';
const glob = require('glob');
/**
* Main method.
* Standalone program that executes codegen using the ngtools API and tests that files were
* properly read and wrote.
*/
function main() {
console.log(`testing ngtools API...`);
Promise.resolve()
.then(() => codeGenTest())
.then(() => lazyRoutesTest())
.then(() => {
console.log('All done!');
process.exit(0);
})
.catch((err) => {
console.error(err.stack);
console.error('Test failed');
process.exit(1);
});
}
function codeGenTest() {
const basePath = path.join(__dirname, '../ngtools_src');
const project = path.join(basePath, 'tsconfig-build.json');
const readResources: string[] = [];
const wroteFiles: string[] = [];
const config = tsc.readConfiguration(project, basePath);
const hostContext = new NodeCompilerHostContext();
const delegateHost = ts.createCompilerHost(config.parsed.options, true);
const host: ts.CompilerHost = Object.assign({}, delegateHost, {
writeFile: (fileName: string, ...rest: any[]) => {
wroteFiles.push(fileName);
return delegateHost.writeFile.call(delegateHost, fileName, ...rest);
}
});
const program = ts.createProgram(config.parsed.fileNames, config.parsed.options, host);
config.ngOptions.basePath = basePath;
console.log(`>>> running codegen for ${project}`);
return __NGTOOLS_PRIVATE_API_2
.codeGen({
basePath,
compilerOptions: config.parsed.options, program, host,
angularCompilerOptions: config.ngOptions,
// i18n options.
i18nFormat: null,
i18nFile: null,
locale: null,
readResource: (fileName: string) => {
readResources.push(fileName);
return hostContext.readResource(fileName);
}
})
.then(() => {
console.log(`>>> codegen done, asserting read and wrote files`);
// Assert for each file that it has been read and each `ts` has a written file associated.
const allFiles = glob.sync(path.join(basePath, '**/*'), {nodir: true});
allFiles.forEach((fileName: string) => {
// Skip tsconfig.
if (fileName.match(/tsconfig-build.json$/)) {
return;
}
// Assert that file was read.
if (fileName.match(/\.module\.ts$/)) {
const factory = fileName.replace(/\.module\.ts$/, '.module.ngfactory.ts');
assert(wroteFiles.indexOf(factory) != -1, `Expected file "${factory}" to be written.`);
} else if (fileName.match(/\.css$/) || fileName.match(/\.html$/)) {
assert(
readResources.indexOf(fileName) != -1,
`Expected resource "${fileName}" to be read.`);
}
});
console.log(`done, no errors.`);
})
.catch((e: any) => {
console.error(e.stack);
console.error('Compilation failed');
throw e;
});
}
function lazyRoutesTest() {
const basePath = path.join(__dirname, '../ngtools_src');
const project = path.join(basePath, 'tsconfig-build.json');
const config = tsc.readConfiguration(project, basePath);
const host = ts.createCompilerHost(config.parsed.options, true);
const program = ts.createProgram(config.parsed.fileNames, config.parsed.options, host);
config.ngOptions.basePath = basePath;
const lazyRoutes = __NGTOOLS_PRIVATE_API_2.listLazyRoutes({
program,
host,
angularCompilerOptions: config.ngOptions,
entryModule: 'app.module#AppModule'
});
const expectations: {[route: string]: string} = {
'./lazy.module#LazyModule': 'lazy.module.ts',
'./feature/feature.module#FeatureModule': 'feature/feature.module.ts',
'./feature/lazy-feature.module#LazyFeatureModule': 'feature/lazy-feature.module.ts',
'feature2/feature2.module#Feature2Module': 'feature2/feature2.module.ts',
'./default.module': 'feature2/default.module.ts',
'feature/feature.module#FeatureModule': 'feature/feature.module.ts'
};
Object.keys(lazyRoutes).forEach((route: string) => {
assert(route in expectations, `Found a route that was not expected: "${route}".`);
assert(
lazyRoutes[route] == path.join(basePath, expectations[route]),
`Route "${route}" does not point to the expected absolute path ` +
`"${path.join(basePath, expectations[route])}". It points to "${lazyRoutes[route]}"`);
});
// Verify that all expectations were met.
assert.deepEqual(
Object.keys(lazyRoutes), Object.keys(expectations), `Expected routes listed to be: \n` +
` ${JSON.stringify(Object.keys(expectations))}\n` +
`Actual:\n` +
` ${JSON.stringify(Object.keys(lazyRoutes))}\n`);
}
main();

View File

@ -21,6 +21,7 @@
"src/module",
"src/bootstrap",
"test/all_spec",
"test/test_ngtools_api",
"test/test_summaries",
"benchmarks/src/tree/ng2/index_aot.ts",
"benchmarks/src/tree/ng2_switch/index_aot.ts",

View File

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

View File

@ -21,9 +21,9 @@ import {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter} from './
import {PathMappedCompilerHost} from './path_mapped_compiler_host';
import {Console} from './private_import_core';
const GENERATED_FILES = /\.ngfactory\.ts$|\.css\.ts$|\.css\.shim\.ts$/;
const GENERATED_FILES = /\.ngfactory\.ts$|\.ngstyle\.ts$/;
const GENERATED_META_FILES = /\.json$/;
const GENERATED_OR_DTS_FILES = /\.d\.ts$|\.ngfactory\.ts$|\.css\.ts$|\.css\.shim\.ts$/;
const GENERATED_OR_DTS_FILES = /\.d\.ts$|\.ngfactory\.ts$|\.ngstyle\.ts$/;
const PREAMBLE = `/**
* @fileoverview This file is generated by the Angular 2 template compiler.

View File

@ -15,7 +15,7 @@ import * as ts from 'typescript';
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
const DTS = /\.d\.ts$/;
const NODE_MODULES = '/node_modules/';
const IS_GENERATED = /\.(ngfactory|css(\.shim)?)$/;
const IS_GENERATED = /\.(ngfactory|ngstyle)$/;
export interface CompilerHostContext extends ts.ModuleResolutionHost {
readResource(fileName: string): Promise<string>;
@ -177,28 +177,31 @@ export class CompilerHost implements AotCompilerHost {
(Array.isArray(metadataOrMetadatas) ? metadataOrMetadatas : [metadataOrMetadatas]) :
[];
const v1Metadata = metadatas.find((m: any) => m['version'] === 1);
let v2Metadata = metadatas.find((m: any) => m['version'] === 2);
if (!v2Metadata && v1Metadata) {
// patch up v1 to v2 by merging the metadata with metadata collected from the d.ts file
let v3Metadata = metadatas.find((m: any) => m['version'] === 3);
if (!v3Metadata && v1Metadata) {
// patch up v1 to v3 by merging the metadata with metadata collected from the d.ts file
// as the only difference between the versions is whether all exports are contained in
// the metadata and the `extends` clause.
v2Metadata = {'__symbolic': 'module', 'version': 2, 'metadata': {}};
v3Metadata = {'__symbolic': 'module', 'version': 3, 'metadata': {}};
if (v1Metadata.exports) {
v2Metadata.exports = v1Metadata.exports;
v3Metadata.exports = v1Metadata.exports;
}
for (let prop in v1Metadata.metadata) {
v2Metadata.metadata[prop] = v1Metadata.metadata[prop];
v3Metadata.metadata[prop] = v1Metadata.metadata[prop];
}
const sourceText = this.context.readFile(dtsFilePath);
const exports = this.metadataCollector.getMetadata(this.getSourceFile(dtsFilePath));
if (exports) {
for (let prop in exports.metadata) {
if (!v2Metadata.metadata[prop]) {
v2Metadata.metadata[prop] = exports.metadata[prop];
if (!v3Metadata.metadata[prop]) {
v3Metadata.metadata[prop] = exports.metadata[prop];
}
}
if (exports.exports) {
v3Metadata.exports = exports.exports;
}
}
metadatas.push(v2Metadata);
metadatas.push(v3Metadata);
}
this.resolverCache.set(filePath, metadatas);
return metadatas;

View File

@ -0,0 +1,124 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* This is a private API for the ngtools toolkit.
*
* This API should be stable for NG 2. It can be removed in NG 4..., but should be replaced by
* something else.
*/
import {AotCompilerHost, StaticReflector} from '@angular/compiler';
import {AngularCompilerOptions, NgcCliOptions} from '@angular/tsc-wrapped';
import * as ts from 'typescript';
import {CodeGenerator} from './codegen';
import {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter} from './compiler_host';
import {listLazyRoutesOfModule} from './ngtools_impl';
import {PathMappedCompilerHost} from './path_mapped_compiler_host';
export interface NgTools_InternalApi_NG2_CodeGen_Options {
basePath: string;
compilerOptions: ts.CompilerOptions;
program: ts.Program;
host: ts.CompilerHost;
angularCompilerOptions: AngularCompilerOptions;
// i18n options.
i18nFormat: string;
i18nFile: string;
locale: string;
readResource: (fileName: string) => Promise<string>;
// Every new property under this line should be optional.
}
export interface NgTools_InternalApi_NG2_ListLazyRoutes_Options {
program: ts.Program;
host: ts.CompilerHost;
angularCompilerOptions: AngularCompilerOptions;
entryModule: string;
// Every new property under this line should be optional.
}
export interface NgTools_InternalApi_NG_2_LazyRouteMap { [route: string]: string; }
/**
* A ModuleResolutionHostAdapter that overrides the readResource() method with the one
* passed in the interface.
*/
class CustomLoaderModuleResolutionHostAdapter extends ModuleResolutionHostAdapter {
constructor(
private _readResource: (path: string) => Promise<string>, host: ts.ModuleResolutionHost) {
super(host);
}
readResource(path: string) { return this._readResource(path); }
}
/**
* @internal
* @private
*/
export class NgTools_InternalApi_NG_2 {
/**
* @internal
* @private
*/
static codeGen(options: NgTools_InternalApi_NG2_CodeGen_Options): Promise<void> {
const hostContext: CompilerHostContext =
new CustomLoaderModuleResolutionHostAdapter(options.readResource, options.host);
const cliOptions: NgcCliOptions = {
i18nFormat: options.i18nFormat,
i18nFile: options.i18nFile,
locale: options.locale,
basePath: options.basePath
};
// Create the Code Generator.
const codeGenerator = CodeGenerator.create(
options.angularCompilerOptions, cliOptions, options.program, options.host, hostContext);
return codeGenerator.codegen();
}
/**
* @internal
* @private
*/
static listLazyRoutes(options: NgTools_InternalApi_NG2_ListLazyRoutes_Options):
NgTools_InternalApi_NG_2_LazyRouteMap {
const angularCompilerOptions = options.angularCompilerOptions;
const program = options.program;
const moduleResolutionHost = new ModuleResolutionHostAdapter(options.host);
const usePathMapping =
!!angularCompilerOptions.rootDirs && angularCompilerOptions.rootDirs.length > 0;
const ngCompilerHost: AotCompilerHost = usePathMapping ?
new PathMappedCompilerHost(program, angularCompilerOptions, moduleResolutionHost) :
new CompilerHost(program, angularCompilerOptions, moduleResolutionHost);
const staticReflector = new StaticReflector(ngCompilerHost);
const routeMap = listLazyRoutesOfModule(options.entryModule, ngCompilerHost, staticReflector);
return Object.keys(routeMap).reduce(
(acc: NgTools_InternalApi_NG_2_LazyRouteMap, route: string) => {
acc[route] = routeMap[route].absoluteFilePath;
return acc;
},
{});
}
}

View File

@ -0,0 +1,205 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* This is a private API for the ngtools toolkit.
*
* This API should be stable for NG 2. It can be removed in NG 4..., but should be replaced by
* something else.
*/
import {AotCompilerHost, StaticReflector, StaticSymbol} from '@angular/compiler';
import {NgModule} from '@angular/core';
// We cannot depend directly to @angular/router.
type Route = any;
const ROUTER_MODULE_PATH = '@angular/router/src/router_config_loader';
const ROUTER_ROUTES_SYMBOL_NAME = 'ROUTES';
// LazyRoute information between the extractors.
export interface LazyRoute {
routeDef: RouteDef;
absoluteFilePath: string;
}
export type LazyRouteMap = {
[route: string]: LazyRoute
};
// A route definition. Normally the short form 'path/to/module#ModuleClassName' is used by
// the user, and this is a helper class to extract information from it.
export class RouteDef {
private constructor(public readonly path: string, public readonly className: string = null) {}
toString() {
return (this.className === null || this.className == 'default') ?
this.path :
`${this.path}#${this.className}`;
}
static fromString(entry: string): RouteDef {
const split = entry.split('#');
return new RouteDef(split[0], split[1] || null);
}
}
/**
*
* @returns {LazyRouteMap}
* @private
*/
export function listLazyRoutesOfModule(
entryModule: string, host: AotCompilerHost, reflector: StaticReflector): LazyRouteMap {
const entryRouteDef = RouteDef.fromString(entryModule);
const containingFile = _resolveModule(entryRouteDef.path, entryRouteDef.path, host);
const modulePath = `./${containingFile.replace(/^(.*)\//, '')}`;
const className = entryRouteDef.className;
// List loadChildren of this single module.
const staticSymbol = reflector.findDeclaration(modulePath, className, containingFile);
const ROUTES = reflector.findDeclaration(ROUTER_MODULE_PATH, ROUTER_ROUTES_SYMBOL_NAME);
const lazyRoutes: LazyRoute[] =
_extractLazyRoutesFromStaticModule(staticSymbol, reflector, host, ROUTES);
const routes: LazyRouteMap = {};
lazyRoutes.forEach((lazyRoute: LazyRoute) => {
const route: string = lazyRoute.routeDef.toString();
_assertRoute(routes, lazyRoute);
routes[route] = lazyRoute;
const lazyModuleSymbol = reflector.findDeclaration(
lazyRoute.absoluteFilePath, lazyRoute.routeDef.className || 'default');
const subRoutes = _extractLazyRoutesFromStaticModule(lazyModuleSymbol, reflector, host, ROUTES);
// Populate the map using the routes we just found.
subRoutes.forEach(subRoute => {
_assertRoute(routes, subRoute);
routes[subRoute.routeDef.toString()] = subRoute;
});
});
return routes;
}
/**
* Try to resolve a module, and returns its absolute path.
* @private
*/
function _resolveModule(modulePath: string, containingFile: string, host: AotCompilerHost) {
const result = host.moduleNameToFileName(modulePath, containingFile);
if (!result) {
throw new Error(`Could not resolve "${modulePath}" from "${containingFile}".`);
}
return result;
}
/**
* Throw an exception if a route is in a route map, but does not point to the same module.
* @private
*/
function _assertRoute(map: LazyRouteMap, route: LazyRoute) {
const r = route.routeDef.toString();
if (map[r] && map[r].absoluteFilePath != route.absoluteFilePath) {
throw new Error(
`Duplicated path in loadChildren detected: "${r}" is used in 2 loadChildren, ` +
`but they point to different modules "(${map[r].absoluteFilePath} and ` +
`"${route.absoluteFilePath}"). Webpack cannot distinguish on context and would fail to ` +
'load the proper one.');
}
}
/**
* Extract all the LazyRoutes from a module. This extracts all `loadChildren` keys from this
* module and all statically referred modules.
* @private
*/
function _extractLazyRoutesFromStaticModule(
staticSymbol: StaticSymbol, reflector: StaticReflector, host: AotCompilerHost,
ROUTES: StaticSymbol): LazyRoute[] {
const moduleMetadata = _getNgModuleMetadata(staticSymbol, reflector);
const allRoutes: any =
(moduleMetadata.imports || [])
.filter(i => 'providers' in i)
.reduce((mem: Route[], m: any) => {
return mem.concat(_collectRoutes(m.providers || [], reflector, ROUTES));
}, _collectRoutes(moduleMetadata.providers || [], reflector, ROUTES));
const lazyRoutes: LazyRoute[] =
_collectLoadChildren(allRoutes).reduce((acc: LazyRoute[], route: string) => {
const routeDef = RouteDef.fromString(route);
const absoluteFilePath = _resolveModule(routeDef.path, staticSymbol.filePath, host);
acc.push({routeDef, absoluteFilePath});
return acc;
}, []);
const importedSymbols = ((moduleMetadata.imports || []) as any[])
.filter(i => i instanceof StaticSymbol) as StaticSymbol[];
return importedSymbols
.reduce(
(acc: LazyRoute[], i: StaticSymbol) => {
return acc.concat(_extractLazyRoutesFromStaticModule(i, reflector, host, ROUTES));
},
[])
.concat(lazyRoutes);
}
/**
* Get the NgModule Metadata of a symbol.
* @private
*/
function _getNgModuleMetadata(staticSymbol: StaticSymbol, reflector: StaticReflector): NgModule {
const ngModules = reflector.annotations(staticSymbol).filter((s: any) => s instanceof NgModule);
if (ngModules.length === 0) {
throw new Error(`${staticSymbol.name} is not an NgModule`);
}
return ngModules[0];
}
/**
* Return the routes from the provider list.
* @private
*/
function _collectRoutes(
providers: any[], reflector: StaticReflector, ROUTES: StaticSymbol): Route[] {
return providers.reduce((routeList: Route[], p: any) => {
if (p.provide === ROUTES) {
return routeList.concat(p.useValue);
} else if (Array.isArray(p)) {
return routeList.concat(_collectRoutes(p, reflector, ROUTES));
} else {
return routeList;
}
}, []);
}
/**
* Return the loadChildren values of a list of Route.
* @private
*/
function _collectLoadChildren(routes: Route[]): string[] {
return routes.reduce((m, r) => {
if (r.loadChildren) {
return m.concat(r.loadChildren);
} else if (Array.isArray(r)) {
return m.concat(_collectLoadChildren(r));
} else if (r.children) {
return m.concat(_collectLoadChildren(r.children));
} else {
return m;
}
}, []);
}

View File

@ -66,11 +66,14 @@ describe('CompilerHost', () => {
'/tmp/project/src/my.other.ngfactory.ts', '/tmp/project/src/my.ngfactory.ts'))
.toEqual('./my.other.ngfactory');
expect(hostNestedGenDir.fileNameToModuleName(
'/tmp/project/src/my.other.css.ts', '/tmp/project/src/a/my.ngfactory.ts'))
.toEqual('../my.other.css');
'/tmp/project/src/my.other.css.ngstyle.ts', '/tmp/project/src/a/my.ngfactory.ts'))
.toEqual('../my.other.css.ngstyle');
expect(hostNestedGenDir.fileNameToModuleName(
'/tmp/project/src/a/my.other.css.shim.ts', '/tmp/project/src/my.ngfactory.ts'))
.toEqual('./a/my.other.css.shim');
'/tmp/project/src/a/my.other.shim.ngstyle.ts', '/tmp/project/src/my.ngfactory.ts'))
.toEqual('./a/my.other.shim.ngstyle');
expect(hostNestedGenDir.fileNameToModuleName(
'/tmp/project/src/my.other.sass.ngstyle.ts', '/tmp/project/src/a/my.ngfactory.ts'))
.toEqual('../my.other.sass.ngstyle');
});
it('should import application from factory', () => {
@ -83,6 +86,12 @@ describe('CompilerHost', () => {
expect(hostNestedGenDir.fileNameToModuleName(
'/tmp/project/src/a/my.other.ts', '/tmp/project/src/my.ngfactory.ts'))
.toEqual('../a/my.other');
expect(hostNestedGenDir.fileNameToModuleName(
'/tmp/project/src/a/my.other.css.ts', '/tmp/project/src/my.ngfactory.ts'))
.toEqual('../a/my.other.css');
expect(hostNestedGenDir.fileNameToModuleName(
'/tmp/project/src/a/my.other.css.shim.ts', '/tmp/project/src/my.ngfactory.ts'))
.toEqual('../a/my.other.css.shim');
});
});
@ -157,16 +166,18 @@ describe('CompilerHost', () => {
expect(hostNestedGenDir.getMetadataFor('node_modules/@angular/missing.d.ts')).toBeUndefined();
});
it('should add missing v2 metadata from v1 metadata and .d.ts files', () => {
it('should add missing v3 metadata from v1 metadata and .d.ts files', () => {
expect(hostNestedGenDir.getMetadataFor('metadata_versions/v1.d.ts')).toEqual([
{__symbolic: 'module', version: 1, metadata: {foo: {__symbolic: 'class'}}}, {
__symbolic: 'module',
version: 2,
version: 3,
metadata: {
foo: {__symbolic: 'class'},
Bar: {__symbolic: 'class', members: {ngOnInit: [{__symbolic: 'method'}]}},
BarChild: {__symbolic: 'class', extends: {__symbolic: 'reference', name: 'Bar'}}
}
BarChild: {__symbolic: 'class', extends: {__symbolic: 'reference', name: 'Bar'}},
ReExport: {__symbolic: 'reference', module: './lib/utils2', name: 'ReExport'},
},
exports: [{from: './lib/utils2', export: ['Export']}],
}
]);
});
@ -202,6 +213,11 @@ const FILES: Entry = {
},
'metadata_versions': {
'v1.d.ts': `
import {ReExport} from './lib/utils2';
export {ReExport};
export {Export} from './lib/utils2';
export declare class Bar {
ngOnInit() {}
}

View File

@ -199,8 +199,9 @@ class _AnimationBuilder implements AnimationAstVisitor {
.set(_ANIMATION_FACTORY_VIEW_CONTEXT.callMethod(
'getAnimationPlayers',
[
_ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName),
_ANIMATION_FACTORY_ELEMENT_VAR,
_ANIMATION_NEXT_STATE_VAR.equals(o.literal(EMPTY_STATE))
.conditional(o.NULL_EXPR, o.literal(this.animationName))
]))
.toDeclStmt());

View File

@ -174,6 +174,11 @@ function _normalizeStyleMetadata(
entry: CompileAnimationStyleMetadata, stateStyles: {[key: string]: AnimationStylesAst},
schema: ElementSchemaRegistry, errors: AnimationParseError[],
permitStateReferences: boolean): {[key: string]: string | number}[] {
const offset = entry.offset;
if (offset > 1 || offset < 0) {
errors.push(new AnimationParseError(`Offset values for animations must be between 0 and 1`));
}
const normalizedStyles: {[key: string]: string | number}[] = [];
entry.styles.forEach(styleEntry => {
if (typeof styleEntry === 'string') {

View File

@ -272,7 +272,7 @@ function _componentFactoryName(comp: CompileIdentifierMetadata): string {
}
function _stylesModuleUrl(stylesheetUrl: string, shim: boolean, suffix: string): string {
return shim ? `${stylesheetUrl}.shim${suffix}` : `${stylesheetUrl}${suffix}`;
return `${stylesheetUrl}${shim ? '.shim' : ''}.ngstyle${suffix}`;
}
function _assertComponent(meta: CompileDirectiveMetadata) {

View File

@ -10,7 +10,7 @@ import {Attribute, Component, ContentChild, ContentChildren, Directive, Host, Ho
import {ReflectorReader} from '../private_import_core';
import {StaticSymbol} from './static_symbol';
const SUPPORTED_SCHEMA_VERSION = 2;
const SUPPORTED_SCHEMA_VERSION = 3;
const ANGULAR_IMPORT_LOCATIONS = {
coreDecorators: '@angular/core/src/metadata',
diDecorators: '@angular/core/src/di/metadata',
@ -20,6 +20,8 @@ const ANGULAR_IMPORT_LOCATIONS = {
provider: '@angular/core/src/di/provider'
};
const HIDDEN_KEY = /^\$.*\$$/;
/**
* The host of the StaticReflector disconnects the implementation from TypeScript / other language
* services and from underlying file systems.
@ -638,6 +640,9 @@ export class StaticReflector implements ReflectorReader {
return simplifyInContext(selectContext, selectTarget[member], depth + 1);
return null;
case 'reference':
if (!expression['name']) {
return context;
}
if (!expression.module) {
const name: string = expression['name'];
const localValue = scope.resolve(name);
@ -749,10 +754,10 @@ export class StaticReflector implements ReflectorReader {
{__symbolic: 'module', version: SUPPORTED_SCHEMA_VERSION, module: module, metadata: {}};
}
if (moduleMetadata['version'] != SUPPORTED_SCHEMA_VERSION) {
this.reportError(
new Error(
`Metadata version mismatch for module ${module}, found version ${moduleMetadata['version']}, expected ${SUPPORTED_SCHEMA_VERSION}`),
null);
const errorMessage = moduleMetadata['version'] == 2 ?
`Unsupported metadata version ${moduleMetadata['version']} for module ${module}. This module should be compiled with a newer version of ngc` :
`Metadata version mismatch for module ${module}, found version ${moduleMetadata['version']}, expected ${SUPPORTED_SCHEMA_VERSION}`;
this.reportError(new Error(errorMessage), null);
}
this.metadataCache.set(module, moduleMetadata);
}
@ -806,7 +811,11 @@ function mapStringMap(input: {[key: string]: any}, transform: (value: any, key:
Object.keys(input).forEach((key) => {
const value = transform(input[key], key);
if (!shouldIgnore(value)) {
result[key] = value;
if (HIDDEN_KEY.test(key)) {
Object.defineProperty(result, key, {enumerable: false, configurable: true, value: value});
} else {
result[key] = value;
}
}
});
return result;

View File

@ -91,8 +91,8 @@ function sanitizedValue(
export function triggerAnimation(
view: o.Expression, componentView: o.Expression, boundProp: BoundElementPropertyAst,
eventListener: o.Expression, renderElement: o.Expression, renderValue: o.Expression,
lastRenderValue: o.Expression) {
boundOutputs: BoundEventAst[], eventListener: o.Expression, renderElement: o.Expression,
renderValue: o.Expression, lastRenderValue: o.Expression) {
const detachStmts: o.Statement[] = [];
const updateStmts: o.Statement[] = [];
@ -121,23 +121,32 @@ export function triggerAnimation(
.set(animationFnExpr.callFn([view, renderElement, lastRenderValue, emptyStateValue]))
.toDeclStmt());
const registerStmts = [
animationTransitionVar
.callMethod(
'onStart',
[eventListener.callMethod(
o.BuiltinMethod.Bind,
[view, o.literal(BoundEventAst.calcFullName(animationName, null, 'start'))])])
.toStmt(),
animationTransitionVar
.callMethod(
'onDone',
[eventListener.callMethod(
o.BuiltinMethod.Bind,
[view, o.literal(BoundEventAst.calcFullName(animationName, null, 'done'))])])
.toStmt(),
const registerStmts: o.Statement[] = [];
const animationStartMethodExists = boundOutputs.find(
event => event.isAnimation && event.name == animationName && event.phase == 'start');
if (animationStartMethodExists) {
registerStmts.push(
animationTransitionVar
.callMethod(
'onStart',
[eventListener.callMethod(
o.BuiltinMethod.Bind,
[view, o.literal(BoundEventAst.calcFullName(animationName, null, 'start'))])])
.toStmt());
}
];
const animationDoneMethodExists = boundOutputs.find(
event => event.isAnimation && event.name == animationName && event.phase == 'done');
if (animationDoneMethodExists) {
registerStmts.push(
animationTransitionVar
.callMethod(
'onDone',
[eventListener.callMethod(
o.BuiltinMethod.Bind,
[view, o.literal(BoundEventAst.calcFullName(animationName, null, 'done'))])])
.toStmt());
}
updateStmts.push(...registerStmts);
detachStmts.push(...registerStmts);

View File

@ -75,9 +75,8 @@ export class DirectiveResolver {
outputs.push(propName);
}
}
const hostBinding =
ListWrapper.findLast(propertyMetadata[propName], (a) => a instanceof HostBinding);
if (hostBinding) {
const hostBindings = propertyMetadata[propName].filter(a => a && a instanceof HostBinding);
hostBindings.forEach(hostBinding => {
if (hostBinding.hostPropertyName) {
const startWith = hostBinding.hostPropertyName[0];
if (startWith === '(') {
@ -90,13 +89,12 @@ export class DirectiveResolver {
} else {
host[`[${propName}]`] = propName;
}
}
const hostListener =
ListWrapper.findLast(propertyMetadata[propName], (a) => a instanceof HostListener);
if (hostListener) {
});
const hostListeners = propertyMetadata[propName].filter(a => a && a instanceof HostListener);
hostListeners.forEach(hostListener => {
const args = hostListener.args || [];
host[`(${hostListener.eventName})`] = `${propName}(${args.join(',')})`;
}
});
const query = ListWrapper.findLast(propertyMetadata[propName], (a) => a instanceof Query);
if (query) {
queries[propName] = query;

View File

@ -70,7 +70,7 @@ export class DirectiveWrapperCompiler {
addCheckInputMethod(inputFieldName, builder);
});
addNgDoCheckMethod(builder);
addCheckHostMethod(hostParseResult.hostProps, builder);
addCheckHostMethod(hostParseResult.hostProps, hostParseResult.hostListeners, builder);
addHandleEventMethod(hostParseResult.hostListeners, builder);
addSubscribeMethod(dirMeta, builder);
@ -235,7 +235,8 @@ function addCheckInputMethod(input: string, builder: DirectiveWrapperBuilder) {
}
function addCheckHostMethod(
hostProps: BoundElementPropertyAst[], builder: DirectiveWrapperBuilder) {
hostProps: BoundElementPropertyAst[], hostEvents: BoundEventAst[],
builder: DirectiveWrapperBuilder) {
const stmts: o.Statement[] = [];
const methodParams: o.FnParam[] = [
new o.FnParam(
@ -262,7 +263,7 @@ function addCheckHostMethod(
let checkBindingStmts: o.Statement[];
if (hostProp.isAnimation) {
const {updateStmts, detachStmts} = triggerAnimation(
VIEW_VAR, COMPONENT_VIEW_VAR, hostProp,
VIEW_VAR, COMPONENT_VIEW_VAR, hostProp, hostEvents,
o.THIS_EXPR.prop(EVENT_HANDLER_FIELD_NAME)
.or(o.importExpr(createIdentifier(Identifiers.noop))),
RENDER_EL_VAR, evalResult.currValExpr, field.expression);

View File

@ -344,7 +344,7 @@ export class _ParseAST {
while (this.optionalCharacter(chars.$COLON)) {
args.push(this.parseExpression());
}
result = new BindingPipe(this.span(result.span.start - this.offset), result, name, args);
result = new BindingPipe(this.span(result.span.start), result, name, args);
} while (this.optionalOperator('|'));
}

View File

@ -70,6 +70,14 @@ export class JitCompiler implements Compiler {
return this._compileModuleAndAllComponents(moduleType, false).asyncResult;
}
getNgContentSelectors(component: Type<any>): string[] {
const template = this._compiledTemplateCache.get(component);
if (!template) {
throw new Error(`The component ${stringify(component)} is not yet compiled!`);
}
return template.compMeta.template.ngContentSelectors;
}
private _compileModuleAndComponents<T>(moduleType: Type<T>, isSync: boolean):
SyncAsyncResult<NgModuleFactory<T>> {
const loadingPromise = this._loadModules(moduleType, isSync);
@ -213,9 +221,8 @@ export class JitCompiler implements Compiler {
const compMeta = this._metadataResolver.getDirectiveMetadata(compType);
assertComponent(compMeta);
class HostClass {
static overriddenName = `${identifierName(compMeta.type)}_Host`;
}
const HostClass = function HostClass() {};
(<any>HostClass).overriddenName = `${identifierName(compMeta.type)}_Host`;
const hostMeta = createHostComponentMeta(HostClass, compMeta);
compiledTemplate = new CompiledTemplate(
@ -342,7 +349,8 @@ export class JitCompiler implements Compiler {
if (!this._compilerConfig.useJit) {
return interpretStatements(result.statements, result.stylesVar);
} else {
return jitStatements(`/${result.meta.moduleUrl}.css.js`, result.statements, result.stylesVar);
return jitStatements(
`/${result.meta.moduleUrl}.ngstyle.js`, result.statements, result.stylesVar);
}
}
}
@ -408,6 +416,11 @@ class ModuleBoundCompiler implements Compiler {
return this._delegate.compileModuleAndAllComponentsAsync(moduleType);
}
getNgContentSelectors(component: Type<any>): string[] {
return this._delegate.getNgContentSelectors(component);
}
/**
* Clears all caches
*/

View File

@ -367,8 +367,8 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex
ctx.print(`{`, useNewLine);
ctx.incIndent();
this.visitAllObjects(entry => {
ctx.print(`${escapeIdentifier(entry[0], this._escapeDollarInStrings, false)}: `);
entry[1].visitExpression(this, ctx);
ctx.print(`${escapeIdentifier(entry.key, this._escapeDollarInStrings, entry.quoted)}: `);
entry.value.visitExpression(this, ctx);
}, ast.entries, ctx, ',', useNewLine);
ctx.decIndent();
ctx.print(`}`, useNewLine);

View File

@ -413,10 +413,13 @@ export class LiteralArrayExpr extends Expression {
}
}
export class LiteralMapEntry {
constructor(public key: string, public value: Expression, public quoted: boolean = false) {}
}
export class LiteralMapExpr extends Expression {
public valueType: Type = null;
constructor(public entries: [string, Expression][], type: MapType = null) {
constructor(public entries: LiteralMapEntry[], type: MapType = null) {
super(type);
if (isPresent(type)) {
this.valueType = type.valueType;
@ -677,7 +680,8 @@ export class ExpressionTransformer implements StatementVisitor, ExpressionVisito
visitLiteralMapExpr(ast: LiteralMapExpr, context: any): any {
const entries = ast.entries.map(
(entry): [string, Expression] => [entry[0], entry[1].visitExpression(this, context), ]);
(entry): LiteralMapEntry => new LiteralMapEntry(
entry.key, entry.value.visitExpression(this, context), entry.quoted));
return new LiteralMapExpr(entries);
}
visitAllExpressions(exprs: Expression[], context: any): Expression[] {
@ -791,7 +795,7 @@ export class RecursiveExpressionVisitor implements StatementVisitor, ExpressionV
return ast;
}
visitLiteralMapExpr(ast: LiteralMapExpr, context: any): any {
ast.entries.forEach((entry) => (<Expression>entry[1]).visitExpression(this, context));
ast.entries.forEach((entry) => entry.value.visitExpression(this, context));
return ast;
}
visitAllExpressions(exprs: Expression[], context: any): void {
@ -891,7 +895,7 @@ export function literalArr(values: Expression[], type: Type = null): LiteralArra
}
export function literalMap(values: [string, Expression][], type: MapType = null): LiteralMapExpr {
return new LiteralMapExpr(values, type);
return new LiteralMapExpr(values.map(entry => new LiteralMapEntry(entry[0], entry[1])), type);
}
export function not(expr: Expression): NotExpr {

View File

@ -301,8 +301,7 @@ class StatementInterpreter implements o.StatementVisitor, o.ExpressionVisitor {
visitLiteralMapExpr(ast: o.LiteralMapExpr, ctx: _ExecutionContext): any {
const result = {};
ast.entries.forEach(
(entry) => (result as any)[<string>entry[0]] =
(<o.Expression>entry[1]).visitExpression(this, ctx));
(entry) => (result as any)[entry.key] = entry.value.visitExpression(this, ctx));
return result;
}

View File

@ -12,6 +12,8 @@ import {ValueTransformer, visitValue} from '../util';
import * as o from './output_ast';
export const QUOTED_KEYS = '$quoted$';
export function convertValueToOutputAst(value: any, type: o.Type = null): o.Expression {
return visitValue(value, new _ValueOutputAstTransformer(), type);
}
@ -22,9 +24,13 @@ class _ValueOutputAstTransformer implements ValueTransformer {
}
visitStringMap(map: {[key: string]: any}, type: o.MapType): o.Expression {
const entries: [string, o.Expression][] = [];
Object.keys(map).forEach(key => { entries.push([key, visitValue(map[key], this, null)]); });
return o.literalMap(entries, type);
const entries: o.LiteralMapEntry[] = [];
const quotedSet = new Set<string>(map && map[QUOTED_KEYS]);
Object.keys(map).forEach(key => {
entries.push(
new o.LiteralMapEntry(key, visitValue(map[key], this, null), quotedSet.has(key)));
});
return new o.LiteralMapExpr(entries, type);
}
visitPrimitive(value: any, type: o.Type): o.Expression { return o.literal(value, type); }

View File

@ -5,6 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as chars from './chars';
import {isPresent} from './facade/lang';
export class ParseLocation {
@ -15,6 +16,38 @@ export class ParseLocation {
toString(): string {
return isPresent(this.offset) ? `${this.file.url}@${this.line}:${this.col}` : this.file.url;
}
moveBy(delta: number): ParseLocation {
const source = this.file.content;
const len = source.length;
let offset = this.offset;
let line = this.line;
let col = this.col;
while (offset > 0 && delta < 0) {
offset--;
delta++;
const ch = source.charCodeAt(offset);
if (ch == chars.$LF) {
line--;
const priorLine = source.substr(0, offset - 1).lastIndexOf(String.fromCharCode(chars.$LF));
col = priorLine > 0 ? offset - priorLine : offset;
} else {
col--;
}
}
while (offset < len && delta > 0) {
const ch = source.charCodeAt(offset);
offset++;
delta--;
if (ch == chars.$LF) {
line++;
col = 0;
} else {
col++;
}
}
return new ParseLocation(this.file, offset, line, col);
}
}
export class ParseSourceFile {

View File

@ -297,12 +297,12 @@ export class SelectorMatcher {
return false;
}
let selectables = map.get(name);
const starSelectables = map.get('*');
let selectables: SelectorContext[] = map.get(name) || [];
const starSelectables: SelectorContext[] = map.get('*');
if (starSelectables) {
selectables = selectables.concat(starSelectables);
}
if (!selectables) {
if (selectables.length === 0) {
return false;
}
let selectable: SelectorContext;

View File

@ -9,13 +9,12 @@
import {SecurityContext} from '@angular/core';
import {CompileDirectiveSummary, CompilePipeSummary} from '../compile_metadata';
import {AST, ASTWithSource, BindingPipe, EmptyExpr, Interpolation, LiteralPrimitive, ParserError, RecursiveAstVisitor, TemplateBinding} from '../expression_parser/ast';
import {ASTWithSource, BindingPipe, EmptyExpr, ParserError, RecursiveAstVisitor, TemplateBinding} from '../expression_parser/ast';
import {Parser} from '../expression_parser/parser';
import {isPresent} from '../facade/lang';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {mergeNsAndName} from '../ml_parser/tags';
import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util';
import {view_utils} from '../private_import_core';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {CssSelector} from '../selector';
import {splitAtColon, splitAtPeriod} from '../util';
@ -246,18 +245,12 @@ export class BindingParser {
let unit: string = null;
let bindingType: PropertyBindingType;
let boundPropertyName: string;
let boundPropertyName: string = null;
const parts = boundProp.name.split(PROPERTY_PARTS_SEPARATOR);
let securityContexts: SecurityContext[];
if (parts.length === 1) {
const partValue = parts[0];
boundPropertyName = this._schemaRegistry.getMappedPropName(partValue);
securityContexts = calcPossibleSecurityContexts(
this._schemaRegistry, elementSelector, boundPropertyName, false);
bindingType = PropertyBindingType.Property;
this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, false);
} else {
// Check check for special cases (prefix style, attr, class)
if (parts.length > 1) {
if (parts[0] == ATTRIBUTE_PREFIX) {
boundPropertyName = parts[1];
this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, true);
@ -281,12 +274,18 @@ export class BindingParser {
boundPropertyName = parts[1];
bindingType = PropertyBindingType.Style;
securityContexts = [SecurityContext.STYLE];
} else {
this._reportError(`Invalid property name '${boundProp.name}'`, boundProp.sourceSpan);
bindingType = null;
securityContexts = [];
}
}
// If not a special case, use the full property name
if (boundPropertyName === null) {
boundPropertyName = this._schemaRegistry.getMappedPropName(boundProp.name);
securityContexts = calcPossibleSecurityContexts(
this._schemaRegistry, elementSelector, boundPropertyName, false);
bindingType = PropertyBindingType.Property;
this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, false);
}
return new BoundElementPropertyAst(
boundPropertyName, bindingType, securityContexts.length === 1 ? securityContexts[0] : null,
securityContexts.length > 1, boundProp.expression, unit, boundProp.sourceSpan);
@ -378,9 +377,12 @@ export class BindingParser {
if (isPresent(ast)) {
const collector = new PipeCollector();
ast.visit(collector);
collector.pipes.forEach((pipeName) => {
collector.pipes.forEach((ast, pipeName) => {
if (!this.pipesByName.has(pipeName)) {
this._reportError(`The pipe '${pipeName}' could not be found`, sourceSpan);
this._reportError(
`The pipe '${pipeName}' could not be found`,
new ParseSourceSpan(
sourceSpan.start.moveBy(ast.span.start), sourceSpan.start.moveBy(ast.span.end)));
}
});
}
@ -403,9 +405,9 @@ export class BindingParser {
}
export class PipeCollector extends RecursiveAstVisitor {
pipes = new Set<string>();
pipes = new Map<string, BindingPipe>();
visitPipe(ast: BindingPipe, context: any): any {
this.pipes.add(ast.name);
this.pipes.set(ast.name, ast);
ast.exp.visit(this);
this.visitAll(ast.args, context);
return null;

View File

@ -6,10 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Inject, Injectable, OpaqueToken, Optional, SchemaMetadata, SecurityContext} from '@angular/core';
import {Inject, Injectable, OpaqueToken, Optional, SchemaMetadata} from '@angular/core';
import {CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeSummary, CompileTemplateMetadata, CompileTemplateSummary, CompileTokenMetadata, CompileTypeMetadata, identifierName} from '../compile_metadata';
import {AST, ASTWithSource, BindingPipe, EmptyExpr, Interpolation, ParserError, RecursiveAstVisitor, TemplateBinding} from '../expression_parser/ast';
import {CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeSummary, CompileTemplateSummary, CompileTokenMetadata, CompileTypeMetadata, identifierName} from '../compile_metadata';
import {Parser} from '../expression_parser/parser';
import {isPresent} from '../facade/lang';
import {I18NHtmlParser} from '../i18n/i18n_html_parser';
@ -18,9 +17,9 @@ import * as html from '../ml_parser/ast';
import {ParseTreeResult} from '../ml_parser/html_parser';
import {expandNodes} from '../ml_parser/icu_ast_expander';
import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {mergeNsAndName, splitNsName} from '../ml_parser/tags';
import {splitNsName} from '../ml_parser/tags';
import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util';
import {Console, view_utils} from '../private_import_core';
import {Console} from '../private_import_core';
import {ProviderElementContext, ProviderViewContext} from '../provider_analyzer';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {CssSelector, SelectorMatcher} from '../selector';
@ -30,8 +29,6 @@ import {BindingParser, BoundProperty} from './binding_parser';
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from './template_ast';
import {PreparsedElementType, preparseElement} from './template_preparser';
// Group 1 = "bind-"
// Group 2 = "let-"
// Group 3 = "ref-/#"
@ -685,7 +682,7 @@ class TemplateParseVisitor implements html.Visitor {
}
elementProps.forEach(prop => {
this._reportError(
`Property binding ${prop.name} not used by any directive on an embedded template. Make sure that the property name is spelled correctly and all directives are listed in the "directives" section.`,
`Property binding ${prop.name} not used by any directive on an embedded template. Make sure that the property name is spelled correctly and all directives are listed in the "@NgModule.declarations".`,
sourceSpan);
});
}
@ -704,7 +701,7 @@ class TemplateParseVisitor implements html.Visitor {
events.forEach(event => {
if (isPresent(event.target) || !allDirectiveEvents.has(event.name)) {
this._reportError(
`Event binding ${event.fullName} not emitted by any directive on an embedded template. Make sure that the event name is spelled correctly and all directives are listed in the "directives" section.`,
`Event binding ${event.fullName} not emitted by any directive on an embedded template. Make sure that the event name is spelled correctly and all directives are listed in the "@NgModule.declarations".`,
event.sourceSpan);
}
});
@ -813,7 +810,8 @@ class ElementContext {
}
}
function createElementCssSelector(elementName: string, matchableAttrs: string[][]): CssSelector {
export function createElementCssSelector(
elementName: string, matchableAttrs: string[][]): CssSelector {
const cssSelector = new CssSelector();
const elNameNoNs = splitNsName(elementName)[1];

View File

@ -17,7 +17,7 @@ import {Identifiers, createIdentifier} from '../identifiers';
import * as o from '../output/output_ast';
import {isDefaultChangeDetectionStrategy} from '../private_import_core';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {BoundElementPropertyAst, BoundTextAst, DirectiveAst, PropertyBindingType} from '../template_parser/template_ast';
import {BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, PropertyBindingType} from '../template_parser/template_ast';
import {CompileElement, CompileNode} from './compile_element';
import {CompileView} from './compile_view';
import {DetectChangesVars} from './constants';
@ -41,7 +41,8 @@ export function bindRenderText(
}
export function bindRenderInputs(
boundProps: BoundElementPropertyAst[], hasEvents: boolean, compileElement: CompileElement) {
boundProps: BoundElementPropertyAst[], boundOutputs: BoundEventAst[], hasEvents: boolean,
compileElement: CompileElement) {
const view = compileElement.view;
const renderNode = compileElement.renderNode;
@ -67,7 +68,7 @@ export function bindRenderInputs(
case PropertyBindingType.Animation:
compileMethod = view.animationBindingsMethod;
const {updateStmts, detachStmts} = triggerAnimation(
o.THIS_EXPR, o.THIS_EXPR, boundProp,
o.THIS_EXPR, o.THIS_EXPR, boundProp, boundOutputs,
(hasEvents ? o.THIS_EXPR.prop(getHandleEventMethodName(compileElement.nodeIndex)) :
o.importExpr(createIdentifier(Identifiers.noop)))
.callMethod(o.BuiltinMethod.Bind, [o.THIS_EXPR]),

View File

@ -44,7 +44,7 @@ class ViewBinderVisitor implements TemplateAstVisitor {
visitElement(ast: ElementAst, parent: CompileElement): any {
const compileElement = <CompileElement>this.view.nodes[this._nodeIndex++];
const hasEvents = bindOutputs(ast.outputs, ast.directives, compileElement, true);
bindRenderInputs(ast.inputs, hasEvents, compileElement);
bindRenderInputs(ast.inputs, ast.outputs, hasEvents, compileElement);
ast.directives.forEach((directiveAst, dirIndex) => {
const directiveWrapperInstance =
compileElement.directiveWrapperInstance.get(directiveAst.directive.type.reference);

View File

@ -576,7 +576,7 @@ function generateDetectChangesMethod(view: CompileView): o.Statement[] {
}
stmts.push(...view.detectChangesRenderPropertiesMethod.finish());
view.viewChildren.forEach((viewChild) => {
stmts.push(viewChild.callMethod('detectChanges', [DetectChangesVars.throwOnChange]).toStmt());
stmts.push(viewChild.callMethod('internalDetectChanges', [DetectChangesVars.throwOnChange]).toStmt());
});
const afterViewStmts =
view.updateViewQueriesMethod.finish().concat(view.afterViewLifecycleCallbacksMethod.finish());

View File

@ -80,7 +80,13 @@ describe('StaticReflector', () => {
it('should throw an exception for unsupported metadata versions', () => {
expect(() => reflector.findDeclaration('src/version-error', 'e'))
.toThrow(new Error(
'Metadata version mismatch for module /tmp/src/version-error.d.ts, found version 100, expected 2'));
'Metadata version mismatch for module /tmp/src/version-error.d.ts, found version 100, expected 3'));
});
it('should throw an exception for version 2 metadata', () => {
expect(() => reflector.findDeclaration('src/version-2-error', 'e'))
.toThrowError(
'Unsupported metadata version 2 for module /tmp/src/version-2-error.d.ts. This module should be compiled with a newer version of ngc');
});
it('should get and empty annotation list for an unknown class', () => {
@ -307,6 +313,12 @@ describe('StaticReflector', () => {
.toEqual('s');
});
it('should not simplify a module reference without a name', () => {
const staticSymbol = new StaticSymbol('/src/cases', '');
expect(simplify(staticSymbol, ({__symbolic: 'reference', module: './extern', name: ''})))
.toEqual(staticSymbol);
});
it('should simplify a non existing reference as a static symbol', () => {
expect(simplify(
new StaticSymbol('/src/cases', ''),
@ -378,7 +390,7 @@ describe('StaticReflector', () => {
const metadata = reflector.getModuleMetadata('/tmp/src/custom-decorator-reference.ts');
expect(metadata).toEqual({
__symbolic: 'module',
version: 2,
version: 3,
metadata: {
Foo: {
__symbolic: 'class',
@ -769,7 +781,7 @@ export class MockStaticReflectorHost implements StaticReflectorHost {
const DEFAULT_TEST_DATA: {[key: string]: any} = {
'/tmp/@angular/common/src/forms-deprecated/directives.d.ts': [{
'__symbolic': 'module',
'version': 2,
'version': 3,
'metadata': {
'FORM_DIRECTIVES': [
{
@ -782,7 +794,7 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
}],
'/tmp/@angular/common/src/directives/ng_for.d.ts': {
'__symbolic': 'module',
'version': 2,
'version': 3,
'metadata': {
'NgFor': {
'__symbolic': 'class',
@ -835,16 +847,16 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
}
},
'/tmp/@angular/core/src/linker/view_container_ref.d.ts':
{version: 2, 'metadata': {'ViewContainerRef': {'__symbolic': 'class'}}},
{version: 3, 'metadata': {'ViewContainerRef': {'__symbolic': 'class'}}},
'/tmp/@angular/core/src/linker/template_ref.d.ts':
{version: 2, 'module': './template_ref', 'metadata': {'TemplateRef': {'__symbolic': 'class'}}},
{version: 3, 'module': './template_ref', 'metadata': {'TemplateRef': {'__symbolic': 'class'}}},
'/tmp/@angular/core/src/change_detection/differs/iterable_differs.d.ts':
{version: 2, 'metadata': {'IterableDiffers': {'__symbolic': 'class'}}},
{version: 3, 'metadata': {'IterableDiffers': {'__symbolic': 'class'}}},
'/tmp/@angular/core/src/change_detection/change_detector_ref.d.ts':
{version: 2, 'metadata': {'ChangeDetectorRef': {'__symbolic': 'class'}}},
{version: 3, 'metadata': {'ChangeDetectorRef': {'__symbolic': 'class'}}},
'/tmp/src/app/hero-detail.component.d.ts': {
'__symbolic': 'module',
'version': 2,
'version': 3,
'metadata': {
'HeroDetailComponent': {
'__symbolic': 'class',
@ -995,11 +1007,12 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
}
}
},
'/src/extern.d.ts': {'__symbolic': 'module', 'version': 2, metadata: {s: 's'}},
'/src/extern.d.ts': {'__symbolic': 'module', 'version': 3, metadata: {s: 's'}},
'/tmp/src/version-error.d.ts': {'__symbolic': 'module', 'version': 100, metadata: {e: 's'}},
'/tmp/src/version-2-error.d.ts': {'__symbolic': 'module', 'version': 2, metadata: {e: 's'}},
'/tmp/src/error-reporting.d.ts': {
__symbolic: 'module',
version: 2,
version: 3,
metadata: {
SomeClass: {
__symbolic: 'class',
@ -1029,7 +1042,7 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
},
'/tmp/src/error-references.d.ts': {
__symbolic: 'module',
version: 2,
version: 3,
metadata: {
Link1: {
__symbolic: 'reference',
@ -1051,7 +1064,7 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
},
'/tmp/src/function-declaration.d.ts': {
__symbolic: 'module',
version: 2,
version: 3,
metadata: {
one: {
__symbolic: 'function',
@ -1080,7 +1093,7 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
},
'/tmp/src/function-reference.ts': {
__symbolic: 'module',
version: 2,
version: 3,
metadata: {
one: {
__symbolic: 'call',
@ -1122,7 +1135,7 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
},
'/tmp/src/function-recursive.d.ts': {
__symbolic: 'modules',
version: 2,
version: 3,
metadata: {
recursive: {
__symbolic: 'function',
@ -1182,7 +1195,7 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
},
'/tmp/src/spread.ts': {
__symbolic: 'module',
version: 2,
version: 3,
metadata: {
spread: [0, {__symbolic: 'spread', expression: [1, 2, 3, 4]}, 5]
}
@ -1332,7 +1345,7 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
`,
'/tmp/src/reexport/reexport.d.ts': {
__symbolic: 'module',
version: 2,
version: 3,
metadata: {},
exports: [
{from: './src/origin1', export: ['One', 'Two', {name: 'Three', as: 'Four'}]},
@ -1341,7 +1354,7 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
},
'/tmp/src/reexport/src/origin1.d.ts': {
__symbolic: 'module',
version: 2,
version: 3,
metadata: {
One: {__symbolic: 'class'},
Two: {__symbolic: 'class'},
@ -1350,26 +1363,26 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
},
'/tmp/src/reexport/src/origin5.d.ts': {
__symbolic: 'module',
version: 2,
version: 3,
metadata: {
Five: {__symbolic: 'class'},
},
},
'/tmp/src/reexport/src/origin30.d.ts': {
__symbolic: 'module',
version: 2,
version: 3,
metadata: {
Thirty: {__symbolic: 'class'},
},
},
'/tmp/src/reexport/src/originNone.d.ts': {
__symbolic: 'module',
version: 2,
version: 3,
metadata: {},
},
'/tmp/src/reexport/src/reexport2.d.ts': {
__symbolic: 'module',
version: 2,
version: 3,
metadata: {},
exports: [{from: './originNone'}, {from: './origin30'}]
}

View File

@ -309,7 +309,8 @@ export function main() {
}
const directiveMetadata = resolver.resolve(Child);
expect(directiveMetadata.host).toEqual({'[p1]': 'p1', '[p22]': 'p2', '[p3]': 'p3'});
expect(directiveMetadata.host)
.toEqual({'[p1]': 'p1', '[p21]': 'p2', '[p22]': 'p2', '[p3]': 'p3'});
});
it('should support inheriting host listeners', () => {
@ -329,7 +330,37 @@ export function main() {
}
const directiveMetadata = resolver.resolve(Child);
expect(directiveMetadata.host).toEqual({'(p1)': 'p1()', '(p22)': 'p2()', '(p3)': 'p3()'});
expect(directiveMetadata.host)
.toEqual({'(p1)': 'p1()', '(p21)': 'p2()', '(p22)': 'p2()', '(p3)': 'p3()'});
});
it('should combine host bindings and listeners during inheritance', () => {
@Directive({selector: 'p'})
class Parent {
@HostListener('p11') @HostListener('p12')
p1() {}
@HostBinding('p21') @HostBinding('p22')
p2: any;
}
class Child extends Parent {
@HostListener('c1')
p1() {}
@HostBinding('c2')
p2: any;
}
const directiveMetadata = resolver.resolve(Child);
expect(directiveMetadata.host).toEqual({
'(p11)': 'p1()',
'(p12)': 'p1()',
'(c1)': 'p1()',
'[p21]': 'p2',
'[p22]': 'p2',
'[c2]': 'p2'
});
});
});

View File

@ -0,0 +1,72 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {StaticReflector, StaticReflectorHost, StaticSymbol} from '@angular/compiler';
import * as o from '@angular/compiler/src/output/output_ast';
import {ImportResolver} from '@angular/compiler/src/output/path_util';
import {TypeScriptEmitter} from '@angular/compiler/src/output/ts_emitter';
import {convertValueToOutputAst} from '@angular/compiler/src/output/value_util';
import {MetadataCollector, isClassMetadata, isMetadataSymbolicCallExpression} from '@angular/tsc-wrapped';
import * as ts from 'typescript';
describe('TypeScriptEmitter (node only)', () => {
it('should quote identifiers quoted in the source', () => {
const sourceText = `
import {Component} from '@angular/core';
@Component({
providers: [{ provide: 'SomeToken', useValue: {a: 1, 'b': 2, c: 3, 'd': 4}}]
})
export class MyComponent {}
`;
const source = ts.createSourceFile('test.ts', sourceText, ts.ScriptTarget.Latest);
const collector = new MetadataCollector({quotedNames: true});
const stubHost = new StubReflectorHost();
const reflector = new StaticReflector(stubHost);
// Get the metadata from the above source
const metadata = collector.getMetadata(source);
const componentMetadata = metadata.metadata['MyComponent'];
// Get the first argument of the decorator call which is passed to @Component
expect(isClassMetadata(componentMetadata)).toBeTruthy();
if (!isClassMetadata(componentMetadata)) return;
const decorators = componentMetadata.decorators;
const firstDecorator = decorators[0];
expect(isMetadataSymbolicCallExpression(firstDecorator)).toBeTruthy();
if (!isMetadataSymbolicCallExpression(firstDecorator)) return;
const firstArgument = firstDecorator.arguments[0];
// Simplify this value using the StaticReflector
const context = reflector.getStaticSymbol('none', 'none');
const argumentValue = reflector.simplify(context, firstArgument);
// Convert the value to an output AST
const outputAst = convertValueToOutputAst(argumentValue);
const statement = outputAst.toStmt();
// Convert the value to text using the typescript emitter
const emitter = new TypeScriptEmitter(new StubImportResolver());
const text = emitter.emitStatements('module', [statement], []);
// Expect the keys for 'b' and 'd' to be quoted but 'a' and 'c' not to be.
expect(text).toContain('\'b\': 2');
expect(text).toContain('\'d\': 4');
expect(text).not.toContain('\'a\'');
expect(text).not.toContain('\'c\'');
});
});
class StubReflectorHost implements StaticReflectorHost {
getMetadataFor(modulePath: string): {[key: string]: any}[] { return []; }
moduleNameToFileName(moduleName: string, containingFile: string): string { return ''; }
}
class StubImportResolver extends ImportResolver {
fileNameToModuleName(importedFilePath: string, containingFilePath: string): string { return ''; }
}

View File

@ -102,6 +102,17 @@ export function main() {
expect(matched).toEqual([s1[0], 1, s2[0], 2]);
});
it('should support "." in attribute names', () => {
matcher.addSelectables(s1 = CssSelector.parse('[foo.bar]'), 1);
expect(matcher.match(CssSelector.parse('[barfoo]')[0], selectableCollector)).toEqual(false);
expect(matched).toEqual([]);
reset();
expect(matcher.match(CssSelector.parse('[foo.bar]')[0], selectableCollector)).toEqual(true);
expect(matched).toEqual([s1[0], 1]);
});
it('should select by attr name only once if the value is from the DOM', () => {
matcher.addSelectables(s1 = CssSelector.parse('[some-decor]'), 1);
@ -223,6 +234,11 @@ export function main() {
expect(matched).toEqual([s1[0], 1, s2[0], 2, s3[0], 3, s4[0], 4]);
});
it('should match * with :not selector', () => {
matcher.addSelectables(CssSelector.parse(':not([a])'), 1);
expect(matcher.match(CssSelector.parse('div')[0], () => {})).toEqual(true);
});
it('should match with multiple :not selectors', () => {
matcher.addSelectables(s1 = CssSelector.parse('div:not([a]):not([b])'), 1);
expect(matcher.match(CssSelector.parse('div[a]')[0], selectableCollector)).toBe(false);

View File

@ -70,6 +70,10 @@ export class TestingCompilerImpl implements TestingCompiler {
return this._compiler.compileModuleAndAllComponentsAsync(moduleType);
}
getNgContentSelectors(component: Type<any>): string[] {
return this._compiler.getNgContentSelectors(component);
}
overrideModule(ngModule: Type<any>, override: MetadataOverride<NgModule>): void {
const oldMetadata = this._moduleResolver.resolve(ngModule, false);
this._moduleResolver.setNgModule(

View File

@ -5,30 +5,47 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injectable} from '../di/metadata';
import {NgZone} from '../zone/ng_zone';
import {AnimationPlayer} from './animation_player';
let _queuedAnimations: AnimationPlayer[] = [];
@Injectable()
export class AnimationQueue {
public entries: AnimationPlayer[] = [];
/** @internal */
export function queueAnimation(player: AnimationPlayer) {
_queuedAnimations.push(player);
}
constructor(private _zone: NgZone) {}
/** @internal */
export function triggerQueuedAnimations() {
// this code is wrapped into a single promise such that the
// onStart and onDone player callbacks are triggered outside
// of the digest cycle of animations
if (_queuedAnimations.length) {
Promise.resolve(null).then(_triggerAnimations);
enqueue(player: AnimationPlayer) { this.entries.push(player); }
flush() {
// given that each animation player may set aside
// microtasks and rely on DOM-based events, this
// will cause Angular to run change detection after
// each request. This sidesteps the issue. If a user
// hooks into an animation via (@anim.start) or (@anim.done)
// then those methods will automatically trigger change
// detection by wrapping themselves inside of a zone
if (this.entries.length) {
this._zone.runOutsideAngular(() => {
// this code is wrapped into a single promise such that the
// onStart and onDone player callbacks are triggered outside
// of the digest cycle of animations
Promise.resolve(null).then(() => this._triggerAnimations());
});
}
}
private _triggerAnimations() {
NgZone.assertNotInAngularZone();
while (this.entries.length) {
const player = this.entries.shift();
// in the event that an animation throws an error then we do
// not want to re-run animations on any previous animations
// if they have already been kicked off beforehand
if (!player.hasStarted()) {
player.play();
}
}
}
}
function _triggerAnimations() {
for (let i = 0; i < _queuedAnimations.length; i++) {
const player = _queuedAnimations[i];
player.play();
}
_queuedAnimations = [];
}

View File

@ -23,12 +23,14 @@ export class AnimationTransition {
}
onStart(callback: (event: AnimationTransitionEvent) => any): void {
const event = this._createEvent('start');
this._player.onStart(() => callback(event));
const fn =
<() => void>Zone.current.wrap(() => callback(this._createEvent('start')), 'player.onStart');
this._player.onStart(fn);
}
onDone(callback: (event: AnimationTransitionEvent) => any): void {
const event = this._createEvent('done');
this._player.onDone(() => callback(event));
const fn =
<() => void>Zone.current.wrap(() => callback(this._createEvent('done')), 'player.onDone');
this._player.onDone(fn);
}
}

View File

@ -44,16 +44,18 @@ export class ViewAnimationMap {
getAllPlayers(): AnimationPlayer[] { return this._allPlayers; }
remove(element: any, animationName: string): void {
remove(element: any, animationName: string, targetPlayer: AnimationPlayer = null): void {
const playersByAnimation = this._map.get(element);
if (playersByAnimation) {
const player = playersByAnimation[animationName];
delete playersByAnimation[animationName];
const index = this._allPlayers.indexOf(player);
this._allPlayers.splice(index, 1);
if (!targetPlayer || player === targetPlayer) {
delete playersByAnimation[animationName];
const index = this._allPlayers.indexOf(player);
this._allPlayers.splice(index, 1);
if (Object.keys(playersByAnimation).length === 0) {
this._map.delete(element);
if (Object.keys(playersByAnimation).length === 0) {
this._map.delete(element);
}
}
}
}

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AnimationQueue} from './animation/animation_queue';
import {ApplicationInitStatus} from './application_init';
import {ApplicationRef, ApplicationRef_} from './application_ref';
import {APP_ID_RANDOM_PROVIDER} from './application_tokens';
@ -37,6 +38,7 @@ export function _keyValueDiffersFactory() {
Compiler,
APP_ID_RANDOM_PROVIDER,
ViewUtils,
AnimationQueue,
{provide: IterableDiffers, useFactory: _iterableDiffersFactory},
{provide: KeyValueDiffers, useFactory: _keyValueDiffersFactory},
{provide: LOCALE_ID, useValue: 'en-US'},

View File

@ -7,14 +7,15 @@
*/
import {AnimationGroupPlayer} from '../animation/animation_group_player';
import {AnimationPlayer} from '../animation/animation_player';
import {queueAnimation as queueAnimationGlobally} from '../animation/animation_queue';
import {AnimationQueue} from '../animation/animation_queue';
import {AnimationSequencePlayer} from '../animation/animation_sequence_player';
import {ViewAnimationMap} from '../animation/view_animation_map';
import {ListWrapper} from '../facade/collection';
export class AnimationViewContext {
private _players = new ViewAnimationMap();
constructor(private _animationQueue: AnimationQueue) {}
onAllActiveAnimationsDone(callback: () => any): void {
const activeAnimationPlayers = this._players.getAllPlayers();
// we check for the length to avoid having GroupAnimationPlayer
@ -27,21 +28,21 @@ export class AnimationViewContext {
}
queueAnimation(element: any, animationName: string, player: AnimationPlayer): void {
queueAnimationGlobally(player);
this._animationQueue.enqueue(player);
this._players.set(element, animationName, player);
player.onDone(() => this._players.remove(element, animationName, player));
}
getAnimationPlayers(element: any, animationName: string, removeAllAnimations: boolean = false):
AnimationPlayer[] {
getAnimationPlayers(element: any, animationName: string = null): AnimationPlayer[] {
const players: AnimationPlayer[] = [];
if (removeAllAnimations) {
this._players.findAllPlayersByElement(element).forEach(
player => { _recursePlayers(player, players); });
} else {
if (animationName) {
const currentPlayer = this._players.find(element, animationName);
if (currentPlayer) {
_recursePlayers(currentPlayer, players);
}
} else {
this._players.findAllPlayersByElement(element).forEach(
player => _recursePlayers(player, players));
}
return players;
}

View File

@ -82,6 +82,14 @@ export class Compiler {
throw _throwError();
}
/**
* Exposes the CSS-style selectors that have been used in `ngContent` directives within
* the template of the given component.
* This is used by the `upgrade` library to compile the appropriate transclude content
* in the Angular 1 wrapper component.
*/
getNgContentSelectors(component: Type<any>): string[] { throw _throwError(); }
/**
* Clears all caches.
*/

View File

@ -9,8 +9,6 @@
import {Injector, THROW_IF_NOT_FOUND} from '../di/injector';
import {AppView} from './view';
const _UNDEFINED = new Object();
export class ElementInjector extends Injector {
constructor(private _view: AppView<any>, private _nodeIndex: number) { super(); }

View File

@ -9,7 +9,6 @@
import {ApplicationRef} from '../application_ref';
import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection';
import {Injector, THROW_IF_NOT_FOUND} from '../di/injector';
import {ListWrapper} from '../facade/collection';
import {isPresent} from '../facade/lang';
import {WtfScopeFn, wtfCreateScope, wtfLeave} from '../profile/profile';
import {DirectRenderer, RenderComponentType, RenderDebugInfo, Renderer} from '../render/api';
@ -64,7 +63,7 @@ export abstract class AppView<T> {
public viewUtils: ViewUtils, public parentView: AppView<any>, public parentIndex: number,
public parentElement: any, public cdMode: ChangeDetectorStatus,
public declaredViewContainer: ViewContainer = null) {
this.ref = new ViewRef_(this);
this.ref = new ViewRef_(this, viewUtils.animationQueue);
if (type === ViewType.COMPONENT || type === ViewType.HOST) {
this.renderer = viewUtils.renderComponent(componentType);
} else {
@ -75,7 +74,7 @@ export abstract class AppView<T> {
get animationContext(): AnimationViewContext {
if (!this._animationContext) {
this._animationContext = new AnimationViewContext();
this._animationContext = new AnimationViewContext(this.viewUtils.animationQueue);
}
return this._animationContext;
}
@ -192,7 +191,8 @@ export abstract class AppView<T> {
} else {
this._renderDetach();
}
if (this.declaredViewContainer && this.declaredViewContainer !== this.viewContainer) {
if (this.declaredViewContainer && this.declaredViewContainer !== this.viewContainer &&
this.declaredViewContainer.projectedViews) {
const projectedViews = this.declaredViewContainer.projectedViews;
const index = projectedViews.indexOf(this);
// perf: pop is faster than splice!
@ -312,11 +312,16 @@ export abstract class AppView<T> {
*/
dirtyParentQueriesInternal(): void {}
internalDetectChanges(throwOnChange: boolean): void {
if (this.cdMode !== ChangeDetectorStatus.Detached) {
this.detectChanges(throwOnChange);
}
}
detectChanges(throwOnChange: boolean): void {
const s = _scope_check(this.clazz);
if (this.cdMode === ChangeDetectorStatus.Checked ||
this.cdMode === ChangeDetectorStatus.Errored ||
this.cdMode === ChangeDetectorStatus.Detached)
this.cdMode === ChangeDetectorStatus.Errored)
return;
if (this.cdMode === ChangeDetectorStatus.Destroyed) {
this.throwDestroyedError('detectChanges');

View File

@ -6,14 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {triggerQueuedAnimations} from '../animation/animation_queue';
import {AnimationQueue} from '../animation/animation_queue';
import {ChangeDetectorRef} from '../change_detection/change_detector_ref';
import {ChangeDetectorStatus} from '../change_detection/constants';
import {unimplemented} from '../facade/errors';
import {AppView} from './view';
/**
* @stable
*/
@ -92,7 +90,7 @@ export class ViewRef_<C> implements EmbeddedViewRef<C>, ChangeDetectorRef {
/** @internal */
_originalMode: ChangeDetectorStatus;
constructor(private _view: AppView<C>) {
constructor(private _view: AppView<C>, public animationQueue: AnimationQueue) {
this._view = _view;
this._originalMode = this._view.cdMode;
}
@ -109,7 +107,7 @@ export class ViewRef_<C> implements EmbeddedViewRef<C>, ChangeDetectorRef {
detach(): void { this._view.cdMode = ChangeDetectorStatus.Detached; }
detectChanges(): void {
this._view.detectChanges(false);
triggerQueuedAnimations();
this.animationQueue.flush();
}
checkNoChanges(): void { this._view.detectChanges(true); }
reattach(): void {

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AnimationQueue} from '../animation/animation_queue';
import {SimpleChange, devModeEqual} from '../change_detection/change_detection';
import {UNINITIALIZED} from '../change_detection/change_detection_util';
import {Inject, Injectable} from '../di';
@ -14,17 +15,21 @@ import {ViewEncapsulation} from '../metadata/view';
import {RenderComponentType, RenderDebugInfo, Renderer, RootRenderer} from '../render/api';
import {Sanitizer} from '../security';
import {VERSION} from '../version';
import {NgZone} from '../zone/ng_zone';
import {ExpressionChangedAfterItHasBeenCheckedError} from './errors';
import {AppView} from './view';
import {ViewContainer} from './view_container';
@Injectable()
export class ViewUtils {
sanitizer: Sanitizer;
private _nextCompTypeId: number = 0;
constructor(private _renderer: RootRenderer, sanitizer: Sanitizer) { this.sanitizer = sanitizer; }
constructor(
private _renderer: RootRenderer, sanitizer: Sanitizer,
public animationQueue: AnimationQueue) {
this.sanitizer = sanitizer;
}
/** @internal */
renderComponent(renderComponentType: RenderComponentType): Renderer {

View File

@ -29,7 +29,7 @@ export enum LifecycleHooks {
*/
export interface SimpleChanges { [propName: string]: SimpleChange; }
export var LIFECYCLE_HOOKS_VALUES = [
export const LIFECYCLE_HOOKS_VALUES = [
LifecycleHooks.OnInit, LifecycleHooks.OnDestroy, LifecycleHooks.DoCheck, LifecycleHooks.OnChanges,
LifecycleHooks.AfterContentInit, LifecycleHooks.AfterContentChecked, LifecycleHooks.AfterViewInit,
LifecycleHooks.AfterViewChecked
@ -92,7 +92,7 @@ export abstract class OnInit { abstract ngOnInit(): void; }
export abstract class DoCheck { abstract ngDoCheck(): void; }
/**
* @whatItDoes Lifecycle hook that is called when a directive or pipe is destroyed.
* @whatItDoes Lifecycle hook that is called when a directive, pipe or service is destroyed.
* @howToUse
* {@example core/ts/metadata/lifecycle_hooks_spec.ts region='OnDestroy'}
*

View File

@ -156,6 +156,60 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(kf[1]).toEqual([1, {'backgroundColor': 'blue'}]);
}));
it('should throw an error when a provided offset for an animation step if an offset value is greater than 1',
fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, {
set: {
template: `
<div *ngIf="exp" [@tooBig]="exp"></div>
`,
animations: [trigger(
'tooBig',
[transition(
'* => *', [animate('444ms', style({'opacity': '1', offset: 1.1}))])])]
}
});
let message = '';
try {
const fixture = TestBed.createComponent(DummyIfCmp);
} catch (e) {
message = e.message;
}
const lines = message.split(/\n+/);
expect(lines[1]).toMatch(
/Unable to parse the animation sequence for "tooBig" on the DummyIfCmp component due to the following errors:/);
expect(lines[2]).toMatch(/Offset values for animations must be between 0 and 1/);
}));
it('should throw an error when a provided offset for an animation step if an offset value is less than 0',
fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, {
set: {
template: `
<div *ngIf="exp" [@tooSmall]="exp"></div>
`,
animations: [trigger(
'tooSmall',
[transition(
'* => *', [animate('444ms', style({'opacity': '0', offset: -1}))])])]
}
});
let message = '';
try {
const fixture = TestBed.createComponent(DummyIfCmp);
} catch (e) {
message = e.message;
}
const lines = message.split(/\n+/);
expect(lines[1]).toMatch(
/Unable to parse the animation sequence for "tooSmall" on the DummyIfCmp component due to the following errors:/);
expect(lines[2]).toMatch(/Offset values for animations must be between 0 and 1/);
}));
describe('animation aliases', () => {
it('should animate the ":enter" animation alias as "void => *"', fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, {
@ -1493,6 +1547,36 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(message).toMatch(/Couldn't find an animation entry for "something"/);
});
it('should throw an error if an animation output is referenced that is not bound to as a property on the same element',
() => {
TestBed.overrideComponent(DummyLoadingCmp, {
set: {
template: `
<if-cmp (@trigger.done)="callback($event)"></if-cmp>
`
}
});
TestBed.overrideComponent(DummyIfCmp, {
set: {
template: `
<div [@trigger]="exp"></div>
`,
animations: [trigger('trigger', [transition('one => two', [animate(1000)])])]
}
});
let message = '';
try {
const fixture = TestBed.createComponent(DummyIfCmp);
fixture.detectChanges();
} catch (e) {
message = e.message;
}
expect(message).toMatch(
/Unable to listen on \(@trigger.done\) because the animation trigger \[@trigger\] isn't being used on the same element/);
});
it('should throw an error if an animation output is referenced that is not bound to as a property on the same element',
() => {
TestBed.overrideComponent(DummyIfCmp, {
@ -2121,6 +2205,43 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(kf[1]).toEqual([1, {'height': '333px', 'opacity': AUTO_STYLE, 'width': '200px'}]);
});
});
it('should not use the previous animation\'s styling if the previous animation has already finished',
fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, {
set: {
template: `
<div [@myAnimation]="exp"></div>
`,
animations: [trigger(
'myAnimation',
[
state('a', style({color: 'red'})), state('b', style({color: 'red'})),
transition('* => *', animate(1000))
])]
}
});
const driver = TestBed.get(AnimationDriver) as MockAnimationDriver;
const fixture = TestBed.createComponent(DummyIfCmp);
const cmp = fixture.componentInstance;
cmp.exp = 'a';
fixture.detectChanges();
flushMicrotasks();
const animation1 = driver.log.shift();
expect(animation1['previousStyles']).toEqual({});
animation1['player'].finish();
cmp.exp = 'b';
fixture.detectChanges();
flushMicrotasks();
const animation2 = driver.log.shift();
expect(animation2['previousStyles']).toEqual({});
}));
});
describe('full animation integration tests', () => {
@ -2140,6 +2261,23 @@ function declareTests({useJit}: {useJit: boolean}) {
];
});
function assertStatus(value: string) {
const text = getDOM().getText(el);
const regexp = new RegExp(`Animation Status: ${value}`);
expect(text).toMatch(regexp);
}
function assertTime(value: number) {
const text = getDOM().getText(el);
const regexp = new RegExp(`Animation Time: ${value}`);
expect(text).toMatch(regexp);
}
function finishAnimation(player: WebAnimationsPlayer, cb: () => any) {
getDOM().dispatchEvent(player.domPlayer, getDOM().createEvent('finish'));
Promise.resolve(null).then(cb);
}
afterEach(() => { destroyPlatform(); });
it('should automatically run change detection when the animation done callback code updates any bindings',
@ -2150,24 +2288,82 @@ function declareTests({useJit}: {useJit: boolean}) {
appRef.components.find(cmp => cmp.componentType === AnimationAppCmp).instance;
const driver: ExtendedWebAnimationsDriver = ref.injector.get(AnimationDriver);
const zone: NgZone = ref.injector.get(NgZone);
let text = '';
zone.run(() => {
text = getDOM().getText(el);
expect(text).toMatch(/Animation Status: pending/);
expect(text).toMatch(/Animation Time: 0/);
assertStatus('pending');
assertTime(0);
appCmp.animationStatus = 'on';
setTimeout(() => {
text = getDOM().getText(el);
expect(text).toMatch(/Animation Status: started/);
expect(text).toMatch(/Animation Time: 555/);
const player = driver.players.pop().domPlayer;
getDOM().dispatchEvent(player, getDOM().createEvent('finish'));
zone.runOutsideAngular(() => {
setTimeout(() => {
text = getDOM().getText(el);
expect(text).toMatch(/Animation Status: done/);
expect(text).toMatch(/Animation Time: 555/);
asyncDone();
assertStatus('started');
assertTime(555);
const player = driver.players.pop();
finishAnimation(player, () => {
assertStatus('done');
assertTime(555);
asyncDone();
});
}, 0);
});
});
});
});
it('should not run change detection for an animation that contains multiple steps until a callback is fired',
(asyncDone: Function) => {
bootstrap(AnimationAppCmp, testProviders).then(ref => {
const appRef = <ApplicationRef>ref.injector.get(ApplicationRef);
const appCmpDetails: any =
appRef.components.find(cmp => cmp.componentType === AnimationAppCmp);
const appCD = appCmpDetails.changeDetectorRef;
const appCmp: AnimationAppCmp = appCmpDetails.instance;
const driver: ExtendedWebAnimationsDriver = ref.injector.get(AnimationDriver);
const zone: NgZone = ref.injector.get(NgZone);
let player: WebAnimationsPlayer;
let onDoneCalls: string[] = [];
function onDoneFn(value: string) {
return () => {
NgZone.assertNotInAngularZone();
onDoneCalls.push(value);
appCmp.status = value;
};
};
zone.run(() => {
assertStatus('pending');
appCmp.animationWithSteps = 'on';
setTimeout(() => {
expect(driver.players.length).toEqual(3);
assertStatus('started');
zone.runOutsideAngular(() => {
setTimeout(() => {
assertStatus('started');
player = driver.players.shift();
player.onDone(onDoneFn('1'));
// step 1 => 2
finishAnimation(player, () => {
assertStatus('started');
player = driver.players.shift();
player.onDone(onDoneFn('2'));
// step 2 => 3
finishAnimation(player, () => {
assertStatus('started');
player = driver.players.shift();
player.onDone(onDoneFn('3'));
// step 3 => done
finishAnimation(player, () => {
assertStatus('done');
asyncDone();
});
});
});
}, 0);
});
}, 0);
});
});
@ -2258,7 +2454,17 @@ class _NaiveElementSchema extends DomElementSchemaRegistry {
@Component({
selector: 'animation-app',
animations: [trigger('animationStatus', [transition('off => on', animate(555))])],
animations: [
trigger('animationStatus', [transition('off => on', animate(555))]),
trigger(
'animationWithSteps',
[transition(
'* => on',
[
style({height: '0px'}), animate(100, style({height: '100px'})),
animate(100, style({height: '200px'})), animate(100, style({height: '300px'}))
])])
],
template: `
Animation Time: {{ time }}
Animation Status: {{ status }}
@ -2272,7 +2478,7 @@ class AnimationAppCmp {
animationStatus = 'off';
@HostListener('@animationStatus.start', ['$event'])
onStart(event: AnimationTransitionEvent) {
onAnimationStartDone(event: AnimationTransitionEvent) {
if (event.toState == 'on') {
this.time = event.totalTime;
this.status = 'started';
@ -2280,7 +2486,26 @@ class AnimationAppCmp {
}
@HostListener('@animationStatus.done', ['$event'])
onDone(event: AnimationTransitionEvent) {
onAnimationStatusDone(event: AnimationTransitionEvent) {
if (event.toState == 'on') {
this.time = event.totalTime;
this.status = 'done';
}
}
@HostBinding('@animationWithSteps')
animationWithSteps = 'off';
@HostListener('@animationWithSteps.start', ['$event'])
onAnimationWithStepsStart(event: AnimationTransitionEvent) {
if (event.toState == 'on') {
this.time = event.totalTime;
this.status = 'started';
}
}
@HostListener('@animationWithSteps.done', ['$event'])
onAnimationWithStepsDone(event: AnimationTransitionEvent) {
if (event.toState == 'on') {
this.time = event.totalTime;
this.status = 'done';

View File

@ -0,0 +1,143 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AnimationQueue} from '@angular/core/src/animation/animation_queue';
import {NgZone} from '../../src/zone/ng_zone';
import {TestBed, fakeAsync, flushMicrotasks} from '../../testing';
import {MockAnimationPlayer} from '../../testing/mock_animation_player';
import {beforeEach, describe, expect, it} from '../../testing/testing_internal';
export function main() {
describe('AnimationQueue', function() {
beforeEach(() => { TestBed.configureTestingModule({declarations: [], imports: []}); });
it('should queue animation players and run when flushed, but only as the next scheduled microtask',
fakeAsync(() => {
const zone = TestBed.get(NgZone);
const queue = new AnimationQueue(zone);
const log: string[] = [];
const p1 = new MockAnimationPlayer();
const p2 = new MockAnimationPlayer();
const p3 = new MockAnimationPlayer();
p1.onStart(() => log.push('1'));
p2.onStart(() => log.push('2'));
p3.onStart(() => log.push('3'));
queue.enqueue(p1);
queue.enqueue(p2);
queue.enqueue(p3);
expect(log).toEqual([]);
queue.flush();
expect(log).toEqual([]);
flushMicrotasks();
expect(log).toEqual(['1', '2', '3']);
}));
it('should always run each of the animation players outside of the angular zone on start',
fakeAsync(() => {
const zone = TestBed.get(NgZone);
const queue = new AnimationQueue(zone);
const player = new MockAnimationPlayer();
let eventHasRun = false;
player.onStart(() => {
NgZone.assertNotInAngularZone();
eventHasRun = true;
});
zone.run(() => {
NgZone.assertInAngularZone();
queue.enqueue(player);
queue.flush();
flushMicrotasks();
});
expect(eventHasRun).toBe(true);
}));
it('should always run each of the animation players outside of the angular zone on done',
fakeAsync(() => {
const zone = TestBed.get(NgZone);
const queue = new AnimationQueue(zone);
const player = new MockAnimationPlayer();
let eventHasRun = false;
player.onDone(() => {
NgZone.assertNotInAngularZone();
eventHasRun = true;
});
zone.run(() => {
NgZone.assertInAngularZone();
queue.enqueue(player);
queue.flush();
flushMicrotasks();
});
expect(eventHasRun).toBe(false);
player.finish();
expect(eventHasRun).toBe(true);
}));
it('should not run animations again incase an animation midway fails', fakeAsync(() => {
const zone = TestBed.get(NgZone);
const queue = new AnimationQueue(zone);
const log: string[] = [];
const p1 = new PlayerThatFails(false);
const p2 = new PlayerThatFails(true);
const p3 = new PlayerThatFails(false);
p1.onStart(() => log.push('1'));
p2.onStart(() => log.push('2'));
p3.onStart(() => log.push('3'));
queue.enqueue(p1);
queue.enqueue(p2);
queue.enqueue(p3);
queue.flush();
expect(() => flushMicrotasks()).toThrowError();
expect(log).toEqual(['1', '2']);
// let's reset this so that it gets triggered again
p2.reset();
p2.onStart(() => log.push('2'));
queue.flush();
expect(() => flushMicrotasks()).not.toThrowError();
expect(log).toEqual(['1', '2', '3']);
}));
});
}
class PlayerThatFails extends MockAnimationPlayer {
private _animationStarted = false;
constructor(public doFail: boolean) { super(); }
play() {
super.play();
this._animationStarted = true;
if (this.doFail) {
throw new Error('Oh nooooo');
}
}
reset() { this._animationStarted = false; }
hasStarted() { return this._animationStarted; }
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, CompilerFactory, Component, NgModule, PlatformRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, CompilerFactory, Component, NgModule, PlatformRef, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
import {ApplicationRef, ApplicationRef_} from '@angular/core/src/application_ref';
import {ErrorHandler} from '@angular/core/src/error_handler';
import {ComponentRef} from '@angular/core/src/linker/component_factory';
@ -275,9 +275,15 @@ export function main() {
vc: ViewContainerRef;
}
@Component({template: '<template #t>Dynamic content</template>'})
class EmbeddedViewComp {
@ViewChild(TemplateRef)
tplRef: TemplateRef<Object>;
}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComp, ContainerComp],
declarations: [MyComp, ContainerComp, EmbeddedViewComp],
providers: [{provide: ComponentFixtureNoNgZone, useValue: true}]
});
});
@ -321,6 +327,17 @@ export function main() {
expect(appRef.viewCount).toBe(0);
});
it('should detach attached embedded views if they are destroyed', () => {
const comp = TestBed.createComponent(EmbeddedViewComp);
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
const embeddedViewRef = comp.componentInstance.tplRef.createEmbeddedView({});
appRef.attachView(embeddedViewRef);
embeddedViewRef.destroy();
expect(appRef.viewCount).toBe(0);
});
it('should not allow to attach a view to both, a view container and the ApplicationRef',
() => {
const comp = TestBed.createComponent(MyComp);

View File

@ -0,0 +1,62 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {el} from '@angular/platform-browser/testing/browser_util';
import {NoOpAnimationPlayer} from '../../src/animation/animation_player';
import {AnimationQueue} from '../../src/animation/animation_queue';
import {AnimationViewContext} from '../../src/linker/animation_view_context';
import {TestBed, fakeAsync, flushMicrotasks} from '../../testing';
import {describe, expect, iit, it} from '../../testing/testing_internal';
export function main() {
describe('AnimationViewContext', function() {
let elm: any;
beforeEach(() => { elm = el('<div></div>'); });
function getPlayers(vc: any) { return vc.getAnimationPlayers(elm); }
it('should remove the player from the registry once the animation is complete',
fakeAsync(() => {
const player = new NoOpAnimationPlayer();
const animationQueue = TestBed.get(AnimationQueue) as AnimationQueue;
const vc = new AnimationViewContext(animationQueue);
expect(getPlayers(vc).length).toEqual(0);
vc.queueAnimation(elm, 'someAnimation', player);
expect(getPlayers(vc).length).toEqual(1);
player.finish();
expect(getPlayers(vc).length).toEqual(0);
}));
it('should not remove a follow-up player from the registry if another player is queued',
fakeAsync(() => {
const player1 = new NoOpAnimationPlayer();
const player2 = new NoOpAnimationPlayer();
const animationQueue = TestBed.get(AnimationQueue) as AnimationQueue;
const vc = new AnimationViewContext(animationQueue);
vc.queueAnimation(elm, 'someAnimation', player1);
expect(getPlayers(vc).length).toBe(1);
expect(getPlayers(vc)[0]).toBe(player1);
vc.queueAnimation(elm, 'someAnimation', player2);
expect(getPlayers(vc).length).toBe(1);
expect(getPlayers(vc)[0]).toBe(player2);
player1.finish();
expect(getPlayers(vc).length).toBe(1);
expect(getPlayers(vc)[0]).toBe(player2);
player2.finish();
expect(getPlayers(vc).length).toBe(0);
}));
});
}

View File

@ -82,6 +82,7 @@ export function main() {
AnotherComponent,
TestLocals,
CompWithRef,
WrapCompWithRef,
EmitterDirective,
PushComp,
OnDestroyDirective,
@ -1133,6 +1134,23 @@ export function main() {
expect(renderLog.log).toEqual([]);
}));
it('Detached view can be checked locally', fakeAsync(() => {
const ctx = createCompFixture('<wrap-comp-with-ref></wrap-comp-with-ref>');
const cmp: CompWithRef = queryDirs(ctx.debugElement, CompWithRef)[0];
cmp.value = 'hello';
cmp.changeDetectorRef.detach();
expect(renderLog.log).toEqual([]);
ctx.detectChanges();
expect(renderLog.log).toEqual([]);
cmp.changeDetectorRef.detectChanges();
expect(renderLog.log).toEqual(['{{hello}}']);
}));
it('Reattaches', fakeAsync(() => {
const ctx = createCompFixture('<comp-with-ref></comp-with-ref>');
const cmp: CompWithRef = queryDirs(ctx.debugElement, CompWithRef)[0];
@ -1346,6 +1364,14 @@ class CompWithRef {
noop() {}
}
@Component({
selector: 'wrap-comp-with-ref',
template: '<comp-with-ref></comp-with-ref>'
})
class WrapCompWithRef {
constructor(public changeDetectorRef: ChangeDetectorRef) {}
}
@Component({
selector: 'push-cmp',
template: '<div (event)="noop()" emitterDirective></div>{{value}}{{renderIncrement}}',

View File

@ -8,7 +8,6 @@
import {Component, Injector, OpaqueToken, Pipe, PipeTransform, Provider} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {beforeEach, describe, it} from '@angular/core/testing/testing_internal';
import {expect} from '@angular/platform-browser/testing/matchers';
export function main() {
@ -139,6 +138,15 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(injector.get(token1)).toEqual(tokenValue1);
expect(injector.get(token2)).toEqual(tokenValue2);
});
it('should support providers that have a `name` property with a number value', () => {
class TestClass {
constructor(public name: number) {}
}
const data = [new TestClass(1), new TestClass(2)];
const injector = createInjector([{provide: 'someToken', useValue: data}]);
expect(injector.get('someToken')).toEqual(data);
});
});
it('should allow logging a previous elements class binding via interpolation', () => {

View File

@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CompilerOptions, Component, Directive, Injector, ModuleWithComponentFactories, NgModule, NgModuleRef, NgZone, OpaqueToken, Pipe, PlatformRef, Provider, SchemaMetadata, Type} from '@angular/core';
import {CompilerOptions, Component, Directive, Injector, ModuleWithComponentFactories, NgModule, NgModuleRef, NgZone, OpaqueToken, Pipe, PlatformRef, Provider, ReflectiveInjector, SchemaMetadata, Type} from '@angular/core';
import {AsyncTestCompleter} from './async_test_completer';
import {ComponentFixture} from './component_fixture';
import {stringify} from './facade/lang';
@ -268,8 +269,10 @@ export class TestBed implements Injector {
}
}
}
this._moduleRef =
this._moduleWithComponentFactories.ngModuleFactory.create(this.platform.injector);
const ngZone = new NgZone({enableLongStackTrace: true});
const ngZoneInjector = ReflectiveInjector.resolveAndCreate(
[{provide: NgZone, useValue: ngZone}], this.platform.injector);
this._moduleRef = this._moduleWithComponentFactories.ngModuleFactory.create(ngZoneInjector);
this._instantiated = true;
}

View File

@ -3,3 +3,32 @@
This folder contains small example apps that get in-lined into our API docs.
Each example contains tests for application behavior (as opposed to testing Angular's
behavior) just like an Angular application developer would write.
# Running the examples
```
# # execute the following command only when framework code changes
./build.sh
# run when test change
./modules/@angular/examples/build.sh
# start server
$(npm bin)/gulp serve-examples
```
navigate to [http://localhost:8001](http://localhost:8001)
# Running the tests
```
# run only when framework code changes
./build.sh
# run to compile tests and run them
./modules/@angular/examples/test.sh
```
NOTE: sometimes the http server does not exit properly and it retains the `8001` port.
in such a case you can use `lsof -i:8001` to see which process it is and then use `kill`
to remove it. (Or in single command: `lsof -i:8001 -t | xargs kill`)

View File

@ -1,11 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="bootstrap.js"></script>
<script type="text/javascript">
System.import('main-dynamic').catch(console.error.bind(console));
</script>
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<script type="text/javascript" src="bootstrap.js"></script>
<script type="text/javascript">
System.import('main-dynamic').catch(console.error.bind(console));
</script>
</head>
<body>

View File

@ -0,0 +1,30 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {browser, by, element, protractor} from 'protractor';
import {verifyNoBrowserErrors} from '../../../../_common/e2e_util';
function waitForElement(selector: string) {
const EC = (<any>protractor).ExpectedConditions;
// Waits for the element with id 'abc' to be present on the dom.
browser.wait(EC.presenceOf($(selector)), 20000);
}
describe('Location', () => {
afterEach(verifyNoBrowserErrors);
it('should verify paths', () => {
browser.get('/common/location/ts/#/bar/baz');
waitForElement('hash-location');
expect(element.all(by.css('path-location code')).get(0).getText())
.toEqual('/common/location/ts');
expect(element.all(by.css('hash-location code')).get(0).getText()).toEqual('/bar/baz');
});
});

View File

@ -0,0 +1,26 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
// #docregion LocationComponent
import {HashLocationStrategy, Location, LocationStrategy} from '@angular/common';
import {Component} from '@angular/core';
@Component({
selector: 'hash-location',
providers: [Location, {provide: LocationStrategy, useClass: HashLocationStrategy}],
template: `
<h1>HashLocationStrategy</h1>
Current URL is: <code>{{location.path()}}</code><br>
Normalize: <code>/foo/bar/</code> is: <code>{{location.normalize('foo/bar')}}</code><br>
`
})
export class HashLocationComponent {
location: Location;
constructor(location: Location) { this.location = location; }
}
// #enddocregion

View File

@ -0,0 +1,30 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {APP_BASE_HREF} from '@angular/common';
import {Component, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {HashLocationComponent} from './hash_location_component';
import {PathLocationComponent} from './path_location_component';
@Component({
selector: 'example-app',
template: `<hash-location></hash-location><path-location></path-location>`
})
export class ExampleAppComponent {
}
@NgModule({
declarations: [ExampleAppComponent, PathLocationComponent, HashLocationComponent],
providers: [{provide: APP_BASE_HREF, useValue: '/'}],
imports: [BrowserModule],
bootstrap: [ExampleAppComponent]
})
export class AppModule {
}

View File

@ -0,0 +1,26 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
// #docregion LocationComponent
import {Location, LocationStrategy, PathLocationStrategy} from '@angular/common';
import {Component} from '@angular/core';
@Component({
selector: 'path-location',
providers: [Location, {provide: LocationStrategy, useClass: PathLocationStrategy}],
template: `
<h1>PathLocationStrategy</h1>
Current URL is: <code>{{location.path()}}</code><br>
Normalize: <code>/foo/bar/</code> is: <code>{{location.normalize('foo/bar')}}</code><br>
`
})
export class PathLocationComponent {
location: Location;
constructor(location: Location) { this.location = location; }
}
// #enddocregion

View File

@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
import {afterEach, beforeEach, beforeEachProviders, describe, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
let db: any;
class MyService {}
@ -70,16 +69,6 @@ describe('some component', () => {
});
// #enddocregion
// #docregion beforeEachProviders
describe('some component', () => {
beforeEachProviders(() => [{provide: MyService, useClass: MyMockService}]);
it('uses MyService', inject(
[MyService], (service: MyMockService) => {
// service is an instance of MyMockService.
}));
});
// #enddocregion
// #docregion afterEach
describe('some component', () => {
afterEach((done: Function) => { db.reset().then((_: any) => done()); });

View File

@ -5,6 +5,7 @@
"stripInternal": true,
"experimentalDecorators": true,
"noImplicitAny": true,
"emitDecoratorMetadata": true,
"module": "commonjs",
"moduleResolution": "node",
"outDir": "../../../dist/examples",
@ -18,14 +19,10 @@
"target": "es5",
"lib": ["es2015", "dom"],
"skipLibCheck": true,
"types": ["jasmine", "node", "angularjs"]
"types": ["jasmine", "node", "angularjs", "systemjs"]
},
"include": [
"./_common/*.ts",
"./**/module.ts",
"./**/test/*.ts",
"./**/e2e_test/*.ts",
"../../system.d.ts",
"./**/*.ts",
"../../../node_modules/zone.js/dist/zone.js.d.ts",
// TODO(i): we can't use protractor's built-in typings because they contain lots of ambient definitions
"../../../node_modules/@types/protractor/index.d.ts"

View File

@ -18,9 +18,12 @@ export class BaseError extends Error {
_nativeError: Error;
constructor(message: string) {
super(message);
// Errors don't use current this, instead they create a new instance.
// We have to do forward all of our api to the nativeInstance.
const nativeError = super(message) as any as Error;
// TODO(bradfordcsmith): Remove this hack when
// google/closure-compiler/issues/2102 is fixed.
const nativeError = new Error(message) as any as Error;
this._nativeError = nativeError;
}

View File

@ -95,11 +95,11 @@ export function stringify(token: any): string {
}
if (token.overriddenName) {
return token.overriddenName;
return `${token.overriddenName}`;
}
if (token.name) {
return token.name;
return `${token.name}`;
}
const res = token.toString();

View File

@ -23,7 +23,7 @@ import {FormGroupDirective} from './directives/reactive_directives/form_group_di
import {FormArrayName, FormGroupName} from './directives/reactive_directives/form_group_name';
import {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor';
import {NgSelectMultipleOption, SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor';
import {MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator} from './directives/validators';
import {CheckboxRequiredValidator, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator} from './directives/validators';
export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor';
export {ControlValueAccessor} from './directives/control_value_accessor';
@ -42,13 +42,24 @@ export {FormGroupDirective} from './directives/reactive_directives/form_group_di
export {FormArrayName, FormGroupName} from './directives/reactive_directives/form_group_name';
export {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor';
export {NgSelectMultipleOption, SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor';
export {MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator} from './directives/validators';
export const SHARED_FORM_DIRECTIVES: Type<any>[] = [
NgSelectOption, NgSelectMultipleOption, DefaultValueAccessor, NumberValueAccessor,
RangeValueAccessor, CheckboxControlValueAccessor, SelectControlValueAccessor,
SelectMultipleControlValueAccessor, RadioControlValueAccessor, NgControlStatus,
NgControlStatusGroup, RequiredValidator, MinLengthValidator, MaxLengthValidator, PatternValidator
NgSelectOption,
NgSelectMultipleOption,
DefaultValueAccessor,
NumberValueAccessor,
RangeValueAccessor,
CheckboxControlValueAccessor,
SelectControlValueAccessor,
SelectMultipleControlValueAccessor,
RadioControlValueAccessor,
NgControlStatus,
NgControlStatusGroup,
RequiredValidator,
MinLengthValidator,
MaxLengthValidator,
PatternValidator,
CheckboxRequiredValidator,
];
export const TEMPLATE_DRIVEN_DIRECTIVES: Type<any>[] = [NgModel, NgModelGroup, NgForm];

View File

@ -66,11 +66,15 @@ export class SelectMultipleControlValueAccessor implements ControlValueAccessor
writeValue(value: any): void {
this.value = value;
if (value == null) return;
const values: Array<any> = <Array<any>>value;
// convert values to ids
const ids = values.map((v) => this._getOptionId(v));
this._optionMap.forEach((opt, o) => { opt._setSelected(ids.indexOf(o.toString()) > -1); });
let optionSelectedStateSetter: (opt: NgSelectMultipleOption, o: any) => void;
if (Array.isArray(value)) {
// convert values to ids
const ids = value.map((v) => this._getOptionId(v));
optionSelectedStateSetter = (opt, o) => { opt._setSelected(ids.indexOf(o.toString()) > -1); };
} else {
optionSelectedStateSetter = (opt, o) => { opt._setSelected(false); };
}
this._optionMap.forEach(optionSelectedStateSetter);
}
registerOnChange(fn: (value: any) => any): void {
@ -95,6 +99,7 @@ export class SelectMultipleControlValueAccessor implements ControlValueAccessor
}
}
}
this.value = selected;
fn(selected);
};
}

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, Input, OnChanges, SimpleChanges, forwardRef} from '@angular/core';
import {Directive, Input, OnChanges, Provider, SimpleChanges, forwardRef} from '@angular/core';
import {AbstractControl} from '../model';
import {NG_VALIDATORS, Validators} from '../validators';
@ -33,12 +33,18 @@ export interface Validator {
registerOnValidatorChange?(fn: () => void): void;
}
export const REQUIRED_VALIDATOR: any = {
export const REQUIRED_VALIDATOR: Provider = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => RequiredValidator),
multi: true
};
export const CHECKBOX_REQUIRED_VALIDATOR: Provider = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CheckboxRequiredValidator),
multi: true
};
/**
* A Directive that adds the `required` validator to any controls marked with the
* `required` attribute, via the {@link NG_VALIDATORS} binding.
@ -52,7 +58,8 @@ export const REQUIRED_VALIDATOR: any = {
* @stable
*/
@Directive({
selector: '[required][formControlName],[required][formControl],[required][ngModel]',
selector:
':not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]',
providers: [REQUIRED_VALIDATOR],
host: {'[attr.required]': 'required ? "" : null'}
})
@ -75,6 +82,30 @@ export class RequiredValidator implements Validator {
registerOnValidatorChange(fn: () => void): void { this._onChange = fn; }
}
/**
* A Directive that adds the `required` validator to checkbox controls marked with the
* `required` attribute, via the {@link NG_VALIDATORS} binding.
*
* ### Example
*
* ```
* <input type="checkbox" name="active" ngModel required>
* ```
*
* @experimental
*/
@Directive({
selector:
'input[type=checkbox][required][formControlName],input[type=checkbox][required][formControl],input[type=checkbox][required][ngModel]',
providers: [CHECKBOX_REQUIRED_VALIDATOR],
host: {'[attr.required]': 'required ? "" : null'}
})
export class CheckboxRequiredValidator extends RequiredValidator {
validate(c: AbstractControl): {[key: string]: any} {
return this.required ? Validators.requiredTrue(c) : null;
}
}
/**
* @stable
*/

View File

@ -38,7 +38,7 @@ export {FormArrayName} from './directives/reactive_directives/form_group_name';
export {FormGroupName} from './directives/reactive_directives/form_group_name';
export {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor';
export {SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor';
export {AsyncValidatorFn, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator, Validator, ValidatorFn} from './directives/validators';
export {AsyncValidatorFn, CheckboxRequiredValidator, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator, Validator, ValidatorFn} from './directives/validators';
export {FormBuilder} from './form_builder';
export {AbstractControl, FormArray, FormControl, FormGroup} from './model';
export {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from './validators';

View File

@ -36,10 +36,6 @@ export const PENDING = 'PENDING';
*/
export const DISABLED = 'DISABLED';
export function isControl(control: Object): boolean {
return control instanceof AbstractControl;
}
function _find(control: AbstractControl, path: Array<string|number>| string, delimiter: string) {
if (path == null) return null;

View File

@ -64,6 +64,13 @@ export class Validators {
return isEmptyInputValue(control.value) ? {'required': true} : null;
}
/**
* Validator that requires control value to be true.
*/
static requiredTrue(control: AbstractControl): {[key: string]: boolean} {
return control.value === true ? null : {'required': true};
}
/**
* Validator that requires controls to have a value of a minimum length.
*/
@ -72,7 +79,7 @@ export class Validators {
if (isEmptyInputValue(control.value)) {
return null; // don't validate empty values to allow optional controls
}
const length = typeof control.value === 'string' ? control.value.length : 0;
const length: number = control.value ? control.value.length : 0;
return length < minLength ?
{'minlength': {'requiredLength': minLength, 'actualLength': length}} :
null;
@ -84,7 +91,7 @@ export class Validators {
*/
static maxLength(maxLength: number): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} => {
const length = typeof control.value === 'string' ? control.value.length : 0;
const length: number = control.value ? control.value.length : 0;
return length > maxLength ?
{'maxlength': {'requiredLength': maxLength, 'actualLength': length}} :
null;

View File

@ -39,7 +39,8 @@ export function main() {
ValidationBindingsForm,
UniqLoginValidator,
UniqLoginWrapper,
NestedFormGroupComp
NestedFormGroupComp,
FormControlCheckboxRequiredValidator,
]
});
});
@ -1311,6 +1312,24 @@ export function main() {
});
describe('validations', () => {
it('required validator should validate checkbox', () => {
const fixture = TestBed.createComponent(FormControlCheckboxRequiredValidator);
const control = new FormControl(false, Validators.requiredTrue);
fixture.componentInstance.control = control;
fixture.detectChanges();
const checkbox = fixture.debugElement.query(By.css('input'));
expect(checkbox.nativeElement.checked).toBe(false);
expect(control.hasError('required')).toEqual(true);
checkbox.nativeElement.checked = true;
dispatchEvent(checkbox.nativeElement, 'change');
fixture.detectChanges();
expect(checkbox.nativeElement.checked).toBe(true);
expect(control.hasError('required')).toEqual(false);
});
it('should use sync validators defined in html', () => {
const fixture = TestBed.createComponent(LoginIsEmptyWrapper);
const form = new FormGroup({
@ -2052,6 +2071,14 @@ class ValidationBindingsForm {
pattern: string;
}
@Component({
selector: 'form-control-checkbox-validator',
template: `<input type="checkbox" [formControl]="control">`
})
class FormControlCheckboxRequiredValidator {
control: FormControl;
}
@Component({
selector: 'uniq-login-wrapper',
template: `

View File

@ -7,7 +7,7 @@
*/
import {Component, Directive, Input, forwardRef} from '@angular/core';
import {TestBed, async, fakeAsync, tick} from '@angular/core/testing';
import {ComponentFixture, TestBed, async, fakeAsync, tick} from '@angular/core/testing';
import {AbstractControl, ControlValueAccessor, FormsModule, NG_ASYNC_VALIDATORS, NG_VALUE_ACCESSOR, NgForm, Validator} from '@angular/forms';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -19,11 +19,26 @@ export function main() {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
StandaloneNgModel, NgModelForm, NgModelGroupForm, NgModelValidBinding, NgModelNgIfForm,
NgModelRadioForm, NgModelRangeForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName,
NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper,
NgModelValidationBindings, NgModelMultipleValidators, NgAsyncValidator,
NgModelAsyncValidation, NgModelSelectWithNullForm
StandaloneNgModel,
NgModelForm,
NgModelGroupForm,
NgModelValidBinding,
NgModelNgIfForm,
NgModelRadioForm,
NgModelRangeForm,
NgModelSelectForm,
NgNoFormComp,
InvalidNgModelNoName,
NgModelOptionsStandalone,
NgModelCustomComp,
NgModelCustomWrapper,
NgModelValidationBindings,
NgModelMultipleValidators,
NgAsyncValidator,
NgModelAsyncValidation,
NgModelSelectMultipleForm,
NgModelSelectWithNullForm,
NgModelCheckboxRequiredValidator,
],
imports: [FormsModule]
});
@ -723,6 +738,70 @@ export function main() {
}));
});
describe('select multiple controls', () => {
let fixture: ComponentFixture<NgModelSelectMultipleForm>;
let comp: NgModelSelectMultipleForm;
beforeEach(() => {
fixture = TestBed.createComponent(NgModelSelectMultipleForm);
comp = fixture.componentInstance;
comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}];
});
const detectChangesAndTick = (): void => {
fixture.detectChanges();
tick();
};
const setSelectedCities = (selectedCities: any): void => {
comp.selectedCities = selectedCities;
detectChangesAndTick();
};
const selectOptionViaUI = (valueString: string): void => {
const select = fixture.debugElement.query(By.css('select'));
select.nativeElement.value = valueString;
dispatchEvent(select.nativeElement, 'change');
detectChangesAndTick();
};
const assertOptionElementSelectedState = (selectedStates: boolean[]): void => {
const options = fixture.debugElement.queryAll(By.css('option'));
if (options.length !== selectedStates.length) {
throw 'the selected state values to assert does not match the number of options';
}
for (let i = 0; i < selectedStates.length; i++) {
expect(options[i].nativeElement.selected).toBe(selectedStates[i]);
}
};
it('should reflect state of model after option selected and new options subsequently added',
fakeAsync(() => {
setSelectedCities([]);
selectOptionViaUI('1: Object');
assertOptionElementSelectedState([false, true, false]);
comp.cities.push({'name': 'Chicago'});
detectChangesAndTick();
assertOptionElementSelectedState([false, true, false, false]);
}));
it('should reflect state of model after option selected and then other options removed',
fakeAsync(() => {
setSelectedCities([]);
selectOptionViaUI('1: Object');
assertOptionElementSelectedState([false, true, false]);
comp.cities.pop();
detectChangesAndTick();
assertOptionElementSelectedState([false, true]);
}));
});
describe('custom value accessors', () => {
it('should support standard writing to view and model', async(() => {
const fixture = TestBed.createComponent(NgModelCustomWrapper);
@ -749,6 +828,42 @@ export function main() {
describe('validation directives', () => {
it('required validator should validate checkbox', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelCheckboxRequiredValidator);
fixture.detectChanges();
tick();
const control =
fixture.debugElement.children[0].injector.get(NgForm).control.get('checkbox');
const input = fixture.debugElement.query(By.css('input'));
expect(input.nativeElement.checked).toBe(false);
expect(control.hasError('required')).toBe(false);
fixture.componentInstance.required = true;
fixture.detectChanges();
tick();
expect(input.nativeElement.checked).toBe(false);
expect(control.hasError('required')).toBe(true);
input.nativeElement.checked = true;
dispatchEvent(input.nativeElement, 'change');
fixture.detectChanges();
tick();
expect(input.nativeElement.checked).toBe(true);
expect(control.hasError('required')).toBe(false);
input.nativeElement.checked = false;
dispatchEvent(input.nativeElement, 'change');
fixture.detectChanges();
tick();
expect(input.nativeElement.checked).toBe(false);
expect(control.hasError('required')).toBe(true);
}));
it('should support dir validators using bindings', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelValidationBindings);
fixture.componentInstance.required = true;
@ -1136,6 +1251,19 @@ class NgModelSelectWithNullForm {
cities: any[] = [];
}
@Component({
selector: 'ng-model-select-multiple-form',
template: `
<select multiple [(ngModel)]="selectedCities">
<option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
</select>
`
})
class NgModelSelectMultipleForm {
selectedCities: any[];
cities: any[] = [];
}
@Component({
selector: 'ng-model-custom-comp',
template: `
@ -1202,6 +1330,16 @@ class NgModelMultipleValidators {
pattern: string|RegExp;
}
@Component({
selector: 'ng-model-checkbox-validator',
template:
`<form><input type="checkbox" [(ngModel)]="accepted" [required]="required" name="checkbox"></form>`
})
class NgModelCheckboxRequiredValidator {
accepted: boolean = false;
required: boolean = false;
}
@Directive({
selector: '[ng-async-validator]',
providers: [

View File

@ -8,7 +8,7 @@
import {fakeAsync, tick} from '@angular/core/testing';
import {describe, expect, it} from '@angular/core/testing/testing_internal';
import {AbstractControl, FormControl, Validators} from '@angular/forms';
import {AbstractControl, FormArray, FormControl, Validators} from '@angular/forms';
import {Observable} from 'rxjs/Observable';
import {normalizeAsyncValidator} from '../src/directives/normalize_validator';
@ -50,6 +50,14 @@ export function main() {
() => { expect(Validators.required(new FormControl(0))).toBeNull(); });
});
describe('requiredTrue', () => {
it('should error on false',
() => expect(Validators.requiredTrue(new FormControl(false))).toEqual({'required': true}));
it('should not error on true',
() => expect(Validators.requiredTrue(new FormControl(true))).toBeNull());
});
describe('minLength', () => {
it('should not error on an empty string',
() => { expect(Validators.minLength(2)(new FormControl(''))).toBeNull(); });
@ -68,6 +76,18 @@ export function main() {
'minlength': {'requiredLength': 2, 'actualLength': 1}
});
});
it('should not error when FormArray has valid length', () => {
const fa = new FormArray([new FormControl(''), new FormControl('')]);
expect(Validators.minLength(2)(fa)).toBeNull();
});
it('should error when FormArray has invalid length', () => {
const fa = new FormArray([new FormControl('')]);
expect(Validators.minLength(2)(fa)).toEqual({
'minlength': {'requiredLength': 2, 'actualLength': 1}
});
});
});
describe('maxLength', () => {
@ -85,6 +105,18 @@ export function main() {
'maxlength': {'requiredLength': 2, 'actualLength': 3}
});
});
it('should not error when FormArray has valid length', () => {
const fa = new FormArray([new FormControl(''), new FormControl('')]);
expect(Validators.maxLength(2)(fa)).toBeNull();
});
it('should error when FormArray has invalid length', () => {
const fa = new FormArray([new FormControl(''), new FormControl('')]);
expect(Validators.maxLength(1)(fa)).toEqual({
'maxlength': {'requiredLength': 1, 'actualLength': 2}
});
});
});
describe('pattern', () => {

View File

@ -113,7 +113,7 @@ export class RequestOptions {
merge(options?: RequestOptionsArgs): RequestOptions {
return new RequestOptions({
method: options && options.method != null ? options.method : this.method,
headers: options && options.headers != null ? options.headers : this.headers,
headers: options && options.headers != null ? options.headers : new Headers(this.headers),
body: options && options.body != null ? options.body : this.body,
url: options && options.url != null ? options.url : this.url,
search: options && options.search != null ?

View File

@ -47,7 +47,7 @@ export abstract class Body {
return String.fromCharCode.apply(null, new Uint16Array(<ArrayBuffer>this._body));
}
if (this._body === null) {
if (this._body == null) {
return '';
}

View File

@ -9,6 +9,7 @@
import {describe, expect, it} from '@angular/core/testing/testing_internal';
import {BaseRequestOptions, RequestOptions} from '../src/base_request_options';
import {RequestMethod} from '../src/enums';
import {Headers} from '../src/headers';
export function main() {
describe('BaseRequestOptions', () => {

View File

@ -88,5 +88,14 @@ export function main() {
expect(req.text()).toEqual('');
});
it('should return empty string if body is undefined', () => {
const reqOptions = new RequestOptions(
{url: 'test', method: 'GET', headers: new Headers({'content-type': 'application/json'})});
delete reqOptions.body;
const req = new Request(reqOptions);
expect(req.text()).toEqual('');
});
});
}

View File

@ -201,22 +201,25 @@ class AstType implements ExpressionVisitor {
visitBinary(ast: Binary): Symbol {
// Treat undefined and null as other.
function normalize(kind: BuiltinType): BuiltinType {
function normalize(kind: BuiltinType, other: BuiltinType): BuiltinType {
switch (kind) {
case BuiltinType.Undefined:
case BuiltinType.Null:
return BuiltinType.Other;
return normalize(other, BuiltinType.Other);
}
return kind;
}
const leftType = this.getType(ast.left);
const rightType = this.getType(ast.right);
const leftKind = normalize(this.query.getTypeKind(leftType));
const rightKind = normalize(this.query.getTypeKind(rightType));
const leftRawKind = this.query.getTypeKind(leftType);
const rightRawKind = this.query.getTypeKind(rightType);
const leftKind = normalize(leftRawKind, rightRawKind);
const rightKind = normalize(rightRawKind, leftRawKind);
// The following swtich implements operator typing similar to the
// type production tables in the TypeScript specification.
// https://github.com/Microsoft/TypeScript/blob/v1.8.10/doc/spec.md#4.19
const operKind = leftKind << 8 | rightKind;
switch (ast.operation) {
case '*':
@ -403,6 +406,8 @@ class AstType implements ExpressionVisitor {
return this.query.getBuiltinType(BuiltinType.Boolean);
case null:
return this.query.getBuiltinType(BuiltinType.Null);
case undefined:
return this.query.getBuiltinType(BuiltinType.Undefined);
default:
switch (typeof ast.value) {
case 'string':

View File

@ -571,25 +571,7 @@ class TypeScriptSymbolQuery implements SymbolQuery {
private program: ts.Program, private checker: ts.TypeChecker, private source: ts.SourceFile,
private fetchPipes: () => SymbolTable) {}
getTypeKind(symbol: Symbol): BuiltinType {
const type = this.getTsTypeOf(symbol);
if (type) {
if (type.flags & ts.TypeFlags.Any) {
return BuiltinType.Any;
} else if (
type.flags &
(ts.TypeFlags.String | ts.TypeFlags.StringLike | ts.TypeFlags.StringLiteral)) {
return BuiltinType.String;
} else if (type.flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLike)) {
return BuiltinType.Number;
} else if (type.flags & (ts.TypeFlags.Undefined)) {
return BuiltinType.Undefined;
} else if (type.flags & (ts.TypeFlags.Null)) {
return BuiltinType.Null;
}
}
return BuiltinType.Other;
}
getTypeKind(symbol: Symbol): BuiltinType { return typeKindOf(this.getTsTypeOf(symbol)); }
getBuiltinType(kind: BuiltinType): Symbol {
// TODO: Replace with typeChecker API when available.
@ -1223,4 +1205,35 @@ function getTypeParameterOf(type: ts.Type, name: string): ts.Type {
return typeArguments[0];
}
}
}
function typeKindOf(type: ts.Type): BuiltinType {
if (type) {
if (type.flags & ts.TypeFlags.Any) {
return BuiltinType.Any;
} else if (
type.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLike | ts.TypeFlags.StringLiteral)) {
return BuiltinType.String;
} else if (type.flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLike)) {
return BuiltinType.Number;
} else if (type.flags & (ts.TypeFlags.Undefined)) {
return BuiltinType.Undefined;
} else if (type.flags & (ts.TypeFlags.Null)) {
return BuiltinType.Null;
} else if (type.flags & ts.TypeFlags.Union) {
// If all the constituent types of a union are the same kind, it is also that kind.
let candidate: BuiltinType;
const unionType = type as ts.UnionType;
if (unionType.types.length > 0) {
candidate = typeKindOf(unionType.types[0]);
for (const subType of unionType.types) {
if (candidate != typeKindOf(subType)) {
return BuiltinType.Other;
}
}
}
return candidate;
}
}
return BuiltinType.Other;
}

View File

@ -121,6 +121,15 @@ describe('diagnostics', () => {
code, fileName => { expect(() => ngService.getDiagnostics(fileName)).not.toThrow(); });
});
it('should not report an error for sub-types of string', () => {
const code =
` @Component({template: \`<div *ngIf="something === 'foo'"></div>\`}) export class MyComponent { something: 'foo' | 'bar'; }`;
addCode(code, fileName => {
const diagnostics = ngService.getDiagnostics(fileName);
onlyModuleDiagnostics(diagnostics);
});
});
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
const fileName = '/app/app.component.ts';
const originalContent = mockHost.getFileContent(fileName);
@ -137,6 +146,13 @@ describe('diagnostics', () => {
function onlyModuleDiagnostics(diagnostics: Diagnostics) {
// Expect only the 'MyComponent' diagnostic
expect(diagnostics.length).toBe(1);
if (diagnostics.length > 1) {
for (const diagnostic of diagnostics) {
if (diagnostic.message.indexOf('MyComponent') >= 0) continue;
console.error(`(${diagnostic.span.start}:${diagnostic.span.end}): ${diagnostic.message}`);
}
return;
}
expect(diagnostics[0].message.indexOf('MyComponent') >= 0).toBeTruthy();
}
});

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