Compare commits

..

116 Commits

Author SHA1 Message Date
f114e40212 docs(changelog): add changelog for 4.0.0-beta.1 2016-12-21 16:48:14 -08:00
952471e25d chore(release): cut the 4.0.0-beta.1 release 2016-12-21 16:44:56 -08:00
c65e428778 docs(changelog): add changelog for 2.4.1 2016-12-21 16:43:13 -08:00
842f52e841 fix(animations): always recover from a failed animation step (#13604) 2016-12-21 14:14:45 -08:00
eb2ceff4ba fix(router): should reset location if a navigation by location is successful (#13545)
Closes #13491
2016-12-21 12:47:58 -08:00
f49ab56160 fix(animations): always quote string map key values in AOT code (#13602) 2016-12-20 18:17:58 -08:00
c0f750af4e fix(compiler): ignore @import in comments (#13368)
* refactor(compiler): clean up style url resolver
* fix(compiler): ignore @import in css comments

Closes #12196
2016-12-20 17:51:02 -08:00
bcd37f52fb Include bower instructions in DEVELOPER.md (#13591) 2016-12-20 17:50:04 -08:00
e69c1fb36c refactor(platform-browser): resolver merge conflict for tslint (#13601) 2016-12-20 17:49:25 -08:00
9da4c259a5 feat(upgrade): support the $doCheck() lifecycle hook in UpgradeComponent (#13015) 2016-12-20 16:18:43 -08:00
fcd116fdc0 fix(common): throw an error if trackBy is not a function (#13420)
* fix(common): throw an error if trackBy is not a function

Closes #13388

* refactor(platform-browser): disable no-console rule in DomAdapter
2016-12-20 16:18:24 -08:00
383adc9ad9 fix(core): improve error message when component factory cannot be found (#13541)
Closes #12678
2016-12-20 16:17:22 -08:00
9b8488f007 build: fix publish-build-artifacts branch detection (#13599) 2016-12-20 15:59:15 -08:00
1817ddb57b build: publish build artifacts to branches (#13529)
Fix #13126
2016-12-20 14:52:50 -08:00
1ee574c51e docs(changelog): add changelog for 2.4.0 2016-12-20 11:14:28 -08:00
171a9bdc85 feat: update to rxjs@5.0.1 and unpin the rxjs peerDeps via ^5.0.1 (#13572)
Now that rxjs is stable and the rxjs team follows semver, we can update and unpin the dependency safely.

From now on the Angular application/library developers are in charge of controlling the rxjs version as long as it's newer than 5.0.1.

closes #13561
closes #13478
closes #13572
2016-12-19 16:24:53 -08:00
896916af29 build(npm): update angular version in shrinkwrap files 2016-12-19 16:23:54 -08:00
e49c7fae22 refactor(compiler-cli): support extracting the mesage bundle without writing a file (#13580) 2016-12-19 15:28:55 -08:00
6b65fc1286 feat(compiler-cli): private i18n API for the CLI (#13536)
Also change the Extractor API to align with the Codegen API (internal APIs)
2016-12-19 11:56:10 -08:00
0e3981afc1 fix(compiler-cli): produce metadata for .d.ts files without metadata (#13526)
Fixes #13307
Fixes #13473
Fixes #13521
2016-12-16 15:33:47 -08:00
e78508507d fix(compiler): do not lex }} when interpolation is disabled (#13531)
* doc(compiler): fix the ICU expander API docs

* test(compiler): add lexer and parser specs

* fix(compiler): do not lex `}}` when interpolation is disabled

fix #13525
2016-12-16 15:33:16 -08:00
a23fa94ca8 fix(common): capitalize first letter of all words in TitleCasePipe (#13511) 2016-12-16 15:24:26 -08:00
4568d5ddac refactor(core): fix typo (#13515)
Closes #13512
2016-12-16 15:21:58 -08:00
c6e893953f fix(upgrade): fix registerForNg1Tests (#13522)
Fix an issue in `registerForNg1Tests`, where it passes a `null` as
`ng1Injector` to `_bootstrapDone`. This causes a "TypeError: Cannot
read property 'get' of null" to be thrown from `_bootstrapDone`.
2016-12-16 15:14:16 -08:00
55dfa1b69d test(forms): refactor integration tests to improve speed (#13500) 2016-12-15 17:07:26 -08:00
0fe3cd9a4c fix(i18n): add a default example to xmb placeholders (#13507)
Otherwise the TC would not be able to load the message
2016-12-15 15:33:42 -08:00
0c19898694 fix(animations): allow players to be destroyed before initialized (#13346)
Closes #13293
Closes #13346
2016-12-15 14:18:57 -08:00
5b6e8ea3ec refactor(compiler): format update (#13506) 2016-12-15 13:54:38 -08:00
732f446ad2 docs(common): fix ngIf example (#13496) 2016-12-15 13:07:36 -08:00
f0e092515c refactor(compiler): don't print stack trace on template parse errors (#13390) 2016-12-15 13:07:12 -08:00
14e785f5b7 fix(build): use bash string comparison operator (#13502) 2016-12-15 12:05:29 -08:00
01d1624884 feature(DEVELOPER.md): add easy way to publish personal snapshot builds (#13469) 2016-12-15 11:19:21 -08:00
33910ddfc9 refactor(compiler): store metadata of top level symbols also in summaries (#13289)
This allows a build using summaries to not need .metadata.json files at all
any more.

Part of #12787
2016-12-15 09:12:40 -08:00
01ca2db6ae docs(changelog): add changelog for 4.0.0-beta.0 2016-12-14 21:54:31 -08:00
6cefccb314 build: bump angular to 4.0.0-beta.0 & tsc-wrapped to 0.5.0 2016-12-14 16:42:44 -08:00
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
54 changed files with 1667 additions and 502 deletions

View File

@ -1,3 +1,12 @@
<a name="4.0.0-beta.1"></a>
# [4.0.0-beta.1](https://github.com/angular/angular/compare/2.4.0-marker...4.0.0-beta.1) (2016-12-22)
### Features
* **upgrade:** support the `$doCheck()` lifecycle hook in `UpgradeComponent` ([#13015](https://github.com/angular/angular/issues/13015)) ([9da4c25](https://github.com/angular/angular/commit/9da4c25))
Note: 4.0.0-beta.1 release also contains all the changes present in the 2.4.0 and the 2.4.1 releases.
<a name="2.4.1"></a>
## [2.4.1](https://github.com/angular/angular/compare/2.4.0...2.4.1) (2016-12-21)
@ -30,6 +39,25 @@
* update to `rxjs@5.0.1` and unpin the rxjs peerDeps via `^5.0.1` ([#13572](https://github.com/angular/angular/issues/13572)) ([8d5da1e](https://github.com/angular/angular/commit/8d5da1e)), closes [#13561](https://github.com/angular/angular/issues/13561) [#13478](https://github.com/angular/angular/issues/13478)
<a name="4.0.0-beta.0"></a>
# [4.0.0-beta.0](https://github.com/angular/angular/compare/2.3.0...4.0.0-beta.0) (2016-12-15)
### Features
* **common:** add a `titlecase` pipe ([#13324](https://github.com/angular/angular/issues/13324)) ([61d7c1e](https://github.com/angular/angular/commit/61d7c1e)), closes [#11436](https://github.com/angular/angular/issues/11436)
* **common:** export NgLocaleLocalization ([#13367](https://github.com/angular/angular/issues/13367)) ([56dce0e](https://github.com/angular/angular/commit/56dce0e)), closes [#11921](https://github.com/angular/angular/issues/11921)
* **compiler:** add id property to i18nMessage ([6dd5201](https://github.com/angular/angular/commit/6dd5201))
* **compiler:** digest methods return i18nMessage id if sets ([562f7a2](https://github.com/angular/angular/commit/562f7a2))
* **forms:** add novalidate by default ([#13092](https://github.com/angular/angular/issues/13092)) ([4c35be3](https://github.com/angular/angular/commit/4c35be3))
* **http:** simplify URLSearchParams creation ([#13338](https://github.com/angular/angular/issues/13338)) ([90c2235](https://github.com/angular/angular/commit/90c2235)), closes [#8858](https://github.com/angular/angular/issues/8858)
* **language-service:** warn when a method isn't called in an event ([#13437](https://github.com/angular/angular/issues/13437)) ([9ec0a4e](https://github.com/angular/angular/commit/9ec0a4e))
* **platform browser:** introduce Meta service ([#12322](https://github.com/angular/angular/issues/12322)) ([72361fb](https://github.com/angular/angular/commit/72361fb))
* **router:** routerLink add tabindex attribute ([#13094](https://github.com/angular/angular/issues/13094)) ([a006c14](https://github.com/angular/angular/commit/a006c14)), closes [#10895](https://github.com/angular/angular/issues/10895)
* **testing:** add overrideTemplate method ([#13372](https://github.com/angular/angular/issues/13372)) ([169ed82](https://github.com/angular/angular/commit/169ed82)), closes [#10685](https://github.com/angular/angular/issues/10685)
* **common** ngIf now supports else; saves condition to local var ([b4db73d](https://github.com/angular/angular/commit/b4db73d)), closes [#13061](https://github.com/angular/angular/issues/13061) [#13297](https://github.com/angular/angular/issues/13297)
Note: 4.0.0-beta.0 release also contains all the changes present in the 2.3.1 release.
<a name="2.3.1"></a>
## [2.3.1](https://github.com/angular/angular/compare/2.3.0...2.3.1) (2016-12-15)
@ -84,7 +112,6 @@ The >=2.3.1 compiler will issue is the following error if it encounters componen
We are adding more tests to our test suite to catch these kinds of problems before we cut a release.
<a name="2.3.0"></a>
# [2.3.0](https://github.com/angular/angular/compare/2.3.0-rc.0...2.3.0) (2016-12-07)

View File

@ -12,9 +12,9 @@
* Entry point for all public APIs of the common package.
*/
export * from './location/index';
export {NgLocalization} from './localization';
export {NgLocaleLocalization, NgLocalization} from './localization';
export {CommonModule} from './common_module';
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 '@angular/core';

View File

@ -6,46 +6,152 @@
* 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
* is removed from the DOM, otherwise a clone of the element is reinserted into the DOM.
* `ngIf` evaluates the `expression` and then renders the `then` or `else` template in its place
* 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">
* <!-- Error message displayed when the errorCount property in the current context is greater
* than 0. -->
* {{errorCount}} errors detected
* </div>
* Hello {{ (userStream|async)?.last }}, {{ (userStream|async)?.first }}!
* ```
*
* 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
*
* Simple form:
* - `<div *ngIf="condition">...</div>`
* - `<div template="ngIf condition">...</div>`
* - `<template [ngIf]="condition"><div>...</div></template>`
*
* 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
*/
@Directive({selector: '[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()
set ngIf(condition: any) {
if (condition && !this._hasView) {
this._hasView = true;
this._viewContainer.createEmbeddedView(this._template);
} else if (!condition && this._hasView) {
this._hasView = false;
this._viewContainer.clear();
this._context.$implicit = condition;
this._updateView();
}
@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._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()
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 {
const plural = getPluralCase(this._locale, value);
const plural = getPluralCase(this.locale, value);
switch (plural) {
case Plural.Zero:
@ -431,4 +431,4 @@ export function getPluralCase(locale: string, nLike: number | string): Plural {
default:
return Plural.Other;
}
}
}

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 {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();
}
}
/**
* Helper method to transform a single word to titlecase.
*
* @stable
*/
function titleCaseWord(word: string) {
if (!word) return word;
return word[0].toUpperCase() + word.substr(1).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.split(/\b/g).map(word => titleCaseWord(word)).join('');
}
}
/**
* 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

@ -12,14 +12,13 @@
* This module provides a set of common Pipes.
*/
import {AsyncPipe} from './async_pipe';
import {LowerCasePipe, TitleCasePipe, UpperCasePipe} from './case_conversion_pipes';
import {DatePipe} from './date_pipe';
import {I18nPluralPipe} from './i18n_plural_pipe';
import {I18nSelectPipe} from './i18n_select_pipe';
import {JsonPipe} from './json_pipe';
import {LowerCasePipe} from './lowercase_pipe';
import {CurrencyPipe, DecimalPipe, PercentPipe} from './number_pipe';
import {SlicePipe} from './slice_pipe';
import {UpperCasePipe} from './uppercase_pipe';
export {
AsyncPipe,
@ -32,9 +31,11 @@ export {
LowerCasePipe,
PercentPipe,
SlicePipe,
TitleCasePipe,
UpperCasePipe
};
/**
* 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,
DecimalPipe,
PercentPipe,
TitleCasePipe,
CurrencyPipe,
DatePipe,
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

@ -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'))
.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,67 @@
/**
* @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 return titlecase for subsequent words',
() => { expect(pipe.transform('one TWO Three fouR')).toEqual('One Two Three Four'); });
it('should support empty strings', () => { expect(pipe.transform('')).toEqual(''); });
it('should persist whitespace',
() => { expect(pipe.transform('one two')).toEqual('One Two'); });
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

@ -9,13 +9,13 @@
import * as i18n from './i18n_ast';
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 {
const visitor = new _SerializerIgnoreIcuExpVisitor();
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_PREFIX = 'i18n-';
const _I18N_COMMENT_PREFIX_REGEXP = /^i18n:?/;
const MEANING_SEPARATOR = '|';
const ID_SEPARATOR = '@@';
/**
* Extract translatable messages from an html AST
@ -77,7 +79,7 @@ class _Visitor implements html.Visitor {
// _VisitorMode.Merge only
private _translations: TranslationBundle;
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[]}) {}
@ -330,15 +332,15 @@ class _Visitor implements html.Visitor {
}
// 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 ||
ast.length == 1 && ast[0] instanceof html.Attribute && !(<html.Attribute>ast[0]).value) {
// Do not create empty messages
return;
}
const [meaning, description] = _splitMeaningAndDesc(meaningAndDesc);
const message = this._createI18nMessage(ast, meaning, description);
const {meaning, description, id} = _parseMessageMeta(msgMeta);
const message = this._createI18nMessage(ast, meaning, description, id);
this._messages.push(message);
return message;
}
@ -368,7 +370,7 @@ class _Visitor implements html.Visitor {
attributes.forEach(attr => {
if (attr.name.startsWith(_I18N_ATTR_PREFIX)) {
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)) {
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);
if (nodes) {
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;
}
function _splitMeaningAndDesc(i18n: string): [string, string] {
if (!i18n) return ['', ''];
const pipeIndex = i18n.indexOf('|');
return pipeIndex == -1 ? ['', i18n] : [i18n.slice(0, pipeIndex), i18n.slice(pipeIndex + 1)];
function _parseMessageMeta(i18n: string): {meaning: string, description: string, id: string} {
if (!i18n) return {meaning: '', description: '', id: ''};
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 meaning
* @param description
* @param id
*/
constructor(
public nodes: Node[], public placeholders: {[phName: string]: string},
public placeholderToMessage: {[phName: string]: Message}, public meaning: string,
public description: string) {}
public description: string, public id: string) {}
}
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
*/
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);
return (nodes: html.Node[], meaning: string, description: string) =>
visitor.toI18nMessage(nodes, meaning, description);
return (nodes: html.Node[], meaning: string, description: string, id: string) =>
visitor.toI18nMessage(nodes, meaning, description, id);
}
class _I18nVisitor implements html.Visitor {
@ -40,7 +40,8 @@ class _I18nVisitor implements html.Visitor {
private _expressionParser: ExpressionParser,
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._icuDepth = 0;
this._placeholderRegistry = new PlaceholderRegistry();
@ -50,7 +51,7 @@ class _I18nVisitor implements html.Visitor {
const i18nodes: i18n.Node[] = html.visitAll(this, nodes, {});
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 {
@ -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
const phName = this._placeholderRegistry.getPlaceholderName('ICU', icu.sourceSpan.toString());
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);
}

View File

@ -987,4 +987,4 @@ function stringifyType(type: any): string {
} else {
return stringify(type);
}
}
}

View File

@ -6,10 +6,23 @@
* 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 {
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', () => {
it('should work on empty strings',
() => { expect(sha1('')).toEqual('da39a3ee5e6b4b0d3255bfef95601890afd80709'); });

View File

@ -20,7 +20,10 @@ export function main() {
describe('elements', () => {
it('should extract from elements', () => {
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(
'<div i18n="m1|d1"><span i18n-title="m2|d2" title="single child">nested</span></div>'))
.toEqual([
[['<ph tag name="START_TAG_SPAN">nested</ph name="CLOSE_TAG_SPAN">'], 'm1', 'd1'],
[['single child'], 'm2', 'd2'],
[['<ph tag name="START_TAG_SPAN">nested</ph name="CLOSE_TAG_SPAN">'], 'm1', 'd1', ''],
[['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', () => {
expect(
extract(
@ -43,10 +80,10 @@ export function main() {
[
'{count, plural, =0 {[<ph tag name="START_PARAGRAPH"></ph name="CLOSE_PARAGRAPH">]}}'
],
'm', 'd'
'm', 'd', ''
],
[['title'], '', ''],
[['desc'], '', ''],
[['title'], '', '', ''],
[['desc'], '', '', ''],
]);
});
@ -55,7 +92,7 @@ export function main() {
it('should ignore implicit elements in translatable elements', () => {
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', () => {
expect(extract(`<!-- i18n: meaning1|desc1 -->message1<!-- /i18n -->
<!-- i18n: desc2 -->message2<!-- /i18n -->
<!-- i18n -->message3<!-- /i18n -->`))
<!-- i18n -->message3<!-- /i18n -->
<!-- i18n: meaning4|desc4@@id4 -->message4<!-- /i18n -->
<!-- i18n: @@id5 -->message5<!-- /i18n -->`))
.toEqual([
[['message1'], 'meaning1', 'desc1'],
[['message2'], '', 'desc2'],
[['message3'], '', ''],
[['message1'], 'meaning1', 'desc1', ''], [['message2'], '', 'desc2', ''],
[['message3'], '', '', ''], [['message4'], 'meaning4', 'desc4', 'id4'],
[['message5'], '', '', 'id5']
]);
});
it('should ignore implicit elements in blocks', () => {
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">]}}'
],
'', ''
'', '', ''
],
[
[
@ -98,15 +137,15 @@ export function main() {
' name="START_TAG_SPAN">html</ph name="CLOSE_TAG_SPAN">]}}</ph>',
'[<ph name="INTERPOLATION">interp</ph>]'
],
'', ''
'', '', ''
],
]);
});
it('should ignore other comments', () => {
expect(extract(`<!-- i18n: meaning1|desc1 --><!-- other -->message1<!-- /i18n -->`))
expect(extract(`<!-- i18n: meaning1|desc1@@id1 --><!-- other -->message1<!-- /i18n -->`))
.toEqual([
[['message1'], 'meaning1', 'desc1'],
[['message1'], 'meaning1', 'desc1', 'id1'],
]);
});
@ -118,34 +157,37 @@ export function main() {
it('should extract ICU messages from translatable elements', () => {
// single message when ICU is the only children
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
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
expect(extract('<div i18n="m|d">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]}}'], '', ''],
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',
'i'
],
[['{count, plural, =0 {[text]}}'], '', '', ''],
]);
});
it('should extract ICU messages from translatable block', () => {
// single message when ICU is the only children
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
expect(extract('<!-- i18n:m|d -->before{count, plural, =0 {text}}after<!-- /i18n -->'))
.toEqual([
[['{count, plural, =0 {[text]}}'], '', ''],
[['{count, plural, =0 {[text]}}'], '', '', ''],
[
['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', () => {
expect(extract('<div i18n="m|d">{count, plural, =0 { {sex, select, male {m}} }}</div>'))
.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', () => {
expect(
extract(
'<div i18n="m|d">{count, plural, =0 { {sex, select, male {<p>ignore</p>}} }}</div>',
['p']))
expect(extract(
'<div i18n="m|d@@i">{count, plural, =0 { {sex, select, male {<p>ignore</p>}}' +
' }}</div>',
['p']))
.toEqual([[
[
'{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', () => {
it('should extract from attributes outside of translatable sections', () => {
expect(extract('<div i18n-title="m|d" title="msg"></div>')).toEqual([
[['msg'], 'm', 'd'],
expect(extract('<div i18n-title="m|d@@i" title="msg"></div>')).toEqual([
[['msg'], 'm', 'd', 'i'],
]);
});
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' +
' name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'],
'', ''
'', '', ''
],
[['msg'], 'm', 'd'],
[['msg'], 'm', 'd', 'i'],
]);
});
it('should extract from attributes in translatable blocks', () => {
expect(extract('<!-- i18n --><p><b i18n-title="m|d" title="msg"></b></p><!-- /i18n -->'))
.toEqual([
[['msg'], 'm', 'd'],
[['msg'], 'm', 'd', ''],
[
['<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph' +
' name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'],
'', ''
'', '', ''
],
]);
});
it('should extract from attributes in translatable ICUs', () => {
expect(
extract(
'<!-- i18n -->{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}<!-- /i18n -->'))
expect(extract(`<!-- i18n -->{count, plural, =0 {<p><b i18n-title="m|d@@i"
title="msg"></b></p>}}<!-- /i18n -->`))
.toEqual([
[['msg'], 'm', 'd'],
[['msg'], 'm', 'd', 'i'],
[
[
'{count, plural, =0 {[<ph tag name="START_PARAGRAPH"><ph tag' +
' 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', () => {
expect(extract('{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}'))
.toEqual([
[['msg'], 'm', 'd'],
[['msg'], 'm', 'd', ''],
]);
});
@ -239,7 +280,7 @@ export function main() {
describe('implicit elements', () => {
it('should extract from implicit elements', () => {
expect(extract('<b>bold</b><i>italic</i>', ['b'])).toEqual([
[['bold'], '', ''],
[['bold'], '', '', ''],
]);
});
@ -251,7 +292,7 @@ export function main() {
}).not.toThrow();
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', () => {
expect(extract('<b title="bb">bold</b><i title="ii">italic</i>', [], {'b': ['title']}))
.toEqual([
[['bb'], '', ''],
[['bb'], '', '', ''],
]);
});
});
@ -433,7 +474,7 @@ function extract(
// clang-format off
// https://github.com/angular/clang-format/issues/35
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
}

View File

@ -59,14 +59,17 @@ export function main() {
tb.detectChanges();
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-17')).nativeElement).toHaveText('un');
cmp.count = 2;
tb.detectChanges();
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-17')).nativeElement).toHaveText('deux');
cmp.count = 3;
tb.detectChanges();
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-17')).nativeElement).toHaveText('beaucoup');
cmp.sex = 'm';
cmp.sexB = 'f';
@ -90,8 +93,8 @@ export function main() {
.toEqual('<h1 id="i18n-12">Balises dans les commentaires html</h1>');
expectHtml(el, '#i18n-13')
.toBe('<div id="i18n-13" title="dans une section traductible"></div>');
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 -->
<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 {
@ -182,6 +187,9 @@ const XTB = `
<ph name="START_TAG_DIV_1"/><ph name="ICU"/><ph name="CLOSE_TAG_DIV"></ph>
</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>`;
// 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>
</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>
`;

View File

@ -18,6 +18,8 @@ const HTML = `
<p i18n-title title="translatable attribute">not translatable</p>
<p i18n>translatable element <b>with placeholders</b> {{ interpolation}}</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>
`;
@ -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="meaning">m</note>
</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">
<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/>
@ -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="meaning">m</note>
</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">
<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>
@ -107,6 +129,8 @@ export function main(): void {
'ec1d033f2436133c14ab038286c4f5df4697484a':
'<ph name="INTERPOLATION"/> footnemele elbatalsnart <ph name="START_BOLD_TEXT"/>sredlohecalp htiw<ph name="CLOSE_BOLD_TEXT"/>',
'db3e0a6a5a96481f60aec61d98c3eecddef5ac23': 'oof',
'i': 'toto',
'bar': 'tata',
'd7fa2d59aaedcaa5309f13028c59af8c85b8c49d':
'<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>
<!-- i18n -->{ count, plural, =0 {<p>test</p>}}<!-- /i18n -->
<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>`;
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"><ex>INTERPOLATION</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="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>
</messagebundle>
`;

View File

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

View File

@ -135,6 +135,11 @@ export class TestBed implements Injector {
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) {
return getTestBed().get(token, notFoundValue);
}

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 {$, ExpectedConditions, browser, by, element} from 'protractor';
import {verifyNoBrowserErrors} from '../../../../_common/e2e_util';
function waitForElement(selector: string) {
const EC = ExpectedConditions;
// Waits for the element with id 'abc' to be present on the dom.
browser.wait(EC.presenceOf($(selector)), 20000);
}
describe('ngIf', () => {
const URL = 'common/ngIf/ts/';
afterEach(verifyNoBrowserErrors);
describe('ng-if-simple', () => {
let comp = 'ng-if-simple';
it('should hide/show content', () => {
browser.get(URL);
waitForElement(comp);
expect(element.all(by.css(comp)).get(0).getText()).toEqual('hide show = true\nText to show');
element(by.css(comp + ' button')).click();
expect(element.all(by.css(comp)).get(0).getText()).toEqual('show show = false');
});
});
describe('ng-if-else', () => {
let comp = 'ng-if-else';
it('should hide/show content', () => {
browser.get(URL);
waitForElement(comp);
expect(element.all(by.css(comp)).get(0).getText()).toEqual('hide show = true\nText to show');
element(by.css(comp + ' button')).click();
expect(element.all(by.css(comp)).get(0).getText())
.toEqual('show show = false\nAlternate text while primary text is hidden');
});
});
describe('ng-if-then-else', () => {
let comp = 'ng-if-then-else';
it('should hide/show content', () => {
browser.get(URL);
waitForElement(comp);
expect(element.all(by.css(comp)).get(0).getText())
.toEqual('hide Switch Primary show = true\nPrimary text to show');
element.all(by.css(comp + ' button')).get(1).click();
expect(element.all(by.css(comp)).get(0).getText())
.toEqual('hide Switch Primary show = true\nSecondary text to show');
element.all(by.css(comp + ' button')).get(0).click();
expect(element.all(by.css(comp)).get(0).getText())
.toEqual('show Switch Primary show = false\nAlternate text while primary text is hidden');
});
});
describe('ng-if-let', () => {
let comp = 'ng-if-let';
it('should hide/show content', () => {
browser.get(URL);
waitForElement(comp);
expect(element.all(by.css(comp)).get(0).getText())
.toEqual('Next User\nWaiting... (user is null)');
element(by.css(comp + ' button')).click();
expect(element.all(by.css(comp)).get(0).getText()).toEqual('Next User\nHello Smith, John!');
});
});
});

View File

@ -0,0 +1,128 @@
/**
* @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, OnInit, TemplateRef, ViewChild} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {Subject} from 'rxjs/Subject';
// #docregion NgIfSimple
@Component({
selector: 'ng-if-simple',
template: `
<button (click)="show = !show">{{show ? 'hide' : 'show'}}</button>
show = {{show}}
<br>
<div *ngIf="show">Text to show</div>
`
})
class NgIfSimple {
show: boolean = true;
}
// #enddocregion
// #docregion NgIfElse
@Component({
selector: 'ng-if-else',
template: `
<button (click)="show = !show">{{show ? 'hide' : 'show'}}</button>
show = {{show}}
<br>
<div *ngIf="show; else elseBlock">Text to show</div>
<template #elseBlock>Alternate text while primary text is hidden</template>
`
})
class NgIfElse {
show: boolean = true;
}
// #enddocregion
// #docregion NgIfThenElse
@Component({
selector: 'ng-if-then-else',
template: `
<button (click)="show = !show">{{show ? 'hide' : 'show'}}</button>
<button (click)="switchPrimary()">Switch Primary</button>
show = {{show}}
<br>
<div *ngIf="show; then thenBlock; else elseBlock">this is ignored</div>
<template #primaryBlock>Primary text to show</template>
<template #secondaryBlock>Secondary text to show</template>
<template #elseBlock>Alternate text while primary text is hidden</template>
`
})
class NgIfThenElse implements OnInit {
thenBlock: TemplateRef<any> = null;
show: boolean = true;
@ViewChild('primaryBlock')
primaryBlock: TemplateRef<any> = null;
@ViewChild('secondaryBlock')
secondaryBlock: TemplateRef<any> = null;
switchPrimary() {
this.thenBlock = this.thenBlock === this.primaryBlock ? this.secondaryBlock : this.primaryBlock;
}
ngOnInit() { this.thenBlock = this.primaryBlock; }
}
// #enddocregion
// #docregion NgIfLet
@Component({
selector: 'ng-if-let',
template: `
<button (click)="nextUser()">Next User</button>
<br>
<div *ngIf="userObservable | async; else loading; let user">
Hello {{user.last}}, {{user.first}}!
</div>
<template #loading let-user>Waiting... (user is {{user|json}})</template>
`
})
class NgIfLet {
userObservable = new Subject<{first: string, last: string}>();
first = ['John', 'Mike', 'Mary', 'Bob'];
firstIndex = 0;
last = ['Smith', 'Novotny', 'Angular'];
lastIndex = 0;
nextUser() {
let first = this.first[this.firstIndex++];
if (this.firstIndex >= this.first.length) this.firstIndex = 0;
let last = this.last[this.lastIndex++];
if (this.lastIndex >= this.last.length) this.lastIndex = 0;
this.userObservable.next({first, last});
}
}
// #enddocregion
@Component({
selector: 'example-app',
template: `
<ng-if-simple></ng-if-simple>
<hr>
<ng-if-else></ng-if-else>
<hr>
<ng-if-then-else></ng-if-then-else>
<hr>
<ng-if-let></ng-if-let>
<hr>
`
})
class ExampleApp {
}
@NgModule({
imports: [BrowserModule],
declarations: [ExampleApp, NgIfSimple, NgIfElse, NgIfThenElse, NgIfLet],
bootstrap: [ExampleApp]
})
export class AppModule {
}

View File

@ -14,6 +14,7 @@ import {NgControlStatus, NgControlStatusGroup} from './directives/ng_control_sta
import {NgForm} from './directives/ng_form';
import {NgModel} from './directives/ng_model';
import {NgModelGroup} from './directives/ng_model_group';
import {NgNovalidate} from './directives/ng_novalidate_directive';
import {NumberValueAccessor} from './directives/number_value_accessor';
import {RadioControlValueAccessor} from './directives/radio_control_value_accessor';
import {RangeValueAccessor} from './directives/range_value_accessor';
@ -44,6 +45,7 @@ export {NgSelectOption, SelectControlValueAccessor} from './directives/select_co
export {NgSelectMultipleOption, SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor';
export const SHARED_FORM_DIRECTIVES: Type<any>[] = [
NgNovalidate,
NgSelectOption,
NgSelectMultipleOption,
DefaultValueAccessor,

View File

@ -0,0 +1,16 @@
/**
* @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 {Directive} from '@angular/core';
@Directive({
selector: 'form:not([ngNoForm])',
host: {'novalidate': ''},
})
export class NgNovalidate {
}

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Directive, EventEmitter, Input, Output, forwardRef} from '@angular/core';
import {TestBed, fakeAsync, tick} from '@angular/core/testing';
import {Component, Directive, EventEmitter, Input, Output, Type, forwardRef} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
import {AbstractControl, ControlValueAccessor, FormArray, FormControl, FormGroup, FormGroupDirective, FormsModule, NG_ASYNC_VALIDATORS, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgControl, ReactiveFormsModule, Validator, Validators} from '@angular/forms';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -16,38 +16,15 @@ import {dispatchEvent} from '@angular/platform-browser/testing/browser_util';
export function main() {
describe('reactive forms integration tests', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FormsModule, ReactiveFormsModule],
declarations: [
FormControlComp,
FormGroupComp,
FormArrayComp,
FormArrayNestedGroup,
FormControlNameSelect,
FormControlNumberInput,
FormControlRangeInput,
FormControlRadioButtons,
WrappedValue,
WrappedValueForm,
MyInput,
MyInputForm,
FormGroupNgModel,
FormControlNgModel,
LoginIsEmptyValidator,
LoginIsEmptyWrapper,
ValidationBindingsForm,
UniqLoginValidator,
UniqLoginWrapper,
NestedFormGroupComp,
FormControlCheckboxRequiredValidator,
]
});
});
function initTest<T>(component: Type<T>, ...directives: Type<any>[]): ComponentFixture<T> {
TestBed.configureTestingModule(
{declarations: [component, ...directives], imports: [FormsModule, ReactiveFormsModule]});
return TestBed.createComponent(component);
}
describe('basic functionality', () => {
it('should work with single controls', () => {
const fixture = TestBed.createComponent(FormControlComp);
const fixture = initTest(FormControlComp);
const control = new FormControl('old value');
fixture.componentInstance.control = control;
fixture.detectChanges();
@ -64,7 +41,7 @@ export function main() {
});
it('should work with formGroups (model -> view)', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
fixture.componentInstance.form = new FormGroup({'login': new FormControl('loginValue')});
fixture.detectChanges();
@ -72,8 +49,17 @@ export function main() {
expect(input.nativeElement.value).toEqual('loginValue');
});
it('should add novalidate by default to form', () => {
const fixture = initTest(FormGroupComp);
fixture.componentInstance.form = new FormGroup({'login': new FormControl('loginValue')});
fixture.detectChanges();
const form = fixture.debugElement.query(By.css('form'));
expect(form.nativeElement.getAttribute('novalidate')).toEqual('');
});
it('work with formGroups (view -> model)', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
const form = new FormGroup({'login': new FormControl('oldValue')});
fixture.componentInstance.form = form;
fixture.detectChanges();
@ -90,7 +76,7 @@ export function main() {
describe('rebound form groups', () => {
it('should update DOM elements initially', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
fixture.componentInstance.form = new FormGroup({'login': new FormControl('oldValue')});
fixture.detectChanges();
@ -102,7 +88,7 @@ export function main() {
});
it('should update model when UI changes', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
fixture.componentInstance.form = new FormGroup({'login': new FormControl('oldValue')});
fixture.detectChanges();
@ -123,7 +109,7 @@ export function main() {
});
it('should work with radio buttons when reusing control', () => {
const fixture = TestBed.createComponent(FormControlRadioButtons);
const fixture = initTest(FormControlRadioButtons);
const food = new FormControl('chicken');
fixture.componentInstance.form =
new FormGroup({'food': food, 'drink': new FormControl('')});
@ -141,7 +127,7 @@ export function main() {
});
it('should update nested form group model when UI changes', () => {
const fixture = TestBed.createComponent(NestedFormGroupComp);
const fixture = initTest(NestedFormGroupComp);
fixture.componentInstance.form = new FormGroup(
{'signin': new FormGroup({'login': new FormControl(), 'password': new FormControl()})});
fixture.detectChanges();
@ -169,7 +155,7 @@ export function main() {
});
it('should pick up dir validators from form controls', () => {
const fixture = TestBed.createComponent(LoginIsEmptyWrapper);
const fixture = initTest(LoginIsEmptyWrapper, LoginIsEmptyValidator);
const form = new FormGroup({
'login': new FormControl(''),
'min': new FormControl(''),
@ -193,7 +179,7 @@ export function main() {
});
it('should pick up dir validators from nested form groups', () => {
const fixture = TestBed.createComponent(NestedFormGroupComp);
const fixture = initTest(NestedFormGroupComp, LoginIsEmptyValidator);
const form = new FormGroup({
'signin':
new FormGroup({'login': new FormControl(''), 'password': new FormControl('')})
@ -213,7 +199,7 @@ export function main() {
});
it('should strip named controls that are not found', () => {
const fixture = TestBed.createComponent(NestedFormGroupComp);
const fixture = initTest(NestedFormGroupComp, LoginIsEmptyValidator);
const form = new FormGroup({
'signin':
new FormGroup({'login': new FormControl(''), 'password': new FormControl('')})
@ -239,7 +225,7 @@ export function main() {
});
it('should strip array controls that are not found', () => {
const fixture = TestBed.createComponent(FormArrayComp);
const fixture = initTest(FormArrayComp);
const cityArray = new FormArray([new FormControl('SF'), new FormControl('NY')]);
const form = new FormGroup({cities: cityArray});
fixture.componentInstance.form = form;
@ -268,7 +254,7 @@ export function main() {
it('should attach dir to control when leaf control changes', () => {
const form = new FormGroup({'login': new FormControl('oldValue')});
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
fixture.componentInstance.form = form;
fixture.detectChanges();
@ -291,7 +277,7 @@ export function main() {
});
it('should attach dirs to all child controls when group control changes', () => {
const fixture = TestBed.createComponent(NestedFormGroupComp);
const fixture = initTest(NestedFormGroupComp, LoginIsEmptyValidator);
const form = new FormGroup({
signin: new FormGroup(
{login: new FormControl('oldLogin'), password: new FormControl('oldPassword')})
@ -323,7 +309,7 @@ export function main() {
});
it('should attach dirs to all present child controls when array control changes', () => {
const fixture = TestBed.createComponent(FormArrayComp);
const fixture = initTest(FormArrayComp);
const cityArray = new FormArray([new FormControl('SF'), new FormControl('NY')]);
const form = new FormGroup({cities: cityArray});
fixture.componentInstance.form = form;
@ -354,7 +340,7 @@ export function main() {
describe('form arrays', () => {
it('should support form arrays', () => {
const fixture = TestBed.createComponent(FormArrayComp);
const fixture = initTest(FormArrayComp);
const cityArray = new FormArray([new FormControl('SF'), new FormControl('NY')]);
const form = new FormGroup({cities: cityArray});
fixture.componentInstance.form = form;
@ -377,7 +363,7 @@ export function main() {
});
it('should support pushing new controls to form arrays', () => {
const fixture = TestBed.createComponent(FormArrayComp);
const fixture = initTest(FormArrayComp);
const cityArray = new FormArray([new FormControl('SF'), new FormControl('NY')]);
const form = new FormGroup({cities: cityArray});
fixture.componentInstance.form = form;
@ -393,7 +379,7 @@ export function main() {
});
it('should support form groups nested in form arrays', () => {
const fixture = TestBed.createComponent(FormArrayNestedGroup);
const fixture = initTest(FormArrayNestedGroup);
const cityArray = new FormArray([
new FormGroup({town: new FormControl('SF'), state: new FormControl('CA')}),
new FormGroup({town: new FormControl('NY'), state: new FormControl('NY')})
@ -425,7 +411,7 @@ export function main() {
describe('programmatic changes', () => {
it('should update the value in the DOM when setValue() is called', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
const login = new FormControl('oldValue');
const form = new FormGroup({'login': login});
fixture.componentInstance.form = form;
@ -441,7 +427,7 @@ export function main() {
describe('disabled controls', () => {
it('should add disabled attribute to an individual control when instantiated as disabled',
() => {
const fixture = TestBed.createComponent(FormControlComp);
const fixture = initTest(FormControlComp);
const control = new FormControl({value: 'some value', disabled: true});
fixture.componentInstance.control = control;
fixture.detectChanges();
@ -455,7 +441,7 @@ export function main() {
});
it('should add disabled attribute to formControlName when instantiated as disabled', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
const control = new FormControl({value: 'some value', disabled: true});
fixture.componentInstance.form = new FormGroup({login: control});
fixture.componentInstance.control = control;
@ -471,7 +457,7 @@ export function main() {
it('should add disabled attribute to an individual control when disable() is called',
() => {
const fixture = TestBed.createComponent(FormControlComp);
const fixture = initTest(FormControlComp);
const control = new FormControl('some value');
fixture.componentInstance.control = control;
fixture.detectChanges();
@ -489,7 +475,7 @@ export function main() {
it('should add disabled attribute to child controls when disable() is called on group',
() => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
const form = new FormGroup({'login': new FormControl('login')});
fixture.componentInstance.form = form;
fixture.detectChanges();
@ -507,7 +493,7 @@ export function main() {
it('should not add disabled attribute to custom controls when disable() is called', () => {
const fixture = TestBed.createComponent(MyInputForm);
const fixture = initTest(MyInputForm, MyInput);
const control = new FormControl('some value');
fixture.componentInstance.form = new FormGroup({login: control});
fixture.detectChanges();
@ -526,7 +512,7 @@ export function main() {
describe('user input', () => {
it('should mark controls as touched after interacting with the DOM control', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
const login = new FormControl('oldValue');
const form = new FormGroup({'login': login});
fixture.componentInstance.form = form;
@ -544,7 +530,7 @@ export function main() {
describe('submit and reset events', () => {
it('should emit ngSubmit event with the original submit event on submit', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
fixture.componentInstance.form = new FormGroup({'login': new FormControl('loginValue')});
fixture.componentInstance.event = null;
fixture.detectChanges();
@ -557,7 +543,7 @@ export function main() {
});
it('should mark formGroup as submitted on submit event', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
fixture.componentInstance.form = new FormGroup({'login': new FormControl('loginValue')});
fixture.detectChanges();
@ -572,7 +558,7 @@ export function main() {
});
it('should set value in UI when form resets to that value programmatically', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
const login = new FormControl('some value');
const form = new FormGroup({'login': login});
fixture.componentInstance.form = form;
@ -586,7 +572,7 @@ export function main() {
});
it('should clear value in UI when form resets programmatically', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
const login = new FormControl('some value');
const form = new FormGroup({'login': login});
fixture.componentInstance.form = form;
@ -604,7 +590,7 @@ export function main() {
describe('value changes and status changes', () => {
it('should mark controls as dirty before emitting a value change event', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
const login = new FormControl('oldValue');
fixture.componentInstance.form = new FormGroup({'login': login});
fixture.detectChanges();
@ -619,7 +605,7 @@ export function main() {
it('should mark control as pristine before emitting a value change event when resetting ',
() => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
const login = new FormControl('oldValue');
const form = new FormGroup({'login': login});
fixture.componentInstance.form = form;
@ -640,7 +626,7 @@ export function main() {
describe('setting status classes', () => {
it('should work with single fields', () => {
const fixture = TestBed.createComponent(FormControlComp);
const fixture = initTest(FormControlComp);
const control = new FormControl('', Validators.required);
fixture.componentInstance.control = control;
fixture.detectChanges();
@ -661,7 +647,7 @@ export function main() {
});
it('should work with single fields and async validators', fakeAsync(() => {
const fixture = TestBed.createComponent(FormControlComp);
const fixture = initTest(FormControlComp);
const control = new FormControl('', null, uniqLoginAsyncValidator('good'));
fixture.debugElement.componentInstance.control = control;
fixture.detectChanges();
@ -682,7 +668,7 @@ export function main() {
}));
it('should work with single fields that combines async and sync validators', fakeAsync(() => {
const fixture = TestBed.createComponent(FormControlComp);
const fixture = initTest(FormControlComp);
const control =
new FormControl('', Validators.required, uniqLoginAsyncValidator('good'));
fixture.debugElement.componentInstance.control = control;
@ -715,7 +701,7 @@ export function main() {
}));
it('should work with single fields in parent forms', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
const form = new FormGroup({'login': new FormControl('', Validators.required)});
fixture.componentInstance.form = form;
fixture.detectChanges();
@ -736,7 +722,7 @@ export function main() {
});
it('should work with formGroup', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
const form = new FormGroup({'login': new FormControl('', Validators.required)});
fixture.componentInstance.form = form;
fixture.detectChanges();
@ -765,7 +751,7 @@ export function main() {
it('should support <input> without type', () => {
TestBed.overrideComponent(
FormControlComp, {set: {template: `<input [formControl]="control">`}});
const fixture = TestBed.createComponent(FormControlComp);
const fixture = initTest(FormControlComp);
const control = new FormControl('old');
fixture.componentInstance.control = control;
fixture.detectChanges();
@ -782,7 +768,7 @@ export function main() {
});
it('should support <input type=text>', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
const form = new FormGroup({'login': new FormControl('old')});
fixture.componentInstance.form = form;
fixture.detectChanges();
@ -799,7 +785,7 @@ export function main() {
});
it('should ignore the change event for <input type=text>', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
const form = new FormGroup({'login': new FormControl('oldValue')});
fixture.componentInstance.form = form;
fixture.detectChanges();
@ -814,7 +800,7 @@ export function main() {
it('should support <textarea>', () => {
TestBed.overrideComponent(
FormControlComp, {set: {template: `<textarea [formControl]="control"></textarea>`}});
const fixture = TestBed.createComponent(FormControlComp);
const fixture = initTest(FormControlComp);
const control = new FormControl('old');
fixture.componentInstance.control = control;
fixture.detectChanges();
@ -833,7 +819,7 @@ export function main() {
it('should support <type=checkbox>', () => {
TestBed.overrideComponent(
FormControlComp, {set: {template: `<input type="checkbox" [formControl]="control">`}});
const fixture = TestBed.createComponent(FormControlComp);
const fixture = initTest(FormControlComp);
const control = new FormControl(true);
fixture.componentInstance.control = control;
fixture.detectChanges();
@ -850,7 +836,7 @@ export function main() {
});
it('should support <select>', () => {
const fixture = TestBed.createComponent(FormControlNameSelect);
const fixture = initTest(FormControlNameSelect);
fixture.detectChanges();
// model -> view
@ -870,7 +856,7 @@ export function main() {
describe('should support <type=number>', () => {
it('with basic use case', () => {
const fixture = TestBed.createComponent(FormControlNumberInput);
const fixture = initTest(FormControlNumberInput);
const control = new FormControl(10);
fixture.componentInstance.control = control;
fixture.detectChanges();
@ -887,7 +873,7 @@ export function main() {
});
it('when value is cleared in the UI', () => {
const fixture = TestBed.createComponent(FormControlNumberInput);
const fixture = initTest(FormControlNumberInput);
const control = new FormControl(10, Validators.required);
fixture.componentInstance.control = control;
fixture.detectChanges();
@ -907,7 +893,7 @@ export function main() {
});
it('when value is cleared programmatically', () => {
const fixture = TestBed.createComponent(FormControlNumberInput);
const fixture = initTest(FormControlNumberInput);
const control = new FormControl(10);
fixture.componentInstance.control = control;
fixture.detectChanges();
@ -922,7 +908,7 @@ export function main() {
describe('should support <type=radio>', () => {
it('should support basic functionality', () => {
const fixture = TestBed.createComponent(FormControlRadioButtons);
const fixture = initTest(FormControlRadioButtons);
const form =
new FormGroup({'food': new FormControl('fish'), 'drink': new FormControl('sprite')});
fixture.componentInstance.form = form;
@ -949,7 +935,7 @@ export function main() {
});
it('should support an initial undefined value', () => {
const fixture = TestBed.createComponent(FormControlRadioButtons);
const fixture = initTest(FormControlRadioButtons);
const form = new FormGroup({'food': new FormControl(), 'drink': new FormControl()});
fixture.componentInstance.form = form;
fixture.detectChanges();
@ -960,7 +946,7 @@ export function main() {
});
it('should reset properly', () => {
const fixture = TestBed.createComponent(FormControlRadioButtons);
const fixture = initTest(FormControlRadioButtons);
const form =
new FormGroup({'food': new FormControl('fish'), 'drink': new FormControl('sprite')});
fixture.componentInstance.form = form;
@ -975,7 +961,7 @@ export function main() {
});
it('should set value to null and undefined properly', () => {
const fixture = TestBed.createComponent(FormControlRadioButtons);
const fixture = initTest(FormControlRadioButtons);
const form = new FormGroup(
{'food': new FormControl('chicken'), 'drink': new FormControl('sprite')});
fixture.componentInstance.form = form;
@ -996,7 +982,7 @@ export function main() {
});
it('should use formControlName to group radio buttons when name is absent', () => {
const fixture = TestBed.createComponent(FormControlRadioButtons);
const fixture = initTest(FormControlRadioButtons);
const foodCtrl = new FormControl('fish');
const drinkCtrl = new FormControl('sprite');
fixture.componentInstance.form = new FormGroup({'food': foodCtrl, 'drink': drinkCtrl});
@ -1028,7 +1014,7 @@ export function main() {
});
it('should support removing controls from <type=radio>', () => {
const fixture = TestBed.createComponent(FormControlRadioButtons);
const fixture = initTest(FormControlRadioButtons);
const showRadio = new FormControl('yes');
const form =
new FormGroup({'food': new FormControl('fish'), 'drink': new FormControl('sprite')});
@ -1062,7 +1048,7 @@ export function main() {
`
}
});
const fixture = TestBed.createComponent(FormControlRadioButtons);
const fixture = initTest(FormControlRadioButtons);
const form = new FormGroup({
food: new FormControl('fish'),
nested: new FormGroup({food: new FormControl('fish')})
@ -1091,7 +1077,7 @@ export function main() {
});
it('should disable all radio buttons when disable() is called', () => {
const fixture = TestBed.createComponent(FormControlRadioButtons);
const fixture = initTest(FormControlRadioButtons);
const form =
new FormGroup({food: new FormControl('fish'), drink: new FormControl('cola')});
fixture.componentInstance.form = form;
@ -1123,7 +1109,7 @@ export function main() {
});
it('should disable all radio buttons when initially disabled', () => {
const fixture = TestBed.createComponent(FormControlRadioButtons);
const fixture = initTest(FormControlRadioButtons);
const form = new FormGroup({
food: new FormControl({value: 'fish', disabled: true}),
drink: new FormControl('cola')
@ -1142,7 +1128,7 @@ export function main() {
describe('should support <type=range>', () => {
it('with basic use case', () => {
const fixture = TestBed.createComponent(FormControlRangeInput);
const fixture = initTest(FormControlRangeInput);
const control = new FormControl(10);
fixture.componentInstance.control = control;
fixture.detectChanges();
@ -1159,7 +1145,7 @@ export function main() {
});
it('when value is cleared in the UI', () => {
const fixture = TestBed.createComponent(FormControlNumberInput);
const fixture = initTest(FormControlNumberInput);
const control = new FormControl(10, Validators.required);
fixture.componentInstance.control = control;
fixture.detectChanges();
@ -1179,7 +1165,7 @@ export function main() {
});
it('when value is cleared programmatically', () => {
const fixture = TestBed.createComponent(FormControlNumberInput);
const fixture = initTest(FormControlNumberInput);
const control = new FormControl(10);
fixture.componentInstance.control = control;
fixture.detectChanges();
@ -1193,7 +1179,7 @@ export function main() {
describe('custom value accessors', () => {
it('should support basic functionality', () => {
const fixture = TestBed.createComponent(WrappedValueForm);
const fixture = initTest(WrappedValueForm, WrappedValue);
const form = new FormGroup({'login': new FormControl('aa')});
fixture.componentInstance.form = form;
fixture.detectChanges();
@ -1216,7 +1202,7 @@ export function main() {
it('should support non builtin input elements that fire a change event without a \'target\' property',
() => {
const fixture = TestBed.createComponent(MyInputForm);
const fixture = initTest(MyInputForm, MyInput);
fixture.componentInstance.form = new FormGroup({'login': new FormControl('aa')});
fixture.detectChanges();
@ -1231,7 +1217,7 @@ export function main() {
});
it('should support custom accessors without setDisabledState - formControlName', () => {
const fixture = TestBed.createComponent(WrappedValueForm);
const fixture = initTest(WrappedValueForm, WrappedValue);
fixture.componentInstance.form = new FormGroup({
'login': new FormControl({value: 'aa', disabled: true}),
});
@ -1245,7 +1231,7 @@ export function main() {
TestBed.overrideComponent(
FormControlComp,
{set: {template: `<input type="text" [formControl]="control" wrapped-value>`}});
const fixture = TestBed.createComponent(FormControlComp);
const fixture = initTest(FormControlComp);
fixture.componentInstance.control = new FormControl({value: 'aa', disabled: true});
fixture.detectChanges();
expect(fixture.componentInstance.control.status).toEqual('DISABLED');
@ -1258,7 +1244,7 @@ export function main() {
describe('ngModel interactions', () => {
it('should support ngModel for complex forms', fakeAsync(() => {
const fixture = TestBed.createComponent(FormGroupNgModel);
const fixture = initTest(FormGroupNgModel);
fixture.componentInstance.form = new FormGroup({'login': new FormControl('')});
fixture.componentInstance.login = 'oldValue';
fixture.detectChanges();
@ -1275,7 +1261,7 @@ export function main() {
}));
it('should support ngModel for single fields', fakeAsync(() => {
const fixture = TestBed.createComponent(FormControlNgModel);
const fixture = initTest(FormControlNgModel);
fixture.componentInstance.control = new FormControl('');
fixture.componentInstance.login = 'oldValue';
fixture.detectChanges();
@ -1292,7 +1278,7 @@ export function main() {
}));
it('should not update the view when the value initially came from the view', fakeAsync(() => {
const fixture = TestBed.createComponent(FormControlNgModel);
const fixture = initTest(FormControlNgModel);
fixture.componentInstance.control = new FormControl('');
fixture.detectChanges();
tick();
@ -1313,7 +1299,7 @@ export function main() {
describe('validations', () => {
it('required validator should validate checkbox', () => {
const fixture = TestBed.createComponent(FormControlCheckboxRequiredValidator);
const fixture = initTest(FormControlCheckboxRequiredValidator);
const control = new FormControl(false, Validators.requiredTrue);
fixture.componentInstance.control = control;
fixture.detectChanges();
@ -1331,7 +1317,7 @@ export function main() {
});
it('should use sync validators defined in html', () => {
const fixture = TestBed.createComponent(LoginIsEmptyWrapper);
const fixture = initTest(LoginIsEmptyWrapper, LoginIsEmptyValidator);
const form = new FormGroup({
'login': new FormControl(''),
'min': new FormControl(''),
@ -1376,7 +1362,7 @@ export function main() {
});
it('should use sync validators using bindings', () => {
const fixture = TestBed.createComponent(ValidationBindingsForm);
const fixture = initTest(ValidationBindingsForm);
const form = new FormGroup({
'login': new FormControl(''),
'min': new FormControl(''),
@ -1424,7 +1410,7 @@ export function main() {
});
it('changes on bound properties should change the validation state of the form', () => {
const fixture = TestBed.createComponent(ValidationBindingsForm);
const fixture = initTest(ValidationBindingsForm);
const form = new FormGroup({
'login': new FormControl(''),
'min': new FormControl(''),
@ -1499,7 +1485,7 @@ export function main() {
});
it('should support rebound controls with rebound validators', () => {
const fixture = TestBed.createComponent(ValidationBindingsForm);
const fixture = initTest(ValidationBindingsForm);
const form = new FormGroup({
'login': new FormControl(''),
'min': new FormControl(''),
@ -1536,7 +1522,7 @@ export function main() {
});
it('should use async validators defined in the html', fakeAsync(() => {
const fixture = TestBed.createComponent(UniqLoginWrapper);
const fixture = initTest(UniqLoginWrapper, UniqLoginValidator);
const form = new FormGroup({'login': new FormControl('')});
tick();
fixture.componentInstance.form = form;
@ -1556,7 +1542,7 @@ export function main() {
}));
it('should use sync validators defined in the model', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
const form = new FormGroup({'login': new FormControl('aa', Validators.required)});
fixture.componentInstance.form = form;
fixture.detectChanges();
@ -1570,7 +1556,7 @@ export function main() {
});
it('should use async validators defined in the model', fakeAsync(() => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
const control =
new FormControl('', Validators.required, uniqLoginAsyncValidator('expected'));
const form = new FormGroup({'login': control});
@ -1601,7 +1587,7 @@ export function main() {
describe('errors', () => {
it('should throw if a form isn\'t passed into formGroup', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
expect(() => fixture.detectChanges())
.toThrowError(new RegExp(`formGroup expects a FormGroup instance`));
@ -1615,7 +1601,7 @@ export function main() {
`
}
});
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
expect(() => fixture.detectChanges())
.toThrowError(
@ -1632,7 +1618,7 @@ export function main() {
`
}
});
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
expect(() => fixture.detectChanges())
.toThrowError(
@ -1651,7 +1637,7 @@ export function main() {
`
}
});
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
expect(() => fixture.detectChanges())
.toThrowError(
@ -1668,7 +1654,7 @@ export function main() {
`
}
});
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
expect(() => fixture.detectChanges())
.toThrowError(
@ -1687,7 +1673,7 @@ export function main() {
`
}
});
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
expect(() => fixture.detectChanges())
.toThrowError(
@ -1703,7 +1689,7 @@ export function main() {
</div>`
}
});
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
expect(() => fixture.detectChanges())
.toThrowError(
@ -1720,7 +1706,7 @@ export function main() {
`
}
});
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
fixture.componentInstance.myGroup = new FormGroup({});
expect(() => fixture.detectChanges())
@ -1738,7 +1724,7 @@ export function main() {
`
}
});
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
fixture.componentInstance.myGroup = new FormGroup({});
expect(() => fixture.detectChanges()).not.toThrowError();
@ -1756,7 +1742,7 @@ export function main() {
`
}
});
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
const myGroup = new FormGroup({person: new FormGroup({})});
fixture.componentInstance.myGroup = new FormGroup({person: new FormGroup({})});
@ -1777,7 +1763,7 @@ export function main() {
`
}
});
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
fixture.componentInstance.myGroup = new FormGroup({});
expect(() => fixture.detectChanges())
@ -1794,7 +1780,7 @@ export function main() {
</form>`
}
});
const fixture = TestBed.createComponent(FormGroupComp);
const fixture = initTest(FormGroupComp);
fixture.componentInstance.form = new FormGroup({'food': new FormControl('fish')});
expect(() => fixture.detectChanges())

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Directive, Input, forwardRef} from '@angular/core';
import {Component, Directive, Input, Type, forwardRef} from '@angular/core';
import {ComponentFixture, TestBed, async, fakeAsync, tick} from '@angular/core/testing';
import {AbstractControl, ControlValueAccessor, FormsModule, NG_ASYNC_VALIDATORS, NG_VALUE_ACCESSOR, NgForm, Validator} from '@angular/forms';
import {By} from '@angular/platform-browser/src/dom/debug/by';
@ -16,37 +16,15 @@ import {dispatchEvent} from '@angular/platform-browser/testing/browser_util';
export function main() {
describe('template-driven forms integration tests', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
StandaloneNgModel,
NgModelForm,
NgModelGroupForm,
NgModelValidBinding,
NgModelNgIfForm,
NgModelRadioForm,
NgModelRangeForm,
NgModelSelectForm,
NgNoFormComp,
InvalidNgModelNoName,
NgModelOptionsStandalone,
NgModelCustomComp,
NgModelCustomWrapper,
NgModelValidationBindings,
NgModelMultipleValidators,
NgAsyncValidator,
NgModelAsyncValidation,
NgModelSelectMultipleForm,
NgModelSelectWithNullForm,
NgModelCheckboxRequiredValidator,
],
imports: [FormsModule]
});
});
function initTest<T>(component: Type<T>, ...directives: Type<any>[]): ComponentFixture<T> {
TestBed.configureTestingModule(
{declarations: [component, ...directives], imports: [FormsModule]});
return TestBed.createComponent(component);
}
describe('basic functionality', () => {
it('should support ngModel for standalone fields', fakeAsync(() => {
const fixture = TestBed.createComponent(StandaloneNgModel);
const fixture = initTest(StandaloneNgModel);
fixture.componentInstance.name = 'oldValue';
fixture.detectChanges();
@ -65,7 +43,7 @@ export function main() {
}));
it('should support ngModel registration with a parent form', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelForm);
const fixture = initTest(NgModelForm);
fixture.componentInstance.name = 'Nancy';
fixture.detectChanges();
@ -76,8 +54,18 @@ export function main() {
expect(form.valid).toBe(false);
}));
it('should add novalidate by default to form element', fakeAsync(() => {
const fixture = initTest(NgModelForm);
fixture.detectChanges();
tick();
const form = fixture.debugElement.query(By.css('form'));
expect(form.nativeElement.getAttribute('novalidate')).toEqual('');
}));
it('should support ngModelGroup', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelGroupForm);
const fixture = initTest(NgModelGroupForm);
fixture.componentInstance.first = 'Nancy';
fixture.componentInstance.last = 'Drew';
fixture.componentInstance.email = 'some email';
@ -100,7 +88,7 @@ export function main() {
}));
it('should add controls and control groups to form control model', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelGroupForm);
const fixture = initTest(NgModelGroupForm);
fixture.componentInstance.first = 'Nancy';
fixture.componentInstance.last = 'Drew';
fixture.componentInstance.email = 'some email';
@ -115,7 +103,7 @@ export function main() {
}));
it('should remove controls and control groups from form control model', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelNgIfForm);
const fixture = initTest(NgModelNgIfForm);
fixture.componentInstance.emailShowing = true;
fixture.componentInstance.first = 'Nancy';
fixture.componentInstance.email = 'some email';
@ -148,7 +136,7 @@ export function main() {
}));
it('should set status classes with ngModel', async(() => {
const fixture = TestBed.createComponent(NgModelForm);
const fixture = initTest(NgModelForm);
fixture.componentInstance.name = 'aa';
fixture.detectChanges();
fixture.whenStable().then(() => {
@ -171,7 +159,7 @@ export function main() {
it('should set status classes with ngModel and async validators', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelAsyncValidation);
const fixture = initTest(NgModelAsyncValidation, NgAsyncValidator);
fixture.whenStable().then(() => {
fixture.detectChanges();
@ -193,7 +181,7 @@ export function main() {
}));
it('should set status classes with ngModelGroup and ngForm', async(() => {
const fixture = TestBed.createComponent(NgModelGroupForm);
const fixture = initTest(NgModelGroupForm);
fixture.componentInstance.first = '';
fixture.detectChanges();
@ -228,27 +216,34 @@ export function main() {
}));
it('should not create a template-driven form when ngNoForm is used', () => {
const fixture = TestBed.createComponent(NgNoFormComp);
const fixture = initTest(NgNoFormComp);
fixture.detectChanges();
expect(fixture.debugElement.children[0].providerTokens.length).toEqual(0);
});
it('should not add novalidate when ngNoForm is used', () => {
const fixture = initTest(NgNoFormComp);
fixture.detectChanges();
const form = fixture.debugElement.query(By.css('form'));
expect(form.nativeElement.hasAttribute('novalidate')).toBeFalsy();
});
});
describe('name and ngModelOptions', () => {
it('should throw if ngModel has a parent form but no name attr or standalone label', () => {
const fixture = TestBed.createComponent(InvalidNgModelNoName);
const fixture = initTest(InvalidNgModelNoName);
expect(() => fixture.detectChanges())
.toThrowError(new RegExp(`name attribute must be set`));
});
it('should not throw if ngModel has a parent form, no name attr, and a standalone label',
() => {
const fixture = TestBed.createComponent(NgModelOptionsStandalone);
const fixture = initTest(NgModelOptionsStandalone);
expect(() => fixture.detectChanges()).not.toThrow();
});
it('should not register standalone ngModels with parent form', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelOptionsStandalone);
const fixture = initTest(NgModelOptionsStandalone);
fixture.componentInstance.one = 'some data';
fixture.componentInstance.two = 'should not show';
fixture.detectChanges();
@ -263,7 +258,7 @@ export function main() {
}));
it('should override name attribute with ngModelOptions name if provided', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelForm);
const fixture = initTest(NgModelForm);
fixture.componentInstance.options = {name: 'override'};
fixture.componentInstance.name = 'some data';
fixture.detectChanges();
@ -276,7 +271,7 @@ export function main() {
describe('submit and reset events', () => {
it('should emit ngSubmit event with the original submit event on submit', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelForm);
const fixture = initTest(NgModelForm);
fixture.componentInstance.event = null;
const form = fixture.debugElement.query(By.css('form'));
@ -287,7 +282,7 @@ export function main() {
}));
it('should mark NgForm as submitted on submit event', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelForm);
const fixture = initTest(NgModelForm);
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
@ -301,7 +296,7 @@ export function main() {
}));
it('should reset the form to empty when reset event is fired', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelForm);
const fixture = initTest(NgModelForm);
fixture.componentInstance.name = 'should be cleared';
fixture.detectChanges();
tick();
@ -324,7 +319,7 @@ export function main() {
}));
it('should reset the form submit state when reset button is clicked', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelForm);
const fixture = initTest(NgModelForm);
const form = fixture.debugElement.children[0].injector.get(NgForm);
const formEl = fixture.debugElement.query(By.css('form'));
@ -342,7 +337,7 @@ export function main() {
describe('valueChange and statusChange events', () => {
it('should emit valueChanges and statusChanges on init', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelForm);
const fixture = initTest(NgModelForm);
const form = fixture.debugElement.children[0].injector.get(NgForm);
fixture.componentInstance.name = 'aa';
fixture.detectChanges();
@ -363,7 +358,7 @@ export function main() {
}));
it('should mark controls dirty before emitting the value change event', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelForm);
const fixture = initTest(NgModelForm);
const form = fixture.debugElement.children[0].injector.get(NgForm).form;
fixture.detectChanges();
@ -380,7 +375,7 @@ export function main() {
it('should mark controls pristine before emitting the value change event when resetting ',
fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelForm);
const fixture = initTest(NgModelForm);
fixture.detectChanges();
tick();
@ -402,7 +397,7 @@ export function main() {
describe('disabled controls', () => {
it('should not consider disabled controls in value or validation', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelGroupForm);
const fixture = initTest(NgModelGroupForm);
fixture.componentInstance.isDisabled = false;
fixture.componentInstance.first = '';
fixture.componentInstance.last = 'Drew';
@ -426,7 +421,7 @@ export function main() {
it('should add disabled attribute in the UI if disable() is called programmatically',
fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelGroupForm);
const fixture = initTest(NgModelGroupForm);
fixture.componentInstance.isDisabled = false;
fixture.componentInstance.first = 'Nancy';
fixture.detectChanges();
@ -442,7 +437,7 @@ export function main() {
}));
it('should disable a custom control if disabled attr is added', async(() => {
const fixture = TestBed.createComponent(NgModelCustomWrapper);
const fixture = initTest(NgModelCustomWrapper, NgModelCustomComp);
fixture.componentInstance.name = 'Nancy';
fixture.componentInstance.isDisabled = true;
fixture.detectChanges();
@ -468,7 +463,7 @@ export function main() {
`,
}
});
const fixture = TestBed.createComponent(NgModelForm);
const fixture = initTest(NgModelForm);
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
@ -484,7 +479,7 @@ export function main() {
}));
it('should disable radio controls properly with programmatic call', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelRadioForm);
const fixture = initTest(NgModelRadioForm);
fixture.componentInstance.food = 'fish';
fixture.detectChanges();
tick();
@ -520,7 +515,7 @@ export function main() {
describe('range control', () => {
it('should support <type=range>', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelRangeForm);
const fixture = initTest(NgModelRangeForm);
// model -> view
fixture.componentInstance.val = 4;
fixture.detectChanges();
@ -540,7 +535,7 @@ export function main() {
describe('radio controls', () => {
it('should support <type=radio>', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelRadioForm);
const fixture = initTest(NgModelRadioForm);
fixture.componentInstance.food = 'fish';
fixture.detectChanges();
tick();
@ -559,7 +554,7 @@ export function main() {
}));
it('should support multiple named <type=radio> groups', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelRadioForm);
const fixture = initTest(NgModelRadioForm);
fixture.componentInstance.food = 'fish';
fixture.componentInstance.drink = 'sprite';
fixture.detectChanges();
@ -582,7 +577,7 @@ export function main() {
}));
it('should support initial undefined value', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelRadioForm);
const fixture = initTest(NgModelRadioForm);
fixture.detectChanges();
tick();
@ -594,7 +589,7 @@ export function main() {
}));
it('should support resetting properly', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelRadioForm);
const fixture = initTest(NgModelRadioForm);
fixture.componentInstance.food = 'chicken';
fixture.detectChanges();
tick();
@ -610,7 +605,7 @@ export function main() {
}));
it('should support setting value to null and undefined', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelRadioForm);
const fixture = initTest(NgModelRadioForm);
fixture.componentInstance.food = 'chicken';
fixture.detectChanges();
tick();
@ -638,7 +633,7 @@ export function main() {
describe('select controls', () => {
it('with option values that are objects', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelSelectForm);
const fixture = initTest(NgModelSelectForm);
const comp = fixture.componentInstance;
comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}];
comp.selectedCity = comp.cities[1];
@ -662,7 +657,7 @@ export function main() {
}));
it('when new options are added', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelSelectForm);
const fixture = initTest(NgModelSelectForm);
const comp = fixture.componentInstance;
comp.cities = [{'name': 'SF'}, {'name': 'NYC'}];
comp.selectedCity = comp.cities[1];
@ -681,7 +676,7 @@ export function main() {
}));
it('when options are removed', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelSelectForm);
const fixture = initTest(NgModelSelectForm);
const comp = fixture.componentInstance;
comp.cities = [{'name': 'SF'}, {'name': 'NYC'}];
comp.selectedCity = comp.cities[1];
@ -699,7 +694,7 @@ export function main() {
}));
it('when option values have same content, but different identities', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelSelectForm);
const fixture = initTest(NgModelSelectForm);
const comp = fixture.componentInstance;
comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'NYC'}];
comp.selectedCity = comp.cities[0];
@ -716,7 +711,7 @@ export function main() {
}));
it('should work with null option', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelSelectWithNullForm);
const fixture = initTest(NgModelSelectWithNullForm);
const comp = fixture.componentInstance;
comp.cities = [{'name': 'SF'}, {'name': 'NYC'}];
comp.selectedCity = null;
@ -743,7 +738,7 @@ export function main() {
let comp: NgModelSelectMultipleForm;
beforeEach(() => {
fixture = TestBed.createComponent(NgModelSelectMultipleForm);
fixture = initTest(NgModelSelectMultipleForm);
comp = fixture.componentInstance;
comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}];
});
@ -804,7 +799,7 @@ export function main() {
describe('custom value accessors', () => {
it('should support standard writing to view and model', async(() => {
const fixture = TestBed.createComponent(NgModelCustomWrapper);
const fixture = initTest(NgModelCustomWrapper, NgModelCustomComp);
fixture.componentInstance.name = 'Nancy';
fixture.detectChanges();
fixture.whenStable().then(() => {
@ -829,7 +824,7 @@ export function main() {
describe('validation directives', () => {
it('required validator should validate checkbox', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelCheckboxRequiredValidator);
const fixture = initTest(NgModelCheckboxRequiredValidator);
fixture.detectChanges();
tick();
@ -865,7 +860,7 @@ export function main() {
}));
it('should support dir validators using bindings', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelValidationBindings);
const fixture = initTest(NgModelValidationBindings);
fixture.componentInstance.required = true;
fixture.componentInstance.minLen = 3;
fixture.componentInstance.maxLen = 3;
@ -909,7 +904,7 @@ export function main() {
}));
it('should support optional fields with string pattern validator', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelMultipleValidators);
const fixture = initTest(NgModelMultipleValidators);
fixture.componentInstance.required = false;
fixture.componentInstance.pattern = '[a-z]+';
fixture.detectChanges();
@ -931,7 +926,7 @@ export function main() {
}));
it('should support optional fields with RegExp pattern validator', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelMultipleValidators);
const fixture = initTest(NgModelMultipleValidators);
fixture.componentInstance.required = false;
fixture.componentInstance.pattern = /^[a-z]+$/;
fixture.detectChanges();
@ -953,7 +948,7 @@ export function main() {
}));
it('should support optional fields with minlength validator', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelMultipleValidators);
const fixture = initTest(NgModelMultipleValidators);
fixture.componentInstance.required = false;
fixture.componentInstance.minLen = 2;
fixture.detectChanges();
@ -976,7 +971,7 @@ export function main() {
it('changes on bound properties should change the validation state of the form',
fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelValidationBindings);
const fixture = initTest(NgModelValidationBindings);
fixture.detectChanges();
tick();
@ -1050,7 +1045,7 @@ export function main() {
describe('ngModel corner cases', () => {
it('should update the view when the model is set back to what used to be in the view',
fakeAsync(() => {
const fixture = TestBed.createComponent(StandaloneNgModel);
const fixture = initTest(StandaloneNgModel);
fixture.componentInstance.name = '';
fixture.detectChanges();
tick();
@ -1078,7 +1073,7 @@ export function main() {
}));
it('should not crash when validity is checked from a binding', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelValidBinding);
const fixture = initTest(NgModelValidBinding);
tick();
expect(() => fixture.detectChanges()).not.toThrowError();
}));

View File

@ -62,7 +62,15 @@ export class RequestOptions {
/**
* Search parameters to be included in a {@link Request}.
*/
search: URLSearchParams;
params: URLSearchParams;
/**
* @deprecated from 4.0.0. Use params instead.
*/
get search(): URLSearchParams { return this.params; }
/**
* @deprecated from 4.0.0. Use params instead.
*/
set search(params: URLSearchParams) { this.params = params; }
/**
* Enable use credentials for a {@link Request}.
*/
@ -72,15 +80,15 @@ export class RequestOptions {
*/
responseType: ResponseContentType;
// TODO(Dzmitry): remove search when this.search is removed
constructor(
{method, headers, body, url, search, withCredentials,
{method, headers, body, url, search, params, withCredentials,
responseType}: RequestOptionsArgs = {}) {
this.method = method != null ? normalizeMethodName(method) : null;
this.headers = headers != null ? headers : null;
this.body = body != null ? body : null;
this.url = url != null ? url : null;
this.search =
search != null ? (typeof search === 'string' ? new URLSearchParams(search) : search) : null;
this.params = this._mergeSearchParams(params || search);
this.withCredentials = withCredentials != null ? withCredentials : null;
this.responseType = responseType != null ? responseType : null;
}
@ -116,18 +124,49 @@ export class RequestOptions {
headers: options && options.headers != null ? options.headers : new Headers(this.headers),
body: options && options.body != null ? options.body : this.body,
url: options && options.url != null ? options.url : this.url,
search: options && options.search != null ?
(typeof options.search === 'string' ? new URLSearchParams(options.search) :
options.search.clone()) :
this.search,
params: options && this._mergeSearchParams(options.params || options.search),
withCredentials: options && options.withCredentials != null ? options.withCredentials :
this.withCredentials,
responseType: options && options.responseType != null ? options.responseType :
this.responseType
});
}
}
private _mergeSearchParams(params: string|URLSearchParams|
{[key: string]: any | any[]}): URLSearchParams {
if (!params) return this.params;
if (params instanceof URLSearchParams) {
return params.clone();
}
if (typeof params === 'string') {
return new URLSearchParams(params);
}
return this._parseParams(params);
}
private _parseParams(objParams: {[key: string]: any | any[]} = {}): URLSearchParams {
const params = new URLSearchParams();
Object.keys(objParams).forEach((key: string) => {
const value: any|any[] = objParams[key];
if (Array.isArray(value)) {
value.forEach((item: any) => this._appendParam(key, item, params));
} else {
this._appendParam(key, value, params);
}
});
return params;
}
private _appendParam(key: string, value: any, params: URLSearchParams): void {
if (typeof value !== 'string') {
value = JSON.stringify(value);
}
params.append(key, value);
}
}
/**
* Subclass of {@link RequestOptions}, with default values.

View File

@ -48,7 +48,9 @@ export abstract class XSRFStrategy { abstract configureRequest(req: Request): vo
export interface RequestOptionsArgs {
url?: string;
method?: string|RequestMethod;
search?: string|URLSearchParams;
/** @deprecated from 4.0.0. Use params instead. */
search?: string|URLSearchParams|{[key: string]: any | any[]};
params?: string|URLSearchParams|{[key: string]: any | any[]};
headers?: Headers;
body?: any;
withCredentials?: boolean;

View File

@ -76,15 +76,15 @@ export class Request extends Body {
// TODO: assert that url is present
const url = requestOptions.url;
this.url = requestOptions.url;
if (requestOptions.search) {
const search = requestOptions.search.toString();
if (search.length > 0) {
if (requestOptions.params) {
const params = requestOptions.params.toString();
if (params.length > 0) {
let prefix = '?';
if (this.url.indexOf('?') != -1) {
prefix = (this.url[this.url.length - 1] == '&') ? '' : '&';
}
// TODO: just delete search-query-looking string in url?
this.url = url + prefix + search;
this.url = url + prefix + params;
}
}
this._body = requestOptions.body;

View File

@ -25,5 +25,32 @@ export function main() {
const options2 = options1.merge(new RequestOptions({method: RequestMethod.Delete}));
expect(options2.method).toBe(RequestMethod.Delete);
});
it('should accept search params as object', () => {
const params = {a: 1, b: 'text', c: [1, 2, '3']};
const options = new RequestOptions({params});
expect(options.params.paramsMap.size).toBe(3);
expect(options.params.paramsMap.get('a')).toEqual(['1']);
expect(options.params.paramsMap.get('b')).toEqual(['text']);
expect(options.params.paramsMap.get('c')).toEqual(['1', '2', '3']);
});
it('should merge search params as object', () => {
const options1 = new BaseRequestOptions();
const params = {a: 1, b: 'text', c: [1, 2, '3']};
const options2 = options1.merge(new RequestOptions({params}));
expect(options2.params.paramsMap.size).toBe(3);
expect(options2.params.paramsMap.get('a')).toEqual(['1']);
expect(options2.params.paramsMap.get('b')).toEqual(['text']);
expect(options2.params.paramsMap.get('c')).toEqual(['1', '2', '3']);
});
it('should create a new headers object when calling merge', () => {
const options1 = new RequestOptions({headers: new Headers()});
const options2 = options1.merge();
expect(options2.headers).not.toBe(options1.headers);
});
});
}

View File

@ -208,12 +208,13 @@ class ExpressionDiagnosticsVisitor extends TemplateAstChildVisitor {
private diagnoseExpression(ast: AST, offset: number, includeEvent: boolean) {
const scope = this.getExpressionScope(this.path, includeEvent);
this.diagnostics.push(
...getExpressionDiagnostics(scope, ast, this.info.template.query)
.map(d => ({
span: offsetSpan(d.ast.span, offset + this.info.template.span.start),
kind: d.kind,
message: d.message
})));
...getExpressionDiagnostics(scope, ast, this.info.template.query, {
event: includeEvent
}).map(d => ({
span: offsetSpan(d.ast.span, offset + this.info.template.span.start),
kind: d.kind,
message: d.message
})));
}
private push(ast: TemplateAst) { this.path.push(ast); }

View File

@ -16,9 +16,12 @@ import {TemplateAstChildVisitor, TemplateAstPath} from './template_path';
import {BuiltinType, CompletionKind, Definition, DiagnosticKind, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './types';
import {inSpan, spanOf} from './utils';
export interface ExpressionDiagnosticsContext { event?: boolean; }
export function getExpressionDiagnostics(
scope: SymbolTable, ast: AST, query: SymbolQuery): TypeDiagnostic[] {
const analyzer = new AstType(scope, query);
scope: SymbolTable, ast: AST, query: SymbolQuery,
context: ExpressionDiagnosticsContext = {}): TypeDiagnostic[] {
const analyzer = new AstType(scope, query, context);
analyzer.getDiagnostics(ast);
return analyzer.diagnostics;
}
@ -30,7 +33,7 @@ export function getExpressionCompletions(
const tail = path.tail;
let result: SymbolTable|undefined = scope;
function getType(ast: AST): Symbol { return new AstType(scope, query).getType(ast); }
function getType(ast: AST): Symbol { return new AstType(scope, query, {}).getType(ast); }
// If the completion request is in a not in a pipe or property access then the global scope
// (that is the scope of the implicit receiver) is the right scope as the user is typing the
@ -88,7 +91,7 @@ export function getExpressionSymbol(
if (path.empty) return undefined;
const tail = path.tail;
function getType(ast: AST): Symbol { return new AstType(scope, query).getType(ast); }
function getType(ast: AST): Symbol { return new AstType(scope, query, {}).getType(ast); }
let symbol: Symbol = undefined;
let span: Span = undefined;
@ -189,13 +192,18 @@ export class TypeDiagnostic {
class AstType implements ExpressionVisitor {
public diagnostics: TypeDiagnostic[];
constructor(private scope: SymbolTable, private query: SymbolQuery) {}
constructor(
private scope: SymbolTable, private query: SymbolQuery,
private context: ExpressionDiagnosticsContext) {}
getType(ast: AST): Symbol { return ast.visit(this); }
getDiagnostics(ast: AST): TypeDiagnostic[] {
this.diagnostics = [];
ast.visit(this);
const type: Symbol = ast.visit(this);
if (this.context.event && type.callable) {
this.reportWarning('Unexpected callable expression. Expected a method call', ast);
}
return this.diagnostics;
}
@ -751,7 +759,7 @@ function refinedVariableType(
const ngForOfBinding = ngForDirective.inputs.find(i => i.directiveName == 'ngForOf');
if (ngForOfBinding) {
const bindingType =
new AstType(info.template.members, info.template.query).getType(ngForOfBinding.value);
new AstType(info.template.members, info.template.query, {}).getType(ngForOfBinding.value);
if (bindingType) {
return info.template.query.getElementType(bindingType);
}

View File

@ -130,6 +130,39 @@ describe('diagnostics', () => {
});
});
it('should report a warning if an event results in a callable expression', () => {
const code =
` @Component({template: \`<div (click)="onClick"></div>\`}) export class MyComponent { onClick() { } }`;
addCode(code, (fileName, content) => {
const diagnostics = ngService.getDiagnostics(fileName);
includeDiagnostic(
diagnostics, 'Unexpected callable expression. Expected a method call', 'onClick',
content);
});
});
// #13412
it('should not report an error for using undefined', () => {
const code =
` @Component({template: \`<div *ngIf="something === undefined"></div>\`}) export class MyComponent { something = 'foo'; }})`;
addCode(code, fileName => {
const diagnostics = ngService.getDiagnostics(fileName);
onlyModuleDiagnostics(diagnostics);
});
});
// Issue #13326
it('should report a narrow span for invalid pipes', () => {
const code =
` @Component({template: '<p> Using an invalid pipe {{data | dat}} </p>'}) export class MyComponent { data = 'some data'; }`;
addCode(code, fileName => {
const diagnostic =
ngService.getDiagnostics(fileName).filter(d => d.message.indexOf('pipe') > 0)[0];
expect(diagnostic).not.toBeUndefined();
expect(diagnostic.span.end - diagnostic.span.start).toBeLessThan(11);
});
});
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
const fileName = '/app/app.component.ts';
const originalContent = mockHost.getFileContent(fileName);

View File

@ -213,7 +213,7 @@ export class UnknownTrackBy {
'ng-if-cases.ts': `
import {Component} from '@angular/core';
@Component({template: '<div ~{implicit}*ngIf="show; let l"~{implicit-end}>Showing now!</div>'})
@Component({template: '<div ~{implicit}*ngIf="show; let l=unknown"~{implicit-end}>Showing now!</div>'})
export class ShowIf {
show = false;
}

View File

@ -186,7 +186,8 @@ describe('plugin', () => {
expectSemanticError('app/ng-if-cases.ts', locationMarker, message);
}
it('should report an implicit context reference', () => {
expectError('implicit', 'The template context does not have an implicit value');
expectError(
'implicit', 'The template context does not defined a member called \'unknown\'');
});
});
});

View File

@ -14,6 +14,7 @@ import {WebAnimationsDriver} from '../src/dom/web_animations_driver';
import {BrowserDomAdapter} from './browser/browser_adapter';
import {BrowserPlatformLocation} from './browser/location/browser_platform_location';
import {Meta} from './browser/meta';
import {BrowserGetTestability} from './browser/testability';
import {Title} from './browser/title';
import {ELEMENT_PROBE_PROVIDERS} from './dom/debug/ng_probe';
@ -46,7 +47,7 @@ export const BROWSER_SANITIZATION_PROVIDERS: Array<any> = [
/**
* @stable
*/
export const platformBrowser =
export const platformBrowser: (extraProviders?: Provider[]) => PlatformRef =
createPlatformFactory(platformCore, 'browser', INTERNAL_BROWSER_PLATFORM_PROVIDERS);
export function initDomAdapter() {
@ -58,6 +59,10 @@ export function errorHandler(): ErrorHandler {
return new ErrorHandler();
}
export function meta(): Meta {
return new Meta(getDOM());
}
export function _document(): any {
return getDOM().defaultDoc();
}
@ -76,7 +81,8 @@ export function _resolveDefaultAnimationDriver(): AnimationDriver {
*/
@NgModule({
providers: [
BROWSER_SANITIZATION_PROVIDERS, {provide: ErrorHandler, useFactory: errorHandler, deps: []},
BROWSER_SANITIZATION_PROVIDERS,
{provide: ErrorHandler, useFactory: errorHandler, deps: []},
{provide: DOCUMENT, useFactory: _document, deps: []},
{provide: EVENT_MANAGER_PLUGINS, useClass: DomEventsPlugin, multi: true},
{provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, multi: true},
@ -85,8 +91,13 @@ export function _resolveDefaultAnimationDriver(): AnimationDriver {
{provide: DomRootRenderer, useClass: DomRootRenderer_},
{provide: RootRenderer, useExisting: DomRootRenderer},
{provide: SharedStylesHost, useExisting: DomSharedStylesHost},
{provide: AnimationDriver, useFactory: _resolveDefaultAnimationDriver}, DomSharedStylesHost,
Testability, EventManager, ELEMENT_PROBE_PROVIDERS, Title
{provide: AnimationDriver, useFactory: _resolveDefaultAnimationDriver},
{provide: Meta, useFactory: meta},
DomSharedStylesHost,
Testability,
EventManager,
ELEMENT_PROBE_PROVIDERS,
Title,
],
exports: [CommonModule, ApplicationModule]
})

View File

@ -0,0 +1,114 @@
/**
* @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 {Injectable} from '@angular/core';
import {DomAdapter} from '../dom/dom_adapter';
/**
* Represents a meta element.
*
* @experimental
*/
export interface MetaDefinition {
charset?: string;
content?: string;
httpEquiv?: string;
id?: string;
itemprop?: string;
name?: string;
property?: string;
scheme?: string;
url?: string;
[prop: string]: string;
}
/**
* A service that can be used to get and add meta tags.
*
* @experimental
*/
@Injectable()
export class Meta {
constructor(private _dom: DomAdapter) {}
addTag(tag: MetaDefinition, forceCreation: boolean = false): HTMLMetaElement {
if (!tag) return null;
return this._getOrCreateElement(tag, forceCreation);
}
addTags(tags: MetaDefinition[], forceCreation: boolean = false): HTMLMetaElement[] {
if (!tags) return [];
return tags.reduce((result: HTMLMetaElement[], tag: MetaDefinition) => {
if (tag) {
result.push(this._getOrCreateElement(tag, forceCreation));
}
return result;
}, []);
}
getTag(attrSelector: string): HTMLMetaElement {
if (!attrSelector) return null;
return this._dom.query(`meta[${attrSelector}]`);
}
getTags(attrSelector: string): HTMLMetaElement[] {
if (!attrSelector) return [];
const list /*NodeList*/ =
this._dom.querySelectorAll(this._dom.defaultDoc(), `meta[${attrSelector}]`);
return list ? [].slice.call(list) : [];
}
updateTag(tag: MetaDefinition, selector?: string): HTMLMetaElement {
if (!tag) return null;
selector = selector || this._parseSelector(tag);
const meta: HTMLMetaElement = this.getTag(selector);
if (meta) {
return this._setMetaElementAttributes(tag, meta);
}
return this._getOrCreateElement(tag, true);
}
removeTag(attrSelector: string): void { this.removeTagElement(this.getTag(attrSelector)); }
removeTagElement(meta: HTMLMetaElement): void {
if (meta) {
this._dom.remove(meta);
}
}
private _getOrCreateElement(meta: MetaDefinition, forceCreation: boolean = false):
HTMLMetaElement {
if (!forceCreation) {
const selector: string = this._parseSelector(meta);
const elem: HTMLMetaElement = this.getTag(selector);
// It's allowed to have multiple elements with the same name so it's not enough to
// just check that element with the same name already present on the page. We also need to
// check if element has tag attributes
if (elem && this._containsAttributes(meta, elem)) return elem;
}
const element: HTMLMetaElement = this._dom.createElement('meta') as HTMLMetaElement;
this._setMetaElementAttributes(meta, element);
const head = this._dom.getElementsByTagName(this._dom.defaultDoc(), 'head')[0];
this._dom.appendChild(head, element);
return element;
}
private _setMetaElementAttributes(tag: MetaDefinition, el: HTMLMetaElement): HTMLMetaElement {
Object.keys(tag).forEach((prop: string) => this._dom.setAttribute(el, prop, tag[prop]));
return el;
}
private _parseSelector(tag: MetaDefinition): string {
const attr: string = tag.name ? 'name' : 'property';
return `${attr}="${tag[attr]}"`;
}
private _containsAttributes(tag: MetaDefinition, elem: HTMLMetaElement): boolean {
return Object.keys(tag).every((key: string) => this._dom.getAttribute(elem, key) === tag[key]);
}
}

View File

@ -7,6 +7,7 @@
*/
export {BrowserModule, platformBrowser} from './browser';
export {Meta, MetaDefinition} from './browser/meta';
export {Title} from './browser/title';
export {disableDebugTools, enableDebugTools} from './browser/tools/tools';
export {AnimationDriver} from './dom/animation_driver';

View File

@ -0,0 +1,192 @@
/**
* @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 {Injectable} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {BrowserModule, Meta} from '@angular/platform-browser';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {expect} from '@angular/platform-browser/testing/matchers';
export function main() {
describe('Meta service', () => {
const metaService: Meta = new Meta(getDOM());
const doc: HTMLDocument = getDOM().defaultDoc();
let defaultMeta: HTMLMetaElement;
beforeEach(() => {
defaultMeta = getDOM().createElement('meta', doc) as HTMLMetaElement;
defaultMeta.setAttribute('property', 'fb:app_id');
defaultMeta.setAttribute('content', '123456789');
getDOM().getElementsByTagName(doc, 'head')[0].appendChild(defaultMeta);
});
afterEach(() => getDOM().remove(defaultMeta));
it('should return meta tag matching selector', () => {
const actual: HTMLMetaElement = metaService.getTag('property="fb:app_id"');
expect(actual).not.toBeNull();
expect(actual.content).toEqual('123456789');
});
it('should return all meta tags matching selector', () => {
const tag1 = metaService.addTag({name: 'author', content: 'page author'});
const tag2 = metaService.addTag({name: 'author', content: 'another page author'});
const actual: HTMLMetaElement[] = metaService.getTags('name=author');
expect(actual.length).toEqual(2);
expect(actual[0].content).toEqual('page author');
expect(actual[1].content).toEqual('another page author');
// clean up
metaService.removeTagElement(tag1);
metaService.removeTagElement(tag2);
});
it('should return null if meta tag does not exist', () => {
const actual: HTMLMetaElement = metaService.getTag('fake=fake');
expect(actual).toBeNull();
});
it('should remove meta tag by the given selector', () => {
expect(metaService.getTag('name=author')).toBeNull();
metaService.addTag({name: 'author', content: 'page author'});
expect(metaService.getTag('name=author')).not.toBeNull();
metaService.removeTag('name=author');
expect(metaService.getTag('name=author')).toBeNull();
});
it('should remove meta tag by the given element', () => {
expect(metaService.getTag('name=keywords')).toBeNull();
metaService.addTags([{name: 'keywords', content: 'meta test'}]);
const meta = metaService.getTag('name=keywords');
expect(meta).not.toBeNull();
metaService.removeTagElement(meta);
expect(metaService.getTag('name=keywords')).toBeNull();
});
it('should update meta tag matching the given selector', () => {
metaService.updateTag({content: '4321'}, 'property="fb:app_id"');
const actual = metaService.getTag('property="fb:app_id"');
expect(actual).not.toBeNull();
expect(actual.content).toEqual('4321');
});
it('should extract selector from the tag definition', () => {
metaService.updateTag({property: 'fb:app_id', content: '666'});
const actual = metaService.getTag('property="fb:app_id"');
expect(actual).not.toBeNull();
expect(actual.content).toEqual('666');
});
it('should create meta tag if it does not exist', () => {
expect(metaService.getTag('name="twitter:title"')).toBeNull();
metaService.updateTag(
{name: 'twitter:title', content: 'Content Title'}, 'name="twitter:title"');
const actual = metaService.getTag('name="twitter:title"');
expect(actual).not.toBeNull();
expect(actual.content).toEqual('Content Title');
// clean up
metaService.removeTagElement(actual);
});
it('should add new meta tag', () => {
expect(metaService.getTag('name="og:title"')).toBeNull();
metaService.addTag({name: 'og:title', content: 'Content Title'});
const actual = metaService.getTag('name="og:title"');
expect(actual).not.toBeNull();
expect(actual.content).toEqual('Content Title');
// clean up
metaService.removeTagElement(actual);
});
it('should add multiple new meta tags', () => {
expect(metaService.getTag('name="twitter:title"')).toBeNull();
expect(metaService.getTag('property="og:title"')).toBeNull();
metaService.addTags([
{name: 'twitter:title', content: 'Content Title'},
{property: 'og:title', content: 'Content Title'}
]);
const twitterMeta = metaService.getTag('name="twitter:title"');
const fbMeta = metaService.getTag('property="og:title"');
expect(twitterMeta).not.toBeNull();
expect(fbMeta).not.toBeNull();
// clean up
metaService.removeTagElement(twitterMeta);
metaService.removeTagElement(fbMeta);
});
it('should not add meta tag if it is already present on the page and has the same attr', () => {
expect(metaService.getTags('property="fb:app_id"').length).toEqual(1);
metaService.addTag({property: 'fb:app_id', content: '123456789'});
expect(metaService.getTags('property="fb:app_id"').length).toEqual(1);
});
it('should add meta tag if it is already present on the page and but has different attr',
() => {
expect(metaService.getTags('property="fb:app_id"').length).toEqual(1);
const meta = metaService.addTag({property: 'fb:app_id', content: '666'});
expect(metaService.getTags('property="fb:app_id"').length).toEqual(2);
// clean up
metaService.removeTagElement(meta);
});
it('should add meta tag if it is already present on the page and force true', () => {
expect(metaService.getTags('property="fb:app_id"').length).toEqual(1);
const meta = metaService.addTag({property: 'fb:app_id', content: '123456789'}, true);
expect(metaService.getTags('property="fb:app_id"').length).toEqual(2);
// clean up
metaService.removeTagElement(meta);
});
});
describe('integration test', () => {
@Injectable()
class DependsOnMeta {
constructor(public meta: Meta) {}
}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [BrowserModule],
providers: [DependsOnMeta],
});
});
it('should inject Meta service when using BrowserModule',
() => expect(TestBed.get(DependsOnMeta).meta).toBeAnInstanceOf(Meta));
});
}

View File

@ -8,7 +8,7 @@
import {CompilerConfig, ResourceLoader} from '@angular/compiler';
import {CUSTOM_ELEMENTS_SCHEMA, Component, Directive, Injectable, Input, NgModule, Pipe} from '@angular/core';
import {TestBed, async, fakeAsync, inject, tick, withModule} from '@angular/core/testing';
import {TestBed, async, fakeAsync, getTestBed, inject, tick, withModule} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/matchers';
import {stringify} from '../src/facade/lang';
@ -356,6 +356,21 @@ export function main() {
expect(compFixture.nativeElement).toHaveText('transformed hello');
});
});
describe('template', () => {
let testBedSpy: any;
beforeEach(() => {
testBedSpy = spyOn(getTestBed(), 'overrideComponent').and.callThrough();
TestBed.overrideTemplate(SomeComponent, 'newText');
});
it(`should override component's template`, () => {
const fixture = TestBed.createComponent(SomeComponent);
expect(fixture.nativeElement).toHaveText('newText');
expect(testBedSpy).toHaveBeenCalledWith(SomeComponent, {
set: {template: 'newText', templateUrl: null}
});
});
});
});
describe('setting up the compiler', () => {

View File

@ -7,7 +7,7 @@
*/
import {LocationStrategy} from '@angular/common';
import {Directive, HostBinding, HostListener, Input, OnChanges, OnDestroy} from '@angular/core';
import {Attribute, Directive, ElementRef, HostBinding, HostListener, Input, OnChanges, OnDestroy, Renderer} from '@angular/core';
import {Subscription} from 'rxjs/Subscription';
import {NavigationEnd, Router} from '../router';
@ -89,7 +89,13 @@ export class RouterLink {
@Input() replaceUrl: boolean;
private commands: any[] = [];
constructor(private router: Router, private route: ActivatedRoute) {}
constructor(
private router: Router, private route: ActivatedRoute,
@Attribute('tabindex') tabIndex: string, renderer: Renderer, el: ElementRef) {
if (tabIndex == null) {
renderer.setElementAttribute(el.nativeElement, 'tabindex', '0');
}
}
@Input()
set routerLink(data: any[]|string) {

View File

@ -1068,8 +1068,9 @@ describe('Integration', () => {
advance(fixture);
expect(fixture.nativeElement).toHaveText('team 22 [ link, right: ]');
const native = fixture.nativeElement.querySelector('button');
native.click();
const button = fixture.nativeElement.querySelector('button');
expect(button.getAttribute('tabindex')).toEqual('0');
button.click();
advance(fixture);
expect(fixture.nativeElement).toHaveText('team 33 [ simple, right: ]');

View File

@ -35,12 +35,13 @@ interface IBindingDestination {
}
interface IControllerInstance extends IBindingDestination {
$doCheck?: () => void;
$onDestroy?: () => void;
$onInit?: () => void;
$postLink?: () => void;
}
type LifecycleHook = '$onChanges' | '$onDestroy' | '$onInit' | '$postLink';
type LifecycleHook = '$doCheck' | '$onChanges' | '$onDestroy' | '$onInit' | '$postLink';
/**
* @whatItDoes
@ -168,6 +169,13 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
this.callLifecycleHook('$onInit', this.controllerInstance);
if (this.controllerInstance && isFunction(this.controllerInstance.$doCheck)) {
const callDoCheck = () => this.callLifecycleHook('$doCheck', this.controllerInstance);
this.$componentScope.$parent.$watch(callDoCheck);
callDoCheck();
}
const link = this.directive.link;
const preLink = (typeof link == 'object') && (link as angular.IDirectivePrePost).pre;
const postLink = (typeof link == 'object') ? (link as angular.IDirectivePrePost).post : link;
@ -228,7 +236,7 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
}
private callLifecycleHook(method: LifecycleHook, context: IBindingDestination, arg?: any) {
if (context && typeof context[method] === 'function') {
if (context && isFunction(context[method])) {
context[method](arg);
}
}
@ -422,7 +430,11 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
function getOrCall<T>(property: Function | T): T {
return typeof(property) === 'function' ? property() : property;
return isFunction(property) ? property() : property;
}
function isFunction(value: any): value is Function {
return typeof value === 'function';
}
// NOTE: Only works for `typeof T !== 'object'`.

View File

@ -2335,6 +2335,155 @@ export function main() {
}));
it('should call `$doCheck()` on controller', async(() => {
const controllerDoCheckA = jasmine.createSpy('controllerDoCheckA');
const controllerDoCheckB = jasmine.createSpy('controllerDoCheckB');
// Define `ng1Directive`
const ng1DirectiveA: angular.IDirective = {
template: 'ng1A',
bindToController: false,
controller: class {$doCheck() { controllerDoCheckA(); }}
};
const ng1DirectiveB: angular.IDirective = {
template: 'ng1B',
bindToController: true,
controller: class {constructor() { (this as any)['$doCheck'] = controllerDoCheckB; }}
};
// Define `Ng1ComponentFacade`
@Directive({selector: 'ng1A'})
class Ng1ComponentAFacade extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1A', elementRef, injector);
}
}
@Directive({selector: 'ng1B'})
class Ng1ComponentBFacade extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1B', elementRef, injector);
}
}
// Define `Ng2Component`
@Component({selector: 'ng2', template: '<ng1A></ng1A> | <ng1B></ng1B>'})
class Ng2Component {
}
// Define `ng1Module`
const ng1Module = angular.module('ng1Module', [])
.directive('ng1A', () => ng1DirectiveA)
.directive('ng1B', () => ng1DirectiveB)
.directive('ng2', downgradeComponent({component: Ng2Component}));
// Define `Ng2Module`
@NgModule({
declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
// Bootstrap
const element = html(`<ng2></ng2>`);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
// Initial change
expect(controllerDoCheckA.calls.count()).toBe(1);
expect(controllerDoCheckB.calls.count()).toBe(1);
// Run a `$digest`
// (Since it's the first one since the `$doCheck` watcher was added,
// the `watchFn` will be run twice.)
digest(adapter);
expect(controllerDoCheckA.calls.count()).toBe(3);
expect(controllerDoCheckB.calls.count()).toBe(3);
// Run another `$digest`
digest(adapter);
expect(controllerDoCheckA.calls.count()).toBe(4);
expect(controllerDoCheckB.calls.count()).toBe(4);
});
}));
it('should not call `$doCheck()` on scope', async(() => {
const scopeDoCheck = jasmine.createSpy('scopeDoCheck');
// Define `ng1Directive`
const ng1DirectiveA: angular.IDirective = {
template: 'ng1A',
bindToController: false,
controller: class {
constructor(private $scope: angular.IScope) { $scope['$doCheck'] = scopeDoCheck; }
}
};
const ng1DirectiveB: angular.IDirective = {
template: 'ng1B',
bindToController: true,
controller: class {
constructor(private $scope: angular.IScope) { $scope['$doCheck'] = scopeDoCheck; }
}
};
// Define `Ng1ComponentFacade`
@Directive({selector: 'ng1A'})
class Ng1ComponentAFacade extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1A', elementRef, injector);
}
}
@Directive({selector: 'ng1B'})
class Ng1ComponentBFacade extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1B', elementRef, injector);
}
}
// Define `Ng2Component`
@Component({selector: 'ng2', template: '<ng1A></ng1A> | <ng1B></ng1B>'})
class Ng2Component {
}
// Define `ng1Module`
const ng1Module = angular.module('ng1Module', [])
.directive('ng1A', () => ng1DirectiveA)
.directive('ng1B', () => ng1DirectiveB)
.directive('ng2', downgradeComponent({component: Ng2Component}));
// Define `Ng2Module`
@NgModule({
declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
// Bootstrap
const element = html(`<ng2></ng2>`);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
// Initial change
expect(scopeDoCheck).not.toHaveBeenCalled();
// Run a `$digest`
digest(adapter);
expect(scopeDoCheck).not.toHaveBeenCalled();
// Run another `$digest`
digest(adapter);
expect(scopeDoCheck).not.toHaveBeenCalled();
});
}));
it('should call `$onDestroy()` on controller', async(() => {
const controllerOnDestroyA = jasmine.createSpy('controllerOnDestroyA');
const controllerOnDestroyB = jasmine.createSpy('controllerOnDestroyB');
@ -2525,17 +2674,24 @@ export function main() {
});
}));
it('should be called in order `$onChanges()` > `$onInit()` > `$postLink()`', async(() => {
it('should be called in order `$onChanges()` > `$onInit()` > `$doCheck()` > `$postLink()`',
async(() => {
// Define `ng1Component`
const ng1Component: angular.IComponent = {
template: '{{ $ctrl.calls.join(" > ") }}',
// `$doCheck()` will keep getting called as long as the interpolated value keeps
// changing (by appending `> $doCheck`). Only care about the first 4 values.
template: '{{ $ctrl.calls.slice(0, 4).join(" > ") }}',
bindings: {value: '<'},
controller: class {
calls: string[] = [];
$onChanges() { this.calls.push('$onChanges'); } $onInit() {
this.calls.push('$onInit');
} $postLink() { this.calls.push('$postLink'); }
$onChanges() { this.calls.push('$onChanges'); }
$onInit() { this.calls.push('$onInit'); }
$doCheck() { this.calls.push('$doCheck'); }
$postLink() { this.calls.push('$postLink'); }
}
};
@ -2573,7 +2729,8 @@ export function main() {
const element = html(`<ng2></ng2>`);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
expect(multiTrim(element.textContent)).toBe('$onChanges > $onInit > $postLink');
expect(multiTrim(element.textContent))
.toBe('$onChanges > $onInit > $doCheck > $postLink');
});
}));
});

View File

@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "2.4.1",
"version": "4.0.0-beta.1",
"private": true,
"branchPattern": "2.0.*",
"description": "Angular 2 - a web framework for modern web apps",

View File

@ -130,7 +130,16 @@ export declare class NgFor implements DoCheck, OnChanges {
/** @stable */
export declare class NgIf {
ngIf: any;
constructor(_viewContainer: ViewContainerRef, _template: TemplateRef<Object>);
ngIfElse: TemplateRef<NgIfContext>;
ngIfThen: TemplateRef<NgIfContext>;
constructor(_viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext>);
}
/** @experimental */
export declare class NgLocaleLocalization extends NgLocalization {
protected locale: string;
constructor(locale: string);
getPluralCategory(value: any): string;
}
/** @experimental */
@ -223,6 +232,11 @@ export declare class SlicePipe implements PipeTransform {
transform(value: any, start: number, end?: number): any;
}
/** @stable */
export declare class TitleCasePipe implements PipeTransform {
transform(value: string): string;
}
/** @stable */
export declare class UpperCasePipe implements PipeTransform {
transform(value: string): string;

View File

@ -89,6 +89,7 @@ export declare class TestBed implements Injector {
static overrideDirective(directive: Type<any>, override: MetadataOverride<Directive>): typeof TestBed;
static overrideModule(ngModule: Type<any>, override: MetadataOverride<NgModule>): typeof TestBed;
static overridePipe(pipe: Type<any>, override: MetadataOverride<Pipe>): typeof TestBed;
static overrideTemplate(component: Type<any>, template: string): typeof TestBed;
/** @experimental */ static resetTestEnvironment(): void;
static resetTestingModule(): typeof TestBed;
}

View File

@ -139,11 +139,12 @@ export declare class RequestOptions {
body: any;
headers: Headers;
method: RequestMethod | string;
params: URLSearchParams;
responseType: ResponseContentType;
search: URLSearchParams;
/** @deprecated */ search: URLSearchParams;
url: string;
withCredentials: boolean;
constructor({method, headers, body, url, search, withCredentials, responseType}?: RequestOptionsArgs);
constructor({method, headers, body, url, search, params, withCredentials, responseType}?: RequestOptionsArgs);
merge(options?: RequestOptionsArgs): RequestOptions;
}
@ -152,8 +153,13 @@ export interface RequestOptionsArgs {
body?: any;
headers?: Headers;
method?: string | RequestMethod;
params?: string | URLSearchParams | {
[key: string]: any | any[];
};
responseType?: ResponseContentType;
search?: string | URLSearchParams;
/** @deprecated */ search?: string | URLSearchParams | {
[key: string]: any | any[];
};
url?: string;
withCredentials?: boolean;
}

View File

@ -58,6 +58,32 @@ export declare class HammerGestureConfig {
buildHammer(element: HTMLElement): HammerInstance;
}
/** @experimental */
export declare class Meta {
constructor(_dom: DomAdapter);
addTag(tag: MetaDefinition, forceCreation?: boolean): HTMLMetaElement;
addTags(tags: MetaDefinition[], forceCreation?: boolean): HTMLMetaElement[];
getTag(attrSelector: string): HTMLMetaElement;
getTags(attrSelector: string): HTMLMetaElement[];
removeTag(attrSelector: string): void;
removeTagElement(meta: HTMLMetaElement): void;
updateTag(tag: MetaDefinition, selector?: string): HTMLMetaElement;
}
/** @experimental */
export interface MetaDefinition {
charset?: string;
content?: string;
httpEquiv?: string;
id?: string;
itemprop?: string;
name?: string;
property?: string;
scheme?: string;
url?: string;
[prop: string]: string;
}
/** @deprecated */
export declare class NgProbeToken {
name: string;

View File

@ -249,7 +249,7 @@ export declare class RouterLink {
routerLink: any[] | string;
skipLocationChange: boolean;
urlTree: UrlTree;
constructor(router: Router, route: ActivatedRoute);
constructor(router: Router, route: ActivatedRoute, tabIndex: string, renderer: Renderer, el: ElementRef);
onClick(): boolean;
}