Compare commits

...

81 Commits

Author SHA1 Message Date
fa9e21e83c fix(compiler): fix merge error in compiler_host 2016-12-14 15:36:49 -08:00
b6078f5887 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 15:28:51 -08:00
c65b4fa9dc refactor: format & lint 2016-12-14 15:10:43 -08:00
169ed82900 feat(testing): add overrideTemplate method (#13372)
Closes #10685
2016-12-14 15:05:17 -08:00
fd8e15b15d chore(animations/aot): always export NoOpAnimationDriver (#13480) 2016-12-14 14:51:29 -08:00
aa40366a92 fix(compiler): fix simplify a reference without a name
closes #13470
2016-12-14 14:33:10 -08:00
40d8d9c3e3 fix(tsc-wrapped): generate metadata for exports without module specifier
fixes #13327
2016-12-14 14:33:04 -08:00
ee2ac025ef fix(compiler): propagate exports when upgrading metadata to v2 2016-12-14 14:33:04 -08:00
aa3769ba69 fix(compiler): resolver should merge host bindings and listeners (#13474)
fixes #13327
2016-12-14 14:31:57 -08:00
d4ddb6004e refactor: format & lint 2016-12-14 13:05:04 -08:00
84400bcc86 docs(upgrade): fix UpgradeAdapter examples
closes #12675
2016-12-14 13:02:31 -08:00
42d9998cbb docs(upgrade/upgrade_adapter): fix up references to AngularJS and Angular 2 2016-12-14 13:02:27 -08:00
c18d2fe5e3 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 13:02:27 -08:00
d91a86aac6 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 13:02:27 -08:00
d6e5e9283c 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 13:02:27 -08:00
eab7e490c9 refactor(upgrade/util): remove unused stringify() method 2016-12-14 13:02:27 -08:00
3e90605db9 refactor(compiler/template_parser): export createElementCssSelector
This is needed in `ngUpgrade`.
2016-12-14 13:02:27 -08:00
79671a6f12 refactor(upgrade): add missing Angular 1 type info 2016-12-14 13:02:27 -08:00
a659259962 fix(core): detectChanges() doesn't work on detached instance
Closes #13426
Closes #13472
2016-12-14 13:01:06 -08:00
b56474d067 fix(animations): throw errors and normalize offset beyond the range of [0,1]
Closes #13348
Closes #13440
2016-12-14 12:59:47 -08:00
8395f0e138 perf(animations): always run the animation queue outside of zones
Related #12732
Closes #13440
2016-12-14 12:59:36 -08:00
dd0519abad 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 12:58:41 -08:00
f238c8ac7a Revert "fix(compiler): xmb <ph> tags should not self close (#13413)"
This reverts commit 4b3d135193.
closes #13463
2016-12-14 12:54:58 -08:00
8c27c62fab Revert "test(i18n): fix a typo in the reference xmb (#13441)"
This reverts commit a8d237581d.
2016-12-14 12:54:50 -08:00
5031adc7a3 refactor(facade): don't expect super() to return a new Error object in BaseError (#12600)
Related to #12575
2016-12-14 11:54:57 -08:00
821b8f09d6 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 08:52:07 -08:00
2bf1bbc071 fix(forms): introduce checkbox required validator
Closes #11459
Closes #13364
2016-12-14 08:44:24 -08:00
7b0a86718c 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 08:34:19 -08:00
3edca4d37e fix(core): properly destroy embedded Views attatched to ApplicationRef (#13459)
Fixes #13062
2016-12-14 08:33:29 -08:00
a0a05041ac refactor: format & lint 2016-12-13 17:44:52 -08:00
7256d0ede5 chore(internal API): introduce an internal API for ngtools. (#13415) 2016-12-13 17:35:06 -08:00
d62d89319e fix(compiler): generated CSS files suffixed with ngstyle. (#13353)
Mirrors factories which ends in `ngfactory`.

Closes #13141.
2016-12-13 17:34:46 -08:00
f5f1d5f65c fix(compiler): make sure provider values with name property don’t break.
Fixes #13394
Closes #13445
2016-12-13 17:25:59 -08:00
a8d237581d test(i18n): fix a typo in the reference xmb (#13441) 2016-12-13 12:35:09 -08:00
d036165a19 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-13 12:34:50 -08:00
d17e690eb4 test(upgrade): fix failing test in browsers which do not support RAF
closes #13399
2016-12-13 12:28:44 -08:00
714f2af0dd ci(browser providers): update browsers in SL and BS (#13431) 2016-12-13 11:32:31 -08:00
2b90cd532f fix(compiler): narrow the span reported for invalid pipes
fixes #13326
closes #13411
2016-12-13 11:23:47 -08:00
3a64ad895a fix(language-service): correctly type undefined
fixes #13412
closes #13414
2016-12-13 11:23:08 -08:00
9ec0a4e105 feat(language-service): warn when a method isn't called in an event (#13437)
Closes 13435
2016-12-13 11:20:45 -08:00
4b3d135193 fix(compiler): xmb <ph> tags should not self close (#13413) 2016-12-12 19:10:20 -08:00
1d0ed6f75f docs(core): update OnDestroy description (#13369)
Closes #11228
2016-12-12 16:45:56 -08:00
6f330a5fc9 fix(language-service): treat string unions as strings (#13406)
Fixes #13403
2016-12-12 16:42:20 -08:00
e23076f767 build: update the package list of the symlinks scripts for Windows (#13408) 2016-12-12 16:41:35 -08:00
7295a5e7f2 refactor: format and lint code 2016-12-12 11:30:25 -08:00
20bed46737 docs(Location): updating Location docs and adding example
closes #11500
2016-12-12 11:19:21 -08:00
2a5012d515 chore: Add @types/systemjs 2016-12-12 11:19:05 -08:00
fb38fba8f9 chore: convert hash_location_strategy example to a tested spec 2016-12-12 11:19:05 -08:00
4c35be3e07 feat(forms): add novalidate by default (#13092) 2016-12-12 11:17:42 -08:00
e9f307f948 fix(forms): fix Validators.min/maxLength with FormArray (#13095)
Fixes #13089
2016-12-12 11:17:12 -08:00
2e500cc85b fix(http): create a copy of headers when merge options (#13365)
Closes #11980
2016-12-12 11:16:34 -08:00
56dce0e26d feat(common): export NgLocaleLocalization (#13367)
Closes #11921
2016-12-12 11:16:12 -08:00
8a8c53250e fix(dom_adapter): remove logError from logGroup (#12925) 2016-12-09 15:40:26 -08:00
08ff2e5249 fix(http): check response body text against undefined (#13017) 2016-12-09 15:39:39 -08:00
a006c1418a feat(router): routerLink add tabindex attribute (#13094)
Fixes #10895
2016-12-09 15:38:50 -08:00
90c223591f feat(http): simplify URLSearchParams creation (#13338)
Closes #8858
2016-12-09 15:38:29 -08:00
aaf6e05f56 refactor(commonn): fix lint issues
closes #13352
2016-12-09 15:37:46 -08:00
3bee521aa4 fix(compiler): support dotted property binding
fixes angular/flex-layout#34
2016-12-09 15:37:41 -08:00
95f48292b1 test(Selector): add a test for dotted attribute names 2016-12-09 15:37:41 -08:00
04cfa1ebdf refactor(Compiler): cleanup 2016-12-09 15:37:41 -08:00
4022173d1e fix(compiler): fix PR 13322 (#13331) 2016-12-09 11:22:44 -08:00
c8baf51f4f style: clang-format the code 2016-12-09 11:19:55 -08:00
b4db73d0bf feat: ngIf now supports else; saves condition to local var
NgIf syntax has been extended to support else clause to display template
when the condition is false. In addition the condition value can now
be stored in local variable, for later reuse. This is especially useful
when used with the `async` pipe.

Example:

```
<div *ngIf="userObservable | async; else loading; let user">
  Hello {{user.last}}, {{user.first}}!
</div>
<template #loading>Waiting...</template>
```

closes #13061
closes #13297
2016-12-09 11:19:08 -08:00
e15a3f273f fix: Better instructions on running examples and their tests 2016-12-09 11:16:49 -08:00
213c713409 fix: Better error when directive not listed in NgModule.declarations 2016-12-09 11:16:28 -08:00
9a8423da36 fix(selector): SelectorMatcher match elements with :not selector (#12977) 2016-12-09 10:45:48 -08:00
f0b0762f4a fix(animations): always cleanup players after they have finished internally (#13334)
Closes #13333
Closes #13334
2016-12-09 10:45:10 -08:00
b5c4bf1c59 refactor(router): misc refactoring (#13330) 2016-12-09 10:44:46 -08:00
56c361ff6a test(compiler): test i18n explicit id
closes #13272
2016-12-09 10:43:57 -08:00
562f7a2f8b feat(compiler): digest methods return i18nMessage id if sets 2016-12-09 10:43:47 -08:00
6dd5201765 feat(compiler): add id property to i18nMessage 2016-12-09 10:43:47 -08:00
72361fb68f feat(platform browser): introduce Meta service (#12322) 2016-12-08 18:44:28 -08:00
5c6ec20c7e refactor(router): simplify regexp
closes #11373
closes #13329
2016-12-08 18:43:17 -08:00
440ef02f29 fix(router): add support for query params with mulitple values
closes #11373
2016-12-08 18:42:58 -08:00
4e3d58a792 Revert "fix(compiler): fix transpiled ES5 code (#13322)"
This reverts commit 4398056146.
2016-12-08 17:53:58 -08:00
61d7c1e0b3 feat(common): add a titlecase pipe (#13324)
closes #11436
2016-12-08 16:33:24 -08:00
bf93389615 doc: update triage owners for language service and router (#13325) 2016-12-08 15:42:34 -08:00
4398056146 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-08 15:02:59 -08:00
1b547886d0 build(tslint): enable no-inner-declarations (#13316) 2016-12-08 13:46:08 -08:00
9591a08dfb fix(router): Use T type in Resolve interface (#13242) 2016-12-08 11:24:38 -08:00
65965c27a8 docs(changelog): fix a typo (#13298) 2016-12-08 11:23:57 -08:00
181 changed files with 11624 additions and 5153 deletions

View File

@ -15,7 +15,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:** 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:** 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)) * **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 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:** 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)) * **router:** make setUpLocationChangeListener idempotent ([25e5b2f](https://github.com/angular/angular/commit/25e5b2f))

View File

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

View File

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

View File

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

View File

@ -12,9 +12,9 @@
* Entry point for all public APIs of the common package. * Entry point for all public APIs of the common package.
*/ */
export * from './location/index'; export * from './location/index';
export {NgLocalization} from './localization'; export {NgLocaleLocalization, NgLocalization} from './localization';
export {CommonModule} from './common_module'; export {CommonModule} from './common_module';
export {NgClass, NgFor, NgIf, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet} from './directives/index'; export {NgClass, NgFor, NgIf, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet} from './directives/index';
export {AsyncPipe, DatePipe, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCasePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe, UpperCasePipe} from './pipes/index'; export {AsyncPipe, DatePipe, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCasePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe, UpperCasePipe, TitleCasePipe} from './pipes/index';
export {VERSION} from './version'; export {VERSION} from './version';
export {Version} from '@angular/core'; export {Version} from '@angular/core';

View File

@ -6,46 +6,152 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Directive, Input, TemplateRef, ViewContainerRef} from '@angular/core'; import {Directive, EmbeddedViewRef, Input, TemplateRef, ViewContainerRef} from '@angular/core';
/** /**
* Removes or recreates a portion of the DOM tree based on an {expression}. * Conditionally includes a template based on the value of an `expression`.
* *
* If the expression assigned to `ngIf` evaluates to a falsy value then the element * `ngIf` evaluates the `expression` and then renders the `then` or `else` template in its place
* is removed from the DOM, otherwise a clone of the element is reinserted into the DOM. * when expression is thruthy or falsy respectively. Typically the:
* - `then` template is the inline template of `ngIf` unless bound to a different value.
* - `else` template is blank unless its bound.
* *
* ### Example ([live demo](http://plnkr.co/edit/fe0kgemFBtmQOY31b4tw?p=preview)): * # Most common usage
*
* The most common usage of the `ngIf` is to conditionally show the inline template as seen in this
* example:
* {@example common/ngIf/ts/module.ts region='NgIfSimple'}
*
* # Showing an alternative template using `else`
*
* If it is necessary to display a template when the `expression` is falsy use the `else` template
* binding as shown. Note that the `else` binding points to a `<template>` labeled `#elseBlock`.
* The template can be defined anywhere in the component view but is typically placed right after
* `ngIf` for readability.
*
* {@example common/ngIf/ts/module.ts region='NgIfElse'}
*
* # Using non-inlined `then` template
*
* Usually the `then` template is the inlined template of the `ngIf`, but it can be changed using
* a binding (just like `else`). Because `then` and `else` are bindings, the template references can
* change at runtime as shown in thise example.
*
* {@example common/ngIf/ts/module.ts region='NgIfThenElse'}
*
* # Storing conditional result in a variable
*
* A common patter is that we need to show a set of properties from the same object. if the
* object is undefined, then we have to use the safe-traversal-operator `?.` to guard against
* dereferencing a `null` value. This is especially the case when waiting on async data such as
* when using the `async` pipe as shown in folowing example:
* *
* ``` * ```
* <div *ngIf="errorCount > 0" class="error"> * Hello {{ (userStream|async)?.last }}, {{ (userStream|async)?.first }}!
* <!-- Error message displayed when the errorCount property in the current context is greater
* than 0. -->
* {{errorCount}} errors detected
* </div>
* ``` * ```
* *
* There are several inefficiencies in the above example.
* - We create multiple subscriptions on the `userStream`. One for each `async` pipe, or two
* as shown in the example above.
* - We can not display an alternative screen while waiting for the data to arrive asynchronously.
* - We have to use the safe-traversal-operator `?.` to access properties, which is cumbersome.
* - We have to place the `async` pipe in parenthesis.
*
* A better way to do this is to use `ngIf` and store the result of the condition in a local
* variable as shown in the the example below:
*
* {@example common/ngIf/ts/module.ts region='NgIfLet'}
*
* Notice that:
* - We use only one `async` pipe and hence only one subscription gets created.
* - `ngIf` stores the result of the `userStream|async` in the local variable `user`.
* - The local `user` can than be bound repeatedly in a more efficient way.
* - No need to use the safe-traversal-operator `?.` to access properties as `ngIf` will only
* display the data if `userStream` returns a value.
* - We can display an alternative template while waiting for the data.
*
* ### Syntax * ### Syntax
* *
* Simple form:
* - `<div *ngIf="condition">...</div>` * - `<div *ngIf="condition">...</div>`
* - `<div template="ngIf condition">...</div>` * - `<div template="ngIf condition">...</div>`
* - `<template [ngIf]="condition"><div>...</div></template>` * - `<ng-container [ngIf]="condition"><div>...</div></ng-container>`
*
* Form with an else block:
* ```
* <div *ngIf="condition; else elseBlock">...</div>
* <template #elseBlock>...</template>
* ```
*
* Form with a `then` and `else` block:
* ```
* <div *ngIf="condition; then thenBlock else elseBlock"></div>
* <template #thenBlock>...</template>
* <template #elseBlock>...</template>
* ```
*
* Form with storing the value locally:
* ```
* <div *ngIf="condition; else elseBlock; let value">{{value}}</div>
* <template #elseBlock>...</template>
* ```
* *
* @stable * @stable
*/ */
@Directive({selector: '[ngIf]'}) @Directive({selector: '[ngIf]'})
export class NgIf { export class NgIf {
private _hasView = false; private _context: NgIfContext = new NgIfContext();
private _thenTemplateRef: TemplateRef<NgIfContext> = null;
private _elseTemplateRef: TemplateRef<NgIfContext> = null;
private _thenViewRef: EmbeddedViewRef<NgIfContext> = null;
private _elseViewRef: EmbeddedViewRef<NgIfContext> = null;
constructor(private _viewContainer: ViewContainerRef, private _template: TemplateRef<Object>) {} constructor(private _viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext>) {
this._thenTemplateRef = templateRef;
}
@Input() @Input()
set ngIf(condition: any) { set ngIf(condition: any) {
if (condition && !this._hasView) { this._context.$implicit = condition;
this._hasView = true; this._updateView();
this._viewContainer.createEmbeddedView(this._template); }
} else if (!condition && this._hasView) {
this._hasView = false; @Input()
set ngIfThen(templateRef: TemplateRef<NgIfContext>) {
this._thenTemplateRef = templateRef;
this._thenViewRef = null; // clear previous view if any.
this._updateView();
}
@Input()
set ngIfElse(templateRef: TemplateRef<NgIfContext>) {
this._elseTemplateRef = templateRef;
this._elseViewRef = null; // clear previous view if any.
this._updateView();
}
private _updateView() {
if (this._context.$implicit) {
if (!this._thenViewRef) {
this._viewContainer.clear(); this._viewContainer.clear();
this._elseViewRef = null;
if (this._thenTemplateRef) {
this._thenViewRef =
this._viewContainer.createEmbeddedView(this._thenTemplateRef, this._context);
}
}
} else {
if (!this._elseViewRef) {
this._viewContainer.clear();
this._thenViewRef = null;
if (this._elseTemplateRef) {
this._elseViewRef =
this._viewContainer.createEmbeddedView(this._elseTemplateRef, this._context);
}
}
} }
} }
} }
export class NgIfContext { public $implicit: any = null; }

View File

@ -49,10 +49,10 @@ export function getPluralCategory(
*/ */
@Injectable() @Injectable()
export class NgLocaleLocalization extends NgLocalization { export class NgLocaleLocalization extends NgLocalization {
constructor(@Inject(LOCALE_ID) private _locale: string) { super(); } constructor(@Inject(LOCALE_ID) protected locale: string) { super(); }
getPluralCategory(value: any): string { getPluralCategory(value: any): string {
const plural = getPluralCase(this._locale, value); const plural = getPluralCase(this.locale, value);
switch (plural) { switch (plural) {
case Plural.Zero: case Plural.Zero:

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 * `HashLocationStrategy` is a {@link LocationStrategy} used to configure the
* {@link Location} service to represent its state in the * {@link Location} service to represent its state in the
* [hash fragment](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax) * [hash fragment](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax)
@ -27,18 +29,7 @@ import {LocationChangeListener, PlatformLocation} from './platform_location';
* *
* ### Example * ### Example
* *
* ``` * {@example common/location/ts/hash_location_component.ts region='LocationComponent'}
* import {Component, NgModule} from '@angular/core';
* import {
* LocationStrategy,
* HashLocationStrategy
* } from '@angular/common';
*
* @NgModule({
* providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}]
* })
* class AppModule {}
* ```
* *
* @stable * @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 * Depending on which {@link LocationStrategy} is used, `Location` will either persist
* to the URL's path or the URL's hash segment. * 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 * - `/my/app/user/123/` **is not** normalized
* *
* ### Example * ### Example
* * {@example common/location/ts/path_location_component.ts region='LocationComponent'}
* ```
* import {Component} from '@angular/core';
* import {Location} from '@angular/common';
*
* @Component({selector: 'app-component'})
* class AppCmp {
* constructor(location: Location) {
* location.go('/foo');
* }
* }
* ```
*
* @stable * @stable
*/ */
@Injectable() @Injectable()

View File

@ -12,7 +12,7 @@ import {LocationChangeListener} from './platform_location';
/** /**
* `LocationStrategy` is responsible for representing and reading route state * `LocationStrategy` is responsible for representing and reading route state
* from the browser's URL. Angular provides two strategies: * 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. * 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 * `PathLocationStrategy` is a {@link LocationStrategy} used to configure the
* {@link Location} service to represent its state in the * {@link Location} service to represent its state in the
* [path](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax) of the * [path](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax) of the
* browser's URL. * 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} * 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 * or add a base element to the document. This URL prefix that will be preserved
* when generating and recognizing URLs. * when generating and recognizing URLs.
@ -37,6 +36,10 @@ import {LocationChangeListener, PlatformLocation} from './platform_location';
* `location.go('/foo')`, the browser's URL will become * `location.go('/foo')`, the browser's URL will become
* `example.com/my/app/foo`. * `example.com/my/app/foo`.
* *
* ### Example
*
* {@example common/location/ts/path_location_component.ts region='LocationComponent'}
*
* @stable * @stable
*/ */
@Injectable() @Injectable()

View File

@ -0,0 +1,61 @@
/**
* @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 {Pipe, PipeTransform} from '@angular/core';
import {InvalidPipeArgumentError} from './invalid_pipe_argument_error';
/**
* Transforms text to lowercase.
*
* {@example core/pipes/ts/lowerupper_pipe/lowerupper_pipe_example.ts region='LowerUpperPipe' }
*
* @stable
*/
@Pipe({name: 'lowercase'})
export class LowerCasePipe implements PipeTransform {
transform(value: string): string {
if (!value) return value;
if (typeof value !== 'string') {
throw new InvalidPipeArgumentError(LowerCasePipe, value);
}
return value.toLowerCase();
}
}
/**
* Transforms text to titlecase.
*
* @stable
*/
@Pipe({name: 'titlecase'})
export class TitleCasePipe implements PipeTransform {
transform(value: string): string {
if (!value) return value;
if (typeof value !== 'string') {
throw new InvalidPipeArgumentError(TitleCasePipe, value);
}
return value[0].toUpperCase() + value.substr(1).toLowerCase();
}
}
/**
* Transforms text to uppercase.
*
* @stable
*/
@Pipe({name: 'uppercase'})
export class UpperCasePipe implements PipeTransform {
transform(value: string): string {
if (!value) return value;
if (typeof value !== 'string') {
throw new InvalidPipeArgumentError(UpperCasePipe, value);
}
return value.toUpperCase();
}
}

View File

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

View File

@ -12,14 +12,13 @@
* This module provides a set of common Pipes. * This module provides a set of common Pipes.
*/ */
import {AsyncPipe} from './async_pipe'; import {AsyncPipe} from './async_pipe';
import {LowerCasePipe, TitleCasePipe, UpperCasePipe} from './case_conversion_pipes';
import {DatePipe} from './date_pipe'; import {DatePipe} from './date_pipe';
import {I18nPluralPipe} from './i18n_plural_pipe'; import {I18nPluralPipe} from './i18n_plural_pipe';
import {I18nSelectPipe} from './i18n_select_pipe'; import {I18nSelectPipe} from './i18n_select_pipe';
import {JsonPipe} from './json_pipe'; import {JsonPipe} from './json_pipe';
import {LowerCasePipe} from './lowercase_pipe';
import {CurrencyPipe, DecimalPipe, PercentPipe} from './number_pipe'; import {CurrencyPipe, DecimalPipe, PercentPipe} from './number_pipe';
import {SlicePipe} from './slice_pipe'; import {SlicePipe} from './slice_pipe';
import {UpperCasePipe} from './uppercase_pipe';
export { export {
AsyncPipe, AsyncPipe,
@ -32,9 +31,11 @@ export {
LowerCasePipe, LowerCasePipe,
PercentPipe, PercentPipe,
SlicePipe, SlicePipe,
TitleCasePipe,
UpperCasePipe UpperCasePipe
}; };
/** /**
* A collection of Angular pipes that are likely to be used in each and every application. * A collection of Angular pipes that are likely to be used in each and every application.
*/ */
@ -46,6 +47,7 @@ export const COMMON_PIPES = [
SlicePipe, SlicePipe,
DecimalPipe, DecimalPipe,
PercentPipe, PercentPipe,
TitleCasePipe,
CurrencyPipe, CurrencyPipe,
DatePipe, DatePipe,
I18nPluralPipe, I18nPluralPipe,

View File

@ -1,37 +0,0 @@
/**
* @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 {Pipe, PipeTransform} from '@angular/core';
import {isBlank} from '../facade/lang';
import {InvalidPipeArgumentError} from './invalid_pipe_argument_error';
/**
* @ngModule CommonModule
* @whatItDoes Transforms string to lowercase.
* @howToUse `expression | lowercase`
* @description
*
* Converts value into a lowercase string using `String.prototype.toLowerCase()`.
*
* ### Example
*
* {@example common/pipes/ts/lowerupper_pipe.ts region='LowerUpperPipe'}
*
* @stable
*/
@Pipe({name: 'lowercase'})
export class LowerCasePipe implements PipeTransform {
transform(value: string): string {
if (isBlank(value)) return value;
if (typeof value !== 'string') {
throw new InvalidPipeArgumentError(LowerCasePipe, value);
}
return value.toLowerCase();
}
}

View File

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

View File

@ -1,36 +0,0 @@
/**
* @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 {Pipe, PipeTransform} from '@angular/core';
import {isBlank} from '../facade/lang';
import {InvalidPipeArgumentError} from './invalid_pipe_argument_error';
/**
* @ngModule CommonModule
* @whatItDoes Transforms string to uppercase.
* @howToUse `expression | uppercase`
* @description
*
* Converts value into an uppercase string using `String.prototype.toUpperCase()`.
*
* ### Example
*
* {@example common/pipes/ts/lowerupper_pipe.ts region='LowerUpperPipe'}
*
* @stable
*/
@Pipe({name: 'uppercase'})
export class UpperCasePipe implements PipeTransform {
transform(value: string): string {
if (isBlank(value)) return value;
if (typeof value !== 'string') {
throw new InvalidPipeArgumentError(UpperCasePipe, value);
}
return value.toUpperCase();
}
}

View File

@ -153,6 +153,78 @@ export function main() {
expect(getDOM().hasClass(getDOM().querySelector(fixture.nativeElement, 'span'), 'foo')) expect(getDOM().hasClass(getDOM().querySelector(fixture.nativeElement, 'span'), 'foo'))
.toBe(true); .toBe(true);
})); }));
describe('else', () => {
it('should support else', async(() => {
const template = '<div>' +
'<span *ngIf="booleanCondition; else elseBlock">TRUE</span>' +
'<template #elseBlock>FALSE</template>' +
'</div>';
fixture = createTestComponent(template);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('TRUE');
getComponent().booleanCondition = false;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('FALSE');
}));
it('should support then and else', async(() => {
const template = '<div>' +
'<span *ngIf="booleanCondition; then thenBlock; else elseBlock">IGNORE</span>' +
'<template #thenBlock>THEN</template>' +
'<template #elseBlock>ELSE</template>' +
'</div>';
fixture = createTestComponent(template);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('THEN');
getComponent().booleanCondition = false;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('ELSE');
}));
it('should support dynamic else', async(() => {
const template = '<div>' +
'<span *ngIf="booleanCondition; else nestedBooleanCondition ? b1 : b2">TRUE</span>' +
'<template #b1>FALSE1</template>' +
'<template #b2>FALSE2</template>' +
'</div>';
fixture = createTestComponent(template);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('TRUE');
getComponent().booleanCondition = false;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('FALSE1');
getComponent().nestedBooleanCondition = false;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('FALSE2');
}));
it('should support binding to variable', async(() => {
const template = '<div>' +
'<span *ngIf="booleanCondition; else elseBlock; let v">{{v}}</span>' +
'<template #elseBlock let-v>{{v}}</template>' +
'</div>';
fixture = createTestComponent(template);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('true');
getComponent().booleanCondition = false;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('false');
}));
});
}); });
} }

View File

@ -0,0 +1,59 @@
/**
* @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 {LowerCasePipe, TitleCasePipe, UpperCasePipe} from '@angular/common';
export function main() {
describe('LowerCasePipe', () => {
let pipe: LowerCasePipe;
beforeEach(() => { pipe = new LowerCasePipe(); });
it('should return lowercase', () => { expect(pipe.transform('FOO')).toEqual('foo'); });
it('should lowercase when there is a new value', () => {
expect(pipe.transform('FOO')).toEqual('foo');
expect(pipe.transform('BAr')).toEqual('bar');
});
it('should not support other objects',
() => { expect(() => pipe.transform(<any>{})).toThrowError(); });
});
describe('TitleCasePipe', () => {
let pipe: TitleCasePipe;
beforeEach(() => { pipe = new TitleCasePipe(); });
it('should return titlecase', () => { expect(pipe.transform('foo')).toEqual('Foo'); });
it('should titlecase when there is a new value', () => {
expect(pipe.transform('bar')).toEqual('Bar');
expect(pipe.transform('foo')).toEqual('Foo');
});
it('should not support other objects',
() => { expect(() => pipe.transform(<any>{})).toThrowError(); });
});
describe('UpperCasePipe', () => {
let pipe: UpperCasePipe;
beforeEach(() => { pipe = new UpperCasePipe(); });
it('should return uppercase', () => { expect(pipe.transform('foo')).toEqual('FOO'); });
it('should uppercase when there is a new value', () => {
expect(pipe.transform('foo')).toEqual('FOO');
expect(pipe.transform('bar')).toEqual('BAR');
});
it('should not support other objects',
() => { expect(() => pipe.transform(<any>{})).toThrowError(); });
});
}

View File

@ -1,42 +0,0 @@
/**
* @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 {LowerCasePipe} from '@angular/common';
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
export function main() {
describe('LowerCasePipe', () => {
let upper: string;
let lower: string;
let pipe: LowerCasePipe;
beforeEach(() => {
lower = 'something';
upper = 'SOMETHING';
pipe = new LowerCasePipe();
});
describe('transform', () => {
it('should return lowercase', () => {
const val = pipe.transform(upper);
expect(val).toEqual(lower);
});
it('should lowercase when there is a new value', () => {
const val = pipe.transform(upper);
expect(val).toEqual(lower);
const val2 = pipe.transform('WAT');
expect(val2).toEqual('wat');
});
it('should not support other objects',
() => { expect(() => pipe.transform(<any>{})).toThrowError(); });
});
});
}

View File

@ -1,43 +0,0 @@
/**
* @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 {UpperCasePipe} from '@angular/common';
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
export function main() {
describe('UpperCasePipe', () => {
let upper: string;
let lower: string;
let pipe: UpperCasePipe;
beforeEach(() => {
lower = 'something';
upper = 'SOMETHING';
pipe = new UpperCasePipe();
});
describe('transform', () => {
it('should return uppercase', () => {
const val = pipe.transform(lower);
expect(val).toEqual(upper);
});
it('should uppercase when there is a new value', () => {
const val = pipe.transform(lower);
expect(val).toEqual(upper);
const val2 = pipe.transform('wat');
expect(val2).toEqual('WAT');
});
it('should not support other objects',
() => { expect(() => pipe.transform(<any>{})).toThrowError(); });
});
});
}

View File

@ -11,3 +11,7 @@ export {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter, NodeComp
export {Extractor} from './src/extractor'; export {Extractor} from './src/extractor';
export * from '@angular/tsc-wrapped'; export * from '@angular/tsc-wrapped';
export {VERSION} from './src/version'; 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/module",
"src/bootstrap", "src/bootstrap",
"test/all_spec", "test/all_spec",
"test/test_ngtools_api",
"test/test_summaries", "test/test_summaries",
"benchmarks/src/tree/ng2/index_aot.ts", "benchmarks/src/tree/ng2/index_aot.ts",
"benchmarks/src/tree/ng2_switch/index_aot.ts", "benchmarks/src/tree/ng2_switch/index_aot.ts",

View File

@ -21,9 +21,9 @@ import {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter} from './
import {PathMappedCompilerHost} from './path_mapped_compiler_host'; import {PathMappedCompilerHost} from './path_mapped_compiler_host';
import {Console} from './private_import_core'; 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_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 = `/** const PREAMBLE = `/**
* @fileoverview This file is generated by the Angular 2 template compiler. * @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 EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
const DTS = /\.d\.ts$/; const DTS = /\.d\.ts$/;
const NODE_MODULES = '/node_modules/'; const NODE_MODULES = '/node_modules/';
const IS_GENERATED = /\.(ngfactory|css(\.shim)?)$/; const IS_GENERATED = /\.(ngfactory|ngstyle)$/;
export interface CompilerHostContext extends ts.ModuleResolutionHost { export interface CompilerHostContext extends ts.ModuleResolutionHost {
readResource(fileName: string): Promise<string>; readResource(fileName: string): Promise<string>;
@ -177,28 +177,31 @@ export class CompilerHost implements AotCompilerHost {
(Array.isArray(metadataOrMetadatas) ? metadataOrMetadatas : [metadataOrMetadatas]) : (Array.isArray(metadataOrMetadatas) ? metadataOrMetadatas : [metadataOrMetadatas]) :
[]; [];
const v1Metadata = metadatas.find((m: any) => m['version'] === 1); const v1Metadata = metadatas.find((m: any) => m['version'] === 1);
let v2Metadata = metadatas.find((m: any) => m['version'] === 2); let v3Metadata = metadatas.find((m: any) => m['version'] === 3);
if (!v2Metadata && v1Metadata) { if (!v3Metadata && v1Metadata) {
// patch up v1 to v2 by merging the metadata with metadata collected from the d.ts file // 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 // as the only difference between the versions is whether all exports are contained in
// the metadata and the `extends` clause. // the metadata and the `extends` clause.
v2Metadata = {'__symbolic': 'module', 'version': 2, 'metadata': {}}; v3Metadata = {'__symbolic': 'module', 'version': 3, 'metadata': {}};
if (v1Metadata.exports) { if (v1Metadata.exports) {
v2Metadata.exports = v1Metadata.exports; v3Metadata.exports = v1Metadata.exports;
} }
for (let prop in v1Metadata.metadata) { 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)); const exports = this.metadataCollector.getMetadata(this.getSourceFile(dtsFilePath));
if (exports) { if (exports) {
for (let prop in exports.metadata) { for (let prop in exports.metadata) {
if (!v2Metadata.metadata[prop]) { if (!v3Metadata.metadata[prop]) {
v2Metadata.metadata[prop] = exports.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); this.resolverCache.set(filePath, metadatas);
return 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')) '/tmp/project/src/my.other.ngfactory.ts', '/tmp/project/src/my.ngfactory.ts'))
.toEqual('./my.other.ngfactory'); .toEqual('./my.other.ngfactory');
expect(hostNestedGenDir.fileNameToModuleName( expect(hostNestedGenDir.fileNameToModuleName(
'/tmp/project/src/my.other.css.ts', '/tmp/project/src/a/my.ngfactory.ts')) '/tmp/project/src/my.other.css.ngstyle.ts', '/tmp/project/src/a/my.ngfactory.ts'))
.toEqual('../my.other.css'); .toEqual('../my.other.css.ngstyle');
expect(hostNestedGenDir.fileNameToModuleName( expect(hostNestedGenDir.fileNameToModuleName(
'/tmp/project/src/a/my.other.css.shim.ts', '/tmp/project/src/my.ngfactory.ts')) '/tmp/project/src/a/my.other.shim.ngstyle.ts', '/tmp/project/src/my.ngfactory.ts'))
.toEqual('./a/my.other.css.shim'); .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', () => { it('should import application from factory', () => {
@ -83,6 +86,12 @@ describe('CompilerHost', () => {
expect(hostNestedGenDir.fileNameToModuleName( expect(hostNestedGenDir.fileNameToModuleName(
'/tmp/project/src/a/my.other.ts', '/tmp/project/src/my.ngfactory.ts')) '/tmp/project/src/a/my.other.ts', '/tmp/project/src/my.ngfactory.ts'))
.toEqual('../a/my.other'); .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(); 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([ expect(hostNestedGenDir.getMetadataFor('metadata_versions/v1.d.ts')).toEqual([
{__symbolic: 'module', version: 1, metadata: {foo: {__symbolic: 'class'}}}, { {__symbolic: 'module', version: 1, metadata: {foo: {__symbolic: 'class'}}}, {
__symbolic: 'module', __symbolic: 'module',
version: 2, version: 3,
metadata: { metadata: {
foo: {__symbolic: 'class'}, foo: {__symbolic: 'class'},
Bar: {__symbolic: 'class', members: {ngOnInit: [{__symbolic: 'method'}]}}, 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': { 'metadata_versions': {
'v1.d.ts': ` 'v1.d.ts': `
import {ReExport} from './lib/utils2';
export {ReExport};
export {Export} from './lib/utils2';
export declare class Bar { export declare class Bar {
ngOnInit() {} ngOnInit() {}
} }

View File

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

View File

@ -174,6 +174,11 @@ function _normalizeStyleMetadata(
entry: CompileAnimationStyleMetadata, stateStyles: {[key: string]: AnimationStylesAst}, entry: CompileAnimationStyleMetadata, stateStyles: {[key: string]: AnimationStylesAst},
schema: ElementSchemaRegistry, errors: AnimationParseError[], schema: ElementSchemaRegistry, errors: AnimationParseError[],
permitStateReferences: boolean): {[key: string]: string | number}[] { 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}[] = []; const normalizedStyles: {[key: string]: string | number}[] = [];
entry.styles.forEach(styleEntry => { entry.styles.forEach(styleEntry => {
if (typeof styleEntry === 'string') { if (typeof styleEntry === 'string') {

View File

@ -272,7 +272,7 @@ function _componentFactoryName(comp: CompileIdentifierMetadata): string {
} }
function _stylesModuleUrl(stylesheetUrl: string, shim: boolean, suffix: string): 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) { 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 {ReflectorReader} from '../private_import_core';
import {StaticSymbol} from './static_symbol'; import {StaticSymbol} from './static_symbol';
const SUPPORTED_SCHEMA_VERSION = 2; const SUPPORTED_SCHEMA_VERSION = 3;
const ANGULAR_IMPORT_LOCATIONS = { const ANGULAR_IMPORT_LOCATIONS = {
coreDecorators: '@angular/core/src/metadata', coreDecorators: '@angular/core/src/metadata',
diDecorators: '@angular/core/src/di/metadata', diDecorators: '@angular/core/src/di/metadata',
@ -20,6 +20,8 @@ const ANGULAR_IMPORT_LOCATIONS = {
provider: '@angular/core/src/di/provider' provider: '@angular/core/src/di/provider'
}; };
const HIDDEN_KEY = /^\$.*\$$/;
/** /**
* The host of the StaticReflector disconnects the implementation from TypeScript / other language * The host of the StaticReflector disconnects the implementation from TypeScript / other language
* services and from underlying file systems. * services and from underlying file systems.
@ -638,6 +640,9 @@ export class StaticReflector implements ReflectorReader {
return simplifyInContext(selectContext, selectTarget[member], depth + 1); return simplifyInContext(selectContext, selectTarget[member], depth + 1);
return null; return null;
case 'reference': case 'reference':
if (!expression['name']) {
return context;
}
if (!expression.module) { if (!expression.module) {
const name: string = expression['name']; const name: string = expression['name'];
const localValue = scope.resolve(name); const localValue = scope.resolve(name);
@ -749,10 +754,10 @@ export class StaticReflector implements ReflectorReader {
{__symbolic: 'module', version: SUPPORTED_SCHEMA_VERSION, module: module, metadata: {}}; {__symbolic: 'module', version: SUPPORTED_SCHEMA_VERSION, module: module, metadata: {}};
} }
if (moduleMetadata['version'] != SUPPORTED_SCHEMA_VERSION) { if (moduleMetadata['version'] != SUPPORTED_SCHEMA_VERSION) {
this.reportError( const errorMessage = moduleMetadata['version'] == 2 ?
new Error( `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}`), `Metadata version mismatch for module ${module}, found version ${moduleMetadata['version']}, expected ${SUPPORTED_SCHEMA_VERSION}`;
null); this.reportError(new Error(errorMessage), null);
} }
this.metadataCache.set(module, moduleMetadata); this.metadataCache.set(module, moduleMetadata);
} }
@ -806,8 +811,12 @@ function mapStringMap(input: {[key: string]: any}, transform: (value: any, key:
Object.keys(input).forEach((key) => { Object.keys(input).forEach((key) => {
const value = transform(input[key], key); const value = transform(input[key], key);
if (!shouldIgnore(value)) { if (!shouldIgnore(value)) {
if (HIDDEN_KEY.test(key)) {
Object.defineProperty(result, key, {enumerable: false, configurable: true, value: value});
} else {
result[key] = value; result[key] = value;
} }
}
}); });
return result; return result;
} }

View File

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

View File

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

View File

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

View File

@ -344,7 +344,7 @@ export class _ParseAST {
while (this.optionalCharacter(chars.$COLON)) { while (this.optionalCharacter(chars.$COLON)) {
args.push(this.parseExpression()); 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('|')); } while (this.optionalOperator('|'));
} }

View File

@ -9,13 +9,13 @@
import * as i18n from './i18n_ast'; import * as i18n from './i18n_ast';
export function digest(message: i18n.Message): string { export function digest(message: i18n.Message): string {
return sha1(serializeNodes(message.nodes).join('') + `[${message.meaning}]`); return message.id || sha1(serializeNodes(message.nodes).join('') + `[${message.meaning}]`);
} }
export function decimalDigest(message: i18n.Message): string { export function decimalDigest(message: i18n.Message): string {
const visitor = new _SerializerIgnoreIcuExpVisitor(); const visitor = new _SerializerIgnoreIcuExpVisitor();
const parts = message.nodes.map(a => a.visit(visitor, null)); const parts = message.nodes.map(a => a.visit(visitor, null));
return computeMsgId(parts.join(''), message.meaning); return message.id || computeMsgId(parts.join(''), message.meaning);
} }
/** /**

View File

@ -18,6 +18,8 @@ import {TranslationBundle} from './translation_bundle';
const _I18N_ATTR = 'i18n'; const _I18N_ATTR = 'i18n';
const _I18N_ATTR_PREFIX = 'i18n-'; const _I18N_ATTR_PREFIX = 'i18n-';
const _I18N_COMMENT_PREFIX_REGEXP = /^i18n:?/; const _I18N_COMMENT_PREFIX_REGEXP = /^i18n:?/;
const MEANING_SEPARATOR = '|';
const ID_SEPARATOR = '@@';
/** /**
* Extract translatable messages from an html AST * Extract translatable messages from an html AST
@ -77,7 +79,7 @@ class _Visitor implements html.Visitor {
// _VisitorMode.Merge only // _VisitorMode.Merge only
private _translations: TranslationBundle; private _translations: TranslationBundle;
private _createI18nMessage: private _createI18nMessage:
(msg: html.Node[], meaning: string, description: string) => i18n.Message; (msg: html.Node[], meaning: string, description: string, id: string) => i18n.Message;
constructor(private _implicitTags: string[], private _implicitAttrs: {[k: string]: string[]}) {} constructor(private _implicitTags: string[], private _implicitAttrs: {[k: string]: string[]}) {}
@ -330,15 +332,15 @@ class _Visitor implements html.Visitor {
} }
// add a translatable message // add a translatable message
private _addMessage(ast: html.Node[], meaningAndDesc?: string): i18n.Message { private _addMessage(ast: html.Node[], msgMeta?: string): i18n.Message {
if (ast.length == 0 || if (ast.length == 0 ||
ast.length == 1 && ast[0] instanceof html.Attribute && !(<html.Attribute>ast[0]).value) { ast.length == 1 && ast[0] instanceof html.Attribute && !(<html.Attribute>ast[0]).value) {
// Do not create empty messages // Do not create empty messages
return; return;
} }
const [meaning, description] = _splitMeaningAndDesc(meaningAndDesc); const {meaning, description, id} = _parseMessageMeta(msgMeta);
const message = this._createI18nMessage(ast, meaning, description); const message = this._createI18nMessage(ast, meaning, description, id);
this._messages.push(message); this._messages.push(message);
return message; return message;
} }
@ -368,7 +370,7 @@ class _Visitor implements html.Visitor {
attributes.forEach(attr => { attributes.forEach(attr => {
if (attr.name.startsWith(_I18N_ATTR_PREFIX)) { if (attr.name.startsWith(_I18N_ATTR_PREFIX)) {
i18nAttributeMeanings[attr.name.slice(_I18N_ATTR_PREFIX.length)] = i18nAttributeMeanings[attr.name.slice(_I18N_ATTR_PREFIX.length)] =
_splitMeaningAndDesc(attr.value)[0]; _parseMessageMeta(attr.value).meaning;
} }
}); });
@ -382,7 +384,7 @@ class _Visitor implements html.Visitor {
if (attr.value && attr.value != '' && i18nAttributeMeanings.hasOwnProperty(attr.name)) { if (attr.value && attr.value != '' && i18nAttributeMeanings.hasOwnProperty(attr.name)) {
const meaning = i18nAttributeMeanings[attr.name]; const meaning = i18nAttributeMeanings[attr.name];
const message: i18n.Message = this._createI18nMessage([attr], meaning, ''); const message: i18n.Message = this._createI18nMessage([attr], meaning, '', '');
const nodes = this._translations.get(message); const nodes = this._translations.get(message);
if (nodes) { if (nodes) {
if (nodes[0] instanceof html.Text) { if (nodes[0] instanceof html.Text) {
@ -496,8 +498,16 @@ function _getI18nAttr(p: html.Element): html.Attribute {
return p.attrs.find(attr => attr.name === _I18N_ATTR) || null; return p.attrs.find(attr => attr.name === _I18N_ATTR) || null;
} }
function _splitMeaningAndDesc(i18n: string): [string, string] { function _parseMessageMeta(i18n: string): {meaning: string, description: string, id: string} {
if (!i18n) return ['', '']; if (!i18n) return {meaning: '', description: '', id: ''};
const pipeIndex = i18n.indexOf('|');
return pipeIndex == -1 ? ['', i18n] : [i18n.slice(0, pipeIndex), i18n.slice(pipeIndex + 1)]; const idIndex = i18n.indexOf(ID_SEPARATOR);
const descIndex = i18n.indexOf(MEANING_SEPARATOR);
const [meaningAndDesc, id] =
(idIndex > -1) ? [i18n.slice(0, idIndex), i18n.slice(idIndex + 2)] : [i18n, ''];
const [meaning, description] = (descIndex > -1) ?
[meaningAndDesc.slice(0, descIndex), meaningAndDesc.slice(descIndex + 1)] :
['', meaningAndDesc];
return {meaning, description, id};
} }

View File

@ -15,11 +15,12 @@ export class Message {
* @param placeholderToMessage maps placeholder names to messages (used for nested ICU messages) * @param placeholderToMessage maps placeholder names to messages (used for nested ICU messages)
* @param meaning * @param meaning
* @param description * @param description
* @param id
*/ */
constructor( constructor(
public nodes: Node[], public placeholders: {[phName: string]: string}, public nodes: Node[], public placeholders: {[phName: string]: string},
public placeholderToMessage: {[phName: string]: Message}, public meaning: string, public placeholderToMessage: {[phName: string]: Message}, public meaning: string,
public description: string) {} public description: string, public id: string) {}
} }
export interface Node { export interface Node {

View File

@ -22,11 +22,11 @@ const _expParser = new ExpressionParser(new ExpressionLexer());
* Returns a function converting html nodes to an i18n Message given an interpolationConfig * Returns a function converting html nodes to an i18n Message given an interpolationConfig
*/ */
export function createI18nMessageFactory(interpolationConfig: InterpolationConfig): ( export function createI18nMessageFactory(interpolationConfig: InterpolationConfig): (
nodes: html.Node[], meaning: string, description: string) => i18n.Message { nodes: html.Node[], meaning: string, description: string, id: string) => i18n.Message {
const visitor = new _I18nVisitor(_expParser, interpolationConfig); const visitor = new _I18nVisitor(_expParser, interpolationConfig);
return (nodes: html.Node[], meaning: string, description: string) => return (nodes: html.Node[], meaning: string, description: string, id: string) =>
visitor.toI18nMessage(nodes, meaning, description); visitor.toI18nMessage(nodes, meaning, description, id);
} }
class _I18nVisitor implements html.Visitor { class _I18nVisitor implements html.Visitor {
@ -40,7 +40,8 @@ class _I18nVisitor implements html.Visitor {
private _expressionParser: ExpressionParser, private _expressionParser: ExpressionParser,
private _interpolationConfig: InterpolationConfig) {} private _interpolationConfig: InterpolationConfig) {}
public toI18nMessage(nodes: html.Node[], meaning: string, description: string): i18n.Message { public toI18nMessage(nodes: html.Node[], meaning: string, description: string, id: string):
i18n.Message {
this._isIcu = nodes.length == 1 && nodes[0] instanceof html.Expansion; this._isIcu = nodes.length == 1 && nodes[0] instanceof html.Expansion;
this._icuDepth = 0; this._icuDepth = 0;
this._placeholderRegistry = new PlaceholderRegistry(); this._placeholderRegistry = new PlaceholderRegistry();
@ -50,7 +51,7 @@ class _I18nVisitor implements html.Visitor {
const i18nodes: i18n.Node[] = html.visitAll(this, nodes, {}); const i18nodes: i18n.Node[] = html.visitAll(this, nodes, {});
return new i18n.Message( return new i18n.Message(
i18nodes, this._placeholderToContent, this._placeholderToMessage, meaning, description); i18nodes, this._placeholderToContent, this._placeholderToMessage, meaning, description, id);
} }
visitElement(el: html.Element, context: any): i18n.Node { visitElement(el: html.Element, context: any): i18n.Node {
@ -115,7 +116,7 @@ class _I18nVisitor implements html.Visitor {
// TODO(vicb): add a html.Node -> i18n.Message cache to avoid having to re-create the msg // TODO(vicb): add a html.Node -> i18n.Message cache to avoid having to re-create the msg
const phName = this._placeholderRegistry.getPlaceholderName('ICU', icu.sourceSpan.toString()); const phName = this._placeholderRegistry.getPlaceholderName('ICU', icu.sourceSpan.toString());
const visitor = new _I18nVisitor(this._expressionParser, this._interpolationConfig); const visitor = new _I18nVisitor(this._expressionParser, this._interpolationConfig);
this._placeholderToMessage[phName] = visitor.toI18nMessage([icu], '', ''); this._placeholderToMessage[phName] = visitor.toI18nMessage([icu], '', '', '');
return new i18n.IcuPlaceholder(i18nIcu, phName, icu.sourceSpan); return new i18n.IcuPlaceholder(i18nIcu, phName, icu.sourceSpan);
} }

View File

@ -70,6 +70,14 @@ export class JitCompiler implements Compiler {
return this._compileModuleAndAllComponents(moduleType, false).asyncResult; 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): private _compileModuleAndComponents<T>(moduleType: Type<T>, isSync: boolean):
SyncAsyncResult<NgModuleFactory<T>> { SyncAsyncResult<NgModuleFactory<T>> {
const loadingPromise = this._loadModules(moduleType, isSync); const loadingPromise = this._loadModules(moduleType, isSync);
@ -213,9 +221,8 @@ export class JitCompiler implements Compiler {
const compMeta = this._metadataResolver.getDirectiveMetadata(compType); const compMeta = this._metadataResolver.getDirectiveMetadata(compType);
assertComponent(compMeta); assertComponent(compMeta);
class HostClass { const HostClass = function HostClass() {};
static overriddenName = `${identifierName(compMeta.type)}_Host`; (<any>HostClass).overriddenName = `${identifierName(compMeta.type)}_Host`;
}
const hostMeta = createHostComponentMeta(HostClass, compMeta); const hostMeta = createHostComponentMeta(HostClass, compMeta);
compiledTemplate = new CompiledTemplate( compiledTemplate = new CompiledTemplate(
@ -342,7 +349,8 @@ export class JitCompiler implements Compiler {
if (!this._compilerConfig.useJit) { if (!this._compilerConfig.useJit) {
return interpretStatements(result.statements, result.stylesVar); return interpretStatements(result.statements, result.stylesVar);
} else { } 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); return this._delegate.compileModuleAndAllComponentsAsync(moduleType);
} }
getNgContentSelectors(component: Type<any>): string[] {
return this._delegate.getNgContentSelectors(component);
}
/** /**
* Clears all caches * Clears all caches
*/ */

View File

@ -367,8 +367,8 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex
ctx.print(`{`, useNewLine); ctx.print(`{`, useNewLine);
ctx.incIndent(); ctx.incIndent();
this.visitAllObjects(entry => { this.visitAllObjects(entry => {
ctx.print(`${escapeIdentifier(entry[0], this._escapeDollarInStrings, false)}: `); ctx.print(`${escapeIdentifier(entry.key, this._escapeDollarInStrings, entry.quoted)}: `);
entry[1].visitExpression(this, ctx); entry.value.visitExpression(this, ctx);
}, ast.entries, ctx, ',', useNewLine); }, ast.entries, ctx, ',', useNewLine);
ctx.decIndent(); ctx.decIndent();
ctx.print(`}`, useNewLine); 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 { export class LiteralMapExpr extends Expression {
public valueType: Type = null; public valueType: Type = null;
constructor(public entries: [string, Expression][], type: MapType = null) { constructor(public entries: LiteralMapEntry[], type: MapType = null) {
super(type); super(type);
if (isPresent(type)) { if (isPresent(type)) {
this.valueType = type.valueType; this.valueType = type.valueType;
@ -677,7 +680,8 @@ export class ExpressionTransformer implements StatementVisitor, ExpressionVisito
visitLiteralMapExpr(ast: LiteralMapExpr, context: any): any { visitLiteralMapExpr(ast: LiteralMapExpr, context: any): any {
const entries = ast.entries.map( 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); return new LiteralMapExpr(entries);
} }
visitAllExpressions(exprs: Expression[], context: any): Expression[] { visitAllExpressions(exprs: Expression[], context: any): Expression[] {
@ -791,7 +795,7 @@ export class RecursiveExpressionVisitor implements StatementVisitor, ExpressionV
return ast; return ast;
} }
visitLiteralMapExpr(ast: LiteralMapExpr, context: any): any { 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; return ast;
} }
visitAllExpressions(exprs: Expression[], context: any): void { 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 { 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 { 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 { visitLiteralMapExpr(ast: o.LiteralMapExpr, ctx: _ExecutionContext): any {
const result = {}; const result = {};
ast.entries.forEach( ast.entries.forEach(
(entry) => (result as any)[<string>entry[0]] = (entry) => (result as any)[entry.key] = entry.value.visitExpression(this, ctx));
(<o.Expression>entry[1]).visitExpression(this, ctx));
return result; return result;
} }

View File

@ -12,6 +12,8 @@ import {ValueTransformer, visitValue} from '../util';
import * as o from './output_ast'; import * as o from './output_ast';
export const QUOTED_KEYS = '$quoted$';
export function convertValueToOutputAst(value: any, type: o.Type = null): o.Expression { export function convertValueToOutputAst(value: any, type: o.Type = null): o.Expression {
return visitValue(value, new _ValueOutputAstTransformer(), type); return visitValue(value, new _ValueOutputAstTransformer(), type);
} }
@ -22,9 +24,13 @@ class _ValueOutputAstTransformer implements ValueTransformer {
} }
visitStringMap(map: {[key: string]: any}, type: o.MapType): o.Expression { visitStringMap(map: {[key: string]: any}, type: o.MapType): o.Expression {
const entries: [string, o.Expression][] = []; const entries: o.LiteralMapEntry[] = [];
Object.keys(map).forEach(key => { entries.push([key, visitValue(map[key], this, null)]); }); const quotedSet = new Set<string>(map && map[QUOTED_KEYS]);
return o.literalMap(entries, type); 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); } 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 * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import * as chars from './chars';
import {isPresent} from './facade/lang'; import {isPresent} from './facade/lang';
export class ParseLocation { export class ParseLocation {
@ -15,6 +16,38 @@ export class ParseLocation {
toString(): string { toString(): string {
return isPresent(this.offset) ? `${this.file.url}@${this.line}:${this.col}` : this.file.url; 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 { export class ParseSourceFile {

View File

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

View File

@ -9,13 +9,12 @@
import {SecurityContext} from '@angular/core'; import {SecurityContext} from '@angular/core';
import {CompileDirectiveSummary, CompilePipeSummary} from '../compile_metadata'; 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 {Parser} from '../expression_parser/parser';
import {isPresent} from '../facade/lang'; 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 {mergeNsAndName} from '../ml_parser/tags';
import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util'; import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util';
import {view_utils} from '../private_import_core';
import {ElementSchemaRegistry} from '../schema/element_schema_registry'; import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {CssSelector} from '../selector'; import {CssSelector} from '../selector';
import {splitAtColon, splitAtPeriod} from '../util'; import {splitAtColon, splitAtPeriod} from '../util';
@ -246,18 +245,12 @@ export class BindingParser {
let unit: string = null; let unit: string = null;
let bindingType: PropertyBindingType; let bindingType: PropertyBindingType;
let boundPropertyName: string; let boundPropertyName: string = null;
const parts = boundProp.name.split(PROPERTY_PARTS_SEPARATOR); const parts = boundProp.name.split(PROPERTY_PARTS_SEPARATOR);
let securityContexts: SecurityContext[]; let securityContexts: SecurityContext[];
if (parts.length === 1) { // Check check for special cases (prefix style, attr, class)
const partValue = parts[0]; if (parts.length > 1) {
boundPropertyName = this._schemaRegistry.getMappedPropName(partValue);
securityContexts = calcPossibleSecurityContexts(
this._schemaRegistry, elementSelector, boundPropertyName, false);
bindingType = PropertyBindingType.Property;
this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, false);
} else {
if (parts[0] == ATTRIBUTE_PREFIX) { if (parts[0] == ATTRIBUTE_PREFIX) {
boundPropertyName = parts[1]; boundPropertyName = parts[1];
this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, true); this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, true);
@ -281,12 +274,18 @@ export class BindingParser {
boundPropertyName = parts[1]; boundPropertyName = parts[1];
bindingType = PropertyBindingType.Style; bindingType = PropertyBindingType.Style;
securityContexts = [SecurityContext.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( return new BoundElementPropertyAst(
boundPropertyName, bindingType, securityContexts.length === 1 ? securityContexts[0] : null, boundPropertyName, bindingType, securityContexts.length === 1 ? securityContexts[0] : null,
securityContexts.length > 1, boundProp.expression, unit, boundProp.sourceSpan); securityContexts.length > 1, boundProp.expression, unit, boundProp.sourceSpan);
@ -378,9 +377,12 @@ export class BindingParser {
if (isPresent(ast)) { if (isPresent(ast)) {
const collector = new PipeCollector(); const collector = new PipeCollector();
ast.visit(collector); ast.visit(collector);
collector.pipes.forEach((pipeName) => { collector.pipes.forEach((ast, pipeName) => {
if (!this.pipesByName.has(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 { export class PipeCollector extends RecursiveAstVisitor {
pipes = new Set<string>(); pipes = new Map<string, BindingPipe>();
visitPipe(ast: BindingPipe, context: any): any { visitPipe(ast: BindingPipe, context: any): any {
this.pipes.add(ast.name); this.pipes.set(ast.name, ast);
ast.exp.visit(this); ast.exp.visit(this);
this.visitAll(ast.args, context); this.visitAll(ast.args, context);
return null; return null;

View File

@ -6,10 +6,9 @@
* found in the LICENSE file at https://angular.io/license * 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 {CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeSummary, CompileTemplateSummary, CompileTokenMetadata, CompileTypeMetadata, identifierName} from '../compile_metadata';
import {AST, ASTWithSource, BindingPipe, EmptyExpr, Interpolation, ParserError, RecursiveAstVisitor, TemplateBinding} from '../expression_parser/ast';
import {Parser} from '../expression_parser/parser'; import {Parser} from '../expression_parser/parser';
import {isPresent} from '../facade/lang'; import {isPresent} from '../facade/lang';
import {I18NHtmlParser} from '../i18n/i18n_html_parser'; 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 {ParseTreeResult} from '../ml_parser/html_parser';
import {expandNodes} from '../ml_parser/icu_ast_expander'; import {expandNodes} from '../ml_parser/icu_ast_expander';
import {InterpolationConfig} from '../ml_parser/interpolation_config'; 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 {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 {ProviderElementContext, ProviderViewContext} from '../provider_analyzer';
import {ElementSchemaRegistry} from '../schema/element_schema_registry'; import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {CssSelector, SelectorMatcher} from '../selector'; 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 {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'; import {PreparsedElementType, preparseElement} from './template_preparser';
// Group 1 = "bind-" // Group 1 = "bind-"
// Group 2 = "let-" // Group 2 = "let-"
// Group 3 = "ref-/#" // Group 3 = "ref-/#"
@ -685,7 +682,7 @@ class TemplateParseVisitor implements html.Visitor {
} }
elementProps.forEach(prop => { elementProps.forEach(prop => {
this._reportError( 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); sourceSpan);
}); });
} }
@ -704,7 +701,7 @@ class TemplateParseVisitor implements html.Visitor {
events.forEach(event => { events.forEach(event => {
if (isPresent(event.target) || !allDirectiveEvents.has(event.name)) { if (isPresent(event.target) || !allDirectiveEvents.has(event.name)) {
this._reportError( 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); 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 cssSelector = new CssSelector();
const elNameNoNs = splitNsName(elementName)[1]; const elNameNoNs = splitNsName(elementName)[1];

View File

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

View File

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

View File

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

View File

@ -80,7 +80,13 @@ describe('StaticReflector', () => {
it('should throw an exception for unsupported metadata versions', () => { it('should throw an exception for unsupported metadata versions', () => {
expect(() => reflector.findDeclaration('src/version-error', 'e')) expect(() => reflector.findDeclaration('src/version-error', 'e'))
.toThrow(new Error( .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', () => { it('should get and empty annotation list for an unknown class', () => {
@ -307,6 +313,12 @@ describe('StaticReflector', () => {
.toEqual('s'); .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', () => { it('should simplify a non existing reference as a static symbol', () => {
expect(simplify( expect(simplify(
new StaticSymbol('/src/cases', ''), new StaticSymbol('/src/cases', ''),
@ -378,7 +390,7 @@ describe('StaticReflector', () => {
const metadata = reflector.getModuleMetadata('/tmp/src/custom-decorator-reference.ts'); const metadata = reflector.getModuleMetadata('/tmp/src/custom-decorator-reference.ts');
expect(metadata).toEqual({ expect(metadata).toEqual({
__symbolic: 'module', __symbolic: 'module',
version: 2, version: 3,
metadata: { metadata: {
Foo: { Foo: {
__symbolic: 'class', __symbolic: 'class',
@ -769,7 +781,7 @@ export class MockStaticReflectorHost implements StaticReflectorHost {
const DEFAULT_TEST_DATA: {[key: string]: any} = { const DEFAULT_TEST_DATA: {[key: string]: any} = {
'/tmp/@angular/common/src/forms-deprecated/directives.d.ts': [{ '/tmp/@angular/common/src/forms-deprecated/directives.d.ts': [{
'__symbolic': 'module', '__symbolic': 'module',
'version': 2, 'version': 3,
'metadata': { 'metadata': {
'FORM_DIRECTIVES': [ 'FORM_DIRECTIVES': [
{ {
@ -782,7 +794,7 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
}], }],
'/tmp/@angular/common/src/directives/ng_for.d.ts': { '/tmp/@angular/common/src/directives/ng_for.d.ts': {
'__symbolic': 'module', '__symbolic': 'module',
'version': 2, 'version': 3,
'metadata': { 'metadata': {
'NgFor': { 'NgFor': {
'__symbolic': 'class', '__symbolic': 'class',
@ -835,16 +847,16 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
} }
}, },
'/tmp/@angular/core/src/linker/view_container_ref.d.ts': '/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': '/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': '/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': '/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': { '/tmp/src/app/hero-detail.component.d.ts': {
'__symbolic': 'module', '__symbolic': 'module',
'version': 2, 'version': 3,
'metadata': { 'metadata': {
'HeroDetailComponent': { 'HeroDetailComponent': {
'__symbolic': 'class', '__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-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': { '/tmp/src/error-reporting.d.ts': {
__symbolic: 'module', __symbolic: 'module',
version: 2, version: 3,
metadata: { metadata: {
SomeClass: { SomeClass: {
__symbolic: 'class', __symbolic: 'class',
@ -1029,7 +1042,7 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
}, },
'/tmp/src/error-references.d.ts': { '/tmp/src/error-references.d.ts': {
__symbolic: 'module', __symbolic: 'module',
version: 2, version: 3,
metadata: { metadata: {
Link1: { Link1: {
__symbolic: 'reference', __symbolic: 'reference',
@ -1051,7 +1064,7 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
}, },
'/tmp/src/function-declaration.d.ts': { '/tmp/src/function-declaration.d.ts': {
__symbolic: 'module', __symbolic: 'module',
version: 2, version: 3,
metadata: { metadata: {
one: { one: {
__symbolic: 'function', __symbolic: 'function',
@ -1080,7 +1093,7 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
}, },
'/tmp/src/function-reference.ts': { '/tmp/src/function-reference.ts': {
__symbolic: 'module', __symbolic: 'module',
version: 2, version: 3,
metadata: { metadata: {
one: { one: {
__symbolic: 'call', __symbolic: 'call',
@ -1122,7 +1135,7 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
}, },
'/tmp/src/function-recursive.d.ts': { '/tmp/src/function-recursive.d.ts': {
__symbolic: 'modules', __symbolic: 'modules',
version: 2, version: 3,
metadata: { metadata: {
recursive: { recursive: {
__symbolic: 'function', __symbolic: 'function',
@ -1182,7 +1195,7 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
}, },
'/tmp/src/spread.ts': { '/tmp/src/spread.ts': {
__symbolic: 'module', __symbolic: 'module',
version: 2, version: 3,
metadata: { metadata: {
spread: [0, {__symbolic: 'spread', expression: [1, 2, 3, 4]}, 5] 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': { '/tmp/src/reexport/reexport.d.ts': {
__symbolic: 'module', __symbolic: 'module',
version: 2, version: 3,
metadata: {}, metadata: {},
exports: [ exports: [
{from: './src/origin1', export: ['One', 'Two', {name: 'Three', as: 'Four'}]}, {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': { '/tmp/src/reexport/src/origin1.d.ts': {
__symbolic: 'module', __symbolic: 'module',
version: 2, version: 3,
metadata: { metadata: {
One: {__symbolic: 'class'}, One: {__symbolic: 'class'},
Two: {__symbolic: 'class'}, Two: {__symbolic: 'class'},
@ -1350,26 +1363,26 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
}, },
'/tmp/src/reexport/src/origin5.d.ts': { '/tmp/src/reexport/src/origin5.d.ts': {
__symbolic: 'module', __symbolic: 'module',
version: 2, version: 3,
metadata: { metadata: {
Five: {__symbolic: 'class'}, Five: {__symbolic: 'class'},
}, },
}, },
'/tmp/src/reexport/src/origin30.d.ts': { '/tmp/src/reexport/src/origin30.d.ts': {
__symbolic: 'module', __symbolic: 'module',
version: 2, version: 3,
metadata: { metadata: {
Thirty: {__symbolic: 'class'}, Thirty: {__symbolic: 'class'},
}, },
}, },
'/tmp/src/reexport/src/originNone.d.ts': { '/tmp/src/reexport/src/originNone.d.ts': {
__symbolic: 'module', __symbolic: 'module',
version: 2, version: 3,
metadata: {}, metadata: {},
}, },
'/tmp/src/reexport/src/reexport2.d.ts': { '/tmp/src/reexport/src/reexport2.d.ts': {
__symbolic: 'module', __symbolic: 'module',
version: 2, version: 3,
metadata: {}, metadata: {},
exports: [{from: './originNone'}, {from: './origin30'}] exports: [{from: './originNone'}, {from: './origin30'}]
} }

View File

@ -309,7 +309,8 @@ export function main() {
} }
const directiveMetadata = resolver.resolve(Child); 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', () => { it('should support inheriting host listeners', () => {
@ -329,7 +330,37 @@ export function main() {
} }
const directiveMetadata = resolver.resolve(Child); 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

@ -6,10 +6,23 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {computeMsgId, sha1} from '../../src/i18n/digest'; import {computeMsgId, digest, sha1} from '../../src/i18n/digest';
export function main(): void { export function main(): void {
describe('digest', () => { describe('digest', () => {
describe('digest', () => {
it('must return the ID if it\'s explicit', () => {
expect(digest({
id: 'i',
nodes: [],
placeholders: {},
placeholderToMessage: {},
meaning: '',
description: '',
})).toEqual('i');
});
});
describe('sha1', () => { describe('sha1', () => {
it('should work on empty strings', it('should work on empty strings',
() => { expect(sha1('')).toEqual('da39a3ee5e6b4b0d3255bfef95601890afd80709'); }); () => { expect(sha1('')).toEqual('da39a3ee5e6b4b0d3255bfef95601890afd80709'); });

View File

@ -20,7 +20,10 @@ export function main() {
describe('elements', () => { describe('elements', () => {
it('should extract from elements', () => { it('should extract from elements', () => {
expect(extract('<div i18n="m|d|e">text<span>nested</span></div>')).toEqual([ expect(extract('<div i18n="m|d|e">text<span>nested</span></div>')).toEqual([
[['text', '<ph tag name="START_TAG_SPAN">nested</ph name="CLOSE_TAG_SPAN">'], 'm', 'd|e'], [
['text', '<ph tag name="START_TAG_SPAN">nested</ph name="CLOSE_TAG_SPAN">'], 'm', 'd|e',
''
],
]); ]);
}); });
@ -29,11 +32,45 @@ export function main() {
extract( extract(
'<div i18n="m1|d1"><span i18n-title="m2|d2" title="single child">nested</span></div>')) '<div i18n="m1|d1"><span i18n-title="m2|d2" title="single child">nested</span></div>'))
.toEqual([ .toEqual([
[['<ph tag name="START_TAG_SPAN">nested</ph name="CLOSE_TAG_SPAN">'], 'm1', 'd1'], [['<ph tag name="START_TAG_SPAN">nested</ph name="CLOSE_TAG_SPAN">'], 'm1', 'd1', ''],
[['single child'], 'm2', 'd2'], [['single child'], 'm2', 'd2', ''],
]); ]);
}); });
it('should extract from attributes with id', () => {
expect(
extract(
'<div i18n="m1|d1@@i1"><span i18n-title="m2|d2@@i2" title="single child">nested</span></div>'))
.toEqual([
[
['<ph tag name="START_TAG_SPAN">nested</ph name="CLOSE_TAG_SPAN">'], 'm1', 'd1',
'i1'
],
[['single child'], 'm2', 'd2', 'i2'],
]);
});
it('should extract from attributes without meaning and with id', () => {
expect(
extract(
'<div i18n="d1@@i1"><span i18n-title="d2@@i2" title="single child">nested</span></div>'))
.toEqual([
[['<ph tag name="START_TAG_SPAN">nested</ph name="CLOSE_TAG_SPAN">'], '', 'd1', 'i1'],
[['single child'], '', 'd2', 'i2'],
]);
});
it('should extract from attributes with id only', () => {
expect(
extract(
'<div i18n="@@i1"><span i18n-title="@@i2" title="single child">nested</span></div>'))
.toEqual([
[['<ph tag name="START_TAG_SPAN">nested</ph name="CLOSE_TAG_SPAN">'], '', '', 'i1'],
[['single child'], '', '', 'i2'],
]);
});
it('should extract from ICU messages', () => { it('should extract from ICU messages', () => {
expect( expect(
extract( extract(
@ -43,10 +80,10 @@ export function main() {
[ [
'{count, plural, =0 {[<ph tag name="START_PARAGRAPH"></ph name="CLOSE_PARAGRAPH">]}}' '{count, plural, =0 {[<ph tag name="START_PARAGRAPH"></ph name="CLOSE_PARAGRAPH">]}}'
], ],
'm', 'd' 'm', 'd', ''
], ],
[['title'], '', ''], [['title'], '', '', ''],
[['desc'], '', ''], [['desc'], '', '', ''],
]); ]);
}); });
@ -55,7 +92,7 @@ export function main() {
it('should ignore implicit elements in translatable elements', () => { it('should ignore implicit elements in translatable elements', () => {
expect(extract('<div i18n="m|d"><p></p></div>', ['p'])).toEqual([ expect(extract('<div i18n="m|d"><p></p></div>', ['p'])).toEqual([
[['<ph tag name="START_PARAGRAPH"></ph name="CLOSE_PARAGRAPH">'], 'm', 'd'] [['<ph tag name="START_PARAGRAPH"></ph name="CLOSE_PARAGRAPH">'], 'm', 'd', '']
]); ]);
}); });
}); });
@ -64,17 +101,19 @@ export function main() {
it('should extract from blocks', () => { it('should extract from blocks', () => {
expect(extract(`<!-- i18n: meaning1|desc1 -->message1<!-- /i18n --> expect(extract(`<!-- i18n: meaning1|desc1 -->message1<!-- /i18n -->
<!-- i18n: desc2 -->message2<!-- /i18n --> <!-- i18n: desc2 -->message2<!-- /i18n -->
<!-- i18n -->message3<!-- /i18n -->`)) <!-- i18n -->message3<!-- /i18n -->
<!-- i18n: meaning4|desc4@@id4 -->message4<!-- /i18n -->
<!-- i18n: @@id5 -->message5<!-- /i18n -->`))
.toEqual([ .toEqual([
[['message1'], 'meaning1', 'desc1'], [['message1'], 'meaning1', 'desc1', ''], [['message2'], '', 'desc2', ''],
[['message2'], '', 'desc2'], [['message3'], '', '', ''], [['message4'], 'meaning4', 'desc4', 'id4'],
[['message3'], '', ''], [['message5'], '', '', 'id5']
]); ]);
}); });
it('should ignore implicit elements in blocks', () => { it('should ignore implicit elements in blocks', () => {
expect(extract('<!-- i18n:m|d --><p></p><!-- /i18n -->', ['p'])).toEqual([ expect(extract('<!-- i18n:m|d --><p></p><!-- /i18n -->', ['p'])).toEqual([
[['<ph tag name="START_PARAGRAPH"></ph name="CLOSE_PARAGRAPH">'], 'm', 'd'] [['<ph tag name="START_PARAGRAPH"></ph name="CLOSE_PARAGRAPH">'], 'm', 'd', '']
]); ]);
}); });
@ -88,7 +127,7 @@ export function main() {
[ [
'{count, plural, =0 {[<ph tag name="START_TAG_SPAN">html</ph name="CLOSE_TAG_SPAN">]}}' '{count, plural, =0 {[<ph tag name="START_TAG_SPAN">html</ph name="CLOSE_TAG_SPAN">]}}'
], ],
'', '' '', '', ''
], ],
[ [
[ [
@ -98,15 +137,15 @@ export function main() {
' name="START_TAG_SPAN">html</ph name="CLOSE_TAG_SPAN">]}}</ph>', ' name="START_TAG_SPAN">html</ph name="CLOSE_TAG_SPAN">]}}</ph>',
'[<ph name="INTERPOLATION">interp</ph>]' '[<ph name="INTERPOLATION">interp</ph>]'
], ],
'', '' '', '', ''
], ],
]); ]);
}); });
it('should ignore other comments', () => { it('should ignore other comments', () => {
expect(extract(`<!-- i18n: meaning1|desc1 --><!-- other -->message1<!-- /i18n -->`)) expect(extract(`<!-- i18n: meaning1|desc1@@id1 --><!-- other -->message1<!-- /i18n -->`))
.toEqual([ .toEqual([
[['message1'], 'meaning1', 'desc1'], [['message1'], 'meaning1', 'desc1', 'id1'],
]); ]);
}); });
@ -118,34 +157,37 @@ export function main() {
it('should extract ICU messages from translatable elements', () => { it('should extract ICU messages from translatable elements', () => {
// single message when ICU is the only children // single message when ICU is the only children
expect(extract('<div i18n="m|d">{count, plural, =0 {text}}</div>')).toEqual([ expect(extract('<div i18n="m|d">{count, plural, =0 {text}}</div>')).toEqual([
[['{count, plural, =0 {[text]}}'], 'm', 'd'], [['{count, plural, =0 {[text]}}'], 'm', 'd', ''],
]); ]);
// single message when ICU is the only (implicit) children // single message when ICU is the only (implicit) children
expect(extract('<div>{count, plural, =0 {text}}</div>', ['div'])).toEqual([ expect(extract('<div>{count, plural, =0 {text}}</div>', ['div'])).toEqual([
[['{count, plural, =0 {[text]}}'], '', ''], [['{count, plural, =0 {[text]}}'], '', '', ''],
]); ]);
// one message for the element content and one message for the ICU // one message for the element content and one message for the ICU
expect(extract('<div i18n="m|d">before{count, plural, =0 {text}}after</div>')).toEqual([ expect(extract('<div i18n="m|d@@i">before{count, plural, =0 {text}}after</div>')).toEqual([
[['before', '<ph icu name="ICU">{count, plural, =0 {[text]}}</ph>', 'after'], 'm', 'd'], [
[['{count, plural, =0 {[text]}}'], '', ''], ['before', '<ph icu name="ICU">{count, plural, =0 {[text]}}</ph>', 'after'], 'm', 'd',
'i'
],
[['{count, plural, =0 {[text]}}'], '', '', ''],
]); ]);
}); });
it('should extract ICU messages from translatable block', () => { it('should extract ICU messages from translatable block', () => {
// single message when ICU is the only children // single message when ICU is the only children
expect(extract('<!-- i18n:m|d -->{count, plural, =0 {text}}<!-- /i18n -->')).toEqual([ expect(extract('<!-- i18n:m|d -->{count, plural, =0 {text}}<!-- /i18n -->')).toEqual([
[['{count, plural, =0 {[text]}}'], 'm', 'd'], [['{count, plural, =0 {[text]}}'], 'm', 'd', ''],
]); ]);
// one message for the block content and one message for the ICU // one message for the block content and one message for the ICU
expect(extract('<!-- i18n:m|d -->before{count, plural, =0 {text}}after<!-- /i18n -->')) expect(extract('<!-- i18n:m|d -->before{count, plural, =0 {text}}after<!-- /i18n -->'))
.toEqual([ .toEqual([
[['{count, plural, =0 {[text]}}'], '', ''], [['{count, plural, =0 {[text]}}'], '', '', ''],
[ [
['before', '<ph icu name="ICU">{count, plural, =0 {[text]}}</ph>', 'after'], 'm', ['before', '<ph icu name="ICU">{count, plural, =0 {[text]}}</ph>', 'after'], 'm',
'd' 'd', ''
], ],
]); ]);
}); });
@ -156,20 +198,20 @@ export function main() {
it('should ignore nested ICU messages', () => { it('should ignore nested ICU messages', () => {
expect(extract('<div i18n="m|d">{count, plural, =0 { {sex, select, male {m}} }}</div>')) expect(extract('<div i18n="m|d">{count, plural, =0 { {sex, select, male {m}} }}</div>'))
.toEqual([ .toEqual([
[['{count, plural, =0 {[{sex, select, male {[m]}}, ]}}'], 'm', 'd'], [['{count, plural, =0 {[{sex, select, male {[m]}}, ]}}'], 'm', 'd', ''],
]); ]);
}); });
it('should ignore implicit elements in non translatable ICU messages', () => { it('should ignore implicit elements in non translatable ICU messages', () => {
expect( expect(extract(
extract( '<div i18n="m|d@@i">{count, plural, =0 { {sex, select, male {<p>ignore</p>}}' +
'<div i18n="m|d">{count, plural, =0 { {sex, select, male {<p>ignore</p>}} }}</div>', ' }}</div>',
['p'])) ['p']))
.toEqual([[ .toEqual([[
[ [
'{count, plural, =0 {[{sex, select, male {[<ph tag name="START_PARAGRAPH">ignore</ph name="CLOSE_PARAGRAPH">]}}, ]}}' '{count, plural, =0 {[{sex, select, male {[<ph tag name="START_PARAGRAPH">ignore</ph name="CLOSE_PARAGRAPH">]}}, ]}}'
], ],
'm', 'd' 'm', 'd', 'i'
]]); ]]);
}); });
@ -181,46 +223,45 @@ export function main() {
describe('attributes', () => { describe('attributes', () => {
it('should extract from attributes outside of translatable sections', () => { it('should extract from attributes outside of translatable sections', () => {
expect(extract('<div i18n-title="m|d" title="msg"></div>')).toEqual([ expect(extract('<div i18n-title="m|d@@i" title="msg"></div>')).toEqual([
[['msg'], 'm', 'd'], [['msg'], 'm', 'd', 'i'],
]); ]);
}); });
it('should extract from attributes in translatable elements', () => { it('should extract from attributes in translatable elements', () => {
expect(extract('<div i18n><p><b i18n-title="m|d" title="msg"></b></p></div>')).toEqual([ expect(extract('<div i18n><p><b i18n-title="m|d@@i" title="msg"></b></p></div>')).toEqual([
[ [
['<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph' + ['<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph' +
' name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'], ' name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'],
'', '' '', '', ''
], ],
[['msg'], 'm', 'd'], [['msg'], 'm', 'd', 'i'],
]); ]);
}); });
it('should extract from attributes in translatable blocks', () => { it('should extract from attributes in translatable blocks', () => {
expect(extract('<!-- i18n --><p><b i18n-title="m|d" title="msg"></b></p><!-- /i18n -->')) expect(extract('<!-- i18n --><p><b i18n-title="m|d" title="msg"></b></p><!-- /i18n -->'))
.toEqual([ .toEqual([
[['msg'], 'm', 'd'], [['msg'], 'm', 'd', ''],
[ [
['<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph' + ['<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph' +
' name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'], ' name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'],
'', '' '', '', ''
], ],
]); ]);
}); });
it('should extract from attributes in translatable ICUs', () => { it('should extract from attributes in translatable ICUs', () => {
expect( expect(extract(`<!-- i18n -->{count, plural, =0 {<p><b i18n-title="m|d@@i"
extract( title="msg"></b></p>}}<!-- /i18n -->`))
'<!-- i18n -->{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}<!-- /i18n -->'))
.toEqual([ .toEqual([
[['msg'], 'm', 'd'], [['msg'], 'm', 'd', 'i'],
[ [
[ [
'{count, plural, =0 {[<ph tag name="START_PARAGRAPH"><ph tag' + '{count, plural, =0 {[<ph tag name="START_PARAGRAPH"><ph tag' +
' name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">]}}' ' name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">]}}'
], ],
'', '' '', '', ''
], ],
]); ]);
}); });
@ -228,7 +269,7 @@ export function main() {
it('should extract from attributes in non translatable ICUs', () => { it('should extract from attributes in non translatable ICUs', () => {
expect(extract('{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}')) expect(extract('{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}'))
.toEqual([ .toEqual([
[['msg'], 'm', 'd'], [['msg'], 'm', 'd', ''],
]); ]);
}); });
@ -239,7 +280,7 @@ export function main() {
describe('implicit elements', () => { describe('implicit elements', () => {
it('should extract from implicit elements', () => { it('should extract from implicit elements', () => {
expect(extract('<b>bold</b><i>italic</i>', ['b'])).toEqual([ expect(extract('<b>bold</b><i>italic</i>', ['b'])).toEqual([
[['bold'], '', ''], [['bold'], '', '', ''],
]); ]);
}); });
@ -251,7 +292,7 @@ export function main() {
}).not.toThrow(); }).not.toThrow();
expect(result).toEqual([ expect(result).toEqual([
[['outer', '<ph tag name="START_TAG_DIV">inner</ph name="CLOSE_TAG_DIV">'], '', ''], [['outer', '<ph tag name="START_TAG_DIV">inner</ph name="CLOSE_TAG_DIV">'], '', '', ''],
]); ]);
}); });
@ -261,7 +302,7 @@ export function main() {
it('should extract implicit attributes', () => { it('should extract implicit attributes', () => {
expect(extract('<b title="bb">bold</b><i title="ii">italic</i>', [], {'b': ['title']})) expect(extract('<b title="bb">bold</b><i title="ii">italic</i>', [], {'b': ['title']}))
.toEqual([ .toEqual([
[['bb'], '', ''], [['bb'], '', '', ''],
]); ]);
}); });
}); });
@ -433,7 +474,7 @@ function extract(
// clang-format off // clang-format off
// https://github.com/angular/clang-format/issues/35 // https://github.com/angular/clang-format/issues/35
return result.messages.map( return result.messages.map(
message => [serializeI18nNodes(message.nodes), message.meaning, message.description, ]) as [string[], string, string][]; message => [serializeI18nNodes(message.nodes), message.meaning, message.description, message.id]) as [string[], string, string][];
// clang-format on // clang-format on
} }

View File

@ -59,14 +59,17 @@ export function main() {
tb.detectChanges(); tb.detectChanges();
expect(el.query(By.css('#i18n-7')).nativeElement).toHaveText('un'); expect(el.query(By.css('#i18n-7')).nativeElement).toHaveText('un');
expect(el.query(By.css('#i18n-14')).nativeElement).toHaveText('un'); expect(el.query(By.css('#i18n-14')).nativeElement).toHaveText('un');
expect(el.query(By.css('#i18n-17')).nativeElement).toHaveText('un');
cmp.count = 2; cmp.count = 2;
tb.detectChanges(); tb.detectChanges();
expect(el.query(By.css('#i18n-7')).nativeElement).toHaveText('deux'); expect(el.query(By.css('#i18n-7')).nativeElement).toHaveText('deux');
expect(el.query(By.css('#i18n-14')).nativeElement).toHaveText('deux'); expect(el.query(By.css('#i18n-14')).nativeElement).toHaveText('deux');
expect(el.query(By.css('#i18n-17')).nativeElement).toHaveText('deux');
cmp.count = 3; cmp.count = 3;
tb.detectChanges(); tb.detectChanges();
expect(el.query(By.css('#i18n-7')).nativeElement).toHaveText('beaucoup'); expect(el.query(By.css('#i18n-7')).nativeElement).toHaveText('beaucoup');
expect(el.query(By.css('#i18n-14')).nativeElement).toHaveText('beaucoup'); expect(el.query(By.css('#i18n-14')).nativeElement).toHaveText('beaucoup');
expect(el.query(By.css('#i18n-17')).nativeElement).toHaveText('beaucoup');
cmp.sex = 'm'; cmp.sex = 'm';
cmp.sexB = 'f'; cmp.sexB = 'f';
@ -90,8 +93,8 @@ export function main() {
.toEqual('<h1 id="i18n-12">Balises dans les commentaires html</h1>'); .toEqual('<h1 id="i18n-12">Balises dans les commentaires html</h1>');
expectHtml(el, '#i18n-13') expectHtml(el, '#i18n-13')
.toBe('<div id="i18n-13" title="dans une section traductible"></div>'); .toBe('<div id="i18n-13" title="dans une section traductible"></div>');
expectHtml(el, '#i18n-15').toMatch(/ca <b>devrait<\/b> marcher/); expectHtml(el, '#i18n-15').toMatch(/ca <b>devrait<\/b> marcher/);
expectHtml(el, '#i18n-16').toMatch(/avec un ID explicite/);
}); });
}); });
} }
@ -141,6 +144,8 @@ function expectHtml(el: DebugElement, cssSelector: string): any {
<!-- /i18n --> <!-- /i18n -->
<div id="i18n-15"><ng-container i18n>it <b>should</b> work</ng-container></div> <div id="i18n-15"><ng-container i18n>it <b>should</b> work</ng-container></div>
<div id="i18n-16" i18n="@@i18n16">with an explicit ID</div>
<div id="i18n-17" i18n="@@i18n17">{count, plural, =0 {zero} =1 {one} =2 {two} other {<b>many</b>}}</div>
` `
}) })
class I18nComponent { class I18nComponent {
@ -182,6 +187,9 @@ const XTB = `
<ph name="START_TAG_DIV_1"/><ph name="ICU"/><ph name="CLOSE_TAG_DIV"></ph> <ph name="START_TAG_DIV_1"/><ph name="ICU"/><ph name="CLOSE_TAG_DIV"></ph>
</translation> </translation>
<translation id="1491627405349178954">ca <ph name="START_BOLD_TEXT"/>devrait<ph name="CLOSE_BOLD_TEXT"/> marcher</translation> <translation id="1491627405349178954">ca <ph name="START_BOLD_TEXT"/>devrait<ph name="CLOSE_BOLD_TEXT"/> marcher</translation>
<translation id="i18n16">avec un ID explicite</translation>
<translation id="i18n17">{VAR_PLURAL, plural, =0 {zero} =1 {un} =2 {deux} other {<ph
name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>beaucoup<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph>} }</translation>
</translationbundle>`; </translationbundle>`;
// unused, for reference only // unused, for reference only
@ -210,5 +218,7 @@ const XMB = `
<ph name="START_TAG_DIV_1"><ex>&lt;div&gt;</ex></ph><ph name="ICU"/><ph name="CLOSE_TAG_DIV"><ex>&lt;/div&gt;</ex></ph> <ph name="START_TAG_DIV_1"><ex>&lt;div&gt;</ex></ph><ph name="ICU"/><ph name="CLOSE_TAG_DIV"><ex>&lt;/div&gt;</ex></ph>
</msg> </msg>
<msg id="1491627405349178954">it <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>should<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> work</msg> <msg id="1491627405349178954">it <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>should<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> work</msg>
<msg id="i18n16">with an explicit ID</msg>
<msg id="i18n17">{VAR_PLURAL, plural, =0 {zero} =1 {one} =2 {two} other {<ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph>} }</msg>
</messagebundle> </messagebundle>
`; `;

View File

@ -18,6 +18,8 @@ const HTML = `
<p i18n-title title="translatable attribute">not translatable</p> <p i18n-title title="translatable attribute">not translatable</p>
<p i18n>translatable element <b>with placeholders</b> {{ interpolation}}</p> <p i18n>translatable element <b>with placeholders</b> {{ interpolation}}</p>
<p i18n="m|d">foo</p> <p i18n="m|d">foo</p>
<p i18n="m|d@@i">foo</p>
<p i18n="@@bar">foo</p>
<p i18n="ph names"><br><img><div></div></p> <p i18n="ph names"><br><img><div></div></p>
`; `;
@ -39,6 +41,16 @@ const WRITE_XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<note priority="1" from="description">d</note> <note priority="1" from="description">d</note>
<note priority="1" from="meaning">m</note> <note priority="1" from="meaning">m</note>
</trans-unit> </trans-unit>
<trans-unit id="i" datatype="html">
<source>foo</source>
<target/>
<note priority="1" from="description">d</note>
<note priority="1" from="meaning">m</note>
</trans-unit>
<trans-unit id="bar" datatype="html">
<source>foo</source>
<target/>
</trans-unit>
<trans-unit id="d7fa2d59aaedcaa5309f13028c59af8c85b8c49d" datatype="html"> <trans-unit id="d7fa2d59aaedcaa5309f13028c59af8c85b8c49d" datatype="html">
<source><x id="LINE_BREAK" ctype="lb"/><x id="TAG_IMG" ctype="image"/><x id="START_TAG_DIV" ctype="x-div"/><x id="CLOSE_TAG_DIV" ctype="x-div"/></source> <source><x id="LINE_BREAK" ctype="lb"/><x id="TAG_IMG" ctype="image"/><x id="START_TAG_DIV" ctype="x-div"/><x id="CLOSE_TAG_DIV" ctype="x-div"/></source>
<target/> <target/>
@ -67,6 +79,16 @@ const LOAD_XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<note priority="1" from="description">d</note> <note priority="1" from="description">d</note>
<note priority="1" from="meaning">m</note> <note priority="1" from="meaning">m</note>
</trans-unit> </trans-unit>
<trans-unit id="i" datatype="html">
<source>foo</source>
<target>toto</target>
<note priority="1" from="description">d</note>
<note priority="1" from="meaning">m</note>
</trans-unit>
<trans-unit id="bar" datatype="html">
<source>foo</source>
<target>tata</target>
</trans-unit>
<trans-unit id="d7fa2d59aaedcaa5309f13028c59af8c85b8c49d" datatype="html"> <trans-unit id="d7fa2d59aaedcaa5309f13028c59af8c85b8c49d" datatype="html">
<source><x id="LINE_BREAK" ctype="lb"/><x id="TAG_IMG" ctype="image"/><x id="START_TAG_DIV" ctype="x-div"/><x id="CLOSE_TAG_DIV" ctype="x-div"/></source> <source><x id="LINE_BREAK" ctype="lb"/><x id="TAG_IMG" ctype="image"/><x id="START_TAG_DIV" ctype="x-div"/><x id="CLOSE_TAG_DIV" ctype="x-div"/></source>
<target><x id="START_TAG_DIV" ctype="x-div"/><x id="CLOSE_TAG_DIV" ctype="x-div"/><x id="TAG_IMG" ctype="image"/><x id="LINE_BREAK" ctype="lb"/></target> <target><x id="START_TAG_DIV" ctype="x-div"/><x id="CLOSE_TAG_DIV" ctype="x-div"/><x id="TAG_IMG" ctype="image"/><x id="LINE_BREAK" ctype="lb"/></target>
@ -107,6 +129,8 @@ export function main(): void {
'ec1d033f2436133c14ab038286c4f5df4697484a': 'ec1d033f2436133c14ab038286c4f5df4697484a':
'<ph name="INTERPOLATION"/> footnemele elbatalsnart <ph name="START_BOLD_TEXT"/>sredlohecalp htiw<ph name="CLOSE_BOLD_TEXT"/>', '<ph name="INTERPOLATION"/> footnemele elbatalsnart <ph name="START_BOLD_TEXT"/>sredlohecalp htiw<ph name="CLOSE_BOLD_TEXT"/>',
'db3e0a6a5a96481f60aec61d98c3eecddef5ac23': 'oof', 'db3e0a6a5a96481f60aec61d98c3eecddef5ac23': 'oof',
'i': 'toto',
'bar': 'tata',
'd7fa2d59aaedcaa5309f13028c59af8c85b8c49d': 'd7fa2d59aaedcaa5309f13028c59af8c85b8c49d':
'<ph name="START_TAG_DIV"/><ph name="CLOSE_TAG_DIV"/><ph name="TAG_IMG"/><ph name="LINE_BREAK"/>', '<ph name="START_TAG_DIV"/><ph name="CLOSE_TAG_DIV"/><ph name="TAG_IMG"/><ph name="LINE_BREAK"/>',
}); });

View File

@ -18,6 +18,9 @@ export function main(): void {
<p i18n>translatable element <b>with placeholders</b> {{ interpolation}}</p> <p i18n>translatable element <b>with placeholders</b> {{ interpolation}}</p>
<!-- i18n -->{ count, plural, =0 {<p>test</p>}}<!-- /i18n --> <!-- i18n -->{ count, plural, =0 {<p>test</p>}}<!-- /i18n -->
<p i18n="m|d">foo</p> <p i18n="m|d">foo</p>
<p i18n="m|d@@i">foo</p>
<p i18n="@@bar">foo</p>
<p i18n="@@baz">{ count, plural, =0 { { sex, select, other {<p>deeply nested</p>}} }}</p>
<p i18n>{ count, plural, =0 { { sex, select, other {<p>deeply nested</p>}} }}</p>`; <p i18n>{ count, plural, =0 { { sex, select, other {<p>deeply nested</p>}} }}</p>`;
const XMB = `<?xml version="1.0" encoding="UTF-8" ?> const XMB = `<?xml version="1.0" encoding="UTF-8" ?>
@ -46,6 +49,9 @@ export function main(): void {
<msg id="7056919470098446707">translatable element <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>with placeholders<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> <ph name="INTERPOLATION"/></msg> <msg id="7056919470098446707">translatable element <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>with placeholders<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> <ph name="INTERPOLATION"/></msg>
<msg id="2981514368455622387">{VAR_PLURAL, plural, =0 {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>} }</msg> <msg id="2981514368455622387">{VAR_PLURAL, plural, =0 {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>} }</msg>
<msg id="7999024498831672133" desc="d" meaning="m">foo</msg> <msg id="7999024498831672133" desc="d" meaning="m">foo</msg>
<msg id="i" desc="d" meaning="m">foo</msg>
<msg id="bar">foo</msg>
<msg id="baz">{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>} } } }</msg>
<msg id="2015957479576096115">{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>} } } }</msg> <msg id="2015957479576096115">{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>} } } }</msg>
</messagebundle> </messagebundle>
`; `;

View File

@ -21,7 +21,7 @@ export function main(): void {
it('should translate a plain message', () => { it('should translate a plain message', () => {
const msgMap = {foo: [new i18n.Text('bar', null)]}; const msgMap = {foo: [new i18n.Text('bar', null)]};
const tb = new TranslationBundle(msgMap, (_) => 'foo'); const tb = new TranslationBundle(msgMap, (_) => 'foo');
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd'); const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
expect(serializeNodes(tb.get(msg))).toEqual(['bar']); expect(serializeNodes(tb.get(msg))).toEqual(['bar']);
}); });
@ -36,7 +36,7 @@ export function main(): void {
ph1: '*phContent*', ph1: '*phContent*',
}; };
const tb = new TranslationBundle(msgMap, (_) => 'foo'); const tb = new TranslationBundle(msgMap, (_) => 'foo');
const msg = new i18n.Message([srcNode], phMap, {}, 'm', 'd'); const msg = new i18n.Message([srcNode], phMap, {}, 'm', 'd', 'i');
expect(serializeNodes(tb.get(msg))).toEqual(['bar*phContent*']); expect(serializeNodes(tb.get(msg))).toEqual(['bar*phContent*']);
}); });
@ -51,8 +51,8 @@ export function main(): void {
new i18n.Text('*refMsg*', null), new i18n.Text('*refMsg*', null),
], ],
}; };
const refMsg = new i18n.Message([srcNode], {}, {}, 'm', 'd'); const refMsg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
const msg = new i18n.Message([srcNode], {}, {ph1: refMsg}, 'm', 'd'); const msg = new i18n.Message([srcNode], {}, {ph1: refMsg}, 'm', 'd', 'i');
let count = 0; let count = 0;
const digest = (_: any) => count++ ? 'ref' : 'foo'; const digest = (_: any) => count++ ? 'ref' : 'foo';
const tb = new TranslationBundle(msgMap, digest); const tb = new TranslationBundle(msgMap, digest);
@ -69,13 +69,13 @@ export function main(): void {
] ]
}; };
const tb = new TranslationBundle(msgMap, (_) => 'foo'); const tb = new TranslationBundle(msgMap, (_) => 'foo');
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd'); const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
expect(() => tb.get(msg)).toThrowError(/Unknown placeholder/); expect(() => tb.get(msg)).toThrowError(/Unknown placeholder/);
}); });
it('should report missing translation', () => { it('should report missing translation', () => {
const tb = new TranslationBundle({}, (_) => 'foo'); const tb = new TranslationBundle({}, (_) => 'foo');
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd'); const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
expect(() => tb.get(msg)).toThrowError(/Missing translation for message foo/); expect(() => tb.get(msg)).toThrowError(/Missing translation for message foo/);
}); });
@ -83,8 +83,8 @@ export function main(): void {
const msgMap = { const msgMap = {
foo: [new i18n.Placeholder('', 'ph1', span)], foo: [new i18n.Placeholder('', 'ph1', span)],
}; };
const refMsg = new i18n.Message([srcNode], {}, {}, 'm', 'd'); const refMsg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
const msg = new i18n.Message([srcNode], {}, {ph1: refMsg}, 'm', 'd'); const msg = new i18n.Message([srcNode], {}, {ph1: refMsg}, 'm', 'd', 'i');
let count = 0; let count = 0;
const digest = (_: any) => count++ ? 'ref' : 'foo'; const digest = (_: any) => count++ ? 'ref' : 'foo';
const tb = new TranslationBundle(msgMap, digest); const tb = new TranslationBundle(msgMap, digest);
@ -102,7 +102,7 @@ export function main(): void {
ph1: '</b>', ph1: '</b>',
}; };
const tb = new TranslationBundle(msgMap, (_) => 'foo'); const tb = new TranslationBundle(msgMap, (_) => 'foo');
const msg = new i18n.Message([srcNode], phMap, {}, 'm', 'd'); const msg = new i18n.Message([srcNode], phMap, {}, 'm', 'd', 'i');
expect(() => tb.get(msg)).toThrowError(/Unexpected closing tag "b"/); expect(() => tb.get(msg)).toThrowError(/Unexpected closing tag "b"/);
}); });
}); });

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]); 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', () => { it('should select by attr name only once if the value is from the DOM', () => {
matcher.addSelectables(s1 = CssSelector.parse('[some-decor]'), 1); 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]); 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', () => { it('should match with multiple :not selectors', () => {
matcher.addSelectables(s1 = CssSelector.parse('div:not([a]):not([b])'), 1); matcher.addSelectables(s1 = CssSelector.parse('div:not([a]):not([b])'), 1);
expect(matcher.match(CssSelector.parse('div[a]')[0], selectableCollector)).toBe(false); expect(matcher.match(CssSelector.parse('div[a]')[0], selectableCollector)).toBe(false);

View File

@ -6,13 +6,13 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {CompileAnimationEntryMetadata, CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileQueryMetadata, CompileTemplateMetadata, CompileTokenMetadata, CompileTypeMetadata, tokenReference} from '@angular/compiler/src/compile_metadata'; import {CompileAnimationEntryMetadata, CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileTemplateMetadata, CompileTokenMetadata, CompileTypeMetadata, tokenReference} from '@angular/compiler/src/compile_metadata';
import {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry'; import {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry';
import {ElementSchemaRegistry} from '@angular/compiler/src/schema/element_schema_registry'; import {ElementSchemaRegistry} from '@angular/compiler/src/schema/element_schema_registry';
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAstType, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '@angular/compiler/src/template_parser/template_ast'; import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAstType, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '@angular/compiler/src/template_parser/template_ast';
import {TEMPLATE_TRANSFORMS, TemplateParser, splitClasses} from '@angular/compiler/src/template_parser/template_parser'; import {TEMPLATE_TRANSFORMS, TemplateParser, splitClasses} from '@angular/compiler/src/template_parser/template_parser';
import {TEST_COMPILER_PROVIDERS} from '@angular/compiler/testing/test_bindings'; import {TEST_COMPILER_PROVIDERS} from '@angular/compiler/testing/test_bindings';
import {SchemaMetadata, SecurityContext, Type} from '@angular/core'; import {SchemaMetadata, SecurityContext} from '@angular/core';
import {Console} from '@angular/core/src/console'; import {Console} from '@angular/core/src/console';
import {TestBed, inject} from '@angular/core/testing'; import {TestBed, inject} from '@angular/core/testing';
@ -257,8 +257,7 @@ export function main() {
}); });
}); });
describe( describe('TemplateParser', () => {
'TemplateParser', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureCompiler({providers: [TEST_COMPILER_PROVIDERS, MOCK_SCHEMA_REGISTRY]}); TestBed.configureCompiler({providers: [TEST_COMPILER_PROVIDERS, MOCK_SCHEMA_REGISTRY]});
}); });
@ -330,6 +329,13 @@ export function main() {
]); ]);
}); });
it('should parse dotted name bound properties', () => {
expect(humanizeTplAst(parse('<div [dot.name]="v">', []))).toEqual([
[ElementAst, 'div'],
[BoundElementPropertyAst, PropertyBindingType.Property, 'dot.name', 'v', null]
]);
});
it('should normalize property names via the element schema', () => { it('should normalize property names via the element schema', () => {
expect(humanizeTplAst(parse('<div [mappedAttr]="v">', []))).toEqual([ expect(humanizeTplAst(parse('<div [mappedAttr]="v">', []))).toEqual([
[ElementAst, 'div'], [ElementAst, 'div'],
@ -365,21 +371,6 @@ export function main() {
]); ]);
}); });
it('should report invalid prefixes', () => {
expect(() => parse('<p [atTr.foo]>', []))
.toThrowError(
`Template parse errors:\nInvalid property name 'atTr.foo' ("<p [ERROR ->][atTr.foo]>"): TestComp@0:3`);
expect(() => parse('<p [sTyle.foo]>', []))
.toThrowError(
`Template parse errors:\nInvalid property name 'sTyle.foo' ("<p [ERROR ->][sTyle.foo]>"): TestComp@0:3`);
expect(() => parse('<p [Class.foo]>', []))
.toThrowError(
`Template parse errors:\nInvalid property name 'Class.foo' ("<p [ERROR ->][Class.foo]>"): TestComp@0:3`);
expect(() => parse('<p [bar.foo]>', []))
.toThrowError(
`Template parse errors:\nInvalid property name 'bar.foo' ("<p [ERROR ->][bar.foo]>"): TestComp@0:3`);
});
describe('errors', () => { describe('errors', () => {
it('should throw error when binding to an unknown property', () => { it('should throw error when binding to an unknown property', () => {
expect(() => parse('<my-component [invalidProp]="bar"></my-component>', [])) expect(() => parse('<my-component [invalidProp]="bar"></my-component>', []))
@ -397,10 +388,8 @@ Can't bind to 'invalidProp' since it isn't a known property of 'my-component'.
2. If 'unknown' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message. ("[ERROR ->]<unknown></unknown>"): TestComp@0:0`); 2. If 'unknown' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message. ("[ERROR ->]<unknown></unknown>"): TestComp@0:0`);
}); });
it('should throw error when binding to an unknown custom element w/o bindings', it('should throw error when binding to an unknown custom element w/o bindings', () => {
() => { expect(() => parse('<un-known></un-known>', [])).toThrowError(`Template parse errors:
expect(() => parse('<un-known></un-known>', []))
.toThrowError(`Template parse errors:
'un-known' is not a known element: 'un-known' is not a known element:
1. If 'un-known' is an Angular component, then verify that it is part of this module. 1. If 'un-known' is an Angular component, then verify that it is part of this module.
2. If 'un-known' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message. ("[ERROR ->]<un-known></un-known>"): TestComp@0:0`); 2. If 'un-known' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message. ("[ERROR ->]<un-known></un-known>"): TestComp@0:0`);
@ -433,20 +422,16 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("<my-componen
]); ]);
}); });
it('should parse bound properties via {{...}} and not report them as attributes', it('should parse bound properties via {{...}} and not report them as attributes', () => {
() => {
expect(humanizeTplAst(parse('<div prop="{{v}}">', []))).toEqual([ expect(humanizeTplAst(parse('<div prop="{{v}}">', []))).toEqual([
[ElementAst, 'div'], [ElementAst, 'div'],
[ [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', '{{ v }}', null]
BoundElementPropertyAst, PropertyBindingType.Property, 'prop', '{{ v }}', null
]
]); ]);
}); });
it('should parse bound properties via bind-animate- and not report them as attributes', it('should parse bound properties via bind-animate- and not report them as attributes',
() => { () => {
expect( expect(humanizeTplAst(parse('<div bind-animate-someAnimation="value2">', [], [], [])))
humanizeTplAst(parse('<div bind-animate-someAnimation="value2">', [], [], [])))
.toEqual([ .toEqual([
[ElementAst, 'div'], [ElementAst, 'div'],
[ [
@ -565,12 +550,12 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("<my-componen
it('should allow events on explicit embedded templates that are emitted by a directive', it('should allow events on explicit embedded templates that are emitted by a directive',
() => { () => {
const dirA = CompileDirectiveMetadata const dirA =
CompileDirectiveMetadata
.create({ .create({
selector: 'template', selector: 'template',
outputs: ['e'], outputs: ['e'],
type: createTypeMeta( type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}})
{reference: {filePath: someModuleUrl, name: 'DirA'}})
}) })
.toSummary(); .toSummary();
expect(humanizeTplAst(parse('<template (e)="f"></template>', [dirA]))).toEqual([ expect(humanizeTplAst(parse('<template (e)="f"></template>', [dirA]))).toEqual([
@ -605,31 +590,31 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("<my-componen
describe('directives', () => { describe('directives', () => {
it('should order directives by the directives array in the View and match them only once', it('should order directives by the directives array in the View and match them only once',
() => { () => {
const dirA = CompileDirectiveMetadata const dirA =
CompileDirectiveMetadata
.create({ .create({
selector: '[a]', selector: '[a]',
type: createTypeMeta( type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}})
{reference: {filePath: someModuleUrl, name: 'DirA'}})
}) })
.toSummary(); .toSummary();
const dirB = CompileDirectiveMetadata const dirB =
CompileDirectiveMetadata
.create({ .create({
selector: '[b]', selector: '[b]',
type: createTypeMeta( type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirB'}})
{reference: {filePath: someModuleUrl, name: 'DirB'}})
}) })
.toSummary(); .toSummary();
const dirC = CompileDirectiveMetadata const dirC =
CompileDirectiveMetadata
.create({ .create({
selector: '[c]', selector: '[c]',
type: createTypeMeta( type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirC'}})
{reference: {filePath: someModuleUrl, name: 'DirC'}})
}) })
.toSummary(); .toSummary();
expect(humanizeTplAst(parse('<div a c b a b>', [dirA, dirB, dirC]))).toEqual([ expect(humanizeTplAst(parse('<div a c b a b>', [dirA, dirB, dirC]))).toEqual([
[ElementAst, 'div'], [AttrAst, 'a', ''], [AttrAst, 'c', ''], [AttrAst, 'b', ''], [ElementAst, 'div'], [AttrAst, 'a', ''], [AttrAst, 'c', ''], [AttrAst, 'b', ''],
[AttrAst, 'a', ''], [AttrAst, 'b', ''], [DirectiveAst, dirA], [AttrAst, 'a', ''], [AttrAst, 'b', ''], [DirectiveAst, dirA], [DirectiveAst, dirB],
[DirectiveAst, dirB], [DirectiveAst, dirC] [DirectiveAst, dirC]
]); ]);
}); });
@ -723,8 +708,7 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("<my-componen
}) })
.toSummary(); .toSummary();
expect(humanizeTplAst(parse('<div [a]="expr"></div>', [dirA]))).toEqual([ expect(humanizeTplAst(parse('<div [a]="expr"></div>', [dirA]))).toEqual([
[ElementAst, 'div'], [DirectiveAst, dirA], [ElementAst, 'div'], [DirectiveAst, dirA], [BoundDirectivePropertyAst, 'b', 'expr']
[BoundDirectivePropertyAst, 'b', 'expr']
]); ]);
}); });
@ -830,8 +814,7 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("<my-componen
} }
function createDir( function createDir(
selector: string, selector: string, {providers = null, viewProviders = null, deps = [], queries = []}: {
{providers = null, viewProviders = null, deps = [], queries = []}: {
providers?: CompileProviderMetadata[], providers?: CompileProviderMetadata[],
viewProviders?: CompileProviderMetadata[], viewProviders?: CompileProviderMetadata[],
deps?: string[], deps?: string[],
@ -991,8 +974,7 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("<my-componen
it('should mark directives and dependencies of directives as eager', () => { it('should mark directives and dependencies of directives as eager', () => {
const provider0 = createProvider('service0'); const provider0 = createProvider('service0');
const provider1 = createProvider('service1'); const provider1 = createProvider('service1');
const dirA = const dirA = createDir('[dirA]', {providers: [provider0, provider1], deps: ['service0']});
createDir('[dirA]', {providers: [provider0, provider1], deps: ['service0']});
const elAst: ElementAst = <ElementAst>parse('<div dirA>', [dirA])[0]; const elAst: ElementAst = <ElementAst>parse('<div dirA>', [dirA])[0];
expect(elAst.providers.length).toBe(3); expect(elAst.providers.length).toBe(3);
expect(elAst.providers[0].providers).toEqual([provider0]); expect(elAst.providers[0].providers).toEqual([provider0]);
@ -1258,34 +1240,33 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
describe('directives', () => { describe('directives', () => {
it('should locate directives in property bindings', () => { it('should locate directives in property bindings', () => {
const dirA = CompileDirectiveMetadata const dirA =
CompileDirectiveMetadata
.create({ .create({
selector: '[a=b]', selector: '[a=b]',
type: createTypeMeta( type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}),
{reference: {filePath: someModuleUrl, name: 'DirA'}}),
inputs: ['a'] inputs: ['a']
}) })
.toSummary(); .toSummary();
const dirB = CompileDirectiveMetadata const dirB =
CompileDirectiveMetadata
.create({ .create({
selector: '[b]', selector: '[b]',
type: createTypeMeta( type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirB'}})
{reference: {filePath: someModuleUrl, name: 'DirB'}})
}) })
.toSummary(); .toSummary();
expect(humanizeTplAst(parse('<div template="a b" b>', [dirA, dirB]))).toEqual([ expect(humanizeTplAst(parse('<div template="a b" b>', [dirA, dirB]))).toEqual([
[EmbeddedTemplateAst], [DirectiveAst, dirA], [EmbeddedTemplateAst], [DirectiveAst, dirA], [BoundDirectivePropertyAst, 'a', 'b'],
[BoundDirectivePropertyAst, 'a', 'b'], [ElementAst, 'div'], [AttrAst, 'b', ''], [ElementAst, 'div'], [AttrAst, 'b', ''], [DirectiveAst, dirB]
[DirectiveAst, dirB]
]); ]);
}); });
it('should not locate directives in variables', () => { it('should not locate directives in variables', () => {
const dirA = CompileDirectiveMetadata const dirA =
CompileDirectiveMetadata
.create({ .create({
selector: '[a]', selector: '[a]',
type: createTypeMeta( type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}})
{reference: {filePath: someModuleUrl, name: 'DirA'}})
}) })
.toSummary(); .toSummary();
expect(humanizeTplAst(parse('<div template="let a=b">', [dirA]))).toEqual([ expect(humanizeTplAst(parse('<div template="let a=b">', [dirA]))).toEqual([
@ -1294,11 +1275,11 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
}); });
it('should not locate directives in references', () => { it('should not locate directives in references', () => {
const dirA = CompileDirectiveMetadata const dirA =
CompileDirectiveMetadata
.create({ .create({
selector: '[a]', selector: '[a]',
type: createTypeMeta( type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}})
{reference: {filePath: someModuleUrl, name: 'DirA'}})
}) })
.toSummary(); .toSummary();
expect(humanizeTplAst(parse('<div ref-a>', [dirA]))).toEqual([ expect(humanizeTplAst(parse('<div ref-a>', [dirA]))).toEqual([
@ -1328,8 +1309,7 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
let compCounter: number; let compCounter: number;
beforeEach(() => { compCounter = 0; }); beforeEach(() => { compCounter = 0; });
function createComp( function createComp(selector: string, ngContentSelectors: string[]): CompileDirectiveSummary {
selector: string, ngContentSelectors: string[]): CompileDirectiveSummary {
return CompileDirectiveMetadata return CompileDirectiveMetadata
.create({ .create({
selector: selector, selector: selector,
@ -1353,9 +1333,8 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
describe('project text nodes', () => { describe('project text nodes', () => {
it('should project text nodes with wildcard selector', () => { it('should project text nodes with wildcard selector', () => {
expect(humanizeContentProjection(parse('<div>hello</div>', [ expect(humanizeContentProjection(parse('<div>hello</div>', [createComp('div', ['*'])])))
createComp('div', ['*']) .toEqual([['div', null], ['#text(hello)', 0]]);
]))).toEqual([['div', null], ['#text(hello)', 0]]);
}); });
}); });
@ -1428,10 +1407,9 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
}); });
it('should project children of components with ngNonBindable', () => { it('should project children of components with ngNonBindable', () => {
expect( expect(humanizeContentProjection(parse('<div ngNonBindable>{{hello}}<span></span></div>', [
humanizeContentProjection(parse( createComp('div', ['*'])
'<div ngNonBindable>{{hello}}<span></span></div>', [createComp('div', ['*'])]))) ]))).toEqual([['div', null], ['#text({{hello}})', 0], ['span', 0]]);
.toEqual([['div', null], ['#text({{hello}})', 0], ['span', 0]]);
}); });
it('should match the element when there is an inline template', () => { it('should match the element when there is an inline template', () => {
@ -1510,8 +1488,7 @@ Can't have multiple template bindings on one element. Use only one attribute nam
}); });
it('should report invalid property names', () => { it('should report invalid property names', () => {
expect(() => parse('<div [invalidProp]></div>', [])) expect(() => parse('<div [invalidProp]></div>', [])).toThrowError(`Template parse errors:
.toThrowError(`Template parse errors:
Can't bind to 'invalidProp' since it isn't a known property of 'div'. ("<div [ERROR ->][invalidProp]></div>"): TestComp@0:5`); Can't bind to 'invalidProp' since it isn't a known property of 'div'. ("<div [ERROR ->][invalidProp]></div>"): TestComp@0:5`);
}); });
@ -1586,9 +1563,9 @@ Parser Error: Unexpected token 'b' at column 3 in [a b] in TestComp@0:5 ("<div [
.toSummary(); .toSummary();
expect(() => parse('<template [a]="b" (e)="f"></template>', [dirA])) expect(() => parse('<template [a]="b" (e)="f"></template>', [dirA]))
.toThrowError(`Template parse errors: .toThrowError(`Template parse errors:
Event binding e 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. ("<template [a]="b" [ERROR ->](e)="f"></template>"): TestComp@0:18 Event binding e 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". ("<template [a]="b" [ERROR ->](e)="f"></template>"): TestComp@0:18
Components on an embedded template: DirA ("[ERROR ->]<template [a]="b" (e)="f"></template>"): TestComp@0:0 Components on an embedded template: DirA ("[ERROR ->]<template [a]="b" (e)="f"></template>"): TestComp@0:0
Property binding a 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. ("[ERROR ->]<template [a]="b" (e)="f"></template>"): TestComp@0:0`); Property binding a 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". ("[ERROR ->]<template [a]="b" (e)="f"></template>"): TestComp@0:0`);
}); });
it('should not allow components or element bindings on inline embedded templates', () => { it('should not allow components or element bindings on inline embedded templates', () => {
@ -1603,7 +1580,7 @@ Property binding a not used by any directive on an embedded template. Make sure
.toSummary(); .toSummary();
expect(() => parse('<div *a="b"></div>', [dirA])).toThrowError(`Template parse errors: expect(() => parse('<div *a="b"></div>', [dirA])).toThrowError(`Template parse errors:
Components on an embedded template: DirA ("[ERROR ->]<div *a="b"></div>"): TestComp@0:0 Components on an embedded template: DirA ("[ERROR ->]<div *a="b"></div>"): TestComp@0:0
Property binding a 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. ("[ERROR ->]<div *a="b"></div>"): TestComp@0:0`); Property binding a 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". ("[ERROR ->]<div *a="b"></div>"): TestComp@0:0`);
}); });
}); });
@ -1655,8 +1632,7 @@ Property binding a not used by any directive on an embedded template. Make sure
}); });
it('should keep nested children of elements with ngNonBindable', () => { it('should keep nested children of elements with ngNonBindable', () => {
expect(humanizeTplAst(parse('<div ngNonBindable><span>{{b}}</span></div>', []))) expect(humanizeTplAst(parse('<div ngNonBindable><span>{{b}}</span></div>', []))).toEqual([
.toEqual([
[ElementAst, 'div'], [AttrAst, 'ngNonBindable', ''], [ElementAst, 'span'], [ElementAst, 'div'], [AttrAst, 'ngNonBindable', ''], [ElementAst, 'span'],
[TextAst, '{{b}}'] [TextAst, '{{b}}']
]); ]);
@ -1680,11 +1656,10 @@ Property binding a not used by any directive on an embedded template. Make sure
it('should convert <ng-content> elements into regular elements inside of elements with ngNonBindable', it('should convert <ng-content> elements into regular elements inside of elements with ngNonBindable',
() => { () => {
expect( expect(humanizeTplAst(parse('<div ngNonBindable><ng-content></ng-content>a</div>', [])))
humanizeTplAst(parse('<div ngNonBindable><ng-content></ng-content>a</div>', [])))
.toEqual([ .toEqual([
[ElementAst, 'div'], [AttrAst, 'ngNonBindable', ''], [ElementAst, 'div'], [AttrAst, 'ngNonBindable', ''], [ElementAst, 'ng-content'],
[ElementAst, 'ng-content'], [TextAst, 'a'] [TextAst, 'a']
]); ]);
}); });
@ -1717,10 +1692,8 @@ Property binding a not used by any directive on an embedded template. Make sure
}); });
it('should support variables', () => { it('should support variables', () => {
expect(humanizeTplAstSourceSpans(parse('<template let-a="b"></template>', []))) expect(humanizeTplAstSourceSpans(parse('<template let-a="b"></template>', []))).toEqual([
.toEqual([ [EmbeddedTemplateAst, '<template let-a="b">'], [VariableAst, 'a', 'b', 'let-a="b"']
[EmbeddedTemplateAst, '<template let-a="b">'],
[VariableAst, 'a', 'b', 'let-a="b"']
]); ]);
}); });
@ -1769,8 +1742,8 @@ Property binding a not used by any directive on an embedded template. Make sure
}) })
.toSummary(); .toSummary();
expect(humanizeTplAstSourceSpans(parse('<div a>', [dirA, comp]))).toEqual([ expect(humanizeTplAstSourceSpans(parse('<div a>', [dirA, comp]))).toEqual([
[ElementAst, 'div', '<div a>'], [AttrAst, 'a', '', 'a'], [ElementAst, 'div', '<div a>'], [AttrAst, 'a', '', 'a'], [DirectiveAst, dirA, '<div a>'],
[DirectiveAst, dirA, '<div a>'], [DirectiveAst, comp, '<div a>'] [DirectiveAst, comp, '<div a>']
]); ]);
}); });
@ -1782,11 +1755,11 @@ Property binding a not used by any directive on an embedded template. Make sure
type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'elDir'}}) type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'elDir'}})
}) })
.toSummary(); .toSummary();
const attrSel = CompileDirectiveMetadata const attrSel =
CompileDirectiveMetadata
.create({ .create({
selector: '[href]', selector: '[href]',
type: createTypeMeta( type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'attrDir'}})
{reference: {filePath: someModuleUrl, name: 'attrDir'}})
}) })
.toSummary(); .toSummary();
@ -1812,8 +1785,7 @@ Property binding a not used by any directive on an embedded template. Make sure
}) })
.toSummary(); .toSummary();
expect(humanizeTplAstSourceSpans(parse('<div [aProp]="foo"></div>', [dirA]))).toEqual([ expect(humanizeTplAstSourceSpans(parse('<div [aProp]="foo"></div>', [dirA]))).toEqual([
[ElementAst, 'div', '<div [aProp]="foo">'], [ElementAst, 'div', '<div [aProp]="foo">'], [DirectiveAst, dirA, '<div [aProp]="foo">'],
[DirectiveAst, dirA, '<div [aProp]="foo">'],
[BoundDirectivePropertyAst, 'aProp', 'foo', '[aProp]="foo"'] [BoundDirectivePropertyAst, 'aProp', 'foo', '[aProp]="foo"']
]); ]);
}); });
@ -1867,7 +1839,7 @@ Property binding a not used by any directive on an embedded template. Make sure
it('should report pipes as error that have not been defined as dependencies', () => { it('should report pipes as error that have not been defined as dependencies', () => {
expect(() => parse('{{a | test}}', [])).toThrowError(`Template parse errors: expect(() => parse('{{a | test}}', [])).toThrowError(`Template parse errors:
The pipe 'test' could not be found ("[ERROR ->]{{a | test}}"): TestComp@0:0`); The pipe 'test' could not be found ("{{[ERROR ->]a | test}}"): TestComp@0:2`);
}); });
}); });
@ -1880,8 +1852,8 @@ The pipe 'test' could not be found ("[ERROR ->]{{a | test}}"): TestComp@0:0`);
'<template ngPluralCase="many">big</template>' + '<template ngPluralCase="many">big</template>' +
'</ng-container>'; '</ng-container>';
expect(humanizeTplAst(parse(shortForm, [ expect(humanizeTplAst(parse(shortForm, []))).toEqual(humanizeTplAst(parse(expandedForm, [
]))).toEqual(humanizeTplAst(parse(expandedForm, []))); ])));
}); });
it('should expand select messages', () => { it('should expand select messages', () => {
@ -1891,8 +1863,8 @@ The pipe 'test' could not be found ("[ERROR ->]{{a | test}}"): TestComp@0:0`);
'<template ngSwitchDefault>bar</template>' + '<template ngSwitchDefault>bar</template>' +
'</ng-container>'; '</ng-container>';
expect(humanizeTplAst(parse(shortForm, [ expect(humanizeTplAst(parse(shortForm, []))).toEqual(humanizeTplAst(parse(expandedForm, [
]))).toEqual(humanizeTplAst(parse(expandedForm, []))); ])));
}); });
it('should be possible to escape ICU messages', () => { it('should be possible to escape ICU messages', () => {

View File

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

View File

@ -5,30 +5,47 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * 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'; import {AnimationPlayer} from './animation_player';
let _queuedAnimations: AnimationPlayer[] = []; @Injectable()
export class AnimationQueue {
public entries: AnimationPlayer[] = [];
/** @internal */ constructor(private _zone: NgZone) {}
export function queueAnimation(player: AnimationPlayer) {
_queuedAnimations.push(player);
}
/** @internal */ enqueue(player: AnimationPlayer) { this.entries.push(player); }
export function triggerQueuedAnimations() {
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 // this code is wrapped into a single promise such that the
// onStart and onDone player callbacks are triggered outside // onStart and onDone player callbacks are triggered outside
// of the digest cycle of animations // of the digest cycle of animations
if (_queuedAnimations.length) { Promise.resolve(null).then(() => this._triggerAnimations());
Promise.resolve(null).then(_triggerAnimations); });
}
} }
}
function _triggerAnimations() { private _triggerAnimations() {
for (let i = 0; i < _queuedAnimations.length; i++) { NgZone.assertNotInAngularZone();
const player = _queuedAnimations[i];
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(); player.play();
} }
_queuedAnimations = []; }
}
} }

View File

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

View File

@ -44,10 +44,11 @@ export class ViewAnimationMap {
getAllPlayers(): AnimationPlayer[] { return this._allPlayers; } 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); const playersByAnimation = this._map.get(element);
if (playersByAnimation) { if (playersByAnimation) {
const player = playersByAnimation[animationName]; const player = playersByAnimation[animationName];
if (!targetPlayer || player === targetPlayer) {
delete playersByAnimation[animationName]; delete playersByAnimation[animationName];
const index = this._allPlayers.indexOf(player); const index = this._allPlayers.indexOf(player);
this._allPlayers.splice(index, 1); this._allPlayers.splice(index, 1);
@ -57,4 +58,5 @@ export class ViewAnimationMap {
} }
} }
} }
}
} }

View File

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

View File

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

View File

@ -82,6 +82,14 @@ export class Compiler {
throw _throwError(); 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. * Clears all caches.
*/ */

View File

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

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@ export enum LifecycleHooks {
*/ */
export interface SimpleChanges { [propName: string]: SimpleChange; } 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.OnInit, LifecycleHooks.OnDestroy, LifecycleHooks.DoCheck, LifecycleHooks.OnChanges,
LifecycleHooks.AfterContentInit, LifecycleHooks.AfterContentChecked, LifecycleHooks.AfterViewInit, LifecycleHooks.AfterContentInit, LifecycleHooks.AfterContentChecked, LifecycleHooks.AfterViewInit,
LifecycleHooks.AfterViewChecked LifecycleHooks.AfterViewChecked
@ -92,7 +92,7 @@ export abstract class OnInit { abstract ngOnInit(): void; }
export abstract class DoCheck { abstract ngDoCheck(): 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 * @howToUse
* {@example core/ts/metadata/lifecycle_hooks_spec.ts region='OnDestroy'} * {@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'}]); 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', () => { describe('animation aliases', () => {
it('should animate the ":enter" animation alias as "void => *"', fakeAsync(() => { it('should animate the ":enter" animation alias as "void => *"', fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, { TestBed.overrideComponent(DummyIfCmp, {
@ -1493,6 +1547,36 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(message).toMatch(/Couldn't find an animation entry for "something"/); 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', 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, { TestBed.overrideComponent(DummyIfCmp, {
@ -2121,6 +2205,43 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(kf[1]).toEqual([1, {'height': '333px', 'opacity': AUTO_STYLE, 'width': '200px'}]); 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', () => { 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(); }); afterEach(() => { destroyPlatform(); });
it('should automatically run change detection when the animation done callback code updates any bindings', 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; appRef.components.find(cmp => cmp.componentType === AnimationAppCmp).instance;
const driver: ExtendedWebAnimationsDriver = ref.injector.get(AnimationDriver); const driver: ExtendedWebAnimationsDriver = ref.injector.get(AnimationDriver);
const zone: NgZone = ref.injector.get(NgZone); const zone: NgZone = ref.injector.get(NgZone);
let text = '';
zone.run(() => { zone.run(() => {
text = getDOM().getText(el); assertStatus('pending');
expect(text).toMatch(/Animation Status: pending/); assertTime(0);
expect(text).toMatch(/Animation Time: 0/);
appCmp.animationStatus = 'on'; appCmp.animationStatus = 'on';
zone.runOutsideAngular(() => {
setTimeout(() => { setTimeout(() => {
text = getDOM().getText(el); assertStatus('started');
expect(text).toMatch(/Animation Status: started/); assertTime(555);
expect(text).toMatch(/Animation Time: 555/); const player = driver.players.pop();
const player = driver.players.pop().domPlayer; finishAnimation(player, () => {
getDOM().dispatchEvent(player, getDOM().createEvent('finish')); assertStatus('done');
setTimeout(() => { assertTime(555);
text = getDOM().getText(el);
expect(text).toMatch(/Animation Status: done/);
expect(text).toMatch(/Animation Time: 555/);
asyncDone(); asyncDone();
});
}, 0); }, 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); }, 0);
}); });
}); });
@ -2258,7 +2454,17 @@ class _NaiveElementSchema extends DomElementSchemaRegistry {
@Component({ @Component({
selector: 'animation-app', 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: ` template: `
Animation Time: {{ time }} Animation Time: {{ time }}
Animation Status: {{ status }} Animation Status: {{ status }}
@ -2272,7 +2478,7 @@ class AnimationAppCmp {
animationStatus = 'off'; animationStatus = 'off';
@HostListener('@animationStatus.start', ['$event']) @HostListener('@animationStatus.start', ['$event'])
onStart(event: AnimationTransitionEvent) { onAnimationStartDone(event: AnimationTransitionEvent) {
if (event.toState == 'on') { if (event.toState == 'on') {
this.time = event.totalTime; this.time = event.totalTime;
this.status = 'started'; this.status = 'started';
@ -2280,7 +2486,26 @@ class AnimationAppCmp {
} }
@HostListener('@animationStatus.done', ['$event']) @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') { if (event.toState == 'on') {
this.time = event.totalTime; this.time = event.totalTime;
this.status = 'done'; 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 * 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 {ApplicationRef, ApplicationRef_} from '@angular/core/src/application_ref';
import {ErrorHandler} from '@angular/core/src/error_handler'; import {ErrorHandler} from '@angular/core/src/error_handler';
import {ComponentRef} from '@angular/core/src/linker/component_factory'; import {ComponentRef} from '@angular/core/src/linker/component_factory';
@ -275,9 +275,15 @@ export function main() {
vc: ViewContainerRef; vc: ViewContainerRef;
} }
@Component({template: '<template #t>Dynamic content</template>'})
class EmbeddedViewComp {
@ViewChild(TemplateRef)
tplRef: TemplateRef<Object>;
}
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [MyComp, ContainerComp], declarations: [MyComp, ContainerComp, EmbeddedViewComp],
providers: [{provide: ComponentFixtureNoNgZone, useValue: true}] providers: [{provide: ComponentFixtureNoNgZone, useValue: true}]
}); });
}); });
@ -321,6 +327,17 @@ export function main() {
expect(appRef.viewCount).toBe(0); 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', it('should not allow to attach a view to both, a view container and the ApplicationRef',
() => { () => {
const comp = TestBed.createComponent(MyComp); 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, AnotherComponent,
TestLocals, TestLocals,
CompWithRef, CompWithRef,
WrapCompWithRef,
EmitterDirective, EmitterDirective,
PushComp, PushComp,
OnDestroyDirective, OnDestroyDirective,
@ -1133,6 +1134,23 @@ export function main() {
expect(renderLog.log).toEqual([]); 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(() => { it('Reattaches', fakeAsync(() => {
const ctx = createCompFixture('<comp-with-ref></comp-with-ref>'); const ctx = createCompFixture('<comp-with-ref></comp-with-ref>');
const cmp: CompWithRef = queryDirs(ctx.debugElement, CompWithRef)[0]; const cmp: CompWithRef = queryDirs(ctx.debugElement, CompWithRef)[0];
@ -1346,6 +1364,11 @@ class CompWithRef {
noop() {} noop() {}
} }
@Component({selector: 'wrap-comp-with-ref', template: '<comp-with-ref></comp-with-ref>'})
class WrapCompWithRef {
constructor(public changeDetectorRef: ChangeDetectorRef) {}
}
@Component({ @Component({
selector: 'push-cmp', selector: 'push-cmp',
template: '<div (event)="noop()" emitterDirective></div>{{value}}{{renderIncrement}}', 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 {Component, Injector, OpaqueToken, Pipe, PipeTransform, Provider} from '@angular/core';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {beforeEach, describe, it} from '@angular/core/testing/testing_internal';
import {expect} from '@angular/platform-browser/testing/matchers'; import {expect} from '@angular/platform-browser/testing/matchers';
export function main() { export function main() {
@ -139,6 +138,15 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(injector.get(token1)).toEqual(tokenValue1); expect(injector.get(token1)).toEqual(tokenValue1);
expect(injector.get(token2)).toEqual(tokenValue2); 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', () => { 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 * 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 {AsyncTestCompleter} from './async_test_completer';
import {ComponentFixture} from './component_fixture'; import {ComponentFixture} from './component_fixture';
import {stringify} from './facade/lang'; import {stringify} from './facade/lang';
@ -134,6 +135,11 @@ export class TestBed implements Injector {
return TestBed; return TestBed;
} }
static overrideTemplate(component: Type<any>, template: string): typeof TestBed {
getTestBed().overrideComponent(component, {set: {template, templateUrl: null}});
return TestBed;
}
static get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND) { static get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND) {
return getTestBed().get(token, notFoundValue); return getTestBed().get(token, notFoundValue);
} }
@ -268,8 +274,10 @@ export class TestBed implements Injector {
} }
} }
} }
this._moduleRef = const ngZone = new NgZone({enableLongStackTrace: true});
this._moduleWithComponentFactories.ngModuleFactory.create(this.platform.injector); const ngZoneInjector = ReflectiveInjector.resolveAndCreate(
[{provide: NgZone, useValue: ngZone}], this.platform.injector);
this._moduleRef = this._moduleWithComponentFactories.ngModuleFactory.create(ngZoneInjector);
this._instantiated = true; this._instantiated = true;
} }

View File

@ -3,3 +3,32 @@
This folder contains small example apps that get in-lined into our API docs. 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 Each example contains tests for application behavior (as opposed to testing Angular's
behavior) just like an Angular application developer would write. 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> <!DOCTYPE html>
<html> <html>
<head> <head>
<script type="text/javascript" src="bootstrap.js"></script> <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<script type="text/javascript"> <script type="text/javascript" src="bootstrap.js"></script>
<script type="text/javascript">
System.import('main-dynamic').catch(console.error.bind(console)); System.import('main-dynamic').catch(console.error.bind(console));
</script> </script>
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
</head> </head>
<body> <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

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