Compare commits

...

75 Commits
7.0.3 ... 2.2.3

Author SHA1 Message Date
380377139b docs(changelog): add changelog for 2.2.3 2016-11-23 13:00:37 -08:00
9c7680ef69 chore(release): cut the 2.2.3 release 2016-11-23 12:53:13 -08:00
69572ac2f1 Revert "refactor(compiler): move static_reflector into @angular/compiler and rename files"
This reverts commit 8f295287a2.
2016-11-23 12:09:10 -08:00
76f53f929c Revert "refactor(compiler): move findDeclaration into the StaticReflector"
This reverts commit e7025c9423.
2016-11-23 12:09:09 -08:00
ba52f2f252 Revert "refactor(compiler): remove asset: urls"
This reverts commit 0c98f45105.
2016-11-23 12:09:08 -08:00
e122f6bf0f Revert "refactor(compiler): add createAotCompiler factory"
This reverts commit 170525a225.
2016-11-23 12:09:07 -08:00
453c758d1a Revert "refactor(compiler): move symbol extraction to AotCompiler"
This reverts commit b5afe51b26.
2016-11-23 12:09:06 -08:00
015ca47336 Revert "fix(compiler): fix versions of @angular/tsc-wrapped"
This reverts commit 2fe6fb1163.
2016-11-23 12:09:04 -08:00
f32e287812 Revert "refactor(tsc-wrapped): collect all exported functions and classes and bump metadata version from 1 to 2"
This reverts commit 39a71eb0ec.
2016-11-23 12:08:46 -08:00
9946ac5cc7 Revert "refactor(compiler): renames"
This reverts commit 38be2b81c6.
2016-11-23 12:08:45 -08:00
593e05dc97 Revert "refactor(comiler): various cleanups"
This reverts commit ef38676091.
2016-11-23 12:08:44 -08:00
da77b580c9 Revert "refactor(compiler): Reintroduce ReflectorHost and move Extractor into @angular/compiler"
This reverts commit 64bd672e3a.
2016-11-23 12:08:42 -08:00
1733ea09bd Revert "refactor(compiler): further minor fixes"
This reverts commit 3d407fc010.
2016-11-23 12:08:41 -08:00
1f4fa28fac Revert "refactor(compiler): allow control of StaticSymbol lifetime (#12986)"
This reverts commit 2ca67e1674.
2016-11-23 12:08:41 -08:00
c12e56ec0c Revert "fix(animations): blend in all previously transitioned styles into next animation if interrupted (#13014)"
This reverts commit ea4fc9b421.
2016-11-23 12:08:40 -08:00
4a5c8bd25f Revert "test(upgrade): remove setTimeout from lifecycle hook tests (#13027)"
This reverts commit a4ab14bf74.
2016-11-23 12:08:38 -08:00
9c954740d1 chore(release): cut tsc-wrapped 0.4.1 release 2016-11-22 14:51:47 -08:00
11ed8f56ab docs(changelog): add changelog for 2.2.2 2016-11-22 14:36:49 -08:00
a49acbf027 chore(release): cut the 2.2.2 release 2016-11-22 14:33:08 -08:00
8e41910429 fix(build): update versions of umd bundles (#13038)
Fixes #13037
2016-11-22 14:26:20 -08:00
a4ab14bf74 test(upgrade): remove setTimeout from lifecycle hook tests (#13027)
* test(upgrade): remove unnecessary NO_ERRORS_SCHEMA

* test(upgrade): remove `setTimeout` from lifecycle hook tests

Closes #13019
2016-11-22 14:26:20 -08:00
ea4fc9b421 fix(animations): blend in all previously transitioned styles into next animation if interrupted (#13014)
Closes #13013
Closes #13014
2016-11-22 14:26:20 -08:00
0956acee58 fix(closure): quote date pattern aliases (#13012)
Quota the pattern aliases to prevent closure renaming. These are quoted in DatePipe and also need to be quoted here.
2016-11-22 14:26:20 -08:00
2ca67e1674 refactor(compiler): allow control of StaticSymbol lifetime (#12986) 2016-11-22 14:26:20 -08:00
472666fc2b refactor(ngUpgrade): Small cleanup with Testability API and resumeBootstrap (#12926)
* With non-static ngUpgrade apps, callbacks to `whenStable` were being invoked with the wrong
  context
* With non-static ngUpgrade apps, `resumeBootstrap` was being run outside the NgZone
* Remove redundent `whenStableContext` variable

Neither of the first two problems were actually causing bugs (as far as I know), but they *might*
have caused problems in the future.

Inspired by https://github.com/angular/angular/pull/12910, but for non-static apps.
2016-11-22 14:26:20 -08:00
462316b0f1 fix(upgrade): call ng1 lifecycle hooks (#12875) 2016-11-22 14:26:20 -08:00
96c2b2cc25 fix(changelog): replace beta.1 with beta.0 (#12961) 2016-11-22 14:26:19 -08:00
3d407fc010 refactor(compiler): further minor fixes 2016-11-22 14:26:19 -08:00
64bd672e3a refactor(compiler): Reintroduce ReflectorHost and move Extractor into @angular/compiler 2016-11-22 14:26:19 -08:00
ef38676091 refactor(comiler): various cleanups 2016-11-22 14:26:19 -08:00
38be2b81c6 refactor(compiler): renames
- `NgHost` to `CompilerHost`
- `AotCompilerHost.resolveFileToImport` to `AotCompilerHost.fileNameToModuleName`
- `AotCompilerHoset.resolveImportToFile` to `AotCompilerHost.moduleNameToFileName`
2016-11-22 14:26:19 -08:00
39a71eb0ec refactor(tsc-wrapped): collect all exported functions and classes and bump metadata version from 1 to 2
This is needed to resolve symbols without `.d.ts` files.
This bumps the version of the metadata from 1 to 2.
This adds logic into `ng_host.ts` to automatically upgrade
version 1 to version 2 metadata by adding the exported symbols
from the `.d.ts` file.
2016-11-22 14:26:19 -08:00
2fe6fb1163 fix(compiler): fix versions of @angular/tsc-wrapped 2016-11-22 14:26:19 -08:00
b5afe51b26 refactor(compiler): move symbol extraction to AotCompiler 2016-11-22 14:26:19 -08:00
170525a225 refactor(compiler): add createAotCompiler factory
Also adds 2 more methods to the `AotCompilerHost`:
- `loadResource`
- `resolveFileToImport`
2016-11-22 14:26:19 -08:00
0c98f45105 refactor(compiler): remove asset: urls
These urls were just relicts from Dart.
2016-11-22 14:26:19 -08:00
e7025c9423 refactor(compiler): move findDeclaration into the StaticReflector
Previously, this was part of the `AotCompilerHost`.
The `AotCompilerHost` is now also greatly simplified.
2016-11-22 14:26:18 -08:00
8f295287a2 refactor(compiler): move static_reflector into @angular/compiler and rename files
- `src/runtime_compiler.ts` -> `src/jit/compiler.ts`
- `src/compiler.ts` -> `src/jit/compiler_factory.ts`
- `src/offline_compiler` -> `src/aot/compiler.ts`

Part of #12867
2016-11-22 14:26:18 -08:00
030facc66a chore(build): update package.json versions during build (#12957) 2016-11-22 14:26:18 -08:00
45af8f6752 fix(ci): pin version of npm on CircleCI (#12954) 2016-11-22 14:26:18 -08:00
33a79028be fix(benchmarks): use sanitized style values (#12943) 2016-11-22 14:26:18 -08:00
09226d96f8 fix(router): support redirects to named outlets
Closes #12740, #9921
2016-11-22 14:26:18 -08:00
6c3166e6e4 chore(release): cut the 2.3.0-beta.0 realse and add change log 2016-11-22 14:26:18 -08:00
8df328b15a fix(router): add a banner file for the router (#12919) 2016-11-22 14:26:17 -08:00
115f18fa06 fix(router): removes a peer dependency from router to upgrade 2016-11-22 14:26:16 -08:00
511cd4d182 fix(router): add a banner file for the router (#12919) 2016-11-22 14:26:14 -08:00
87d5d49530 fix(router): removes a peer dependency from router to upgrade 2016-11-22 14:26:14 -08:00
933caacad3 chore(release): cut angular 2.2.1 2016-11-16 16:38:28 -08:00
efe9c4f35c fix(tools): fix error when running test.sh (#12927) 2016-11-16 16:26:44 -08:00
5b0f9e2f51 refactor(compiler): allows synchronous retrieving of metadata (#12908)
Allows non-normalized metadata to be retrieved synchronously.

Related to #7482
2016-11-16 16:26:44 -08:00
462879887a fix(core): support ngTemplateOutlet in production mode (#12921)
Fixes #12911
2016-11-16 16:26:44 -08:00
dae0d0fd66 docs(upgrade/static): improve API docs with examples
Closes #12717
2016-11-16 16:26:44 -08:00
c7f750dd5a chore(public_api): remove Angular 1 types from upgrade/static API 2016-11-16 16:26:44 -08:00
73de925551 chore(examples): add upgrade/static example 2016-11-16 16:26:44 -08:00
547c22029a chore(examples): support upgrade/static examples 2016-11-16 16:26:44 -08:00
364642d58c fix(router): add a banner file for the router (#12919) 2016-11-16 16:26:44 -08:00
7b67badc43 fix(platform_browser): fix disableDebugTools() (#12918) 2016-11-16 16:26:44 -08:00
dc1662a447 fix(ngUpgrade): make AoT ngUpgrade work with the testability API and resumeBootstrap() (#12910) 2016-11-16 16:26:43 -08:00
b5f433626b build(build.sh): echo before building examples 2016-11-16 16:26:43 -08:00
dabaf858d9 fix(router): should not create a route state if navigation is canceled (#12868)
Closes #12776
2016-11-16 16:26:43 -08:00
bbc3c9ce0e refactor(forms): remove facade (#12558) 2016-11-16 16:26:43 -08:00
1dcf1f484e fix(router): removes a peer dependency from router to upgrade 2016-11-16 16:26:43 -08:00
583d2833db fix(animations): only pass in same typed players as previous players into web-animations (#12907)
Closes #12907
2016-11-16 16:26:43 -08:00
f502a768d3 chore(router): remove @angular/upgrade peer dep (#12896) 2016-11-16 16:26:43 -08:00
16303ac487 refactor(http): remove all facade methods from http module (#12870) 2016-11-16 16:26:43 -08:00
6cdc3b5c12 fix(tsickle): support ctorParams in function closure (#12876)
See https://github.com/angular/tsickle/issues/261 for context.
2016-11-16 16:26:43 -08:00
5c46c493f2 fix(animations): retain styling when transition destinations are changed (#12208)
Closes #9661
Closes #12208
2016-11-16 16:26:43 -08:00
e02c18049d fix(select): allow for null values in HTML select options bound with ngValue
closes #12829
2016-11-16 16:26:43 -08:00
e0ce5458a2 fix: allow for null values in HTML select options bound with ngValue
This corrects the case of <option [ngValue]="null"> binding a string like "{0: null}" to the model instead of an actual null object.

Closes #10349
2016-11-16 16:26:43 -08:00
6a5ba0ec81 fix: allow for null values in HTML select options bound with ngValue
This corrects the case of <option [ngValue]="null"> binding a string like "{0: null}" to the model instead of an actual null object.

Closes #10349
2016-11-16 16:26:42 -08:00
828c0d24eb refactor(core): remove dead code (#12871) 2016-11-16 16:26:42 -08:00
22536442d6 refactor(core): remove ListWrapper from i18n 2016-11-16 16:26:42 -08:00
845ea235ee fix(http): return request url if it cannot be retrieved from response
closes #12837
2016-11-16 16:26:42 -08:00
21a4de999b fix(http): correctly handle response body for 204 status code
closes #12830
fixes #12393
2016-11-16 16:26:42 -08:00
82b34838bf refactor(xhr_backend): remove facade 2016-11-16 16:26:42 -08:00
89 changed files with 2162 additions and 623 deletions

View File

@ -1,3 +1,74 @@
<a name="2.2.3"></a>
## [2.2.3](https://github.com/angular/angular/compare/2.2.2...2.2.3) (2016-11-23)
### Bug Fixes
* **compiler:** Revert: fix versions of `@angular/tsc-wrapped` ([015ca47](https://github.com/angular/angular/commit/015ca47))
* **animations:** Revert: blend in all previously transitioned styles into next animation if interrupted ([c12e56e](https://github.com/angular/angular/commit/c12e56e))
<a name="2.2.2"></a>
## [2.2.2](https://github.com/angular/angular/compare/2.2.1...2.2.2) (2016-11-22)
### Bug Fixes
* **animations:** blend in all previously transitioned styles into next animation if interrupted ([#13014](https://github.com/angular/angular/issues/13014)) ([ea4fc9b](https://github.com/angular/angular/commit/ea4fc9b)), closes [#13013](https://github.com/angular/angular/issues/13013)
* **benchmarks:** use sanitized style values ([#12943](https://github.com/angular/angular/issues/12943)) ([33a7902](https://github.com/angular/angular/commit/33a7902))
* **closure:** quote date pattern aliases ([#13012](https://github.com/angular/angular/issues/13012)) ([0956ace](https://github.com/angular/angular/commit/0956ace))
* **compiler:** fix versions of `@angular/tsc-wrapped` ([2fe6fb1](https://github.com/angular/angular/commit/2fe6fb1))
* **router:** add a banner file for the router ([#12919](https://github.com/angular/angular/issues/12919)) ([8df328b](https://github.com/angular/angular/commit/8df328b))
* **router:** add a banner file for the router ([#12919](https://github.com/angular/angular/issues/12919)) ([511cd4d](https://github.com/angular/angular/commit/511cd4d))
* **router:** removes a peer dependency from router to upgrade ([115f18f](https://github.com/angular/angular/commit/115f18f))
* **router:** removes a peer dependency from router to upgrade ([87d5d49](https://github.com/angular/angular/commit/87d5d49))
* **router:** support redirects to named outlets ([09226d9](https://github.com/angular/angular/commit/09226d9)), closes [#12740](https://github.com/angular/angular/issues/12740) [#9921](https://github.com/angular/angular/issues/9921)
* **upgrade:** call ng1 lifecycle hooks ([#12875](https://github.com/angular/angular/issues/12875)) ([462316b](https://github.com/angular/angular/commit/462316b))
<a name="2.3.0-beta.0"></a>
# [2.3.0-beta.0](https://github.com/angular/angular/compare/2.2.0...2.3.0-beta.0) (2016-11-17)
### Bug Fixes
* **compiler:** assert xliff messages have translations ([7908679](https://github.com/angular/angular/commit/7908679)), closes [#12815](https://github.com/angular/angular/issues/12815) [#12604](https://github.com/angular/angular/issues/12604)
* **compiler:** updates hash algo for xmb/xtb files ([2f14415](https://github.com/angular/angular/commit/2f14415))
* **core:** fix placeholders handling in i18n. ([76e4911](https://github.com/angular/angular/commit/76e4911)), closes [#12512](https://github.com/angular/angular/issues/12512)
* **core:** misc i18n fixes ([ed5e98d](https://github.com/angular/angular/commit/ed5e98d))
* **core:** xmb serializer uses decimal messaged IDs ([08c038e](https://github.com/angular/angular/commit/08c038e)), closes [#12511](https://github.com/angular/angular/issues/12511)
* **platform-browser:** enable AOT ([efbbefd](https://github.com/angular/angular/commit/efbbefd)), closes [#12783](https://github.com/angular/angular/issues/12783)
### Features
* **core:** add `attachView` / `detachView` to ApplicationRef ([9f7d32a](https://github.com/angular/angular/commit/9f7d32a)), closes [#9293](https://github.com/angular/angular/issues/9293)
* **core:** expose `ViewRef` as `ChangeDetectorRef` ([1b5384e](https://github.com/angular/angular/commit/1b5384e)), closes [#12722](https://github.com/angular/angular/issues/12722)
* **core:** implements a decimal fingerprint for i18n ([582550a](https://github.com/angular/angular/commit/582550a))
* **router:** register router with ngprobe ([c2fae72](https://github.com/angular/angular/commit/c2fae72))
* **router_link:** add skipLocationChange and replaceUrl inputs ([#12850](https://github.com/angular/angular/issues/12850)) ([46d1502](https://github.com/angular/angular/commit/46d1502))
Note: The 2.3.0-beta.0 release also contains all the changes present in the 2.2.1 release.
<a name="2.2.1"></a>
## [2.2.1](https://github.com/angular/angular/compare/2.2.0...2.2.1) (2016-11-17)
### Bug Fixes
* **animations:** only pass in same typed players as previous players into web-animations ([#12907](https://github.com/angular/angular/issues/12907)) ([583d283](https://github.com/angular/angular/commit/583d283))
* **animations:** retain styling when transition destinations are changed ([#12208](https://github.com/angular/angular/issues/12208)) ([5c46c49](https://github.com/angular/angular/commit/5c46c49)), closes [#9661](https://github.com/angular/angular/issues/9661)
* **core:** support `ngTemplateOutlet` in production mode ([#12921](https://github.com/angular/angular/issues/12921)) ([4628798](https://github.com/angular/angular/commit/4628798)), closes [#12911](https://github.com/angular/angular/issues/12911)
* **http:** correctly handle response body for 204 status code ([21a4de9](https://github.com/angular/angular/commit/21a4de9)), closes [#12830](https://github.com/angular/angular/issues/12830) [#12393](https://github.com/angular/angular/issues/12393)
* **http:** return request url if it cannot be retrieved from response ([845ea23](https://github.com/angular/angular/commit/845ea23)), closes [#12837](https://github.com/angular/angular/issues/12837)
* **upgrade:** make AoT ngUpgrade work with the testability API and resumeBootstrap() ([#12910](https://github.com/angular/angular/issues/12910)) ([dc1662a](https://github.com/angular/angular/commit/dc1662a))
* **platform-browser:** fix disableDebugTools() ([#12918](https://github.com/angular/angular/issues/12918)) ([7b67bad](https://github.com/angular/angular/commit/7b67bad))
* **router:** add a banner file for the router ([#12919](https://github.com/angular/angular/issues/12919)) ([364642d](https://github.com/angular/angular/commit/364642d))
* **router:** removes a peer dependency from router to upgrade ([1dcf1f4](https://github.com/angular/angular/commit/1dcf1f4))
* **forms** allow for null values in HTML select options bound with ngValue ([e0ce545](https://github.com/angular/angular/commit/e0ce545)), closes [#10349](https://github.com/angular/angular/issues/10349)
* **router:** should not create a route state if navigation is canceled ([#12868](https://github.com/angular/angular/issues/12868)) ([dabaf85](https://github.com/angular/angular/commit/dabaf85)), closes [#12776](https://github.com/angular/angular/issues/12776)
* **common:** select should allow for null values in HTML select options bound with ngValue ([e02c180](https://github.com/angular/angular/commit/e02c180)), closes [#12829](https://github.com/angular/angular/issues/12829)
* **compiler-cli:** support ctorParams in function closure ([#12876](https://github.com/angular/angular/issues/12876)) ([6cdc3b5](https://github.com/angular/angular/commit/6cdc3b5))
<a name="2.2.0"></a> <a name="2.2.0"></a>
# [2.2.0 upgrade-firebooster](https://github.com/angular/angular/compare/2.2.0-rc.0...2.2.0) (2016-11-14) # [2.2.0 upgrade-firebooster](https://github.com/angular/angular/compare/2.2.0-rc.0...2.2.0) (2016-11-14)

View File

@ -20,6 +20,10 @@ PACKAGES=(core
benchpress) benchpress)
BUILD_ALL=true BUILD_ALL=true
BUNDLE=true BUNDLE=true
VERSION_PREFIX=$(node -p "require('./package.json').version")
VERSION_SUFFIX="-$(git log --oneline -1 | awk '{print $1}')"
ROUTER_VERSION_PREFIX=$(node -p "require('./package.json').version.replace(/^2/, '3')")
REMOVE_BENCHPRESS=false
for ARG in "$@"; do for ARG in "$@"; do
case "$ARG" in case "$ARG" in
@ -31,6 +35,10 @@ for ARG in "$@"; do
--bundle=*) --bundle=*)
BUNDLE=( "${ARG#--bundle=}" ) BUNDLE=( "${ARG#--bundle=}" )
;; ;;
--publish)
VERSION_SUFFIX=""
REMOVE_BENCHPRESS=true
;;
*) *)
echo "Unknown option $ARG." echo "Unknown option $ARG."
exit 1 exit 1
@ -38,6 +46,10 @@ for ARG in "$@"; do
esac esac
done done
VERSION="${VERSION_PREFIX}${VERSION_SUFFIX}"
ROUTER_VERSION="${ROUTER_VERSION_PREFIX}${VERSION_SUFFIX}"
echo "====== BUILDING: Version ${VERSION} (Router ${ROUTER_VERSION})"
export NODE_PATH=${NODE_PATH}:$(pwd)/dist/all:$(pwd)/dist/tools export NODE_PATH=${NODE_PATH}:$(pwd)/dist/all:$(pwd)/dist/tools
TSC="node --max-old-space-size=3000 dist/tools/@angular/tsc-wrapped/src/main" TSC="node --max-old-space-size=3000 dist/tools/@angular/tsc-wrapped/src/main"
UGLIFYJS=`pwd`/node_modules/.bin/uglifyjs UGLIFYJS=`pwd`/node_modules/.bin/uglifyjs
@ -106,7 +118,13 @@ do
UMD_ES5_MIN_PATH=${DESTDIR}/bundles/${PACKAGE}.umd.min.js UMD_ES5_MIN_PATH=${DESTDIR}/bundles/${PACKAGE}.umd.min.js
UMD_STATIC_ES5_MIN_PATH=${DESTDIR}/bundles/${PACKAGE}-static.umd.min.js UMD_STATIC_ES5_MIN_PATH=${DESTDIR}/bundles/${PACKAGE}-static.umd.min.js
UMD_UPGRADE_ES5_MIN_PATH=${DESTDIR}/bundles/${PACKAGE}-upgrade.umd.min.js UMD_UPGRADE_ES5_MIN_PATH=${DESTDIR}/bundles/${PACKAGE}-upgrade.umd.min.js
LICENSE_BANNER=${PWD}/modules/@angular/license-banner.txt
if [[ ${PACKAGE} != router ]]; then
LICENSE_BANNER=${PWD}/modules/@angular/license-banner.txt
fi
if [[ ${PACKAGE} == router ]]; then
LICENSE_BANNER=${PWD}/modules/@angular/router-license-banner.txt
fi
rm -rf ${DESTDIR} rm -rf ${DESTDIR}
@ -191,8 +209,24 @@ do
$UGLIFYJS -c --screw-ie8 --comments -o ${UMD_UPGRADE_ES5_MIN_PATH} ${UMD_UPGRADE_ES5_PATH} $UGLIFYJS -c --screw-ie8 --comments -o ${UMD_UPGRADE_ES5_MIN_PATH} ${UMD_UPGRADE_ES5_PATH}
fi fi
) 2>&1 | grep -v "as external dependency" ) 2>&1 | grep -v "as external dependency"
fi fi
(
echo "====== VERSION: Updating version references"
cd ${DESTDIR}
echo "====== EXECUTE: perl -p -i -e \"s/0\.0\.0\-PLACEHOLDER/${VERSION}/g\" $""(grep -ril 0\.0\.0\-PLACEHOLDER .)"
perl -p -i -e "s/0\.0\.0\-PLACEHOLDER/${VERSION}/g" $(grep -ril 0\.0\.0\-PLACEHOLDER .) < /dev/null 2> /dev/null
echo "====== EXECUTE: perl -p -i -e \"s/0\.0\.0\-ROUTERPLACEHOLDER/${ROUTER_VERSION}/g\" $""(grep -ril 0\.0\.0\-ROUTERPLACEHOLDER .)"
perl -p -i -e "s/0\.0\.0\-ROUTERPLACEHOLDER/${ROUTER_VERSION}/g" $(grep -ril 0\.0\.0\-ROUTERPLACEHOLDER .) < /dev/null 2> /dev/null
)
done done
echo ""
echo "====== Building examples: ./modules/@angular/examples/build.sh ====="
./modules/@angular/examples/build.sh ./modules/@angular/examples/build.sh
if [[ ${REMOVE_BENCHPRESS} == true ]]; then
echo ""
echo "==== Removing benchpress from publication"
rm -r dist/packages-dist/benchpress
fi

View File

@ -4,7 +4,7 @@ machine:
dependencies: dependencies:
pre: pre:
- npm install -g npm - npm install -g npm@3.6.0
test: test:
override: override:

View File

@ -34,34 +34,34 @@ export class Extractor {
const programSymbols: StaticSymbol[] = const programSymbols: StaticSymbol[] =
extractProgramSymbols(this.program, this.staticReflector, this.reflectorHost, this.options); extractProgramSymbols(this.program, this.staticReflector, this.reflectorHost, this.options);
return compiler const {ngModules, files} = compiler.analyzeAndValidateNgModules(
.analyzeNgModules(programSymbols, {transitiveModules: true}, this.metadataResolver) programSymbols, {transitiveModules: true}, this.metadataResolver);
.then(({files}) => { return compiler.loadNgModuleDirectives(ngModules).then(() => {
const errors: compiler.ParseError[] = []; const errors: compiler.ParseError[] = [];
files.forEach(file => { files.forEach(file => {
const compMetas: compiler.CompileDirectiveMetadata[] = []; const compMetas: compiler.CompileDirectiveMetadata[] = [];
file.directives.forEach(directiveType => { file.directives.forEach(directiveType => {
const dirMeta = this.metadataResolver.getDirectiveMetadata(directiveType); const dirMeta = this.metadataResolver.getDirectiveMetadata(directiveType);
if (dirMeta && dirMeta.isComponent) { if (dirMeta && dirMeta.isComponent) {
compMetas.push(dirMeta); compMetas.push(dirMeta);
}
});
compMetas.forEach(compMeta => {
const html = compMeta.template.template;
const interpolationConfig =
compiler.InterpolationConfig.fromArray(compMeta.template.interpolation);
errors.push(
...this.messageBundle.updateFromTemplate(html, file.srcUrl, interpolationConfig));
});
});
if (errors.length) {
throw new Error(errors.map(e => e.toString()).join('\n'));
} }
return this.messageBundle;
}); });
compMetas.forEach(compMeta => {
const html = compMeta.template.template;
const interpolationConfig =
compiler.InterpolationConfig.fromArray(compMeta.template.interpolation);
errors.push(
...this.messageBundle.updateFromTemplate(html, file.srcUrl, interpolationConfig));
});
});
if (errors.length) {
throw new Error(errors.map(e => e.toString()).join('\n'));
}
return this.messageBundle;
});
} }
static create( static create(

View File

@ -132,7 +132,6 @@ export class StaticReflector implements ReflectorReader {
const ctor = (<any[]>ctorData).find(a => a['__symbolic'] == 'constructor'); const ctor = (<any[]>ctorData).find(a => a['__symbolic'] == 'constructor');
const parameterTypes = <any[]>this.simplify(type, ctor['parameters'] || []); const parameterTypes = <any[]>this.simplify(type, ctor['parameters'] || []);
const parameterDecorators = <any[]>this.simplify(type, ctor['parameterDecorators'] || []); const parameterDecorators = <any[]>this.simplify(type, ctor['parameterDecorators'] || []);
parameters = []; parameters = [];
parameterTypes.forEach((paramType, index) => { parameterTypes.forEach((paramType, index) => {
const nestedResult: any[] = []; const nestedResult: any[] = [];

View File

@ -7,7 +7,7 @@
*/ */
import {StaticReflector, StaticReflectorHost, StaticSymbol} from '@angular/compiler-cli/src/static_reflector'; import {StaticReflector, StaticReflectorHost, StaticSymbol} from '@angular/compiler-cli/src/static_reflector';
import {HostListener, animate, group, keyframes, sequence, state, style, transition, trigger} from '@angular/core'; import {HostListener, Inject, animate, group, keyframes, sequence, state, style, transition, trigger} from '@angular/core';
import {MetadataCollector} from '@angular/tsc-wrapped'; import {MetadataCollector} from '@angular/tsc-wrapped';
import * as ts from 'typescript'; import * as ts from 'typescript';
@ -410,6 +410,13 @@ describe('StaticReflector', () => {
expect(props).toEqual({foo: []}); expect(props).toEqual({foo: []});
}); });
it('should read ctor parameters with forwardRef', () => {
const src = '/tmp/src/forward-ref.ts';
const dep = host.getStaticSymbol(src, 'Dep');
const props = reflector.parameters(host.getStaticSymbol(src, 'Forward'));
expect(props).toEqual([[dep, new Inject(dep)]]);
});
it('should report an error for invalid function calls', () => { it('should report an error for invalid function calls', () => {
expect( expect(
() => () =>
@ -1068,6 +1075,18 @@ class MockReflectorHost implements StaticReflectorHost {
providers: [ { provider: 'a', useValue: (() => 1)() }] providers: [ { provider: 'a', useValue: (() => 1)() }]
}) })
export class InvalidMetadata {} export class InvalidMetadata {}
`,
'/tmp/src/forward-ref.ts': `
import {forwardRef} from 'angular2/core';
import {Component} from 'angular2/src/core/metadata';
import {Inject} from 'angular2/src/core/di/metadata';
@Component({})
export class Forward {
constructor(@Inject(forwardRef(() => Dep)) d: Dep) {}
}
export class Dep {
@Input f: Forward;
}
` `
}; };

View File

@ -41,7 +41,9 @@ const _ANIMATION_TIME_VAR = o.variable('totalTime');
const _ANIMATION_START_STATE_STYLES_VAR = o.variable('startStateStyles'); const _ANIMATION_START_STATE_STYLES_VAR = o.variable('startStateStyles');
const _ANIMATION_END_STATE_STYLES_VAR = o.variable('endStateStyles'); const _ANIMATION_END_STATE_STYLES_VAR = o.variable('endStateStyles');
const _ANIMATION_COLLECTED_STYLES = o.variable('collectedStyles'); const _ANIMATION_COLLECTED_STYLES = o.variable('collectedStyles');
const EMPTY_MAP = o.literalMap([]); const _PREVIOUS_ANIMATION_PLAYERS = o.variable('previousPlayers');
const _EMPTY_MAP = o.literalMap([]);
const _EMPTY_ARRAY = o.literalArr([]);
class _AnimationBuilder implements AnimationAstVisitor { class _AnimationBuilder implements AnimationAstVisitor {
private _fnVarName: string; private _fnVarName: string;
@ -110,10 +112,15 @@ class _AnimationBuilder implements AnimationAstVisitor {
_callAnimateMethod( _callAnimateMethod(
ast: AnimationStepAst, startingStylesExpr: any, keyframesExpr: any, ast: AnimationStepAst, startingStylesExpr: any, keyframesExpr: any,
context: _AnimationBuilderContext) { context: _AnimationBuilderContext) {
let previousStylesValue: o.Expression = _EMPTY_ARRAY;
if (context.isExpectingFirstAnimateStep) {
previousStylesValue = _PREVIOUS_ANIMATION_PLAYERS;
context.isExpectingFirstAnimateStep = false;
}
context.totalTransitionTime += ast.duration + ast.delay; context.totalTransitionTime += ast.duration + ast.delay;
return _ANIMATION_FACTORY_RENDERER_VAR.callMethod('animate', [ return _ANIMATION_FACTORY_RENDERER_VAR.callMethod('animate', [
_ANIMATION_FACTORY_ELEMENT_VAR, startingStylesExpr, keyframesExpr, o.literal(ast.duration), _ANIMATION_FACTORY_ELEMENT_VAR, startingStylesExpr, keyframesExpr, o.literal(ast.duration),
o.literal(ast.delay), o.literal(ast.easing) o.literal(ast.delay), o.literal(ast.easing), previousStylesValue
]); ]);
} }
@ -150,6 +157,7 @@ class _AnimationBuilder implements AnimationAstVisitor {
context.totalTransitionTime = 0; context.totalTransitionTime = 0;
context.isExpectingFirstStyleStep = true; context.isExpectingFirstStyleStep = true;
context.isExpectingFirstAnimateStep = true;
const stateChangePreconditions: o.Expression[] = []; const stateChangePreconditions: o.Expression[] = [];
@ -187,17 +195,16 @@ class _AnimationBuilder implements AnimationAstVisitor {
context.stateMap.registerState(DEFAULT_STATE, {}); context.stateMap.registerState(DEFAULT_STATE, {});
const statements: o.Statement[] = []; const statements: o.Statement[] = [];
statements.push(_ANIMATION_FACTORY_VIEW_CONTEXT statements.push(_PREVIOUS_ANIMATION_PLAYERS
.callMethod( .set(_ANIMATION_FACTORY_VIEW_CONTEXT.callMethod(
'cancelActiveAnimation', 'getAnimationPlayers',
[ [
_ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName), _ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName),
_ANIMATION_NEXT_STATE_VAR.equals(o.literal(EMPTY_STATE)) _ANIMATION_NEXT_STATE_VAR.equals(o.literal(EMPTY_STATE))
]) ]))
.toStmt()); .toDeclStmt());
statements.push(_ANIMATION_COLLECTED_STYLES.set(_EMPTY_MAP).toDeclStmt());
statements.push(_ANIMATION_COLLECTED_STYLES.set(EMPTY_MAP).toDeclStmt());
statements.push(_ANIMATION_PLAYER_VAR.set(o.NULL_EXPR).toDeclStmt()); statements.push(_ANIMATION_PLAYER_VAR.set(o.NULL_EXPR).toDeclStmt());
statements.push(_ANIMATION_TIME_VAR.set(o.literal(0)).toDeclStmt()); statements.push(_ANIMATION_TIME_VAR.set(o.literal(0)).toDeclStmt());
@ -223,17 +230,6 @@ class _AnimationBuilder implements AnimationAstVisitor {
const RENDER_STYLES_FN = o.importExpr(resolveIdentifier(Identifiers.renderStyles)); const RENDER_STYLES_FN = o.importExpr(resolveIdentifier(Identifiers.renderStyles));
// before we start any animation we want to clear out the starting
// styles from the element's style property (since they were placed
// there at the end of the last animation
statements.push(RENDER_STYLES_FN
.callFn([
_ANIMATION_FACTORY_ELEMENT_VAR, _ANIMATION_FACTORY_RENDERER_VAR,
o.importExpr(resolveIdentifier(Identifiers.clearStyles))
.callFn([_ANIMATION_START_STATE_STYLES_VAR])
])
.toStmt());
ast.stateTransitions.forEach(transAst => statements.push(transAst.visit(this, context))); ast.stateTransitions.forEach(transAst => statements.push(transAst.visit(this, context)));
// this check ensures that the animation factory always returns a player // this check ensures that the animation factory always returns a player
@ -269,6 +265,22 @@ class _AnimationBuilder implements AnimationAstVisitor {
])]) ])])
.toStmt()); .toStmt());
statements.push(o.importExpr(resolveIdentifier(Identifiers.AnimationSequencePlayer))
.instantiate([_PREVIOUS_ANIMATION_PLAYERS])
.callMethod('destroy', [])
.toStmt());
// before we start any animation we want to clear out the starting
// styles from the element's style property (since they were placed
// there at the end of the last animation
statements.push(RENDER_STYLES_FN
.callFn([
_ANIMATION_FACTORY_ELEMENT_VAR, _ANIMATION_FACTORY_RENDERER_VAR,
o.importExpr(resolveIdentifier(Identifiers.clearStyles))
.callFn([_ANIMATION_START_STATE_STYLES_VAR])
])
.toStmt());
statements.push(_ANIMATION_FACTORY_VIEW_CONTEXT statements.push(_ANIMATION_FACTORY_VIEW_CONTEXT
.callMethod( .callMethod(
'queueAnimation', 'queueAnimation',
@ -304,7 +316,7 @@ class _AnimationBuilder implements AnimationAstVisitor {
const lookupMap: any[] = []; const lookupMap: any[] = [];
Object.keys(context.stateMap.states).forEach(stateName => { Object.keys(context.stateMap.states).forEach(stateName => {
const value = context.stateMap.states[stateName]; const value = context.stateMap.states[stateName];
let variableValue = EMPTY_MAP; let variableValue = _EMPTY_MAP;
if (isPresent(value)) { if (isPresent(value)) {
const styleMap: any[] = []; const styleMap: any[] = [];
Object.keys(value).forEach(key => { styleMap.push([key, o.literal(value[key])]); }); Object.keys(value).forEach(key => { styleMap.push([key, o.literal(value[key])]); });
@ -324,6 +336,7 @@ class _AnimationBuilderContext {
stateMap = new _AnimationBuilderStateMap(); stateMap = new _AnimationBuilderStateMap();
endStateAnimateStep: AnimationStepAst = null; endStateAnimateStep: AnimationStepAst = null;
isExpectingFirstStyleStep = false; isExpectingFirstStyleStep = false;
isExpectingFirstAnimateStep = false;
totalTransitionTime = 0; totalTransitionTime = 0;
} }

View File

@ -589,7 +589,7 @@ export interface CompileNgModuleDirectiveSummary extends CompileSummary {
exportedDirectives: CompileIdentifierMetadata[]; exportedDirectives: CompileIdentifierMetadata[];
exportedPipes: CompileIdentifierMetadata[]; exportedPipes: CompileIdentifierMetadata[];
exportedModules: CompileNgModuleDirectiveSummary[]; exportedModules: CompileNgModuleDirectiveSummary[];
loadingPromises: Promise<any>[]; directiveLoaders: (() => Promise<void>)[];
} }
export type CompileNgModuleSummary = export type CompileNgModuleSummary =
@ -661,7 +661,7 @@ export class CompileNgModuleMetadata implements CompileMetadataWithIdentifier {
exportedModules: this.exportedModules, exportedModules: this.exportedModules,
exportedDirectives: this.exportedDirectives, exportedDirectives: this.exportedDirectives,
exportedPipes: this.exportedPipes, exportedPipes: this.exportedPipes,
loadingPromises: this.transitiveModule.loadingPromises directiveLoaders: this.transitiveModule.directiveLoaders
}; };
} }
@ -682,7 +682,7 @@ export class CompileNgModuleMetadata implements CompileMetadataWithIdentifier {
exportedDirectives: this.exportedDirectives, exportedDirectives: this.exportedDirectives,
exportedPipes: this.exportedPipes, exportedPipes: this.exportedPipes,
exportedModules: this.exportedModules, exportedModules: this.exportedModules,
loadingPromises: this.transitiveModule.loadingPromises directiveLoaders: this.transitiveModule.directiveLoaders
}; };
} }
} }
@ -695,7 +695,7 @@ export class TransitiveCompileNgModuleMetadata {
public modules: CompileNgModuleInjectorSummary[], public providers: CompileProviderMetadata[], public modules: CompileNgModuleInjectorSummary[], public providers: CompileProviderMetadata[],
public entryComponents: CompileIdentifierMetadata[], public entryComponents: CompileIdentifierMetadata[],
public directives: CompileIdentifierMetadata[], public pipes: CompileIdentifierMetadata[], public directives: CompileIdentifierMetadata[], public pipes: CompileIdentifierMetadata[],
public loadingPromises: Promise<any>[]) { public directiveLoaders: (() => Promise<void>)[]) {
directives.forEach(dir => this.directivesSet.add(dir.reference)); directives.forEach(dir => this.directivesSet.add(dir.reference));
pipes.forEach(pipe => this.pipesSet.add(pipe.reference)); pipes.forEach(pipe => this.pipesSet.add(pipe.reference));
} }

View File

@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ListWrapper} from '../../facade/collection';
import * as ml from '../../ml_parser/ast'; import * as ml from '../../ml_parser/ast';
import {HtmlParser} from '../../ml_parser/html_parser'; import {HtmlParser} from '../../ml_parser/html_parser';
import {InterpolationConfig} from '../../ml_parser/interpolation_config'; import {InterpolationConfig} from '../../ml_parser/interpolation_config';
@ -162,7 +161,7 @@ class _WriteVisitor implements i18n.Visitor {
serialize(nodes: i18n.Node[]): xml.Node[] { serialize(nodes: i18n.Node[]): xml.Node[] {
this._isInIcu = false; this._isInIcu = false;
return ListWrapper.flatten(nodes.map(node => node.visit(this))); return [].concat(...nodes.map(node => node.visit(this)));
} }
} }

View File

@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ListWrapper} from '../../facade/collection';
import * as html from '../../ml_parser/ast'; import * as html from '../../ml_parser/ast';
import * as i18n from '../i18n_ast'; import * as i18n from '../i18n_ast';
import {MessageBundle} from '../message_bundle'; import {MessageBundle} from '../message_bundle';
@ -121,6 +120,6 @@ class _Visitor implements i18n.Visitor {
} }
serialize(nodes: i18n.Node[]): xml.Node[] { serialize(nodes: i18n.Node[]): xml.Node[] {
return ListWrapper.flatten(nodes.map(node => node.visit(this))); return [].concat(...nodes.map(node => node.visit(this)));
} }
} }

View File

@ -146,97 +146,43 @@ export class CompileMetadataResolver {
return; return;
} }
directiveType = resolveForwardRef(directiveType); directiveType = resolveForwardRef(directiveType);
const dirMeta = this._directiveResolver.resolve(directiveType); const nonNormalizedMetadata = this.getNonNormalizedDirectiveMetadata(directiveType);
if (!dirMeta) {
return null;
}
let moduleUrl = staticTypeModuleUrl(directiveType);
const createDirectiveMetadata = (templateMeta: cpl.CompileTemplateMetadata) => { const createDirectiveMetadata = (templateMetadata: cpl.CompileTemplateMetadata) => {
let changeDetectionStrategy: ChangeDetectionStrategy = null; const normalizedDirMeta = new cpl.CompileDirectiveMetadata({
let viewProviders: Array<cpl.CompileProviderMetadata|cpl.CompileTypeMetadata|any[]> = []; type: nonNormalizedMetadata.type,
let entryComponentMetadata: cpl.CompileIdentifierMetadata[] = []; isComponent: nonNormalizedMetadata.isComponent,
let selector = dirMeta.selector; selector: nonNormalizedMetadata.selector,
exportAs: nonNormalizedMetadata.exportAs,
if (dirMeta instanceof Component) { changeDetection: nonNormalizedMetadata.changeDetection,
// Component inputs: nonNormalizedMetadata.inputs,
changeDetectionStrategy = dirMeta.changeDetection; outputs: nonNormalizedMetadata.outputs,
if (dirMeta.viewProviders) { hostListeners: nonNormalizedMetadata.hostListeners,
viewProviders = this._getProvidersMetadata( hostProperties: nonNormalizedMetadata.hostProperties,
dirMeta.viewProviders, entryComponentMetadata, hostAttributes: nonNormalizedMetadata.hostAttributes,
`viewProviders for "${stringify(directiveType)}"`); providers: nonNormalizedMetadata.providers,
} viewProviders: nonNormalizedMetadata.viewProviders,
if (dirMeta.entryComponents) { queries: nonNormalizedMetadata.queries,
entryComponentMetadata = viewQueries: nonNormalizedMetadata.viewQueries,
flattenAndDedupeArray(dirMeta.entryComponents) entryComponents: nonNormalizedMetadata.entryComponents,
.map((type) => this._getIdentifierMetadata(type, staticTypeModuleUrl(type))) template: templateMetadata
.concat(entryComponentMetadata);
}
if (!selector) {
selector = this._schemaRegistry.getDefaultComponentElementName();
}
} else {
// Directive
if (!selector) {
throw new Error(`Directive ${stringify(directiveType)} has no selector, please add it!`);
}
}
let providers: Array<cpl.CompileProviderMetadata|cpl.CompileTypeMetadata|any[]> = [];
if (isPresent(dirMeta.providers)) {
providers = this._getProvidersMetadata(
dirMeta.providers, entryComponentMetadata,
`providers for "${stringify(directiveType)}"`);
}
let queries: cpl.CompileQueryMetadata[] = [];
let viewQueries: cpl.CompileQueryMetadata[] = [];
if (isPresent(dirMeta.queries)) {
queries = this._getQueriesMetadata(dirMeta.queries, false, directiveType);
viewQueries = this._getQueriesMetadata(dirMeta.queries, true, directiveType);
}
const meta = cpl.CompileDirectiveMetadata.create({
selector: selector,
exportAs: dirMeta.exportAs,
isComponent: !!templateMeta,
type: this._getTypeMetadata(directiveType, moduleUrl),
template: templateMeta,
changeDetection: changeDetectionStrategy,
inputs: dirMeta.inputs,
outputs: dirMeta.outputs,
host: dirMeta.host,
providers: providers,
viewProviders: viewProviders,
queries: queries,
viewQueries: viewQueries,
entryComponents: entryComponentMetadata
}); });
this._directiveCache.set(directiveType, meta); this._directiveCache.set(directiveType, normalizedDirMeta);
this._directiveSummaryCache.set(directiveType, meta.toSummary()); this._directiveSummaryCache.set(directiveType, normalizedDirMeta.toSummary());
return meta; return normalizedDirMeta;
}; };
if (dirMeta instanceof Component) { if (nonNormalizedMetadata.isComponent) {
// component
moduleUrl = componentModuleUrl(this._reflector, directiveType, dirMeta);
assertArrayOfStrings('styles', dirMeta.styles);
assertArrayOfStrings('styleUrls', dirMeta.styleUrls);
assertInterpolationSymbols('interpolation', dirMeta.interpolation);
const animations = dirMeta.animations ?
dirMeta.animations.map(e => this.getAnimationEntryMetadata(e)) :
null;
const templateMeta = this._directiveNormalizer.normalizeTemplate({ const templateMeta = this._directiveNormalizer.normalizeTemplate({
componentType: directiveType, componentType: directiveType,
moduleUrl: moduleUrl, moduleUrl: nonNormalizedMetadata.type.moduleUrl,
encapsulation: dirMeta.encapsulation, encapsulation: nonNormalizedMetadata.template.encapsulation,
template: dirMeta.template, template: nonNormalizedMetadata.template.template,
templateUrl: dirMeta.templateUrl, templateUrl: nonNormalizedMetadata.template.templateUrl,
styles: dirMeta.styles, styles: nonNormalizedMetadata.template.styles,
styleUrls: dirMeta.styleUrls, styleUrls: nonNormalizedMetadata.template.styleUrls,
animations: animations, animations: nonNormalizedMetadata.template.animations,
interpolation: dirMeta.interpolation interpolation: nonNormalizedMetadata.template.interpolation
}); });
if (templateMeta.syncResult) { if (templateMeta.syncResult) {
createDirectiveMetadata(templateMeta.syncResult); createDirectiveMetadata(templateMeta.syncResult);
@ -254,6 +200,96 @@ export class CompileMetadataResolver {
} }
} }
getNonNormalizedDirectiveMetadata(directiveType: any): cpl.CompileDirectiveMetadata {
directiveType = resolveForwardRef(directiveType);
const dirMeta = this._directiveResolver.resolve(directiveType);
if (!dirMeta) {
return null;
}
let moduleUrl = staticTypeModuleUrl(directiveType);
let nonNormalizedTemplateMetadata: cpl.CompileTemplateMetadata;
if (dirMeta instanceof Component) {
// component
moduleUrl = componentModuleUrl(this._reflector, directiveType, dirMeta);
assertArrayOfStrings('styles', dirMeta.styles);
assertArrayOfStrings('styleUrls', dirMeta.styleUrls);
assertInterpolationSymbols('interpolation', dirMeta.interpolation);
const animations = dirMeta.animations ?
dirMeta.animations.map(e => this.getAnimationEntryMetadata(e)) :
null;
nonNormalizedTemplateMetadata = new cpl.CompileTemplateMetadata({
encapsulation: dirMeta.encapsulation,
template: dirMeta.template,
templateUrl: dirMeta.templateUrl,
styles: dirMeta.styles,
styleUrls: dirMeta.styleUrls,
animations: animations,
interpolation: dirMeta.interpolation
});
}
let changeDetectionStrategy: ChangeDetectionStrategy = null;
let viewProviders: Array<cpl.CompileProviderMetadata|cpl.CompileTypeMetadata|any[]> = [];
let entryComponentMetadata: cpl.CompileIdentifierMetadata[] = [];
let selector = dirMeta.selector;
if (dirMeta instanceof Component) {
// Component
changeDetectionStrategy = dirMeta.changeDetection;
if (dirMeta.viewProviders) {
viewProviders = this._getProvidersMetadata(
dirMeta.viewProviders, entryComponentMetadata,
`viewProviders for "${stringify(directiveType)}"`);
}
if (dirMeta.entryComponents) {
entryComponentMetadata =
flattenAndDedupeArray(dirMeta.entryComponents)
.map((type) => this._getIdentifierMetadata(type, staticTypeModuleUrl(type)))
.concat(entryComponentMetadata);
}
if (!selector) {
selector = this._schemaRegistry.getDefaultComponentElementName();
}
} else {
// Directive
if (!selector) {
throw new Error(`Directive ${stringify(directiveType)} has no selector, please add it!`);
}
}
let providers: Array<cpl.CompileProviderMetadata|cpl.CompileTypeMetadata|any[]> = [];
if (isPresent(dirMeta.providers)) {
providers = this._getProvidersMetadata(
dirMeta.providers, entryComponentMetadata, `providers for "${stringify(directiveType)}"`);
}
let queries: cpl.CompileQueryMetadata[] = [];
let viewQueries: cpl.CompileQueryMetadata[] = [];
if (isPresent(dirMeta.queries)) {
queries = this._getQueriesMetadata(dirMeta.queries, false, directiveType);
viewQueries = this._getQueriesMetadata(dirMeta.queries, true, directiveType);
}
return cpl.CompileDirectiveMetadata.create({
selector: selector,
exportAs: dirMeta.exportAs,
isComponent: !!nonNormalizedTemplateMetadata,
type: this._getTypeMetadata(directiveType, moduleUrl),
template: nonNormalizedTemplateMetadata,
changeDetection: changeDetectionStrategy,
inputs: dirMeta.inputs,
outputs: dirMeta.outputs,
host: dirMeta.host,
providers: providers,
viewProviders: viewProviders,
queries: queries,
viewQueries: viewQueries,
entryComponents: entryComponentMetadata
});
}
/** /**
* Gets the metadata for the given directive. * Gets the metadata for the given directive.
* This assumes `loadNgModuleMetadata` has been called first. * This assumes `loadNgModuleMetadata` has been called first.
@ -309,11 +345,20 @@ export class CompileMetadataResolver {
loadNgModuleMetadata(moduleType: any, isSync: boolean, throwIfNotFound = true): loadNgModuleMetadata(moduleType: any, isSync: boolean, throwIfNotFound = true):
{ngModule: cpl.CompileNgModuleMetadata, loading: Promise<any>} { {ngModule: cpl.CompileNgModuleMetadata, loading: Promise<any>} {
const ngModule = this._loadNgModuleMetadata(moduleType, isSync, throwIfNotFound); const ngModule = this._loadNgModuleMetadata(moduleType, isSync, throwIfNotFound);
const loading = const loading = ngModule ?
ngModule ? Promise.all(ngModule.transitiveModule.loadingPromises) : Promise.resolve(null); Promise.all(ngModule.transitiveModule.directiveLoaders.map(loader => loader())) :
Promise.resolve(null);
return {ngModule, loading}; return {ngModule, loading};
} }
/**
* Get the NgModule metadata without loading the directives.
*/
getUnloadedNgModuleMetadata(moduleType: any, isSync: boolean, throwIfNotFound = true):
cpl.CompileNgModuleMetadata {
return this._loadNgModuleMetadata(moduleType, isSync, throwIfNotFound);
}
private _loadNgModuleMetadata(moduleType: any, isSync: boolean, throwIfNotFound = true): private _loadNgModuleMetadata(moduleType: any, isSync: boolean, throwIfNotFound = true):
cpl.CompileNgModuleMetadata { cpl.CompileNgModuleMetadata {
moduleType = resolveForwardRef(moduleType); moduleType = resolveForwardRef(moduleType);
@ -396,10 +441,8 @@ export class CompileMetadataResolver {
transitiveModule.directives.push(declaredIdentifier); transitiveModule.directives.push(declaredIdentifier);
declaredDirectives.push(declaredIdentifier); declaredDirectives.push(declaredIdentifier);
this._addTypeToModule(declaredType, moduleType); this._addTypeToModule(declaredType, moduleType);
const loadingPromise = this._loadDirectiveMetadata(declaredType, isSync); transitiveModule.directiveLoaders.push(
if (loadingPromise) { () => this._loadDirectiveMetadata(declaredType, isSync));
transitiveModule.loadingPromises.push(loadingPromise);
}
} else if (this._pipeResolver.isPipe(declaredType)) { } else if (this._pipeResolver.isPipe(declaredType)) {
transitiveModule.pipesSet.add(declaredType); transitiveModule.pipesSet.add(declaredType);
transitiveModule.pipes.push(declaredIdentifier); transitiveModule.pipes.push(declaredIdentifier);
@ -525,10 +568,10 @@ export class CompileMetadataResolver {
const directives = const directives =
flattenArray(transitiveExportedModules.map((ngModule) => ngModule.exportedDirectives)); flattenArray(transitiveExportedModules.map((ngModule) => ngModule.exportedDirectives));
const pipes = flattenArray(transitiveExportedModules.map((ngModule) => ngModule.exportedPipes)); const pipes = flattenArray(transitiveExportedModules.map((ngModule) => ngModule.exportedPipes));
const loadingPromises = const directiveLoaders =
ListWrapper.flatten(transitiveExportedModules.map(ngModule => ngModule.loadingPromises)); ListWrapper.flatten(transitiveExportedModules.map(ngModule => ngModule.directiveLoaders));
return new cpl.TransitiveCompileNgModuleMetadata( return new cpl.TransitiveCompileNgModuleMetadata(
transitiveModules, providers, entryComponents, directives, pipes, loadingPromises); transitiveModules, providers, entryComponents, directives, pipes, directiveLoaders);
} }
private _getIdentifierMetadata(type: Type<any>, moduleUrl: string): private _getIdentifierMetadata(type: Type<any>, moduleUrl: string):
@ -584,20 +627,26 @@ export class CompileMetadataResolver {
return pipeSummary; return pipeSummary;
} }
private _loadPipeMetadata(pipeType: Type<any>): void { getOrLoadPipeMetadata(pipeType: any): cpl.CompilePipeMetadata {
pipeType = resolveForwardRef(pipeType); let pipeMeta = this._pipeCache.get(pipeType);
const pipeMeta = this._pipeResolver.resolve(pipeType);
if (!pipeMeta) { if (!pipeMeta) {
return null; pipeMeta = this._loadPipeMetadata(pipeType);
} }
return pipeMeta;
}
const meta = new cpl.CompilePipeMetadata({ private _loadPipeMetadata(pipeType: any): cpl.CompilePipeMetadata {
pipeType = resolveForwardRef(pipeType);
const pipeAnnotation = this._pipeResolver.resolve(pipeType);
const pipeMeta = new cpl.CompilePipeMetadata({
type: this._getTypeMetadata(pipeType, staticTypeModuleUrl(pipeType)), type: this._getTypeMetadata(pipeType, staticTypeModuleUrl(pipeType)),
name: pipeMeta.name, name: pipeAnnotation.name,
pure: pipeMeta.pure pure: pipeAnnotation.pure
}); });
this._pipeCache.set(pipeType, meta); this._pipeCache.set(pipeType, pipeMeta);
this._pipeSummaryCache.set(pipeType, meta.toSummary()); this._pipeSummaryCache.set(pipeType, pipeMeta.toSummary());
return pipeMeta;
} }
private _getDependenciesMetadata(typeOrFunc: Type<any>|Function, dependencies: any[]): private _getDependenciesMetadata(typeOrFunc: Type<any>|Function, dependencies: any[]):

View File

@ -27,17 +27,46 @@ export class SourceModule {
constructor(public fileUrl: string, public moduleUrl: string, public source: string) {} constructor(public fileUrl: string, public moduleUrl: string, public source: string) {}
} }
export interface NgAnalyzedModules {
ngModules: CompileNgModuleMetadata[];
ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>;
files: Array<{srcUrl: string, directives: StaticSymbol[], ngModules: StaticSymbol[]}>;
symbolsMissingModule?: StaticSymbol[];
}
// Returns all the source files and a mapping from modules to directives // Returns all the source files and a mapping from modules to directives
export function analyzeNgModules( export function analyzeNgModules(
programStaticSymbols: StaticSymbol[], options: {transitiveModules: boolean}, programStaticSymbols: StaticSymbol[], options: {transitiveModules: boolean},
metadataResolver: CompileMetadataResolver): Promise<{ metadataResolver: CompileMetadataResolver): NgAnalyzedModules {
ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>, const {ngModules, symbolsMissingModule} =
files: Array<{srcUrl: string, directives: StaticSymbol[], ngModules: StaticSymbol[]}> _createNgModules(programStaticSymbols, options, metadataResolver);
}> { return _analyzeNgModules(ngModules, symbolsMissingModule);
return _loadNgModules(programStaticSymbols, options, metadataResolver).then(_analyzeNgModules);
} }
function _analyzeNgModules(ngModuleMetas: CompileNgModuleMetadata[]) {
export function analyzeAndValidateNgModules(
programStaticSymbols: StaticSymbol[], options: {transitiveModules: boolean},
metadataResolver: CompileMetadataResolver): NgAnalyzedModules {
const result = analyzeNgModules(programStaticSymbols, options, metadataResolver);
if (result.symbolsMissingModule && result.symbolsMissingModule.length) {
const messages = result.symbolsMissingModule.map(
s => `Cannot determine the module for class ${s.name} in ${s.filePath}!`);
throw new Error(messages.join('\n'));
}
return result;
}
// Wait for the directives in the given modules have been loaded
export function loadNgModuleDirectives(ngModules: CompileNgModuleMetadata[]) {
return Promise
.all(ListWrapper.flatten(ngModules.map(
(ngModule) => ngModule.transitiveModule.directiveLoaders.map(loader => loader()))))
.then(() => {});
}
function _analyzeNgModules(
ngModuleMetas: CompileNgModuleMetadata[],
symbolsMissingModule: StaticSymbol[]): NgAnalyzedModules {
const moduleMetasByRef = new Map<any, CompileNgModuleMetadata>(); const moduleMetasByRef = new Map<any, CompileNgModuleMetadata>();
ngModuleMetas.forEach((ngModule) => moduleMetasByRef.set(ngModule.type.reference, ngModule)); ngModuleMetas.forEach((ngModule) => moduleMetasByRef.set(ngModule.type.reference, ngModule));
const ngModuleByPipeOrDirective = new Map<StaticSymbol, CompileNgModuleMetadata>(); const ngModuleByPipeOrDirective = new Map<StaticSymbol, CompileNgModuleMetadata>();
@ -78,10 +107,11 @@ function _analyzeNgModules(ngModuleMetas: CompileNgModuleMetadata[]) {
}); });
return { return {
// map directive/pipe to module // map directive/pipe to module
ngModuleByPipeOrDirective, ngModuleByPipeOrDirective,
// list modules and directives for every source file // list modules and directives for every source file
files, files,
ngModules: ngModuleMetas, symbolsMissingModule
}; };
} }
@ -100,13 +130,14 @@ export class OfflineCompiler {
compileModules(staticSymbols: StaticSymbol[], options: {transitiveModules: boolean}): compileModules(staticSymbols: StaticSymbol[], options: {transitiveModules: boolean}):
Promise<SourceModule[]> { Promise<SourceModule[]> {
return analyzeNgModules(staticSymbols, options, this._metadataResolver) const {ngModuleByPipeOrDirective, files, ngModules} =
.then(({ngModuleByPipeOrDirective, files}) => { analyzeAndValidateNgModules(staticSymbols, options, this._metadataResolver);
const sourceModules = files.map( return loadNgModuleDirectives(ngModules).then(() => {
file => this._compileSrcFile( const sourceModules = files.map(
file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.ngModules)); file => this._compileSrcFile(
return ListWrapper.flatten(sourceModules); file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.ngModules));
}); return ListWrapper.flatten(sourceModules);
});
} }
private _compileSrcFile( private _compileSrcFile(
@ -328,22 +359,21 @@ function _splitTypescriptSuffix(path: string): string[] {
// Load the NgModules and check // Load the NgModules and check
// that all directives / pipes that are present in the program // that all directives / pipes that are present in the program
// are also declared by a module. // are also declared by a module.
function _loadNgModules( function _createNgModules(
programStaticSymbols: StaticSymbol[], options: {transitiveModules: boolean}, programStaticSymbols: StaticSymbol[], options: {transitiveModules: boolean},
metadataResolver: CompileMetadataResolver): Promise<CompileNgModuleMetadata[]> { metadataResolver: CompileMetadataResolver):
{ngModules: CompileNgModuleMetadata[], symbolsMissingModule: StaticSymbol[]} {
const ngModules = new Map<any, CompileNgModuleMetadata>(); const ngModules = new Map<any, CompileNgModuleMetadata>();
const programPipesAndDirectives: StaticSymbol[] = []; const programPipesAndDirectives: StaticSymbol[] = [];
const ngModulePipesAndDirective = new Set<StaticSymbol>(); const ngModulePipesAndDirective = new Set<StaticSymbol>();
const loadingPromises: Promise<any>[] = [];
const addNgModule = (staticSymbol: any) => { const addNgModule = (staticSymbol: any) => {
if (ngModules.has(staticSymbol)) { if (ngModules.has(staticSymbol)) {
return false; return false;
} }
const {ngModule, loading} = metadataResolver.loadNgModuleMetadata(staticSymbol, false, false); const ngModule = metadataResolver.getUnloadedNgModuleMetadata(staticSymbol, false, false);
if (ngModule) { if (ngModule) {
ngModules.set(ngModule.type.reference, ngModule); ngModules.set(ngModule.type.reference, ngModule);
loadingPromises.push(loading);
ngModule.declaredDirectives.forEach((dir) => ngModulePipesAndDirective.add(dir.reference)); ngModule.declaredDirectives.forEach((dir) => ngModulePipesAndDirective.add(dir.reference));
ngModule.declaredPipes.forEach((pipe) => ngModulePipesAndDirective.add(pipe.reference)); ngModule.declaredPipes.forEach((pipe) => ngModulePipesAndDirective.add(pipe.reference));
if (options.transitiveModules) { if (options.transitiveModules) {
@ -364,11 +394,5 @@ function _loadNgModules(
const symbolsMissingModule = const symbolsMissingModule =
programPipesAndDirectives.filter(s => !ngModulePipesAndDirective.has(s)); programPipesAndDirectives.filter(s => !ngModulePipesAndDirective.has(s));
if (symbolsMissingModule.length) { return {ngModules: Array.from(ngModules.values()), symbolsMissingModule};
const messages = symbolsMissingModule.map(
s => `Cannot determine the module for class ${s.name} in ${s.filePath}!`);
throw new Error(messages.join('\n'));
}
return Promise.all(loadingPromises).then(() => Array.from(ngModules.values()));
} }

View File

@ -88,7 +88,7 @@ export class AnimationGroupPlayer implements AnimationPlayer {
this._started = false; this._started = false;
} }
setPosition(p: any /** TODO #9100 */): void { setPosition(p: number): void {
this._players.forEach(player => { player.setPosition(p); }); this._players.forEach(player => { player.setPosition(p); });
} }
@ -100,4 +100,6 @@ export class AnimationGroupPlayer implements AnimationPlayer {
}); });
return min; return min;
} }
get players(): AnimationPlayer[] { return this._players; }
} }

View File

@ -56,6 +56,6 @@ export class NoOpAnimationPlayer implements AnimationPlayer {
finish(): void { this._onFinish(); } finish(): void { this._onFinish(); }
destroy(): void {} destroy(): void {}
reset(): void {} reset(): void {}
setPosition(p: any /** TODO #9100 */): void {} setPosition(p: number): void {}
getPosition(): number { return 0; } getPosition(): number { return 0; }
} }

View File

@ -104,7 +104,9 @@ export class AnimationSequencePlayer implements AnimationPlayer {
} }
} }
setPosition(p: any /** TODO #9100 */): void { this._players[0].setPosition(p); } setPosition(p: number): void { this._players[0].setPosition(p); }
getPosition(): number { return this._players[0].getPosition(); } getPosition(): number { return this._players[0].getPosition(); }
get players(): AnimationPlayer[] { return this._players; }
} }

View File

@ -84,6 +84,8 @@ export function balanceAnimationKeyframes(
firstKeyframe.styles.styles.push(extraFirstKeyframeStyles); firstKeyframe.styles.styles.push(extraFirstKeyframeStyles);
} }
collectAndResolveStyles(collectedStyles, [finalStateStyles]);
return keyframes; return keyframes;
} }

View File

@ -22,7 +22,7 @@ export class DebugDomRootRenderer implements RootRenderer {
} }
} }
export class DebugDomRenderer implements Renderer { export class DebugDomRenderer {
constructor(private _delegate: Renderer) {} constructor(private _delegate: Renderer) {}
selectRootElement(selectorOrNode: string|any, debugInfo?: RenderDebugInfo): any { selectRootElement(selectorOrNode: string|any, debugInfo?: RenderDebugInfo): any {
@ -150,7 +150,9 @@ export class DebugDomRenderer implements Renderer {
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
return this._delegate.animate(element, startingStyles, keyframes, duration, delay, easing); previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
return this._delegate.animate(
element, startingStyles, keyframes, duration, delay, easing, previousPlayers);
} }
} }

View File

@ -8,8 +8,9 @@
import {AnimationGroupPlayer} from '../animation/animation_group_player'; import {AnimationGroupPlayer} from '../animation/animation_group_player';
import {AnimationPlayer} from '../animation/animation_player'; import {AnimationPlayer} from '../animation/animation_player';
import {queueAnimation as queueAnimationGlobally} from '../animation/animation_queue'; import {queueAnimation as queueAnimationGlobally} from '../animation/animation_queue';
import {AnimationTransitionEvent} from '../animation/animation_transition_event'; import {AnimationSequencePlayer} from '../animation/animation_sequence_player';
import {ViewAnimationMap} from '../animation/view_animation_map'; import {ViewAnimationMap} from '../animation/view_animation_map';
import {ListWrapper} from '../facade/collection';
export class AnimationViewContext { export class AnimationViewContext {
private _players = new ViewAnimationMap(); private _players = new ViewAnimationMap();
@ -30,15 +31,26 @@ export class AnimationViewContext {
this._players.set(element, animationName, player); this._players.set(element, animationName, player);
} }
cancelActiveAnimation(element: any, animationName: string, removeAllAnimations: boolean = false): getAnimationPlayers(element: any, animationName: string, removeAllAnimations: boolean = false):
void { AnimationPlayer[] {
const players: AnimationPlayer[] = [];
if (removeAllAnimations) { if (removeAllAnimations) {
this._players.findAllPlayersByElement(element).forEach(player => player.destroy()); this._players.findAllPlayersByElement(element).forEach(
player => { _recursePlayers(player, players); });
} else { } else {
const player = this._players.find(element, animationName); const currentPlayer = this._players.find(element, animationName);
if (player) { if (currentPlayer) {
player.destroy(); _recursePlayers(currentPlayer, players);
} }
} }
return players;
}
}
function _recursePlayers(player: AnimationPlayer, collectedPlayers: AnimationPlayer[]) {
if ((player instanceof AnimationGroupPlayer) || (player instanceof AnimationSequencePlayer)) {
player.players.forEach(player => _recursePlayers(player, collectedPlayers));
} else {
collectedPlayers.push(player);
} }
} }

View File

@ -232,8 +232,10 @@ export abstract class AppView<T> {
if (nextSibling) { if (nextSibling) {
this.visitRootNodesInternal(this._directRenderer.insertBefore, nextSibling); this.visitRootNodesInternal(this._directRenderer.insertBefore, nextSibling);
} else { } else {
this.visitRootNodesInternal( const parentElement = this._directRenderer.parentElement(prevNode);
this._directRenderer.appendChild, this._directRenderer.parentElement(prevNode)); if (parentElement) {
this.visitRootNodesInternal(this._directRenderer.appendChild, parentElement);
}
} }
} else { } else {
this.renderer.attachViewAfter(prevNode, this.flatRootNodes); this.renderer.attachViewAfter(prevNode, this.flatRootNodes);

View File

@ -56,8 +56,12 @@ export class ReflectionCapabilities implements PlatformReflectionCapabilities {
} }
// API of tsickle for lowering decorators to properties on the class. // API of tsickle for lowering decorators to properties on the class.
if ((<any>type).ctorParameters) { const tsickleCtorParams = (<any>type).ctorParameters;
const ctorParameters = (<any>type).ctorParameters; if (tsickleCtorParams) {
// Newer tsickle uses a function closure
// Retain the non-function case for compatibility with older tsickle
const ctorParameters =
typeof tsickleCtorParams === 'function' ? tsickleCtorParams() : tsickleCtorParams;
const paramTypes = ctorParameters.map((ctorParam: any) => ctorParam && ctorParam.type); const paramTypes = ctorParameters.map((ctorParam: any) => ctorParam && ctorParam.type);
const paramAnnotations = ctorParameters.map( const paramAnnotations = ctorParameters.map(
(ctorParam: any) => (ctorParam: any) =>

View File

@ -88,7 +88,8 @@ export abstract class Renderer {
abstract animate( abstract animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer; duration: number, delay: number, easing: string,
previousPlayers?: AnimationPlayer[]): AnimationPlayer;
} }
/** /**

View File

@ -1854,6 +1854,8 @@ function declareTests({useJit}: {useJit: boolean}) {
let animation = driver.log.pop(); let animation = driver.log.pop();
let kf = animation['keyframeLookup']; let kf = animation['keyframeLookup'];
expect(kf[1]).toEqual([1, {'background': 'green'}]); expect(kf[1]).toEqual([1, {'background': 'green'}]);
let player = animation['player'];
player.finish();
cmp.exp = 'blue'; cmp.exp = 'blue';
fixture.detectChanges(); fixture.detectChanges();
@ -1863,6 +1865,8 @@ function declareTests({useJit}: {useJit: boolean}) {
kf = animation['keyframeLookup']; kf = animation['keyframeLookup'];
expect(kf[0]).toEqual([0, {'background': 'green'}]); expect(kf[0]).toEqual([0, {'background': 'green'}]);
expect(kf[1]).toEqual([1, {'background': 'grey'}]); expect(kf[1]).toEqual([1, {'background': 'grey'}]);
player = animation['player'];
player.finish();
cmp.exp = 'red'; cmp.exp = 'red';
fixture.detectChanges(); fixture.detectChanges();
@ -1872,6 +1876,8 @@ function declareTests({useJit}: {useJit: boolean}) {
kf = animation['keyframeLookup']; kf = animation['keyframeLookup'];
expect(kf[0]).toEqual([0, {'background': 'grey'}]); expect(kf[0]).toEqual([0, {'background': 'grey'}]);
expect(kf[1]).toEqual([1, {'background': 'red'}]); expect(kf[1]).toEqual([1, {'background': 'red'}]);
player = animation['player'];
player.finish();
cmp.exp = 'orange'; cmp.exp = 'orange';
fixture.detectChanges(); fixture.detectChanges();
@ -1881,6 +1887,8 @@ function declareTests({useJit}: {useJit: boolean}) {
kf = animation['keyframeLookup']; kf = animation['keyframeLookup'];
expect(kf[0]).toEqual([0, {'background': 'red'}]); expect(kf[0]).toEqual([0, {'background': 'red'}]);
expect(kf[1]).toEqual([1, {'background': 'grey'}]); expect(kf[1]).toEqual([1, {'background': 'grey'}]);
player = animation['player'];
player.finish();
})); }));
it('should seed in the origin animation state styles into the first animation step', it('should seed in the origin animation state styles into the first animation step',
@ -1911,6 +1919,44 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(animation['startingStyles']).toEqual({'height': '100px'}); expect(animation['startingStyles']).toEqual({'height': '100px'});
})); }));
it('should seed in the previous animation styles into the transition if the previous transition was interupted midway',
fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, {
set: {
template: `
<div class="target" [@status]="exp"></div>
`,
animations: [trigger(
'status',
[
state('*', style({ opacity: 0 })),
state('a', style({height: '100px', width: '200px'})),
state('b', style({height: '1000px' })),
transition('* => *', [
animate(1000, style({ fontSize: '20px' })),
animate(1000)
])
])]
}
});
const driver = TestBed.get(AnimationDriver) as MockAnimationDriver;
const fixture = TestBed.createComponent(DummyIfCmp);
const cmp = fixture.componentInstance;
cmp.exp = 'a';
fixture.detectChanges();
flushMicrotasks();
driver.log = [];
cmp.exp = 'b';
fixture.detectChanges();
flushMicrotasks();
const animation = driver.log[0];
expect(animation['previousStyles']).toEqual({opacity: '0', fontSize: '*'});
}));
it('should perform a state change even if there is no transition that is found', it('should perform a state change even if there is no transition that is found',
fakeAsync(() => { fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, { TestBed.overrideComponent(DummyIfCmp, {

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Component, Injectable, RenderComponentType, Renderer, RootRenderer} from '@angular/core'; import {Component, ContentChild, Injectable, Input, RenderComponentType, Renderer, RootRenderer, TemplateRef} from '@angular/core';
import {DebugDomRenderer} from '@angular/core/src/debug/debug_renderer'; import {DebugDomRenderer} from '@angular/core/src/debug/debug_renderer';
import {DirectRenderer} from '@angular/core/src/render/api'; import {DirectRenderer} from '@angular/core/src/render/api';
import {TestBed, inject} from '@angular/core/testing'; import {TestBed, inject} from '@angular/core/testing';
@ -125,6 +125,46 @@ export function main() {
const projectedNode = childHostEl.childNodes[1]; const projectedNode = childHostEl.childNodes[1];
expect(directRenderer.appendChild).toHaveBeenCalledWith(projectedNode, childHostEl); expect(directRenderer.appendChild).toHaveBeenCalledWith(projectedNode, childHostEl);
}); });
it('should support using structural directives with ngTemplateOutlet', () => {
@Component({
template:
'<child [templateCtx]="templateCtx"><template let-shown="shown" #tpl><span *ngIf="shown">hello</span></template></child>'
})
class Parent {
templateCtx = {shown: false};
}
@Component({
selector: 'child',
template:
'(<template [ngTemplateOutlet]="templateRef" [ngOutletContext]="templateCtx"></template>)'
})
class Child {
@Input()
templateCtx: any;
@ContentChild('tpl')
templateRef: TemplateRef<any>;
}
TestBed.configureTestingModule({declarations: [Parent, Child]});
let fixture = TestBed.createComponent(Parent);
fixture.componentInstance.templateCtx.shown = false;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('()');
fixture.destroy();
fixture = TestBed.createComponent(Parent);
fixture.componentInstance.templateCtx.shown = true;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('(hello)');
fixture.componentInstance.templateCtx.shown = false;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('()');
});
}); });
} }

View File

@ -89,6 +89,25 @@ export function main() {
const p = reflector.parameters(ClassWithoutDecorators); const p = reflector.parameters(ClassWithoutDecorators);
expect(p.length).toEqual(2); expect(p.length).toEqual(2);
}); });
// See https://github.com/angular/tsickle/issues/261
it('should read forwardRef down-leveled type', () => {
class Dep {}
class ForwardLegacy {
constructor(d: Dep) {}
// Older tsickle had a bug: wrote a forward reference
static ctorParameters = [{type: Dep}];
}
expect(reflector.parameters(ForwardLegacy)).toEqual([[Dep]]);
class Forward {
constructor(d: Dep) {}
// Newer tsickle generates a functionClosure
static ctorParameters = () => [{type: ForwardDep}];
}
class ForwardDep {}
expect(reflector.parameters(Forward)).toEqual([[ForwardDep]]);
});
}); });
describe('propMetadata', () => { describe('propMetadata', () => {

View File

@ -5,8 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be * Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AUTO_STYLE, AnimationPlayer} from '@angular/core';
import {AnimationPlayer} from '@angular/core';
export class MockAnimationPlayer implements AnimationPlayer { export class MockAnimationPlayer implements AnimationPlayer {
private _onDoneFns: Function[] = []; private _onDoneFns: Function[] = [];
@ -16,8 +15,21 @@ export class MockAnimationPlayer implements AnimationPlayer {
private _started = false; private _started = false;
public parentPlayer: AnimationPlayer = null; public parentPlayer: AnimationPlayer = null;
public previousStyles: {[styleName: string]: string | number} = {};
public log: any[] /** TODO #9100 */ = []; public log: any[] = [];
constructor(
public startingStyles: {[key: string]: string | number} = {},
public keyframes: Array<[number, {[style: string]: string | number}]> = [],
previousPlayers: AnimationPlayer[] = []) {
previousPlayers.forEach(player => {
if (player instanceof MockAnimationPlayer) {
const styles = player._captureStyles();
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
}
});
}
private _onFinish(): void { private _onFinish(): void {
if (!this._finished) { if (!this._finished) {
@ -67,6 +79,32 @@ export class MockAnimationPlayer implements AnimationPlayer {
} }
} }
setPosition(p: any /** TODO #9100 */): void {} setPosition(p: number): void {}
getPosition(): number { return 0; } getPosition(): number { return 0; }
private _captureStyles(): {[styleName: string]: string | number} {
const captures: {[prop: string]: string | number} = {};
if (this.hasStarted()) {
// when assembling the captured styles, it's important that
// we build the keyframe styles in the following order:
// {startingStyles, ... other styles within keyframes, ... previousStyles }
Object.keys(this.startingStyles).forEach(prop => {
captures[prop] = this.startingStyles[prop];
});
this.keyframes.forEach(kf => {
const [offset, styles] = kf;
const newStyles: {[prop: string]: string | number} = {};
Object.keys(styles).forEach(
prop => { captures[prop] = this._finished ? styles[prop] : AUTO_STYLE; });
});
}
Object.keys(this.previousStyles).forEach(prop => {
captures[prop] = this.previousStyles[prop];
});
return captures;
}
} }

View File

@ -10,6 +10,9 @@
writeScriptTag('/vendor/system.js'); writeScriptTag('/vendor/system.js');
writeScriptTag('/vendor/Reflect.js'); writeScriptTag('/vendor/Reflect.js');
writeScriptTag('/_common/system-config.js'); writeScriptTag('/_common/system-config.js');
if (location.pathname.indexOf('/upgrade/') != -1) {
writeScriptTag('/vendor/angular.js');
}
function writeScriptTag(scriptUrl: string, onload: string = '') { function writeScriptTag(scriptUrl: string, onload: string = '') {
document.write('<script src="' + scriptUrl + '" onload="' + onload + '"></script>'); document.write('<script src="' + scriptUrl + '" onload="' + onload + '"></script>');

View File

@ -6,6 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModule} from './module'; import * as module from './module';
platformBrowserDynamic().bootstrapModule(AppModule); if (module.AppModule) {
platformBrowserDynamic().bootstrapModule(module.AppModule);
}

View File

@ -19,6 +19,7 @@ System.config({
'/vendor/@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js', '/vendor/@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/router': '/vendor/@angular/router/bundles/router.umd.js', '@angular/router': '/vendor/@angular/router/bundles/router.umd.js',
'@angular/upgrade': '/vendor/@angular/upgrade/bundles/upgrade.umd.js', '@angular/upgrade': '/vendor/@angular/upgrade/bundles/upgrade.umd.js',
'@angular/upgrade/static': '/vendor/@angular/upgrade/bundles/upgrade-static.umd.js',
'rxjs': '/vendor/rxjs', 'rxjs': '/vendor/rxjs',
}, },
packages: { packages: {

View File

@ -20,6 +20,7 @@ mkdir $DIST/vendor/
ln -s ../../../dist/packages-dist/ $DIST/vendor/@angular ln -s ../../../dist/packages-dist/ $DIST/vendor/@angular
for FILE in \ for FILE in \
../../../node_modules/angular/angular.js \
../../../node_modules/zone.js/dist/zone.js \ ../../../node_modules/zone.js/dist/zone.js \
../../../node_modules/systemjs/dist/system.js \ ../../../node_modules/systemjs/dist/system.js \
../../../node_modules/reflect-metadata/Reflect.js \ ../../../node_modules/reflect-metadata/Reflect.js \
@ -35,4 +36,6 @@ for MODULE in `find . -name module.ts`; do
cp _common/*.html $FINAL_DIR_PATH cp _common/*.html $FINAL_DIR_PATH
cp $DIST/_common/*.js $FINAL_DIR_PATH cp $DIST/_common/*.js $FINAL_DIR_PATH
cp $DIST/_common/*.js.map $FINAL_DIR_PATH cp $DIST/_common/*.js.map $FINAL_DIR_PATH
find `dirname $MODULE` -name \*.css -exec cp {} $FINAL_DIR_PATH \;
done done

View File

@ -18,7 +18,7 @@
"target": "es5", "target": "es5",
"lib": ["es2015", "dom"], "lib": ["es2015", "dom"],
"skipLibCheck": true, "skipLibCheck": true,
"types": ["jasmine", "node"] "types": ["jasmine", "node", "angularjs"]
}, },
"include": [ "include": [
"./_common/*.ts", "./_common/*.ts",

View File

@ -0,0 +1,54 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {browser, by, element} from 'protractor';
import {verifyNoBrowserErrors} from '../../../../_common/e2e_util';
function loadPage(url: string) {
browser.ng12Hybrid = true;
browser.rootEl = 'example-app';
browser.get(url);
}
describe('upgrade(static)', () => {
beforeEach(() => { loadPage('/upgrade/static/ts/'); });
afterEach(verifyNoBrowserErrors);
it('should render the `ng2-heroes` component', () => {
expect(element(by.css('h1')).getText()).toEqual('Heroes');
expect(element.all(by.css('p')).get(0).getText()).toEqual('There are 3 heroes.');
});
it('should render 3 ng1-hero components', () => {
const heroComponents = element.all(by.css('ng1-hero'));
expect(heroComponents.count()).toEqual(3);
});
it('should add a new hero when the "Add Hero" button is pressed', () => {
const addHeroButton = element.all(by.css('button')).last();
expect(addHeroButton.getText()).toEqual('Add Hero');
addHeroButton.click();
const heroComponents = element.all(by.css('ng1-hero'));
expect(heroComponents.last().element(by.css('h2')).getText()).toEqual('Kamala Khan');
});
it('should remove a hero when the "Remove" button is pressed', () => {
let firstHero = element.all(by.css('ng1-hero')).get(0);
expect(firstHero.element(by.css('h2')).getText()).toEqual('Superman');
const removeHeroButton = firstHero.element(by.css('button'));
expect(removeHeroButton.getText()).toEqual('Remove');
removeHeroButton.click();
const heroComponents = element.all(by.css('ng1-hero'));
expect(heroComponents.count()).toEqual(2);
firstHero = element.all(by.css('ng1-hero')).get(0);
expect(firstHero.element(by.css('h2')).getText()).toEqual('Wonder Woman');
});
});

View File

@ -0,0 +1,184 @@
/**
* @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, Directive, DoCheck, ElementRef, EventEmitter, Inject, Injectable, Injector, Input, NgModule, OnChanges, OnDestroy, OnInit, Output, SimpleChanges} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {UpgradeComponent, UpgradeModule, downgradeComponent, downgradeInjectable} from '@angular/upgrade/static';
interface Hero {
name: string;
description: string;
}
// #docregion Angular 2 Stuff
// #docregion ng2-heroes
// This Angular 2 component will be "downgraded" to be used in Angular 1
@Component({
selector: 'ng2-heroes',
// This template uses the upgraded `ng1-hero` component
// Note that because its element is compiled by Angular 2+ we must use camelCased attribute names
template: `<h1>Heroes</h1>
<p><ng-content></ng-content></p>
<div *ngFor="let hero of heroes">
<ng1-hero [hero]="hero" (onRemove)="removeHero.emit(hero)"><strong>Super Hero</strong></ng1-hero>
</div>
<button (click)="addHero.emit()">Add Hero</button>`,
})
class Ng2HeroesComponent {
@Input() heroes: Hero[];
@Output() addHero = new EventEmitter();
@Output() removeHero = new EventEmitter();
}
// #enddocregion
// #docregion ng2-heroes-service
// This Angular 2 service will be "downgraded" to be used in Angular 1
@Injectable()
class HeroesService {
heroes: Hero[] = [
{name: 'superman', description: 'The man of steel'},
{name: 'wonder woman', description: 'Princess of the Amazons'},
{name: 'thor', description: 'The hammer-wielding god'}
];
// #docregion use-ng1-upgraded-service
constructor(@Inject('titleCase') titleCase: (v: string) => string) {
// Change all the hero names to title case, using the "upgraded" Angular 1 service
this.heroes.forEach((hero: Hero) => hero.name = titleCase(hero.name));
}
// #enddocregion
addHero() {
this.heroes =
this.heroes.concat([{name: 'Kamala Khan', description: 'Epic shape-shifting healer'}]);
}
removeHero(hero: Hero) { this.heroes = this.heroes.filter((item: Hero) => item !== hero); }
}
// #enddocregion
// #docregion ng1-hero-wrapper
// This Angular 2 directive will act as an interface to the "upgraded" Angular 1 component
@Directive({selector: 'ng1-hero'})
class Ng1HeroComponentWrapper extends UpgradeComponent implements OnInit, OnChanges, DoCheck,
OnDestroy {
// The names of the input and output properties here must match the names of the
// `<` and `&` bindings in the Angular 1 component that is being wrapped
@Input() hero: Hero;
@Output() onRemove: EventEmitter<void>;
constructor(@Inject(ElementRef) elementRef: ElementRef, @Inject(Injector) injector: Injector) {
// We must pass the name of the directive as used by Angular 1 to the super
super('ng1Hero', elementRef, injector);
}
// For this class to work when compiled with AoT, we must implement these lifecycle hooks
// because the AoT compiler will not realise that the super class implements them
ngOnInit() { super.ngOnInit(); }
ngOnChanges(changes: SimpleChanges) { super.ngOnChanges(changes); }
ngDoCheck() { super.ngDoCheck(); }
ngOnDestroy() { super.ngOnDestroy(); }
}
// #enddocregion
// #docregion ng2-module
// This NgModule represents the Angular 2 pieces of the application
@NgModule({
declarations: [Ng2HeroesComponent, Ng1HeroComponentWrapper],
providers: [
HeroesService,
// #docregion upgrade-ng1-service
// Register an Angular 2+ provider whose value is the "upgraded" Angular 1 service
{provide: 'titleCase', useFactory: (i: any) => i.get('titleCase'), deps: ['$injector']}
// #enddocregion
],
// All components that are to be "downgraded" must be declared as `entryComponents`
entryComponents: [Ng2HeroesComponent],
// We must import `UpgradeModule` to get access to the Angular 1 core services
imports: [BrowserModule, UpgradeModule]
})
class Ng2AppModule {
ngDoBootstrap() { /* this is a placeholder to stop the boostrapper from complaining */
}
}
// #enddocregion
// #enddocregion
// #docregion Angular 1 Stuff
// #docregion ng1-module
// This Angular 1 module represents the Angular 1 pieces of the application
const ng1AppModule = angular.module('ng1AppModule', []);
// #enddocregion
// #docregion ng1-hero
// This Angular 1 component will be "upgraded" to be used in Angular 2+
ng1AppModule.component('ng1Hero', {
bindings: {hero: '<', onRemove: '&'},
transclude: true,
template: `<div class="title" ng-transclude></div>
<h2>{{ $ctrl.hero.name }}</h2>
<p>{{ $ctrl.hero.description }}</p>
<button ng-click="$ctrl.onRemove()">Remove</button>`
});
// #enddocregion
// #docregion ng1-title-case-service
// This Angular 1 service will be "upgraded" to be used in Angular 2+
ng1AppModule.factory(
'titleCase',
() => (value: string) => value.replace(/((^|\s)[a-z])/g, (_, c) => c.toUpperCase()));
// #enddocregion
// #docregion downgrade-ng2-heroes-service
// Register an Angular 1 service, whose value is the "downgraded" Angular 2+ injectable.
ng1AppModule.factory('heroesService', downgradeInjectable(HeroesService));
// #enddocregion
// #docregion ng2-heroes-wrapper
// This is directive will act as the interface to the "downgraded" Angular 2+ component
ng1AppModule.directive(
'ng2Heroes',
downgradeComponent(
// The inputs and outputs here must match the relevant names of the properties on the
// "downgraded" component
{component: Ng2HeroesComponent, inputs: ['heroes'], outputs: ['addHero', 'removeHero']}));
// #enddocregion
// #docregion example-app
// This is our top level application component
ng1AppModule.component('exampleApp', {
// We inject the "downgraded" HeroesService into this Angular 1 component
// (We don't need the `HeroesService` type for Angular 1 DI - it just helps with TypeScript
// compilation)
controller: [
'heroesService', function(heroesService: HeroesService) { this.heroesService = heroesService; }
],
// This template make use of the downgraded `ng2-heroes` component
// Note that because its element is compiled by Angular 1 we must use kebab-case attributes for
// inputs and outputs
template: `<link rel="stylesheet" href="./styles.css">
<ng2-heroes [heroes]="$ctrl.heroesService.heroes" (add-hero)="$ctrl.heroesService.addHero()" (remove-hero)="$ctrl.heroesService.removeHero($event)">
There are {{ $ctrl.heroesService.heroes.length }} heroes.
</ng2-heroes>`
});
// #enddocregion
// #enddocregion
// #docregion bootstrap
// First we bootstrap the Angular 2 HybridModule
// (We are using the dynamic browser platform as this example has not been compiled AoT)
platformBrowserDynamic().bootstrapModule(Ng2AppModule).then(ref => {
// Once Angular 2 bootstrap is complete then we bootstrap the Angular 1 module
const upgrade = ref.injector.get(UpgradeModule) as UpgradeModule;
upgrade.bootstrap(document.body, [ng1AppModule.name]);
});
// #enddocregion

View File

@ -0,0 +1,17 @@
ng2-heroes {
border: solid black 2px;
display: block;
padding: 5px;
}
ng1-hero {
border: solid green 2px;
margin-top: 5px;
padding: 5px;
display: block;
}
.title {
background-color: blue;
color: white;
}

View File

@ -44,7 +44,7 @@ const DATE_FORMATS_SPLIT =
/((?:[^yMLdHhmsazZEwGjJ']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|J+|j+|m+|s+|a|z|Z|G+|w+))(.*)/; /((?:[^yMLdHhmsazZEwGjJ']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|J+|j+|m+|s+|a|z|Z|G+|w+))(.*)/;
const PATTERN_ALIASES: {[format: string]: DateFormatterFn} = { const PATTERN_ALIASES: {[format: string]: DateFormatterFn} = {
yMMMdjms: datePartGetterFactory(combine([ 'yMMMdjms': datePartGetterFactory(combine([
digitCondition('year', 1), digitCondition('year', 1),
nameCondition('month', 3), nameCondition('month', 3),
digitCondition('day', 1), digitCondition('day', 1),
@ -52,23 +52,23 @@ const PATTERN_ALIASES: {[format: string]: DateFormatterFn} = {
digitCondition('minute', 1), digitCondition('minute', 1),
digitCondition('second', 1), digitCondition('second', 1),
])), ])),
yMdjm: datePartGetterFactory(combine([ 'yMdjm': datePartGetterFactory(combine([
digitCondition('year', 1), digitCondition('month', 1), digitCondition('day', 1), digitCondition('year', 1), digitCondition('month', 1), digitCondition('day', 1),
digitCondition('hour', 1), digitCondition('minute', 1) digitCondition('hour', 1), digitCondition('minute', 1)
])), ])),
yMMMMEEEEd: datePartGetterFactory(combine([ 'yMMMMEEEEd': datePartGetterFactory(combine([
digitCondition('year', 1), nameCondition('month', 4), nameCondition('weekday', 4), digitCondition('year', 1), nameCondition('month', 4), nameCondition('weekday', 4),
digitCondition('day', 1) digitCondition('day', 1)
])), ])),
yMMMMd: datePartGetterFactory( 'yMMMMd': datePartGetterFactory(
combine([digitCondition('year', 1), nameCondition('month', 4), digitCondition('day', 1)])), combine([digitCondition('year', 1), nameCondition('month', 4), digitCondition('day', 1)])),
yMMMd: datePartGetterFactory( 'yMMMd': datePartGetterFactory(
combine([digitCondition('year', 1), nameCondition('month', 3), digitCondition('day', 1)])), combine([digitCondition('year', 1), nameCondition('month', 3), digitCondition('day', 1)])),
yMd: datePartGetterFactory( 'yMd': datePartGetterFactory(
combine([digitCondition('year', 1), digitCondition('month', 1), digitCondition('day', 1)])), combine([digitCondition('year', 1), digitCondition('month', 1), digitCondition('day', 1)])),
jms: datePartGetterFactory(combine( 'jms': datePartGetterFactory(combine(
[digitCondition('hour', 1), digitCondition('second', 1), digitCondition('minute', 1)])), [digitCondition('hour', 1), digitCondition('second', 1), digitCondition('minute', 1)])),
jm: datePartGetterFactory(combine([digitCondition('hour', 1), digitCondition('minute', 1)])) 'jm': datePartGetterFactory(combine([digitCondition('hour', 1), digitCondition('minute', 1)]))
}; };
const DATE_FORMATS: {[format: string]: DateFormatterFn} = { const DATE_FORMATS: {[format: string]: DateFormatterFn} = {

View File

@ -56,22 +56,12 @@ export const TEMPLATE_DRIVEN_DIRECTIVES: Type<any>[] = [NgModel, NgModelGroup, N
export const REACTIVE_DRIVEN_DIRECTIVES: Type<any>[] = export const REACTIVE_DRIVEN_DIRECTIVES: Type<any>[] =
[FormControlDirective, FormGroupDirective, FormControlName, FormGroupName, FormArrayName]; [FormControlDirective, FormGroupDirective, FormControlName, FormGroupName, FormArrayName];
/**
* A list of all the form directives.
*
* @stable
*/
export const FORM_DIRECTIVES: Type<any>[][] = [TEMPLATE_DRIVEN_DIRECTIVES, SHARED_FORM_DIRECTIVES];
/**
* @stable
*/
export const REACTIVE_FORM_DIRECTIVES: Type<any>[][] =
[REACTIVE_DRIVEN_DIRECTIVES, SHARED_FORM_DIRECTIVES];
/** /**
* Internal module used for sharing directives between FormsModule and ReactiveFormsModule * Internal module used for sharing directives between FormsModule and ReactiveFormsModule
*/ */
@NgModule({declarations: SHARED_FORM_DIRECTIVES, exports: SHARED_FORM_DIRECTIVES}) @NgModule({
declarations: SHARED_FORM_DIRECTIVES,
exports: SHARED_FORM_DIRECTIVES,
})
export class InternalFormsSharedModule { export class InternalFormsSharedModule {
} }

View File

@ -6,13 +6,13 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Directive, ElementRef, Host, Input, OnDestroy, Optional, Renderer, forwardRef} from '@angular/core'; import {Directive, ElementRef, Host, Input, OnDestroy, Optional, Provider, Renderer, forwardRef} from '@angular/core';
import {isPrimitive, looseIdentical} from '../facade/lang'; import {isPrimitive, looseIdentical} from '../facade/lang';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
export const SELECT_VALUE_ACCESSOR: any = { export const SELECT_VALUE_ACCESSOR: Provider = {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SelectControlValueAccessor), useExisting: forwardRef(() => SelectControlValueAccessor),
multi: true multi: true
@ -115,8 +115,8 @@ export class SelectControlValueAccessor implements ControlValueAccessor {
/** @internal */ /** @internal */
_getOptionValue(valueString: string): any { _getOptionValue(valueString: string): any {
const value = this._optionMap.get(_extractId(valueString)); const id: string = _extractId(valueString);
return value != null ? value : valueString; return this._optionMap.has(id) ? this._optionMap.get(id) : valueString;
} }
} }
@ -158,7 +158,7 @@ export class NgSelectOption implements OnDestroy {
this._renderer.setElementProperty(this._element.nativeElement, 'value', value); this._renderer.setElementProperty(this._element.nativeElement, 'value', value);
} }
ngOnDestroy() { ngOnDestroy(): void {
if (this._select) { if (this._select) {
this._select._optionMap.delete(this.id); this._select._optionMap.delete(this.id);
this._select.writeValue(this._select.value); this._select.writeValue(this._select.value);

View File

@ -6,13 +6,13 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Directive, ElementRef, Host, Input, OnDestroy, OpaqueToken, Optional, Renderer, Type, forwardRef} from '@angular/core'; import {Directive, ElementRef, Host, Input, OnDestroy, Optional, Provider, Renderer, forwardRef} from '@angular/core';
import {isPrimitive, looseIdentical} from '../facade/lang'; import {isPrimitive, looseIdentical} from '../facade/lang';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
export const SELECT_MULTIPLE_VALUE_ACCESSOR = { export const SELECT_MULTIPLE_VALUE_ACCESSOR: Provider = {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SelectMultipleControlValueAccessor), useExisting: forwardRef(() => SelectMultipleControlValueAccessor),
multi: true multi: true
@ -121,8 +121,8 @@ export class SelectMultipleControlValueAccessor implements ControlValueAccessor
/** @internal */ /** @internal */
_getOptionValue(valueString: string): any { _getOptionValue(valueString: string): any {
const opt = this._optionMap.get(_extractId(valueString)); const id: string = _extractId(valueString);
return opt ? opt._value : valueString; return this._optionMap.has(id) ? this._optionMap.get(id)._value : valueString;
} }
} }
@ -180,12 +180,10 @@ export class NgSelectMultipleOption implements OnDestroy {
this._renderer.setElementProperty(this._element.nativeElement, 'selected', selected); this._renderer.setElementProperty(this._element.nativeElement, 'selected', selected);
} }
ngOnDestroy() { ngOnDestroy(): void {
if (this._select) { if (this._select) {
this._select._optionMap.delete(this.id); this._select._optionMap.delete(this.id);
this._select.writeValue(this._select.value); this._select.writeValue(this._select.value);
} }
} }
} }
export const SELECT_DIRECTIVES = [SelectMultipleControlValueAccessor, NgSelectMultipleOption];

View File

@ -5,10 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be * Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Directive, Input, OnChanges, SimpleChanges, forwardRef} from '@angular/core'; import {Directive, Input, OnChanges, SimpleChanges, forwardRef} from '@angular/core';
import {isPresent} from '../facade/lang';
import {AbstractControl} from '../model'; import {AbstractControl} from '../model';
import {NG_VALIDATORS, Validators} from '../validators'; import {NG_VALIDATORS, Validators} from '../validators';
@ -57,17 +54,17 @@ export const REQUIRED_VALIDATOR: any = {
@Directive({ @Directive({
selector: '[required][formControlName],[required][formControl],[required][ngModel]', selector: '[required][formControlName],[required][formControl],[required][ngModel]',
providers: [REQUIRED_VALIDATOR], providers: [REQUIRED_VALIDATOR],
host: {'[attr.required]': 'required? "" : null'} host: {'[attr.required]': 'required ? "" : null'}
}) })
export class RequiredValidator implements Validator { export class RequiredValidator implements Validator {
private _required: boolean; private _required: boolean;
private _onChange: () => void; private _onChange: () => void;
@Input() @Input()
get required(): boolean { return this._required; } get required(): boolean /*| string*/ { return this._required; }
set required(value: boolean) { set required(value: boolean) {
this._required = isPresent(value) && `${value}` !== 'false'; this._required = value != null && value !== false && `${value}` !== 'false';
if (this._onChange) this._onChange(); if (this._onChange) this._onChange();
} }
@ -75,7 +72,7 @@ export class RequiredValidator implements Validator {
return this.required ? Validators.required(c) : null; return this.required ? Validators.required(c) : null;
} }
registerOnValidatorChange(fn: () => void) { this._onChange = fn; } registerOnValidatorChange(fn: () => void): void { this._onChange = fn; }
} }
/** /**
@ -112,7 +109,7 @@ export const MIN_LENGTH_VALIDATOR: any = {
@Directive({ @Directive({
selector: '[minlength][formControlName],[minlength][formControl],[minlength][ngModel]', selector: '[minlength][formControlName],[minlength][formControl],[minlength][ngModel]',
providers: [MIN_LENGTH_VALIDATOR], providers: [MIN_LENGTH_VALIDATOR],
host: {'[attr.minlength]': 'minlength? minlength : null'} host: {'[attr.minlength]': 'minlength ? minlength : null'}
}) })
export class MinLengthValidator implements Validator, export class MinLengthValidator implements Validator,
OnChanges { OnChanges {
@ -121,12 +118,8 @@ export class MinLengthValidator implements Validator,
@Input() minlength: string; @Input() minlength: string;
private _createValidator() { ngOnChanges(changes: SimpleChanges): void {
this._validator = Validators.minLength(parseInt(this.minlength, 10)); if ('minlength' in changes) {
}
ngOnChanges(changes: SimpleChanges) {
if (changes['minlength']) {
this._createValidator(); this._createValidator();
if (this._onChange) this._onChange(); if (this._onChange) this._onChange();
} }
@ -136,7 +129,11 @@ export class MinLengthValidator implements Validator,
return this.minlength == null ? null : this._validator(c); return this.minlength == null ? null : this._validator(c);
} }
registerOnValidatorChange(fn: () => void) { this._onChange = fn; } registerOnValidatorChange(fn: () => void): void { this._onChange = fn; }
private _createValidator(): void {
this._validator = Validators.minLength(parseInt(this.minlength, 10));
}
} }
/** /**
@ -162,7 +159,7 @@ export const MAX_LENGTH_VALIDATOR: any = {
@Directive({ @Directive({
selector: '[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]', selector: '[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]',
providers: [MAX_LENGTH_VALIDATOR], providers: [MAX_LENGTH_VALIDATOR],
host: {'[attr.maxlength]': 'maxlength? maxlength : null'} host: {'[attr.maxlength]': 'maxlength ? maxlength : null'}
}) })
export class MaxLengthValidator implements Validator, export class MaxLengthValidator implements Validator,
OnChanges { OnChanges {
@ -171,22 +168,22 @@ export class MaxLengthValidator implements Validator,
@Input() maxlength: string; @Input() maxlength: string;
private _createValidator() { ngOnChanges(changes: SimpleChanges): void {
this._validator = Validators.maxLength(parseInt(this.maxlength, 10)); if ('maxlength' in changes) {
}
ngOnChanges(changes: SimpleChanges) {
if (changes['maxlength']) {
this._createValidator(); this._createValidator();
if (this._onChange) this._onChange(); if (this._onChange) this._onChange();
} }
} }
validate(c: AbstractControl): {[key: string]: any} { validate(c: AbstractControl): {[key: string]: any} {
return isPresent(this.maxlength) ? this._validator(c) : null; return this.maxlength != null ? this._validator(c) : null;
} }
registerOnValidatorChange(fn: () => void) { this._onChange = fn; } registerOnValidatorChange(fn: () => void): void { this._onChange = fn; }
private _createValidator(): void {
this._validator = Validators.maxLength(parseInt(this.maxlength, 10));
}
} }
@ -220,20 +217,18 @@ export class PatternValidator implements Validator,
private _validator: ValidatorFn; private _validator: ValidatorFn;
private _onChange: () => void; private _onChange: () => void;
@Input() pattern: string; @Input() pattern: string /*|RegExp*/;
private _createValidator() { this._validator = Validators.pattern(this.pattern); } ngOnChanges(changes: SimpleChanges): void {
if ('pattern' in changes) {
ngOnChanges(changes: SimpleChanges) {
if (changes['pattern']) {
this._createValidator(); this._createValidator();
if (this._onChange) this._onChange(); if (this._onChange) this._onChange();
} }
} }
validate(c: AbstractControl): {[key: string]: any} { validate(c: AbstractControl): {[key: string]: any} { return this._validator(c); }
return this.pattern ? this._validator(c) : null;
}
registerOnValidatorChange(fn: () => void) { this._onChange = fn; } registerOnValidatorChange(fn: () => void): void { this._onChange = fn; }
private _createValidator(): void { this._validator = Validators.pattern(this.pattern); }
} }

View File

@ -23,7 +23,7 @@ export function main() {
NgModelRadioForm, NgModelRangeForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName, NgModelRadioForm, NgModelRangeForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName,
NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper, NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper,
NgModelValidationBindings, NgModelMultipleValidators, NgAsyncValidator, NgModelValidationBindings, NgModelMultipleValidators, NgAsyncValidator,
NgModelAsyncValidation NgModelAsyncValidation, NgModelSelectWithNullForm
], ],
imports: [FormsModule] imports: [FormsModule]
}); });
@ -699,6 +699,28 @@ export function main() {
expect(select.nativeElement.value).toEqual('2: Object'); expect(select.nativeElement.value).toEqual('2: Object');
expect(secondNYC.nativeElement.selected).toBe(true); expect(secondNYC.nativeElement.selected).toBe(true);
})); }));
it('should work with null option', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelSelectWithNullForm);
const comp = fixture.componentInstance;
comp.cities = [{'name': 'SF'}, {'name': 'NYC'}];
comp.selectedCity = null;
fixture.detectChanges();
const select = fixture.debugElement.query(By.css('select'));
select.nativeElement.value = '2: Object';
dispatchEvent(select.nativeElement, 'change');
fixture.detectChanges();
tick();
expect(comp.selectedCity['name']).toEqual('NYC');
select.nativeElement.value = '0: null';
dispatchEvent(select.nativeElement, 'change');
fixture.detectChanges();
tick();
expect(comp.selectedCity).toEqual(null);
}));
}); });
describe('custom value accessors', () => { describe('custom value accessors', () => {
@ -771,7 +793,7 @@ export function main() {
expect(form.valid).toEqual(true); expect(form.valid).toEqual(true);
})); }));
it('should support optional fields with pattern validator', fakeAsync(() => { it('should support optional fields with string pattern validator', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelMultipleValidators); const fixture = TestBed.createComponent(NgModelMultipleValidators);
fixture.componentInstance.required = false; fixture.componentInstance.required = false;
fixture.componentInstance.pattern = '[a-z]+'; fixture.componentInstance.pattern = '[a-z]+';
@ -793,6 +815,28 @@ export function main() {
expect(form.control.hasError('pattern', ['tovalidate'])).toBeTruthy(); expect(form.control.hasError('pattern', ['tovalidate'])).toBeTruthy();
})); }));
it('should support optional fields with RegExp pattern validator', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelMultipleValidators);
fixture.componentInstance.required = false;
fixture.componentInstance.pattern = /^[a-z]+$/;
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
const input = fixture.debugElement.query(By.css('input'));
input.nativeElement.value = '';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
expect(form.valid).toBeTruthy();
input.nativeElement.value = '1';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
expect(form.valid).toBeFalsy();
expect(form.control.hasError('pattern', ['tovalidate'])).toBeTruthy();
}));
it('should support optional fields with minlength validator', fakeAsync(() => { it('should support optional fields with minlength validator', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelMultipleValidators); const fixture = TestBed.createComponent(NgModelMultipleValidators);
fixture.componentInstance.required = false; fixture.componentInstance.required = false;
@ -1078,6 +1122,20 @@ class NgModelSelectForm {
cities: any[] = []; cities: any[] = [];
} }
@Component({
selector: 'ng-model-select-null-form',
template: `
<select [(ngModel)]="selectedCity">
<option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
<option [ngValue]="null">Unspecified</option>
</select>
`
})
class NgModelSelectWithNullForm {
selectedCity: {[k: string]: string} = {};
cities: any[] = [];
}
@Component({ @Component({
selector: 'ng-model-custom-comp', selector: 'ng-model-custom-comp',
template: ` template: `
@ -1141,7 +1199,7 @@ class NgModelValidationBindings {
class NgModelMultipleValidators { class NgModelMultipleValidators {
required: boolean; required: boolean;
minLen: number; minLen: number;
pattern: string; pattern: string|RegExp;
} }
@Directive({ @Directive({

View File

@ -7,15 +7,15 @@
*/ */
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {global} from '../facade/lang';
let _nextRequestId = 0; let _nextRequestId = 0;
export const JSONP_HOME = '__ng_jsonp__'; export const JSONP_HOME = '__ng_jsonp__';
let _jsonpConnections: {[key: string]: any} = null; let _jsonpConnections: {[key: string]: any} = null;
function _getJsonpConnections(): {[key: string]: any} { function _getJsonpConnections(): {[key: string]: any} {
const w: {[key: string]: any} = typeof window == 'object' ? window : {};
if (_jsonpConnections === null) { if (_jsonpConnections === null) {
_jsonpConnections = (<{[key: string]: any}>global)[JSONP_HOME] = {}; _jsonpConnections = w[JSONP_HOME] = {};
} }
return _jsonpConnections; return _jsonpConnections;
} }

View File

@ -12,7 +12,6 @@ import {Observer} from 'rxjs/Observer';
import {ResponseOptions} from '../base_response_options'; import {ResponseOptions} from '../base_response_options';
import {ReadyState, RequestMethod, ResponseType} from '../enums'; import {ReadyState, RequestMethod, ResponseType} from '../enums';
import {isPresent} from '../facade/lang';
import {Connection, ConnectionBackend} from '../interfaces'; import {Connection, ConnectionBackend} from '../interfaces';
import {Request} from '../static_request'; import {Request} from '../static_request';
import {Response} from '../static_response'; import {Response} from '../static_response';
@ -89,7 +88,7 @@ export class JSONPConnection_ extends JSONPConnection {
if (!this._finished) { if (!this._finished) {
let responseOptions = let responseOptions =
new ResponseOptions({body: JSONP_ERR_NO_CALLBACK, type: ResponseType.Error, url}); new ResponseOptions({body: JSONP_ERR_NO_CALLBACK, type: ResponseType.Error, url});
if (isPresent(baseResponseOptions)) { if (baseResponseOptions) {
responseOptions = baseResponseOptions.merge(responseOptions); responseOptions = baseResponseOptions.merge(responseOptions);
} }
responseObserver.error(new Response(responseOptions)); responseObserver.error(new Response(responseOptions));
@ -97,7 +96,7 @@ export class JSONPConnection_ extends JSONPConnection {
} }
let responseOptions = new ResponseOptions({body: this._responseData, url}); let responseOptions = new ResponseOptions({body: this._responseData, url});
if (isPresent(this.baseResponseOptions)) { if (this.baseResponseOptions) {
responseOptions = this.baseResponseOptions.merge(responseOptions); responseOptions = this.baseResponseOptions.merge(responseOptions);
} }
@ -110,7 +109,7 @@ export class JSONPConnection_ extends JSONPConnection {
this.readyState = ReadyState.Done; this.readyState = ReadyState.Done;
_dom.cleanup(script); _dom.cleanup(script);
let responseOptions = new ResponseOptions({body: error.message, type: ResponseType.Error}); let responseOptions = new ResponseOptions({body: error.message, type: ResponseType.Error});
if (isPresent(baseResponseOptions)) { if (baseResponseOptions) {
responseOptions = baseResponseOptions.merge(responseOptions); responseOptions = baseResponseOptions.merge(responseOptions);
} }
responseObserver.error(new Response(responseOptions)); responseObserver.error(new Response(responseOptions));
@ -125,10 +124,7 @@ export class JSONPConnection_ extends JSONPConnection {
this.readyState = ReadyState.Cancelled; this.readyState = ReadyState.Cancelled;
script.removeEventListener('load', onLoad); script.removeEventListener('load', onLoad);
script.removeEventListener('error', onError); script.removeEventListener('error', onError);
if (isPresent(script)) { this._dom.cleanup(script);
this._dom.cleanup(script);
}
}; };
}); });
} }

View File

@ -10,16 +10,13 @@ import {Injectable} from '@angular/core';
import {__platform_browser_private__} from '@angular/platform-browser'; import {__platform_browser_private__} from '@angular/platform-browser';
import {Observable} from 'rxjs/Observable'; import {Observable} from 'rxjs/Observable';
import {Observer} from 'rxjs/Observer'; import {Observer} from 'rxjs/Observer';
import {ResponseOptions} from '../base_response_options'; import {ResponseOptions} from '../base_response_options';
import {ContentType, ReadyState, RequestMethod, ResponseContentType, ResponseType} from '../enums'; import {ContentType, ReadyState, RequestMethod, ResponseContentType, ResponseType} from '../enums';
import {isPresent} from '../facade/lang';
import {Headers} from '../headers'; import {Headers} from '../headers';
import {getResponseURL, isSuccess} from '../http_utils'; import {getResponseURL, isSuccess} from '../http_utils';
import {Connection, ConnectionBackend, XSRFStrategy} from '../interfaces'; import {Connection, ConnectionBackend, XSRFStrategy} from '../interfaces';
import {Request} from '../static_request'; import {Request} from '../static_request';
import {Response} from '../static_response'; import {Response} from '../static_response';
import {BrowserXhr} from './browser_xhr'; import {BrowserXhr} from './browser_xhr';
const XSSI_PREFIX = /^\)\]\}',?\n/; const XSSI_PREFIX = /^\)\]\}',?\n/;
@ -47,24 +44,29 @@ export class XHRConnection implements Connection {
this.response = new Observable<Response>((responseObserver: Observer<Response>) => { this.response = new Observable<Response>((responseObserver: Observer<Response>) => {
const _xhr: XMLHttpRequest = browserXHR.build(); const _xhr: XMLHttpRequest = browserXHR.build();
_xhr.open(RequestMethod[req.method].toUpperCase(), req.url); _xhr.open(RequestMethod[req.method].toUpperCase(), req.url);
if (isPresent(req.withCredentials)) { if (req.withCredentials != null) {
_xhr.withCredentials = req.withCredentials; _xhr.withCredentials = req.withCredentials;
} }
// load event handler // load event handler
const onLoad = () => { const onLoad = () => {
// responseText is the old-school way of retrieving response (supported by IE8 & 9)
// response/responseType properties were introduced in ResourceLoader Level2 spec (supported
// by IE10)
let body = _xhr.response === undefined ? _xhr.responseText : _xhr.response;
// Implicitly strip a potential XSSI prefix.
if (typeof body === 'string') body = body.replace(XSSI_PREFIX, '');
const headers = Headers.fromResponseHeaderString(_xhr.getAllResponseHeaders());
const url = getResponseURL(_xhr);
// normalize IE9 bug (http://bugs.jquery.com/ticket/1450) // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)
let status: number = _xhr.status === 1223 ? 204 : _xhr.status; let status: number = _xhr.status === 1223 ? 204 : _xhr.status;
let body: any = null;
// HTTP 204 means no content
if (status !== 204) {
// responseText is the old-school way of retrieving response (supported by IE8 & 9)
// response/responseType properties were introduced in ResourceLoader Level2 spec
// (supported by IE10)
body = _xhr.response == null ? _xhr.responseText : _xhr.response;
// Implicitly strip a potential XSSI prefix.
if (typeof body === 'string') {
body = body.replace(XSSI_PREFIX, '');
}
}
// fix status code when it is 0 (0 status is undocumented). // fix status code when it is 0 (0 status is undocumented).
// Occurs when accessing file resources or on Android 4.1 stock browser // Occurs when accessing file resources or on Android 4.1 stock browser
// while retrieving files from application cache. // while retrieving files from application cache.
@ -72,10 +74,13 @@ export class XHRConnection implements Connection {
status = body ? 200 : 0; status = body ? 200 : 0;
} }
const statusText = _xhr.statusText || 'OK'; const headers: Headers = Headers.fromResponseHeaderString(_xhr.getAllResponseHeaders());
// IE 9 does not provide the way to get URL of response
const url = getResponseURL(_xhr) || req.url;
const statusText: string = _xhr.statusText || 'OK';
let responseOptions = new ResponseOptions({body, status, headers, statusText, url}); let responseOptions = new ResponseOptions({body, status, headers, statusText, url});
if (isPresent(baseResponseOptions)) { if (baseResponseOptions != null) {
responseOptions = baseResponseOptions.merge(responseOptions); responseOptions = baseResponseOptions.merge(responseOptions);
} }
const response = new Response(responseOptions); const response = new Response(responseOptions);
@ -89,14 +94,14 @@ export class XHRConnection implements Connection {
responseObserver.error(response); responseObserver.error(response);
}; };
// error event handler // error event handler
const onError = (err: any) => { const onError = (err: ErrorEvent) => {
let responseOptions = new ResponseOptions({ let responseOptions = new ResponseOptions({
body: err, body: err,
type: ResponseType.Error, type: ResponseType.Error,
status: _xhr.status, status: _xhr.status,
statusText: _xhr.statusText, statusText: _xhr.statusText,
}); });
if (isPresent(baseResponseOptions)) { if (baseResponseOptions != null) {
responseOptions = baseResponseOptions.merge(responseOptions); responseOptions = baseResponseOptions.merge(responseOptions);
} }
responseObserver.error(new Response(responseOptions)); responseObserver.error(new Response(responseOptions));
@ -104,12 +109,12 @@ export class XHRConnection implements Connection {
this.setDetectedContentType(req, _xhr); this.setDetectedContentType(req, _xhr);
if (isPresent(req.headers)) { if (req.headers != null) {
req.headers.forEach((values, name) => _xhr.setRequestHeader(name, values.join(','))); req.headers.forEach((values, name) => _xhr.setRequestHeader(name, values.join(',')));
} }
// Select the correct buffer type to store the response // Select the correct buffer type to store the response
if (isPresent(req.responseType) && isPresent(_xhr.responseType)) { if (req.responseType != null && _xhr.responseType != null) {
switch (req.responseType) { switch (req.responseType) {
case ResponseContentType.ArrayBuffer: case ResponseContentType.ArrayBuffer:
_xhr.responseType = 'arraybuffer'; _xhr.responseType = 'arraybuffer';
@ -141,9 +146,9 @@ export class XHRConnection implements Connection {
}); });
} }
setDetectedContentType(req: any /** TODO #9100 */, _xhr: any /** TODO #9100 */) { setDetectedContentType(req: any /** TODO Request */, _xhr: any /** XMLHttpRequest */) {
// Skip if a custom Content-Type header is provided // Skip if a custom Content-Type header is provided
if (isPresent(req.headers) && isPresent(req.headers.get('Content-Type'))) { if (req.headers != null && req.headers.get('Content-Type') != null) {
return; return;
} }
@ -161,7 +166,7 @@ export class XHRConnection implements Connection {
_xhr.setRequestHeader('content-type', 'text/plain'); _xhr.setRequestHeader('content-type', 'text/plain');
break; break;
case ContentType.BLOB: case ContentType.BLOB:
let blob = req.blob(); const blob = req.blob();
if (blob.type) { if (blob.type) {
_xhr.setRequestHeader('content-type', blob.type); _xhr.setRequestHeader('content-type', blob.type);
} }
@ -185,7 +190,7 @@ export class CookieXSRFStrategy implements XSRFStrategy {
constructor( constructor(
private _cookieName: string = 'XSRF-TOKEN', private _headerName: string = 'X-XSRF-TOKEN') {} private _cookieName: string = 'XSRF-TOKEN', private _headerName: string = 'X-XSRF-TOKEN') {}
configureRequest(req: Request) { configureRequest(req: Request): void {
const xsrfToken = __platform_browser_private__.getDOM().getCookie(this._cookieName); const xsrfToken = __platform_browser_private__.getDOM().getCookie(this._cookieName);
if (xsrfToken) { if (xsrfToken) {
req.headers.set(this._headerName, xsrfToken); req.headers.set(this._headerName, xsrfToken);

View File

@ -8,8 +8,6 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {isPresent} from '../src/facade/lang';
import {RequestMethod, ResponseContentType} from './enums'; import {RequestMethod, ResponseContentType} from './enums';
import {Headers} from './headers'; import {Headers} from './headers';
import {normalizeMethodName} from './http_utils'; import {normalizeMethodName} from './http_utils';
@ -77,16 +75,14 @@ export class RequestOptions {
constructor( constructor(
{method, headers, body, url, search, withCredentials, {method, headers, body, url, search, withCredentials,
responseType}: RequestOptionsArgs = {}) { responseType}: RequestOptionsArgs = {}) {
this.method = isPresent(method) ? normalizeMethodName(method) : null; this.method = method != null ? normalizeMethodName(method) : null;
this.headers = isPresent(headers) ? headers : null; this.headers = headers != null ? headers : null;
this.body = isPresent(body) ? body : null; this.body = body != null ? body : null;
this.url = isPresent(url) ? url : null; this.url = url != null ? url : null;
this.search = isPresent(search) ? this.search =
(typeof search === 'string' ? new URLSearchParams(<string>(search)) : search != null ? (typeof search === 'string' ? new URLSearchParams(search) : search) : null;
<URLSearchParams>(search)) : this.withCredentials = withCredentials != null ? withCredentials : null;
null; this.responseType = responseType != null ? responseType : null;
this.withCredentials = isPresent(withCredentials) ? withCredentials : null;
this.responseType = isPresent(responseType) ? responseType : null;
} }
/** /**
@ -116,18 +112,18 @@ export class RequestOptions {
*/ */
merge(options?: RequestOptionsArgs): RequestOptions { merge(options?: RequestOptionsArgs): RequestOptions {
return new RequestOptions({ return new RequestOptions({
method: options && isPresent(options.method) ? options.method : this.method, method: options && options.method != null ? options.method : this.method,
headers: options && isPresent(options.headers) ? options.headers : this.headers, headers: options && options.headers != null ? options.headers : this.headers,
body: options && isPresent(options.body) ? options.body : this.body, body: options && options.body != null ? options.body : this.body,
url: options && isPresent(options.url) ? options.url : this.url, url: options && options.url != null ? options.url : this.url,
search: options && isPresent(options.search) ? search: options && options.search != null ?
(typeof options.search === 'string' ? new URLSearchParams(options.search) : (typeof options.search === 'string' ? new URLSearchParams(options.search) :
(<URLSearchParams>(options.search)).clone()) : options.search.clone()) :
this.search, this.search,
withCredentials: options && isPresent(options.withCredentials) ? options.withCredentials : withCredentials: options && options.withCredentials != null ? options.withCredentials :
this.withCredentials, this.withCredentials,
responseType: options && isPresent(options.responseType) ? options.responseType : responseType: options && options.responseType != null ? options.responseType :
this.responseType this.responseType
}); });
} }
} }

View File

@ -8,8 +8,6 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {isPresent} from '../src/facade/lang';
import {ResponseType} from './enums'; import {ResponseType} from './enums';
import {Headers} from './headers'; import {Headers} from './headers';
import {ResponseOptionsArgs} from './interfaces'; import {ResponseOptionsArgs} from './interfaces';
@ -68,12 +66,12 @@ export class ResponseOptions {
type: ResponseType; type: ResponseType;
url: string; url: string;
constructor({body, status, headers, statusText, type, url}: ResponseOptionsArgs = {}) { constructor({body, status, headers, statusText, type, url}: ResponseOptionsArgs = {}) {
this.body = isPresent(body) ? body : null; this.body = body != null ? body : null;
this.status = isPresent(status) ? status : null; this.status = status != null ? status : null;
this.headers = isPresent(headers) ? headers : null; this.headers = headers != null ? headers : null;
this.statusText = isPresent(statusText) ? statusText : null; this.statusText = statusText != null ? statusText : null;
this.type = isPresent(type) ? type : null; this.type = type != null ? type : null;
this.url = isPresent(url) ? url : null; this.url = url != null ? url : null;
} }
/** /**
@ -103,13 +101,12 @@ export class ResponseOptions {
*/ */
merge(options?: ResponseOptionsArgs): ResponseOptions { merge(options?: ResponseOptionsArgs): ResponseOptions {
return new ResponseOptions({ return new ResponseOptions({
body: isPresent(options) && isPresent(options.body) ? options.body : this.body, body: options && options.body != null ? options.body : this.body,
status: isPresent(options) && isPresent(options.status) ? options.status : this.status, status: options && options.status != null ? options.status : this.status,
headers: isPresent(options) && isPresent(options.headers) ? options.headers : this.headers, headers: options && options.headers != null ? options.headers : this.headers,
statusText: isPresent(options) && isPresent(options.statusText) ? options.statusText : statusText: options && options.statusText != null ? options.statusText : this.statusText,
this.statusText, type: options && options.type != null ? options.type : this.type,
type: isPresent(options) && isPresent(options.type) ? options.type : this.type, url: options && options.url != null ? options.url : this.url,
url: isPresent(options) && isPresent(options.url) ? options.url : this.url,
}); });
} }
} }

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {isJsObject, stringToArrayBuffer} from './http_utils'; import {stringToArrayBuffer} from './http_utils';
import {URLSearchParams} from './url_search_params'; import {URLSearchParams} from './url_search_params';
@ -51,7 +51,7 @@ export abstract class Body {
return ''; return '';
} }
if (isJsObject(this._body)) { if (typeof this._body === 'object') {
return JSON.stringify(this._body, null, 2); return JSON.stringify(this._body, null, 2);
} }

View File

@ -1 +0,0 @@
../../facade/src

View File

@ -8,7 +8,6 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {Observable} from 'rxjs/Observable'; import {Observable} from 'rxjs/Observable';
import {isPresent} from '../src/facade/lang';
import {BaseRequestOptions, RequestOptions} from './base_request_options'; import {BaseRequestOptions, RequestOptions} from './base_request_options';
import {RequestMethod} from './enums'; import {RequestMethod} from './enums';
import {ConnectionBackend, RequestOptionsArgs} from './interfaces'; import {ConnectionBackend, RequestOptionsArgs} from './interfaces';
@ -23,7 +22,7 @@ function mergeOptions(
defaultOpts: BaseRequestOptions, providedOpts: RequestOptionsArgs, method: RequestMethod, defaultOpts: BaseRequestOptions, providedOpts: RequestOptionsArgs, method: RequestMethod,
url: string): RequestOptions { url: string): RequestOptions {
const newOptions = defaultOpts; const newOptions = defaultOpts;
if (isPresent(providedOpts)) { if (providedOpts) {
// Hack so Dart can used named parameters // Hack so Dart can used named parameters
return newOptions.merge(new RequestOptions({ return newOptions.merge(new RequestOptions({
method: providedOpts.method || method, method: providedOpts.method || method,
@ -35,11 +34,8 @@ function mergeOptions(
responseType: providedOpts.responseType responseType: providedOpts.responseType
})); }));
} }
if (isPresent(method)) {
return newOptions.merge(new RequestOptions({method: method, url: url})); return newOptions.merge(new RequestOptions({method, url}));
} else {
return newOptions.merge(new RequestOptions({url: url}));
}
} }
/** /**

View File

@ -49,5 +49,3 @@ export function stringToArrayBuffer(input: String): ArrayBuffer {
} }
return view.buffer; return view.buffer;
} }
export {isJsObject} from '../src/facade/lang';

View File

@ -6,8 +6,6 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {isPresent} from '../src/facade/lang';
import {Body} from './body'; import {Body} from './body';
import {ContentType, RequestMethod, ResponseContentType} from './enums'; import {ContentType, RequestMethod, ResponseContentType} from './enums';
import {Headers} from './headers'; import {Headers} from './headers';
@ -78,7 +76,7 @@ export class Request extends Body {
// TODO: assert that url is present // TODO: assert that url is present
const url = requestOptions.url; const url = requestOptions.url;
this.url = requestOptions.url; this.url = requestOptions.url;
if (isPresent(requestOptions.search)) { if (requestOptions.search) {
const search = requestOptions.search.toString(); const search = requestOptions.search.toString();
if (search.length > 0) { if (search.length > 0) {
let prefix = '?'; let prefix = '?';
@ -93,7 +91,6 @@ export class Request extends Body {
this.method = normalizeMethodName(requestOptions.method); this.method = normalizeMethodName(requestOptions.method);
// TODO(jeffbcross): implement behavior // TODO(jeffbcross): implement behavior
// Defaults to 'omit', consistent with browser // Defaults to 'omit', consistent with browser
// TODO(jeffbcross): implement behavior
this.headers = new Headers(requestOptions.headers); this.headers = new Headers(requestOptions.headers);
this.contentType = this.detectContentType(); this.contentType = this.detectContentType();
this.withCredentials = requestOptions.withCredentials; this.withCredentials = requestOptions.withCredentials;

View File

@ -14,7 +14,6 @@ import {JSONPBackend, JSONPBackend_, JSONPConnection, JSONPConnection_} from '..
import {BaseRequestOptions, RequestOptions} from '../../src/base_request_options'; import {BaseRequestOptions, RequestOptions} from '../../src/base_request_options';
import {BaseResponseOptions, ResponseOptions} from '../../src/base_response_options'; import {BaseResponseOptions, ResponseOptions} from '../../src/base_response_options';
import {ReadyState, RequestMethod, ResponseType} from '../../src/enums'; import {ReadyState, RequestMethod, ResponseType} from '../../src/enums';
import {isPresent} from '../../src/facade/lang';
import {Request} from '../../src/static_request'; import {Request} from '../../src/static_request';
let existingScripts: MockBrowserJsonp[] = []; let existingScripts: MockBrowserJsonp[] = [];
@ -22,18 +21,14 @@ let existingScripts: MockBrowserJsonp[] = [];
class MockBrowserJsonp extends BrowserJsonp { class MockBrowserJsonp extends BrowserJsonp {
src: string; src: string;
callbacks = new Map<string, (data: any) => any>(); callbacks = new Map<string, (data: any) => any>();
constructor() { super(); }
addEventListener(type: string, cb: (data: any) => any) { this.callbacks.set(type, cb); } addEventListener(type: string, cb: (data: any) => any) { this.callbacks.set(type, cb); }
removeEventListener(type: string, cb: Function) { this.callbacks.delete(type); } removeEventListener(type: string, cb: Function) { this.callbacks.delete(type); }
dispatchEvent(type: string, argument?: any) { dispatchEvent(type: string, argument: any = {}) {
if (!isPresent(argument)) {
argument = {};
}
const cb = this.callbacks.get(type); const cb = this.callbacks.get(type);
if (isPresent(cb)) { if (cb) {
cb(argument); cb(argument);
} }
} }

View File

@ -9,7 +9,6 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {AsyncTestCompleter, SpyObject, afterEach, beforeEach, beforeEachProviders, describe, expect, inject, it} from '@angular/core/testing/testing_internal'; import {AsyncTestCompleter, SpyObject, afterEach, beforeEach, beforeEachProviders, describe, expect, inject, it} from '@angular/core/testing/testing_internal';
import {__platform_browser_private__} from '@angular/platform-browser'; import {__platform_browser_private__} from '@angular/platform-browser';
import {BrowserXhr} from '../../src/backends/browser_xhr'; import {BrowserXhr} from '../../src/backends/browser_xhr';
import {CookieXSRFStrategy, XHRBackend, XHRConnection} from '../../src/backends/xhr_backend'; import {CookieXSRFStrategy, XHRBackend, XHRConnection} from '../../src/backends/xhr_backend';
import {BaseRequestOptions, RequestOptions} from '../../src/base_request_options'; import {BaseRequestOptions, RequestOptions} from '../../src/base_request_options';
@ -486,6 +485,7 @@ export function main() {
existingXHRs[0].setStatusCode(statusCode); existingXHRs[0].setStatusCode(statusCode);
existingXHRs[0].dispatchEvent('load'); existingXHRs[0].dispatchEvent('load');
})); }));
it('should normalize IE\'s 1223 status code into 204', it('should normalize IE\'s 1223 status code into 204',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const statusCode = 1223; const statusCode = 1223;
@ -502,6 +502,22 @@ export function main() {
existingXHRs[0].dispatchEvent('load'); existingXHRs[0].dispatchEvent('load');
})); }));
it('should ignore response body for 204 status code',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const statusCode = 204;
const connection = new XHRConnection(
sampleRequest, new MockBrowserXHR(), new ResponseOptions({status: statusCode}));
connection.response.subscribe((res: Response) => {
expect(res.text()).toBe('');
async.done();
});
existingXHRs[0].setStatusCode(statusCode);
existingXHRs[0].setResponseText('Doge');
existingXHRs[0].dispatchEvent('load');
}));
it('should normalize responseText and response', it('should normalize responseText and response',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const responseBody = 'Doge'; const responseBody = 'Doge';
@ -623,6 +639,21 @@ Connection: keep-alive`;
existingXHRs[0].dispatchEvent('load'); existingXHRs[0].dispatchEvent('load');
})); }));
it('should return request url if it cannot be retrieved from response',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const statusCode = 200;
const connection = new XHRConnection(
sampleRequest, new MockBrowserXHR(), new ResponseOptions({status: statusCode}));
connection.response.subscribe((res: Response) => {
expect(res.url).toEqual('https://google.com');
async.done();
});
existingXHRs[0].setStatusCode(statusCode);
existingXHRs[0].dispatchEvent('load');
}));
it('should set the status text property from the XMLHttpRequest instance if present', it('should set the status text property from the XMLHttpRequest instance if present',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const statusText = 'test'; const statusText = 'test';

View File

@ -37,5 +37,7 @@ export function enableDebugTools<T>(ref: ComponentRef<T>): ComponentRef<T> {
* @experimental All debugging apis are currently experimental. * @experimental All debugging apis are currently experimental.
*/ */
export function disableDebugTools(): void { export function disableDebugTools(): void {
delete context.ng.profiler; if (context.ng) {
delete context.ng.profiler;
}
} }

View File

@ -13,7 +13,8 @@ import {AnimationKeyframe, AnimationStyles, NoOpAnimationPlayer} from '../privat
class _NoOpAnimationDriver implements AnimationDriver { class _NoOpAnimationDriver implements AnimationDriver {
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
return new NoOpAnimationPlayer(); return new NoOpAnimationPlayer();
} }
} }
@ -25,5 +26,6 @@ export abstract class AnimationDriver {
static NOOP: AnimationDriver = new _NoOpAnimationDriver(); static NOOP: AnimationDriver = new _NoOpAnimationDriver();
abstract animate( abstract animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer; duration: number, delay: number, easing: string,
previousPlayers?: AnimationPlayer[]): AnimationPlayer;
} }

View File

@ -260,9 +260,10 @@ export class DomRenderer implements Renderer {
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
return this._animationDriver.animate( return this._animationDriver.animate(
element, startingStyles, keyframes, duration, delay, easing); element, startingStyles, keyframes, duration, delay, easing, previousPlayers);
} }
} }

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AnimationPlayer} from '@angular/core';
import {isPresent} from '../facade/lang'; import {isPresent} from '../facade/lang';
import {AnimationKeyframe, AnimationStyles} from '../private_import_core'; import {AnimationKeyframe, AnimationStyles} from '../private_import_core';
@ -15,17 +16,18 @@ import {WebAnimationsPlayer} from './web_animations_player';
export class WebAnimationsDriver implements AnimationDriver { export class WebAnimationsDriver implements AnimationDriver {
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): WebAnimationsPlayer { duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): WebAnimationsPlayer {
let formattedSteps: {[key: string]: string | number}[] = []; let formattedSteps: {[key: string]: string | number}[] = [];
let startingStyleLookup: {[key: string]: string | number} = {}; let startingStyleLookup: {[key: string]: string | number} = {};
if (isPresent(startingStyles) && startingStyles.styles.length > 0) { if (isPresent(startingStyles) && startingStyles.styles.length > 0) {
startingStyleLookup = _populateStyles(element, startingStyles, {}); startingStyleLookup = _populateStyles(startingStyles, {});
startingStyleLookup['offset'] = 0; startingStyleLookup['offset'] = 0;
formattedSteps.push(startingStyleLookup); formattedSteps.push(startingStyleLookup);
} }
keyframes.forEach((keyframe: AnimationKeyframe) => { keyframes.forEach((keyframe: AnimationKeyframe) => {
const data = _populateStyles(element, keyframe.styles, startingStyleLookup); const data = _populateStyles(keyframe.styles, startingStyleLookup);
data['offset'] = keyframe.offset; data['offset'] = keyframe.offset;
formattedSteps.push(data); formattedSteps.push(data);
}); });
@ -52,13 +54,16 @@ export class WebAnimationsDriver implements AnimationDriver {
playerOptions['easing'] = easing; playerOptions['easing'] = easing;
} }
return new WebAnimationsPlayer(element, formattedSteps, playerOptions); // there may be a chance a NoOp player is returned depending
// on when the previous animation was cancelled
previousPlayers = previousPlayers.filter(filterWebAnimationPlayerFn);
return new WebAnimationsPlayer(
element, formattedSteps, playerOptions, <WebAnimationsPlayer[]>previousPlayers);
} }
} }
function _populateStyles( function _populateStyles(styles: AnimationStyles, defaultStyles: {[key: string]: string | number}):
element: any, styles: AnimationStyles, {[key: string]: string | number} {
defaultStyles: {[key: string]: string | number}): {[key: string]: string | number} {
const data: {[key: string]: string | number} = {}; const data: {[key: string]: string | number} = {};
styles.styles.forEach( styles.styles.forEach(
(entry) => { Object.keys(entry).forEach(prop => { data[prop] = entry[prop]; }); }); (entry) => { Object.keys(entry).forEach(prop => { data[prop] = entry[prop]; }); });
@ -69,3 +74,7 @@ function _populateStyles(
}); });
return data; return data;
} }
function filterWebAnimationPlayerFn(player: AnimationPlayer) {
return player instanceof WebAnimationsPlayer;
}

View File

@ -7,6 +7,8 @@
*/ */
import {AUTO_STYLE} from '@angular/core'; import {AUTO_STYLE} from '@angular/core';
import {isPresent} from '../facade/lang';
import {AnimationPlayer} from '../private_import_core'; import {AnimationPlayer} from '../private_import_core';
import {getDOM} from './dom_adapter'; import {getDOM} from './dom_adapter';
@ -21,13 +23,22 @@ export class WebAnimationsPlayer implements AnimationPlayer {
private _finished = false; private _finished = false;
private _started = false; private _started = false;
private _destroyed = false; private _destroyed = false;
private _finalKeyframe: {[key: string]: string | number};
public parentPlayer: AnimationPlayer = null; public parentPlayer: AnimationPlayer = null;
public previousStyles: {[styleName: string]: string | number};
constructor( constructor(
public element: any, public keyframes: {[key: string]: string | number}[], public element: any, public keyframes: {[key: string]: string | number}[],
public options: {[key: string]: string | number}) { public options: {[key: string]: string | number},
previousPlayers: WebAnimationsPlayer[] = []) {
this._duration = <number>options['duration']; this._duration = <number>options['duration'];
this.previousStyles = {};
previousPlayers.forEach(player => {
let styles = player._captureStyles();
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
});
} }
private _onFinish() { private _onFinish() {
@ -44,14 +55,30 @@ export class WebAnimationsPlayer implements AnimationPlayer {
const keyframes = this.keyframes.map(styles => { const keyframes = this.keyframes.map(styles => {
const formattedKeyframe: {[key: string]: string | number} = {}; const formattedKeyframe: {[key: string]: string | number} = {};
Object.keys(styles).forEach(prop => { Object.keys(styles).forEach((prop, index) => {
const value = styles[prop]; let value = styles[prop];
formattedKeyframe[prop] = value == AUTO_STYLE ? _computeStyle(this.element, prop) : value; if (value == AUTO_STYLE) {
value = _computeStyle(this.element, prop);
}
if (value != undefined) {
formattedKeyframe[prop] = value;
}
}); });
return formattedKeyframe; return formattedKeyframe;
}); });
const previousStyleProps = Object.keys(this.previousStyles);
if (previousStyleProps.length) {
let startingKeyframe = findStartingKeyframe(keyframes);
previousStyleProps.forEach(prop => {
if (isPresent(startingKeyframe[prop])) {
startingKeyframe[prop] = this.previousStyles[prop];
}
});
}
this._player = this._triggerWebAnimation(this.element, keyframes, this.options); this._player = this._triggerWebAnimation(this.element, keyframes, this.options);
this._finalKeyframe = _copyKeyframeStyles(keyframes[keyframes.length - 1]);
// this is required so that the player doesn't start to animate right away // this is required so that the player doesn't start to animate right away
this._resetDomPlayerState(); this._resetDomPlayerState();
@ -119,8 +146,47 @@ export class WebAnimationsPlayer implements AnimationPlayer {
setPosition(p: number): void { this._player.currentTime = p * this.totalTime; } setPosition(p: number): void { this._player.currentTime = p * this.totalTime; }
getPosition(): number { return this._player.currentTime / this.totalTime; } getPosition(): number { return this._player.currentTime / this.totalTime; }
private _captureStyles(): {[prop: string]: string | number} {
const styles: {[key: string]: string | number} = {};
if (this.hasStarted()) {
Object.keys(this._finalKeyframe).forEach(prop => {
if (prop != 'offset') {
styles[prop] =
this._finished ? this._finalKeyframe[prop] : _computeStyle(this.element, prop);
}
});
}
return styles;
}
} }
function _computeStyle(element: any, prop: string): string { function _computeStyle(element: any, prop: string): string {
return getDOM().getComputedStyle(element)[prop]; return getDOM().getComputedStyle(element)[prop];
} }
function _copyKeyframeStyles(styles: {[style: string]: string | number}):
{[style: string]: string | number} {
const newStyles: {[style: string]: string | number} = {};
Object.keys(styles).forEach(prop => {
if (prop != 'offset') {
newStyles[prop] = styles[prop];
}
});
return newStyles;
}
function findStartingKeyframe(keyframes: {[prop: string]: string | number}[]):
{[prop: string]: string | number} {
let startingKeyframe = keyframes[0];
// it's important that we find the LAST keyframe
// to ensure that style overidding is final.
for (let i = 1; i < keyframes.length; i++) {
const kf = keyframes[i];
const offset = kf['offset'];
if (offset !== 0) break;
startingKeyframe = kf;
}
return startingKeyframe;
}

View File

@ -6,13 +6,13 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal'; import {AnimationPlayer} from '@angular/core';
import {el} from '@angular/platform-browser/testing/browser_util'; import {el} from '@angular/platform-browser/testing/browser_util';
import {DomAnimatePlayer} from '../../src/dom/dom_animate_player'; import {DomAnimatePlayer} from '../../src/dom/dom_animate_player';
import {WebAnimationsDriver} from '../../src/dom/web_animations_driver'; import {WebAnimationsDriver} from '../../src/dom/web_animations_driver';
import {WebAnimationsPlayer} from '../../src/dom/web_animations_player'; import {WebAnimationsPlayer} from '../../src/dom/web_animations_player';
import {AnimationKeyframe, AnimationStyles} from '../../src/private_import_core'; import {AnimationKeyframe, AnimationStyles, NoOpAnimationPlayer} from '../../src/private_import_core';
import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_player'; import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_player';
class ExtendedWebAnimationsDriver extends WebAnimationsDriver { class ExtendedWebAnimationsDriver extends WebAnimationsDriver {
@ -48,8 +48,7 @@ export function main() {
it('should use a fill mode of `both`', () => { it('should use a fill mode of `both`', () => {
const startingStyles = _makeStyles({}); const startingStyles = _makeStyles({});
const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})]; const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})];
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'linear', []);
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'linear');
const details = _formatOptions(player); const details = _formatOptions(player);
const options = details['options']; const options = details['options'];
expect(options['fill']).toEqual('both'); expect(options['fill']).toEqual('both');
@ -58,8 +57,7 @@ export function main() {
it('should apply the provided easing', () => { it('should apply the provided easing', () => {
const startingStyles = _makeStyles({}); const startingStyles = _makeStyles({});
const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})]; const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})];
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'ease-out', []);
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'ease-out');
const details = _formatOptions(player); const details = _formatOptions(player);
const options = details['options']; const options = details['options'];
expect(options['easing']).toEqual('ease-out'); expect(options['easing']).toEqual('ease-out');
@ -68,16 +66,32 @@ export function main() {
it('should only apply the provided easing if present', () => { it('should only apply the provided easing if present', () => {
const startingStyles = _makeStyles({}); const startingStyles = _makeStyles({});
const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})]; const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})];
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, null, []);
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, null);
const details = _formatOptions(player); const details = _formatOptions(player);
const options = details['options']; const options = details['options'];
const keys = Object.keys(options); const keys = Object.keys(options);
expect(keys.indexOf('easing')).toEqual(-1); expect(keys.indexOf('easing')).toEqual(-1);
}); });
it('should only apply the provided easing if present', () => {
const previousPlayers = [
new NoOpAnimationPlayerWithStyles(),
new NoOpAnimationPlayerWithStyles(),
new NoOpAnimationPlayerWithStyles(),
];
const startingStyles = _makeStyles({});
const styles = [_makeKeyframe(0, {}), _makeKeyframe(1, {})];
const player = driver.animate(
elm, startingStyles, styles, 1000, 1000, null, <AnimationPlayer[]>previousPlayers);
expect(player.previousStyles).toEqual({});
});
}); });
} }
class NoOpAnimationPlayerWithStyles extends NoOpAnimationPlayer {
private _captureStyles() { return {color: 'red'}; }
}
function _formatOptions(player: WebAnimationsPlayer): {[key: string]: any} { function _formatOptions(player: WebAnimationsPlayer): {[key: string]: any} {
return {'element': player.element, 'keyframes': player.keyframes, 'options': player.options}; return {'element': player.element, 'keyframes': player.keyframes, 'options': player.options};
} }

View File

@ -6,7 +6,9 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {MockAnimationPlayer, beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal'; import {AUTO_STYLE, AnimationPlayer} from '@angular/core';
import {MockAnimationPlayer} from '@angular/core/testing/testing_internal';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {el} from '@angular/platform-browser/testing/browser_util'; import {el} from '@angular/platform-browser/testing/browser_util';
import {DomAnimatePlayer} from '../../src/dom/dom_animate_player'; import {DomAnimatePlayer} from '../../src/dom/dom_animate_player';
@ -18,14 +20,16 @@ class ExtendedWebAnimationsPlayer extends WebAnimationsPlayer {
constructor( constructor(
public element: HTMLElement, public keyframes: {[key: string]: string | number}[], public element: HTMLElement, public keyframes: {[key: string]: string | number}[],
public options: {[key: string]: string | number}) { public options: {[key: string]: string | number},
super(element, keyframes, options); public previousPlayers: WebAnimationsPlayer[] = []) {
super(element, keyframes, options, previousPlayers);
} }
get domPlayer() { return this._overriddenDomPlayer; } get domPlayer() { return this._overriddenDomPlayer; }
/** @internal */ /** @internal */
_triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer { _triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer {
this._overriddenDomPlayer._capture('trigger', {elm, keyframes, options});
return this._overriddenDomPlayer; return this._overriddenDomPlayer;
} }
} }
@ -33,7 +37,7 @@ class ExtendedWebAnimationsPlayer extends WebAnimationsPlayer {
export function main() { export function main() {
function makePlayer(): {[key: string]: any} { function makePlayer(): {[key: string]: any} {
const someElm = el('<div></div>'); const someElm = el('<div></div>');
const player = new ExtendedWebAnimationsPlayer(someElm, [], {}); const player = new ExtendedWebAnimationsPlayer(someElm, [{}, {}], {}, []);
player.init(); player.init();
return {'captures': player.domPlayer.captures, 'player': player}; return {'captures': player.domPlayer.captures, 'player': player};
} }
@ -156,5 +160,72 @@ export function main() {
player.destroy(); player.destroy();
expect(captures['cancel'].length).toBe(1); expect(captures['cancel'].length).toBe(1);
}); });
it('should resolve auto styles based on what is computed from the provided element', () => {
const elm = el('<div></div>');
document.body.appendChild(elm); // required for getComputedStyle() to work
elm.style.opacity = '0.5';
const player = new ExtendedWebAnimationsPlayer(
elm, [{opacity: AUTO_STYLE}, {opacity: '1'}], {duration: 1000}, []);
player.init();
const data = player.domPlayer.captures['trigger'][0];
expect(data['keyframes']).toEqual([{opacity: '0.5'}, {opacity: '1'}]);
});
describe('previousStyle', () => {
it('should merge keyframe styles based on the previous styles passed in when the player has finished its operation',
() => {
const elm = el('<div></div>');
const previousStyles = {width: '100px', height: '666px'};
const previousPlayer =
new ExtendedWebAnimationsPlayer(elm, [previousStyles, previousStyles], {}, []);
previousPlayer.play();
previousPlayer.finish();
const player = new ExtendedWebAnimationsPlayer(
elm,
[
{width: '0px', height: '0px', opacity: 0, offset: 0},
{width: '0px', height: '0px', opacity: 1, offset: 1}
],
{duration: 1000}, [previousPlayer]);
player.init();
const data = player.domPlayer.captures['trigger'][0];
expect(data['keyframes']).toEqual([
{width: '100px', height: '666px', opacity: 0, offset: 0},
{width: '0px', height: '0px', opacity: 1, offset: 1}
]);
});
it('should properly calculate the previous styles for the player even when its currently playing',
() => {
if (!getDOM().supportsWebAnimation()) return;
const elm = el('<div></div>');
document.body.appendChild(elm);
const fromStyles = {width: '100px', height: '666px'};
const toStyles = {width: '50px', height: '333px'};
const previousPlayer =
new WebAnimationsPlayer(elm, [fromStyles, toStyles], {duration: 1000}, []);
previousPlayer.play();
previousPlayer.setPosition(0.5);
previousPlayer.pause();
const newStyles = {width: '0px', height: '0px'};
const player = new WebAnimationsPlayer(
elm, [newStyles, newStyles], {duration: 1000}, [previousPlayer]);
player.init();
const data = player.previousStyles;
expect(player.previousStyles).toEqual({width: '75px', height: '499.5px'});
});
});
}); });
} }

View File

@ -9,19 +9,29 @@
import {AnimationPlayer} from '@angular/core'; import {AnimationPlayer} from '@angular/core';
import {MockAnimationPlayer} from '@angular/core/testing/testing_internal'; import {MockAnimationPlayer} from '@angular/core/testing/testing_internal';
import {AnimationDriver} from '@angular/platform-browser'; import {AnimationDriver} from '@angular/platform-browser';
import {ListWrapper} from './facade/collection';
import {AnimationKeyframe, AnimationStyles} from './private_import_core'; import {AnimationKeyframe, AnimationStyles} from './private_import_core';
export class MockAnimationDriver extends AnimationDriver { export class MockAnimationDriver extends AnimationDriver {
public log: {[key: string]: any}[] = []; public log: {[key: string]: any}[] = [];
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
const player = new MockAnimationPlayer(); previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
const mockPlayers = <MockAnimationPlayer[]>previousPlayers.filter(
player => player instanceof MockAnimationPlayer);
const normalizedStartingStyles = _serializeStyles(startingStyles);
const normalizedKeyframes = _serializeKeyframes(keyframes);
const player =
new MockAnimationPlayer(normalizedStartingStyles, normalizedKeyframes, previousPlayers);
this.log.push({ this.log.push({
'element': element, 'element': element,
'startingStyles': _serializeStyles(startingStyles), 'startingStyles': normalizedStartingStyles,
'previousStyles': player.previousStyles,
'keyframes': keyframes, 'keyframes': keyframes,
'keyframeLookup': _serializeKeyframes(keyframes), 'keyframeLookup': normalizedKeyframes,
'duration': duration, 'duration': duration,
'delay': delay, 'delay': delay,
'easing': easing, 'easing': easing,

View File

@ -206,9 +206,10 @@ export class ServerRenderer implements Renderer {
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
return this._animationDriver.animate( return this._animationDriver.animate(
element, startingStyles, keyframes, duration, delay, easing); element, startingStyles, keyframes, duration, delay, easing, previousPlayers);
} }
} }

View File

@ -89,7 +89,7 @@ export class MessageBasedRenderer {
'animate', 'animate',
[ [
RenderStoreObject, RenderStoreObject, PRIMITIVE, PRIMITIVE, PRIMITIVE, PRIMITIVE, RenderStoreObject, RenderStoreObject, PRIMITIVE, PRIMITIVE, PRIMITIVE, PRIMITIVE,
PRIMITIVE, PRIMITIVE PRIMITIVE, PRIMITIVE, PRIMITIVE
], ],
this._animate.bind(this)); this._animate.bind(this));
@ -248,8 +248,14 @@ export class MessageBasedRenderer {
private _animate( private _animate(
renderer: Renderer, element: any, startingStyles: any, keyframes: any[], duration: number, renderer: Renderer, element: any, startingStyles: any, keyframes: any[], duration: number,
delay: number, easing: string, playerId: any) { delay: number, easing: string, previousPlayers: number[], playerId: any) {
const player = renderer.animate(element, startingStyles, keyframes, duration, delay, easing); let normalizedPreviousPlayers: AnimationPlayer[];
if (previousPlayers && previousPlayers.length) {
normalizedPreviousPlayers =
previousPlayers.map(playerId => this._renderStore.deserialize(playerId));
}
const player = renderer.animate(
element, startingStyles, keyframes, duration, delay, easing, normalizedPreviousPlayers);
this._renderStore.store(player, playerId); this._renderStore.store(player, playerId);
} }

View File

@ -16,6 +16,7 @@ import {MessageBus} from '../shared/message_bus';
import {EVENT_CHANNEL, RENDERER_CHANNEL} from '../shared/messaging_api'; import {EVENT_CHANNEL, RENDERER_CHANNEL} from '../shared/messaging_api';
import {RenderStore} from '../shared/render_store'; import {RenderStore} from '../shared/render_store';
import {ANIMATION_WORKER_PLAYER_PREFIX, RenderStoreObject, Serializer} from '../shared/serializer'; import {ANIMATION_WORKER_PLAYER_PREFIX, RenderStoreObject, Serializer} from '../shared/serializer';
import {deserializeGenericEvent} from './event_deserializer'; import {deserializeGenericEvent} from './event_deserializer';
@Injectable() @Injectable()
@ -239,13 +240,16 @@ export class WebWorkerRenderer implements Renderer, RenderStoreObject {
animate( animate(
renderElement: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], renderElement: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
const playerId = this._rootRenderer.allocateId(); const playerId = this._rootRenderer.allocateId();
const previousPlayerIds: number[] =
previousPlayers.map(player => this._rootRenderer.renderStore.serialize(player));
this._runOnService('animate', [ this._runOnService('animate', [
new FnArg(renderElement, RenderStoreObject), new FnArg(startingStyles, null), new FnArg(renderElement, RenderStoreObject), new FnArg(startingStyles, null),
new FnArg(keyframes, null), new FnArg(duration, null), new FnArg(delay, null), new FnArg(keyframes, null), new FnArg(duration, null), new FnArg(delay, null),
new FnArg(easing, null), new FnArg(playerId, null) new FnArg(easing, null), new FnArg(previousPlayerIds, null), new FnArg(playerId, null)
]); ]);
const player = new _AnimationWorkerRendererPlayer(this._rootRenderer, renderElement); const player = new _AnimationWorkerRendererPlayer(this._rootRenderer, renderElement);
@ -325,7 +329,7 @@ export class WebWorkerRenderNode {
animationPlayerEvents = new AnimationPlayerEmitter(); animationPlayerEvents = new AnimationPlayerEmitter();
} }
class _AnimationWorkerRendererPlayer implements AnimationPlayer, RenderStoreObject { class _AnimationWorkerRendererPlayer implements RenderStoreObject {
public parentPlayer: AnimationPlayer = null; public parentPlayer: AnimationPlayer = null;
private _destroyed: boolean = false; private _destroyed: boolean = false;

View File

@ -289,6 +289,30 @@ export function main() {
expect(player.log.indexOf('destroy') >= 0).toBe(true); expect(player.log.indexOf('destroy') >= 0).toBe(true);
})); }));
it('should properly transition to the next animation if the current one is cancelled',
fakeAsync(() => {
const fixture = TestBed.createComponent(AnimationCmp);
const cmp = fixture.componentInstance;
cmp.state = 'on';
fixture.detectChanges();
flushMicrotasks();
let player = <MockAnimationPlayer>uiDriver.log.shift()['player'];
player.finish();
player = <MockAnimationPlayer>uiDriver.log.shift()['player'];
player.setPosition(0.5);
uiDriver.log = [];
cmp.state = 'off';
fixture.detectChanges();
flushMicrotasks();
const step = uiDriver.log.shift();
expect(step['previousStyles']).toEqual({opacity: AUTO_STYLE, fontSize: AUTO_STYLE});
}));
}); });
} }

View File

@ -0,0 +1,5 @@
/**
* @license Angular v0.0.0-ROUTERPLACEHOLDER
* (c) 2010-2016 Google, Inc. https://angular.io/
* License: MIT
*/

View File

@ -1,6 +1,6 @@
{ {
"name": "@angular/router", "name": "@angular/router",
"version": "3.0.0-rc.1", "version": "0.0.0-ROUTERPLACEHOLDER",
"description": "Angular - the routing library", "description": "Angular - the routing library",
"main": "bundles/router.umd.js", "main": "bundles/router.umd.js",
"module": "index.js", "module": "index.js",
@ -24,7 +24,6 @@
"@angular/core": "0.0.0-PLACEHOLDER", "@angular/core": "0.0.0-PLACEHOLDER",
"@angular/common": "0.0.0-PLACEHOLDER", "@angular/common": "0.0.0-PLACEHOLDER",
"@angular/platform-browser": "0.0.0-PLACEHOLDER", "@angular/platform-browser": "0.0.0-PLACEHOLDER",
"@angular/upgrade": "0.0.0-PLACEHOLDER",
"rxjs": "5.0.0-beta.12" "rxjs": "5.0.0-beta.12"
} }
} }

View File

@ -20,8 +20,8 @@ import {EmptyError} from 'rxjs/util/EmptyError';
import {Route, Routes, UrlMatchResult} from './config'; import {Route, Routes, UrlMatchResult} from './config';
import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader'; import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader';
import {NavigationCancelingError, PRIMARY_OUTLET, defaultUrlMatcher} from './shared'; import {NavigationCancelingError, PRIMARY_OUTLET, Params, defaultUrlMatcher} from './shared';
import {UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree'; import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
import {andObservables, forEach, merge, waitForMap, wrapIntoObservable} from './utils/collection'; import {andObservables, forEach, merge, waitForMap, wrapIntoObservable} from './utils/collection';
class NoMatch { class NoMatch {
@ -29,7 +29,7 @@ class NoMatch {
} }
class AbsoluteRedirect { class AbsoluteRedirect {
constructor(public segments: UrlSegment[]) {} constructor(public urlTree: UrlTree) {}
} }
function noMatch(segmentGroup: UrlSegmentGroup): Observable<UrlSegmentGroup> { function noMatch(segmentGroup: UrlSegmentGroup): Observable<UrlSegmentGroup> {
@ -37,9 +37,15 @@ function noMatch(segmentGroup: UrlSegmentGroup): Observable<UrlSegmentGroup> {
(obs: Observer<UrlSegmentGroup>) => obs.error(new NoMatch(segmentGroup))); (obs: Observer<UrlSegmentGroup>) => obs.error(new NoMatch(segmentGroup)));
} }
function absoluteRedirect(segments: UrlSegment[]): Observable<UrlSegmentGroup> { function absoluteRedirect(newTree: UrlTree): Observable<any> {
return new Observable<UrlSegmentGroup>( return new Observable<UrlSegmentGroup>(
(obs: Observer<UrlSegmentGroup>) => obs.error(new AbsoluteRedirect(segments))); (obs: Observer<UrlSegmentGroup>) => obs.error(new AbsoluteRedirect(newTree)));
}
function namedOutletsRedirect(redirectTo: string): Observable<any> {
return new Observable<UrlSegmentGroup>(
(obs: Observer<UrlSegmentGroup>) => obs.error(new Error(
`Only absolute redirects can have named outlets. redirectTo: '${redirectTo}'`)));
} }
function canLoadFails(route: Route): Observable<LoadedRouterConfig> { function canLoadFails(route: Route): Observable<LoadedRouterConfig> {
@ -50,9 +56,9 @@ function canLoadFails(route: Route): Observable<LoadedRouterConfig> {
export function applyRedirects( export function applyRedirects(
injector: Injector, configLoader: RouterConfigLoader, urlTree: UrlTree, injector: Injector, configLoader: RouterConfigLoader, urlSerializer: UrlSerializer,
config: Routes): Observable<UrlTree> { urlTree: UrlTree, config: Routes): Observable<UrlTree> {
return new ApplyRedirects(injector, configLoader, urlTree, config).apply(); return new ApplyRedirects(injector, configLoader, urlSerializer, urlTree, config).apply();
} }
class ApplyRedirects { class ApplyRedirects {
@ -60,21 +66,20 @@ class ApplyRedirects {
constructor( constructor(
private injector: Injector, private configLoader: RouterConfigLoader, private injector: Injector, private configLoader: RouterConfigLoader,
private urlTree: UrlTree, private config: Routes) {} private urlSerializer: UrlSerializer, private urlTree: UrlTree, private config: Routes) {}
apply(): Observable<UrlTree> { apply(): Observable<UrlTree> {
const expanded$ = const expanded$ =
this.expandSegmentGroup(this.injector, this.config, this.urlTree.root, PRIMARY_OUTLET); this.expandSegmentGroup(this.injector, this.config, this.urlTree.root, PRIMARY_OUTLET);
const urlTrees$ = map.call( const urlTrees$ = map.call(
expanded$, (rootSegmentGroup: UrlSegmentGroup) => this.createUrlTree(rootSegmentGroup)); expanded$, (rootSegmentGroup: UrlSegmentGroup) => this.createUrlTree(
rootSegmentGroup, this.urlTree.queryParams, this.urlTree.fragment));
return _catch.call(urlTrees$, (e: any) => { return _catch.call(urlTrees$, (e: any) => {
if (e instanceof AbsoluteRedirect) { if (e instanceof AbsoluteRedirect) {
// after an absolute redirect we do not apply any more redirects! // after an absolute redirect we do not apply any more redirects!
this.allowRedirects = false; this.allowRedirects = false;
const group =
new UrlSegmentGroup([], {[PRIMARY_OUTLET]: new UrlSegmentGroup(e.segments, {})});
// we need to run matching, so we can fetch all lazy-loaded modules // we need to run matching, so we can fetch all lazy-loaded modules
return this.match(group); return this.match(e.urlTree);
} else if (e instanceof NoMatch) { } else if (e instanceof NoMatch) {
throw this.noMatchError(e); throw this.noMatchError(e);
} else { } else {
@ -83,11 +88,12 @@ class ApplyRedirects {
}); });
} }
private match(segmentGroup: UrlSegmentGroup): Observable<UrlTree> { private match(tree: UrlTree): Observable<UrlTree> {
const expanded$ = const expanded$ =
this.expandSegmentGroup(this.injector, this.config, segmentGroup, PRIMARY_OUTLET); this.expandSegmentGroup(this.injector, this.config, tree.root, PRIMARY_OUTLET);
const mapped$ = map.call( const mapped$ = map.call(
expanded$, (rootSegmentGroup: UrlSegmentGroup) => this.createUrlTree(rootSegmentGroup)); expanded$, (rootSegmentGroup: UrlSegmentGroup) =>
this.createUrlTree(rootSegmentGroup, tree.queryParams, tree.fragment));
return _catch.call(mapped$, (e: any): Observable<UrlTree> => { return _catch.call(mapped$, (e: any): Observable<UrlTree> => {
if (e instanceof NoMatch) { if (e instanceof NoMatch) {
throw this.noMatchError(e); throw this.noMatchError(e);
@ -101,11 +107,12 @@ class ApplyRedirects {
return new Error(`Cannot match any routes. URL Segment: '${e.segmentGroup}'`); return new Error(`Cannot match any routes. URL Segment: '${e.segmentGroup}'`);
} }
private createUrlTree(rootCandidate: UrlSegmentGroup): UrlTree { private createUrlTree(rootCandidate: UrlSegmentGroup, queryParams: Params, fragment: string):
UrlTree {
const root = rootCandidate.segments.length > 0 ? const root = rootCandidate.segments.length > 0 ?
new UrlSegmentGroup([], {[PRIMARY_OUTLET]: rootCandidate}) : new UrlSegmentGroup([], {[PRIMARY_OUTLET]: rootCandidate}) :
rootCandidate; rootCandidate;
return new UrlTree(root, this.urlTree.queryParams, this.urlTree.fragment); return new UrlTree(root, queryParams, fragment);
} }
private expandSegmentGroup( private expandSegmentGroup(
@ -191,12 +198,14 @@ class ApplyRedirects {
private expandWildCardWithParamsAgainstRouteUsingRedirect( private expandWildCardWithParamsAgainstRouteUsingRedirect(
injector: Injector, routes: Route[], route: Route, injector: Injector, routes: Route[], route: Route,
outlet: string): Observable<UrlSegmentGroup> { outlet: string): Observable<UrlSegmentGroup> {
const newSegments = applyRedirectCommands([], route.redirectTo, {}); const newTree = this.applyRedirectCommands([], route.redirectTo, {});
if (route.redirectTo.startsWith('/')) { if (route.redirectTo.startsWith('/')) {
return absoluteRedirect(newSegments); return absoluteRedirect(newTree);
} else { } else {
const group = new UrlSegmentGroup(newSegments, {}); return mergeMap.call(this.lineralizeSegments(route, newTree), (newSegments: UrlSegment[]) => {
return this.expandSegment(injector, group, routes, newSegments, outlet, false); const group = new UrlSegmentGroup(newSegments, {});
return this.expandSegment(injector, group, routes, newSegments, outlet, false);
});
} }
} }
@ -207,14 +216,16 @@ class ApplyRedirects {
match(segmentGroup, route, segments); match(segmentGroup, route, segments);
if (!matched) return noMatch(segmentGroup); if (!matched) return noMatch(segmentGroup);
const newSegments = const newTree = this.applyRedirectCommands(
applyRedirectCommands(consumedSegments, route.redirectTo, <any>positionalParamSegments); consumedSegments, route.redirectTo, <any>positionalParamSegments);
if (route.redirectTo.startsWith('/')) { if (route.redirectTo.startsWith('/')) {
return absoluteRedirect(newSegments); return absoluteRedirect(newTree);
} else { } else {
return this.expandSegment( return mergeMap.call(this.lineralizeSegments(route, newTree), (newSegments: UrlSegment[]) => {
injector, segmentGroup, routes, newSegments.concat(segments.slice(lastChild)), outlet, return this.expandSegment(
false); injector, segmentGroup, routes, newSegments.concat(segments.slice(lastChild)), outlet,
false);
});
} }
} }
@ -284,6 +295,92 @@ class ApplyRedirects {
return of (new LoadedRouterConfig([], injector, null, null)); return of (new LoadedRouterConfig([], injector, null, null));
} }
} }
private lineralizeSegments(route: Route, urlTree: UrlTree): Observable<UrlSegment[]> {
let res: UrlSegment[] = [];
let c = urlTree.root;
while (true) {
res = res.concat(c.segments);
if (c.numberOfChildren === 0) {
return of (res);
} else if (c.numberOfChildren > 1 || !c.children[PRIMARY_OUTLET]) {
return namedOutletsRedirect(route.redirectTo);
} else {
c = c.children[PRIMARY_OUTLET];
}
}
}
private applyRedirectCommands(
segments: UrlSegment[], redirectTo: string, posParams: {[k: string]: UrlSegment}): UrlTree {
const t = this.urlSerializer.parse(redirectTo);
return this.applyRedirectCreatreUrlTree(
redirectTo, this.urlSerializer.parse(redirectTo), segments, posParams);
}
private applyRedirectCreatreUrlTree(
redirectTo: string, urlTree: UrlTree, segments: UrlSegment[],
posParams: {[k: string]: UrlSegment}): UrlTree {
const newRoot = this.createSegmentGroup(redirectTo, urlTree.root, segments, posParams);
return new UrlTree(
newRoot, this.createQueryParams(urlTree.queryParams, this.urlTree.queryParams),
urlTree.fragment);
}
private createQueryParams(redirectToParams: Params, actualParams: Params): Params {
const res: Params = {};
forEach(redirectToParams, (v: any, k: string) => {
if (v.startsWith(':')) {
res[k] = actualParams[v.substring(1)];
} else {
res[k] = v;
}
});
return res;
}
private createSegmentGroup(
redirectTo: string, group: UrlSegmentGroup, segments: UrlSegment[],
posParams: {[k: string]: UrlSegment}): UrlSegmentGroup {
const updatedSegments = this.createSegments(redirectTo, group.segments, segments, posParams);
let children: {[n: string]: UrlSegmentGroup} = {};
forEach(group.children, (child: UrlSegmentGroup, name: string) => {
children[name] = this.createSegmentGroup(redirectTo, child, segments, posParams);
});
return new UrlSegmentGroup(updatedSegments, children);
}
private createSegments(
redirectTo: string, redirectToSegments: UrlSegment[], actualSegments: UrlSegment[],
posParams: {[k: string]: UrlSegment}): UrlSegment[] {
return redirectToSegments.map(
s => s.path.startsWith(':') ? this.findPosParam(redirectTo, s, posParams) :
this.findOrReturn(s, actualSegments));
}
private findPosParam(
redirectTo: string, redirectToUrlSegment: UrlSegment,
posParams: {[k: string]: UrlSegment}): UrlSegment {
const pos = posParams[redirectToUrlSegment.path.substring(1)];
if (!pos)
throw new Error(
`Cannot redirect to '${redirectTo}'. Cannot find '${redirectToUrlSegment.path}'.`);
return pos;
}
private findOrReturn(redirectToUrlSegment: UrlSegment, actualSegments: UrlSegment[]): UrlSegment {
let idx = 0;
for (const s of actualSegments) {
if (s.path === redirectToUrlSegment.path) {
actualSegments.splice(idx);
return s;
}
idx++;
}
return redirectToUrlSegment;
}
} }
function runGuards(injector: Injector, route: Route): Observable<boolean> { function runGuards(injector: Injector, route: Route): Observable<boolean> {
@ -328,46 +425,6 @@ function match(segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment
}; };
} }
function applyRedirectCommands(
segments: UrlSegment[], redirectTo: string,
posParams: {[k: string]: UrlSegment}): UrlSegment[] {
const r = redirectTo.startsWith('/') ? redirectTo.substring(1) : redirectTo;
if (r === '') {
return [];
} else {
return createSegments(redirectTo, r.split('/'), segments, posParams);
}
}
function createSegments(
redirectTo: string, parts: string[], segments: UrlSegment[],
posParams: {[k: string]: UrlSegment}): UrlSegment[] {
return parts.map(
p => p.startsWith(':') ? findPosParam(p, posParams, redirectTo) :
findOrCreateSegment(p, segments));
}
function findPosParam(
part: string, posParams: {[k: string]: UrlSegment}, redirectTo: string): UrlSegment {
const paramName = part.substring(1);
const pos = posParams[paramName];
if (!pos) throw new Error(`Cannot redirect to '${redirectTo}'. Cannot find '${part}'.`);
return pos;
}
function findOrCreateSegment(part: string, segments: UrlSegment[]): UrlSegment {
let idx = 0;
for (const s of segments) {
if (s.path === part) {
segments.splice(idx);
return s;
}
idx++;
}
return new UrlSegment(part, {});
}
function split( function split(
segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[], segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[],
config: Route[]) { config: Route[]) {

View File

@ -154,6 +154,9 @@ import {UrlSegment, UrlSegmentGroup} from './url_tree';
* When navigating to `/team/11/user/jim`, the router will instantiate the wrapper component with * When navigating to `/team/11/user/jim`, the router will instantiate the wrapper component with
* the user component in it. * the user component in it.
* *
* An empty path route inherits its parent's params and data. This is because it cannot have its
* own params, and, as a result, it often uses its parent's params and data as its own.
*
* ### Matching Strategy * ### Matching Strategy
* *
* By default the router will look at what is left in the url, and check if it starts with * By default the router will look at what is left in the url, and check if it starts with
@ -219,7 +222,8 @@ import {UrlSegment, UrlSegmentGroup} from './url_tree';
* has to have the primary and aux outlets defined. * has to have the primary and aux outlets defined.
* *
* The router will also merge the `params`, `data`, and `resolve` of the componentless parent into * The router will also merge the `params`, `data`, and `resolve` of the componentless parent into
* the `params`, `data`, and `resolve` of the children. * the `params`, `data`, and `resolve` of the children. This is done because there is no component
* that can inject the activated route of the componentless parent.
* *
* This is especially useful when child components are defined as follows: * This is especially useful when child components are defined as follows:
* *

View File

@ -623,7 +623,8 @@ export class Router {
Promise.resolve() Promise.resolve()
.then( .then(
(_) => this.runNavigate( (_) => this.runNavigate(
url, rawUrl, false, false, id, createEmptyState(url, this.rootComponentType))) url, rawUrl, false, false, id,
createEmptyState(url, this.rootComponentType).snapshot))
.then(resolve, reject); .then(resolve, reject);
} else { } else {
@ -634,7 +635,7 @@ export class Router {
private runNavigate( private runNavigate(
url: UrlTree, rawUrl: UrlTree, shouldPreventPushState: boolean, shouldReplaceUrl: boolean, url: UrlTree, rawUrl: UrlTree, shouldPreventPushState: boolean, shouldReplaceUrl: boolean,
id: number, precreatedState: RouterState): Promise<boolean> { id: number, precreatedState: RouterStateSnapshot): Promise<boolean> {
if (id !== this.navigationId) { if (id !== this.navigationId) {
this.location.go(this.urlSerializer.serialize(this.currentUrlTree)); this.location.go(this.urlSerializer.serialize(this.currentUrlTree));
this.routerEvents.next(new NavigationCancel( this.routerEvents.next(new NavigationCancel(
@ -644,68 +645,80 @@ export class Router {
} }
return new Promise((resolvePromise, rejectPromise) => { return new Promise((resolvePromise, rejectPromise) => {
let state: RouterState; // create an observable of the url and route state snapshot
let navigationIsSuccessful: boolean; // this operation do not result in any side effects
let preActivation: PreActivation; let urlAndSnapshot$: Observable<{appliedUrl: UrlTree, snapshot: RouterStateSnapshot}>;
let appliedUrl: UrlTree;
const storedState = this.currentRouterState;
const storedUrl = this.currentUrlTree;
let routerState$: any;
if (!precreatedState) { if (!precreatedState) {
const redirectsApplied$ = const redirectsApplied$ =
applyRedirects(this.injector, this.configLoader, url, this.config); applyRedirects(this.injector, this.configLoader, this.urlSerializer, url, this.config);
const snapshot$ = mergeMap.call(redirectsApplied$, (u: UrlTree) => { urlAndSnapshot$ = mergeMap.call(redirectsApplied$, (appliedUrl: UrlTree) => {
appliedUrl = u; return map.call(
return recognize( recognize(
this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl)); this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl)),
}); (snapshot: any) => {
const emitRecognzied$ = this.routerEvents.next(new RoutesRecognized(
map.call(snapshot$, (newRouterStateSnapshot: RouterStateSnapshot) => { id, this.serializeUrl(url), this.serializeUrl(appliedUrl), snapshot));
this.routerEvents.next(new RoutesRecognized(
id, this.serializeUrl(url), this.serializeUrl(appliedUrl),
newRouterStateSnapshot));
return newRouterStateSnapshot;
});
routerState$ = map.call(emitRecognzied$, (routerStateSnapshot: RouterStateSnapshot) => { return {appliedUrl, snapshot};
return createRouterState(routerStateSnapshot, this.currentRouterState); });
}); });
} else { } else {
appliedUrl = url; urlAndSnapshot$ = of ({appliedUrl: url, snapshot: precreatedState});
routerState$ = of (precreatedState);
} }
const preactivation$ = map.call(routerState$, (newState: RouterState) => {
state = newState; // run preactivation: guards and data resolvers
let preActivation: PreActivation;
const preactivationTraverse$ = map.call(urlAndSnapshot$, ({appliedUrl, snapshot}: any) => {
preActivation = preActivation =
new PreActivation(state.snapshot, this.currentRouterState.snapshot, this.injector); new PreActivation(snapshot, this.currentRouterState.snapshot, this.injector);
preActivation.traverse(this.outletMap); preActivation.traverse(this.outletMap);
return {appliedUrl, snapshot};
}); });
const preactivation2$ = mergeMap.call(preactivation$, () => { const preactivationCheckGuards =
mergeMap.call(preactivationTraverse$, ({appliedUrl, snapshot}: any) => {
if (this.navigationId !== id) return of (false);
return map.call(preActivation.checkGuards(), (shouldActivate: boolean) => {
return {appliedUrl: appliedUrl, snapshot: snapshot, shouldActivate: shouldActivate};
});
});
const preactivationResolveData$ = mergeMap.call(preactivationCheckGuards, (p: any) => {
if (this.navigationId !== id) return of (false); if (this.navigationId !== id) return of (false);
return preActivation.checkGuards(); if (p.shouldActivate) {
}); return map.call(preActivation.resolveData(), () => p);
const resolveData$ = mergeMap.call(preactivation2$, (shouldActivate: boolean) => {
if (this.navigationId !== id) return of (false);
if (shouldActivate) {
return map.call(preActivation.resolveData(), () => shouldActivate);
} else { } else {
return of (shouldActivate); return of (p);
} }
}); });
resolveData$
.forEach((shouldActivate: boolean) => { // create router state
// this operation has side effects => route state is being affected
const routerState$ =
map.call(preactivationResolveData$, ({appliedUrl, snapshot, shouldActivate}: any) => {
if (shouldActivate) {
const state = createRouterState(snapshot, this.currentRouterState);
return {appliedUrl, state, shouldActivate};
} else {
return {appliedUrl, state: null, shouldActivate};
}
});
// applied the new router state
// this operation has side effects
let navigationIsSuccessful: boolean;
const storedState = this.currentRouterState;
const storedUrl = this.currentUrlTree;
routerState$
.forEach(({appliedUrl, state, shouldActivate}: any) => {
if (!shouldActivate || id !== this.navigationId) { if (!shouldActivate || id !== this.navigationId) {
navigationIsSuccessful = false; navigationIsSuccessful = false;
return; return;
@ -733,8 +746,8 @@ export class Router {
() => { () => {
this.navigated = true; this.navigated = true;
if (navigationIsSuccessful) { if (navigationIsSuccessful) {
this.routerEvents.next( this.routerEvents.next(new NavigationEnd(
new NavigationEnd(id, this.serializeUrl(url), this.serializeUrl(appliedUrl))); id, this.serializeUrl(url), this.serializeUrl(this.currentUrlTree)));
resolvePromise(true); resolvePromise(true);
} else { } else {
this.resetUrlToCurrentUrlTree(); this.resetUrlToCurrentUrlTree();

View File

@ -15,6 +15,7 @@ import {LoadedRouterConfig} from '../src/router_config_loader';
import {DefaultUrlSerializer, UrlSegmentGroup, UrlTree, equalSegments} from '../src/url_tree'; import {DefaultUrlSerializer, UrlSegmentGroup, UrlTree, equalSegments} from '../src/url_tree';
describe('applyRedirects', () => { describe('applyRedirects', () => {
const serializer = new DefaultUrlSerializer();
it('should return the same url tree when no redirects', () => { it('should return the same url tree when no redirects', () => {
checkRedirect( checkRedirect(
@ -38,7 +39,7 @@ describe('applyRedirects', () => {
}); });
it('should throw when cannot handle a positional parameter', () => { it('should throw when cannot handle a positional parameter', () => {
applyRedirects(null, null, tree('/a/1'), [ applyRedirects(null, null, serializer, tree('/a/1'), [
{path: 'a/:id', redirectTo: 'a/:other'} {path: 'a/:id', redirectTo: 'a/:other'}
]).subscribe(() => {}, (e) => { ]).subscribe(() => {}, (e) => {
expect(e.message).toEqual('Cannot redirect to \'a/:other\'. Cannot find \':other\'.'); expect(e.message).toEqual('Cannot redirect to \'a/:other\'. Cannot find \':other\'.');
@ -133,11 +134,11 @@ describe('applyRedirects', () => {
{ {
path: 'a', path: 'a',
component: ComponentA, component: ComponentA,
children: [{path: 'b/:id', redirectTo: '/absolute/:id'}] children: [{path: 'b/:id', redirectTo: '/absolute/:id?a=1&b=:b#f1'}]
}, },
{path: '**', component: ComponentC} {path: '**', component: ComponentC}
], ],
'/a/b/1', (t: UrlTree) => { compareTrees(t, tree('/absolute/1')); }); '/a/b/1?b=2', (t: UrlTree) => { compareTrees(t, tree('/absolute/1?a=1&b=2#f1')); });
}); });
describe('lazy loading', () => { describe('lazy loading', () => {
@ -153,10 +154,11 @@ describe('applyRedirects', () => {
}; };
const config = [{path: 'a', component: ComponentA, loadChildren: 'children'}]; const config = [{path: 'a', component: ComponentA, loadChildren: 'children'}];
applyRedirects(<any>'providedInjector', <any>loader, tree('a/b'), config).forEach(r => { applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('a/b'), config)
compareTrees(r, tree('/a/b')); .forEach(r => {
expect((<any>config[0])._loadedConfig).toBe(loadedConfig); compareTrees(r, tree('/a/b'));
}); expect((<any>config[0])._loadedConfig).toBe(loadedConfig);
});
}); });
it('should handle the case when the loader errors', () => { it('should handle the case when the loader errors', () => {
@ -165,9 +167,8 @@ describe('applyRedirects', () => {
}; };
const config = [{path: 'a', component: ComponentA, loadChildren: 'children'}]; const config = [{path: 'a', component: ComponentA, loadChildren: 'children'}];
applyRedirects(null, <any>loader, tree('a/b'), config).subscribe(() => {}, (e) => { applyRedirects(null, <any>loader, serializer, tree('a/b'), config)
expect(e.message).toEqual('Loading Error'); .subscribe(() => {}, (e) => { expect(e.message).toEqual('Loading Error'); });
});
}); });
it('should load when all canLoad guards return true', () => { it('should load when all canLoad guards return true', () => {
@ -186,7 +187,7 @@ describe('applyRedirects', () => {
loadChildren: 'children' loadChildren: 'children'
}]; }];
applyRedirects(<any>injector, <any>loader, tree('a/b'), config).forEach(r => { applyRedirects(<any>injector, <any>loader, serializer, tree('a/b'), config).forEach(r => {
compareTrees(r, tree('/a/b')); compareTrees(r, tree('/a/b'));
}); });
}); });
@ -208,7 +209,7 @@ describe('applyRedirects', () => {
loadChildren: 'children' loadChildren: 'children'
}]; }];
applyRedirects(<any>injector, <any>loader, tree('a/b'), config) applyRedirects(<any>injector, <any>loader, serializer, tree('a/b'), config)
.subscribe( .subscribe(
() => { throw 'Should not reach'; }, () => { throw 'Should not reach'; },
(e) => { (e) => {
@ -234,7 +235,7 @@ describe('applyRedirects', () => {
loadChildren: 'children' loadChildren: 'children'
}]; }];
applyRedirects(<any>injector, <any>loader, tree('a/b'), config) applyRedirects(<any>injector, <any>loader, serializer, tree('a/b'), config)
.subscribe( .subscribe(
() => { throw 'Should not reach'; }, (e) => { expect(e).toEqual('someError'); }); () => { throw 'Should not reach'; }, (e) => { expect(e).toEqual('someError'); });
}); });
@ -251,7 +252,7 @@ describe('applyRedirects', () => {
const config = const config =
[{path: 'a', component: ComponentA, canLoad: ['guard'], loadChildren: 'children'}]; [{path: 'a', component: ComponentA, canLoad: ['guard'], loadChildren: 'children'}];
applyRedirects(<any>injector, <any>loader, tree('a/b'), config) applyRedirects(<any>injector, <any>loader, serializer, tree('a/b'), config)
.subscribe( .subscribe(
(r) => { compareTrees(r, tree('/a/b')); }, (e) => { throw 'Should not reach'; }); (r) => { compareTrees(r, tree('/a/b')); }, (e) => { throw 'Should not reach'; });
@ -267,10 +268,11 @@ describe('applyRedirects', () => {
const config = const config =
[{path: '', pathMatch: 'full', redirectTo: '/a'}, {path: 'a', loadChildren: 'children'}]; [{path: '', pathMatch: 'full', redirectTo: '/a'}, {path: 'a', loadChildren: 'children'}];
applyRedirects(<any>'providedInjector', <any>loader, tree(''), config).forEach(r => { applyRedirects(<any>'providedInjector', <any>loader, serializer, tree(''), config)
compareTrees(r, tree('a')); .forEach(r => {
expect((<any>config[1])._loadedConfig).toBe(loadedConfig); compareTrees(r, tree('a'));
}); expect((<any>config[1])._loadedConfig).toBe(loadedConfig);
});
}); });
it('should load the configuration only once', () => { it('should load the configuration only once', () => {
@ -289,12 +291,13 @@ describe('applyRedirects', () => {
const config = [{path: 'a', loadChildren: 'children'}]; const config = [{path: 'a', loadChildren: 'children'}];
applyRedirects(<any>'providedInjector', <any>loader, tree('a?k1'), config).subscribe(r => {}); applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('a?k1'), config)
.subscribe(r => {});
applyRedirects(<any>'providedInjector', <any>loader, tree('a?k2'), config) applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('a?k2'), config)
.subscribe( .subscribe(
r => { r => {
compareTrees(r, tree('a')); compareTrees(r, tree('a?k2'));
expect((<any>config[0])._loadedConfig).toBe(loadedConfig); expect((<any>config[0])._loadedConfig).toBe(loadedConfig);
}, },
(e) => { throw 'Should not reach'; }); (e) => { throw 'Should not reach'; });
@ -309,9 +312,8 @@ describe('applyRedirects', () => {
const config = [{path: '**', loadChildren: 'children'}]; const config = [{path: '**', loadChildren: 'children'}];
applyRedirects(<any>'providedInjector', <any>loader, tree('xyz'), config).forEach(r => { applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('xyz'), config)
expect((<any>config[0])._loadedConfig).toBe(loadedConfig); .forEach(r => { expect((<any>config[0])._loadedConfig).toBe(loadedConfig); });
});
}); });
it('should load the configuration after a local redirect from a wildcard route', () => { it('should load the configuration after a local redirect from a wildcard route', () => {
@ -324,9 +326,8 @@ describe('applyRedirects', () => {
const config = const config =
[{path: 'not-found', loadChildren: 'children'}, {path: '**', redirectTo: 'not-found'}]; [{path: 'not-found', loadChildren: 'children'}, {path: '**', redirectTo: 'not-found'}];
applyRedirects(<any>'providedInjector', <any>loader, tree('xyz'), config).forEach(r => { applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('xyz'), config)
expect((<any>config[0])._loadedConfig).toBe(loadedConfig); .forEach(r => { expect((<any>config[0])._loadedConfig).toBe(loadedConfig); });
});
}); });
it('should load the configuration after an absolute redirect from a wildcard route', () => { it('should load the configuration after an absolute redirect from a wildcard route', () => {
@ -339,9 +340,8 @@ describe('applyRedirects', () => {
const config = const config =
[{path: 'not-found', loadChildren: 'children'}, {path: '**', redirectTo: '/not-found'}]; [{path: 'not-found', loadChildren: 'children'}, {path: '**', redirectTo: '/not-found'}];
applyRedirects(<any>'providedInjector', <any>loader, tree('xyz'), config).forEach(r => { applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('xyz'), config)
expect((<any>config[0])._loadedConfig).toBe(loadedConfig); .forEach(r => { expect((<any>config[0])._loadedConfig).toBe(loadedConfig); });
});
}); });
}); });
@ -388,7 +388,7 @@ describe('applyRedirects', () => {
{path: '', redirectTo: 'a', pathMatch: 'full'} {path: '', redirectTo: 'a', pathMatch: 'full'}
]; ];
applyRedirects(null, null, tree('b'), config) applyRedirects(null, null, serializer, tree('b'), config)
.subscribe( .subscribe(
(_) => { throw 'Should not be reached'; }, (_) => { throw 'Should not be reached'; },
e => { expect(e.message).toEqual('Cannot match any routes. URL Segment: \'b\''); }); e => { expect(e.message).toEqual('Cannot match any routes. URL Segment: \'b\''); });
@ -518,7 +518,7 @@ describe('applyRedirects', () => {
] ]
}]; }];
applyRedirects(null, null, tree('a/(d//aux:e)'), config) applyRedirects(null, null, serializer, tree('a/(d//aux:e)'), config)
.subscribe( .subscribe(
(_) => { throw 'Should not be reached'; }, (_) => { throw 'Should not be reached'; },
e => { expect(e.message).toEqual('Cannot match any routes. URL Segment: \'a\''); }); e => { expect(e.message).toEqual('Cannot match any routes. URL Segment: \'a\''); });
@ -549,7 +549,7 @@ describe('applyRedirects', () => {
it('should error when no children matching and some url is left', () => { it('should error when no children matching and some url is left', () => {
applyRedirects( applyRedirects(
null, null, tree('/a/c'), null, null, serializer, tree('/a/c'),
[{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}]) [{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}])
.subscribe( .subscribe(
(_) => { throw 'Should not be reached'; }, (_) => { throw 'Should not be reached'; },
@ -576,10 +576,46 @@ describe('applyRedirects', () => {
'/a/1/b', (t: UrlTree) => { compareTrees(t, tree('a/1/b')); }); '/a/1/b', (t: UrlTree) => { compareTrees(t, tree('a/1/b')); });
}); });
}); });
describe('redirecting to named outlets', () => {
it('should work when using absolute redirects', () => {
checkRedirect(
[
{path: 'a/:id', redirectTo: '/b/:id(aux:c/:id)'},
{path: 'b/:id', component: ComponentB},
{path: 'c/:id', component: ComponentC, outlet: 'aux'}
],
'a/1;p=99', (t: UrlTree) => { compareTrees(t, tree('/b/1;p=99(aux:c/1;p=99)')); });
});
it('should work when using absolute redirects (wildcard)', () => {
checkRedirect(
[
{path: '**', redirectTo: '/b(aux:c)'}, {path: 'b', component: ComponentB},
{path: 'c', component: ComponentC, outlet: 'aux'}
],
'a/1', (t: UrlTree) => { compareTrees(t, tree('/b(aux:c)')); });
});
it('should throw when using non-absolute redirects', () => {
applyRedirects(
null, null, serializer, tree('a'),
[
{path: 'a', redirectTo: 'b(aux:c)'},
])
.subscribe(
() => { throw new Error('should not be reached'); },
(e) => {
expect(e.message).toEqual(
'Only absolute redirects can have named outlets. redirectTo: \'b(aux:c)\'');
});
});
});
}); });
function checkRedirect(config: Routes, url: string, callback: any): void { function checkRedirect(config: Routes, url: string, callback: any): void {
applyRedirects(null, null, tree(url), config).subscribe(callback, e => { throw e; }); applyRedirects(null, null, new DefaultUrlSerializer(), tree(url), config)
.subscribe(callback, e => { throw e; });
} }
function tree(url: string): UrlTree { function tree(url: string): UrlTree {
@ -591,6 +627,8 @@ function compareTrees(actual: UrlTree, expected: UrlTree): void {
const error = const error =
`"${serializer.serialize(actual)}" is not equal to "${serializer.serialize(expected)}"`; `"${serializer.serialize(actual)}" is not equal to "${serializer.serialize(expected)}"`;
compareSegments(actual.root, expected.root, error); compareSegments(actual.root, expected.root, error);
expect(actual.queryParams).toEqual(expected.queryParams);
expect(actual.fragment).toEqual(expected.fragment);
} }
function compareSegments(actual: UrlSegmentGroup, expected: UrlSegmentGroup, error: string): void { function compareSegments(actual: UrlSegmentGroup, expected: UrlSegmentGroup, error: string): void {

View File

@ -1156,8 +1156,6 @@ describe('Integration', () => {
advance(fixture); advance(fixture);
expect(location.path()).toEqual('/initial'); expect(location.path()).toEqual('/initial');
}))); })));
// should not break the back button when trigger by initial navigation
}); });
describe('guards', () => { describe('guards', () => {
@ -1380,6 +1378,11 @@ describe('Integration', () => {
return true; return true;
} }
}, },
{
provide: 'alwaysFalse',
useValue:
(c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { return false; }
},
] ]
}); });
}); });
@ -1504,6 +1507,31 @@ describe('Integration', () => {
advance(fixture); advance(fixture);
expect(location.path()).toEqual('/team/33/user/fedor'); expect(location.path()).toEqual('/team/33/user/fedor');
}))); })));
it('should not create a route state if navigation is canceled',
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
const fixture = createRoot(router, RootCmp);
router.resetConfig([{
path: 'main',
component: TeamCmp,
children: [
{path: 'component1', component: SimpleCmp, canDeactivate: ['alwaysFalse']},
{path: 'component2', component: SimpleCmp}
]
}]);
router.navigateByUrl('/main/component1');
advance(fixture);
router.navigateByUrl('/main/component2');
advance(fixture);
const teamCmp = fixture.debugElement.children[1].componentInstance;
expect(teamCmp.route.firstChild.url.value[0].path).toEqual('component1');
expect(location.path()).toEqual('/main/component1');
})));
}); });
describe('should work when given a class', () => { describe('should work when given a class', () => {

View File

@ -14,6 +14,12 @@ export interface ComponentInfo {
outputs?: string[]; outputs?: string[];
} }
/**
* A `PropertyBinding` represents a mapping between a property name
* and an attribute name. It is parsed from a string of the form
* `"prop: attr"`; or simply `"propAndAttr" where the property
* and attribute have the same identifier.
*/
export class PropertyBinding { export class PropertyBinding {
prop: string; prop: string;
attr: string; attr: string;

View File

@ -13,8 +13,11 @@ export const $INJECTOR = '$injector';
export const $PARSE = '$parse'; export const $PARSE = '$parse';
export const $ROOT_SCOPE = '$rootScope'; export const $ROOT_SCOPE = '$rootScope';
export const $SCOPE = '$scope'; export const $SCOPE = '$scope';
export const $PROVIDE = '$provide';
export const $DELEGATE = '$delegate';
export const $$TESTABILITY = '$$testability';
export const $COMPILE = '$compile'; export const $COMPILE = '$compile';
export const $TEMPLATE_CACHE = '$templateCache'; export const $TEMPLATE_CACHE = '$templateCache';
export const $HTTP_BACKEND = '$httpBackend'; export const $HTTP_BACKEND = '$httpBackend';
export const $CONTROLLER = '$controller'; export const $CONTROLLER = '$controller';

View File

@ -6,20 +6,67 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ComponentFactory, ComponentFactoryResolver, Injector} from '@angular/core'; import {ComponentFactory, ComponentFactoryResolver, Injector, Type} from '@angular/core';
import * as angular from '../angular_js'; import * as angular from '../angular_js';
import {ComponentInfo} from './component_info';
import {$INJECTOR, $PARSE, INJECTOR_KEY} from './constants'; import {$INJECTOR, $PARSE, INJECTOR_KEY} from './constants';
import {DowngradeComponentAdapter} from './downgrade_component_adapter'; import {DowngradeComponentAdapter} from './downgrade_component_adapter';
let downgradeCount = 0; let downgradeCount = 0;
/** /**
* @whatItDoes
*
* *Part of the [upgrade/static](/docs/ts/latest/api/#!?query=upgrade%2Fstatic)
* library for hybrid upgrade apps that support AoT compilation*
*
* Allows an Angular 2+ component to be used from Angular 1.
*
* @howToUse
*
* Let's assume that you have an Angular 2+ component called `ng2Heroes` that needs
* to be made available in Angular 1 templates.
*
* {@example upgrade/static/ts/module.ts region="ng2-heroes"}
*
* We must create an Angular 1 [directive](https://docs.angularjs.org/guide/directive)
* that will make this Angular 2+ component available inside Angular 1 templates.
* The `downgradeComponent()` function returns a factory function that we
* can use to define the Angular 1 directive that wraps the "downgraded" component.
*
* {@example upgrade/static/ts/module.ts region="ng2-heroes-wrapper"}
*
* In this example you can see that we must provide information about the component being
* "downgraded". This is because once the AoT compiler has run, all metadata about the
* component has been removed from the code, and so cannot be inferred.
*
* We must do the following:
* * specify the Angular 2+ component class that is to be downgraded
* * specify all inputs and outputs that the Angular 1 component expects
*
* @description
*
* A helper function that returns a factory function to be used for registering an
* Angular 1 wrapper directive for "downgrading" an Angular 2+ component.
*
* The parameter contains information about the Component that is being downgraded:
*
* * `component: Type<any>`: The type of the Component that will be downgraded
* * `inputs: string[]`: A collection of strings that specify what inputs the component accepts.
* * `outputs: string[]`: A collection of strings that specify what outputs the component emits.
*
* The `inputs` and `outputs` are strings that map the names of properties to camelCased
* attribute names. They are of the form `"prop: attr"`; or simply `"propAndAttr" where the
* property and attribute have the same identifier.
*
* @experimental * @experimental
*/ */
export function downgradeComponent(info: ComponentInfo): angular.IInjectable { export function downgradeComponent(info: /* ComponentInfo */ {
component: Type<any>;
inputs?: string[];
outputs?: string[];
}): any /* angular.IInjectable */ {
const idPrefix = `NG2_UPGRADE_${downgradeCount++}_`; const idPrefix = `NG2_UPGRADE_${downgradeCount++}_`;
let idCount = 0; let idCount = 0;

View File

@ -10,14 +10,44 @@ import {Injector} from '@angular/core';
import {INJECTOR_KEY} from './constants'; import {INJECTOR_KEY} from './constants';
/** /**
* Create an Angular 1 factory that will return an Angular 2 injectable thing * @whatItDoes
* (e.g. service, pipe, component, etc)
* *
* Usage: * *Part of the [upgrade/static](/docs/ts/latest/api/#!?query=upgrade%2Fstatic)
* library for hybrid upgrade apps that support AoT compilation*
* *
* ``` * Allow an Angular 2+ service to be accessible from Angular 1.
* angular1Module.factory('someService', downgradeInjectable(SomeService)) *
* ``` * @howToUse
*
* First ensure that the service to be downgraded is provided in an {@link NgModule}
* that will be part of the upgrade application. For example, let's assume we have
* defined `HeroesService`
*
* {@example upgrade/static/ts/module.ts region="ng2-heroes-service"}
*
* and that we have included this in our upgrade app {@link NgModule}
*
* {@example upgrade/static/ts/module.ts region="ng2-module"}
*
* Now we can register the `downgradeInjectable` factory function for the service
* on an Angular 1 module.
*
* {@example upgrade/static/ts/module.ts region="downgrade-ng2-heroes-service"}
*
* Inside an Angular 1 component's controller we can get hold of the
* downgraded service via the name we gave when downgrading.
*
* {@example upgrade/static/ts/module.ts region="example-app"}
*
* @description
*
* Takes a `token` that identifies a service provided from Angular 2+.
*
* Returns a [factory function](https://docs.angularjs.org/guide/di) that can be
* used to register the service on an Angular 1 module.
*
* The factory function provides access to the Angular 2+ service that
* is identified by the `token` parameter.
* *
* @experimental * @experimental
*/ */

View File

@ -43,6 +43,43 @@ interface IControllerInstance extends IBindingDestination {
type LifecycleHook = '$onChanges' | '$onDestroy' | '$onInit' | '$postLink'; type LifecycleHook = '$onChanges' | '$onDestroy' | '$onInit' | '$postLink';
/** /**
* @whatItDoes
*
* *Part of the [upgrade/static](/docs/ts/latest/api/#!?query=upgrade%2Fstatic)
* library for hybrid upgrade apps that support AoT compilation*
*
* Allows an Angular 1 component to be used from Angular 2+.
*
* @howToUse
*
* Let's assume that you have an Angular 1 component called `ng1Hero` that needs
* to be made available in Angular 2+ templates.
*
* {@example upgrade/static/ts/module.ts region="ng1-hero"}
*
* We must create a {@link Directive} that will make this Angular 1 component
* available inside Angular 2+ templates.
*
* {@example upgrade/static/ts/module.ts region="ng1-hero-wrapper"}
*
* In this example you can see that we must derive from the {@link UpgradeComponent}
* base class but also provide an {@link Directive `@Directive`} decorator. This is
* because the AoT compiler requires that this information is statically available at
* compile time.
*
* Note that we must do the following:
* * specify the directive's selector (`ng1-hero`)
* * specify all inputs and outputs that the Angular 1 component expects
* * derive from `UpgradeComponent`
* * call the base class from the constructor, passing
* * the Angular 1 name of the component (`ng1Hero`)
* * the {@link ElementRef} and {@link Injector} for the component wrapper
*
* @description
*
* A helper class that should be used as a base class for creating Angular directives
* that wrap Angular 1 components that need to be "upgraded".
*
* @experimental * @experimental
*/ */
export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy { export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
@ -63,6 +100,22 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
private controllerInstance: IControllerInstance = null; private controllerInstance: IControllerInstance = null;
private bindingDestination: IBindingDestination = null; private bindingDestination: IBindingDestination = null;
/**
* Create a new `UpgradeComponent` instance. You should not normally need to do this.
* Instead you should derive a new class from this one and call the super constructor
* from the base class.
*
* {@example upgrade/static/ts/module.ts region="ng1-hero-wrapper" }
*
* * The `name` parameter should be the name of the Angular 1 directive.
* * The `elementRef` and `injector` parameters should be acquired from Angular by dependency
* injection into the base class constructor.
*
* Note that we must manually implement lifecycle hooks that call through to the super class.
* This is because, at the moment, the AoT compiler is not able to tell that the
* `UpgradeComponent`
* already implements them and so does not wire up calls to them at runtime.
*/
constructor(private name: string, private elementRef: ElementRef, private injector: Injector) { constructor(private name: string, private elementRef: ElementRef, private injector: Injector) {
this.$injector = injector.get($INJECTOR); this.$injector = injector.get($INJECTOR);
this.$compile = this.$injector.get($COMPILE); this.$compile = this.$injector.get($COMPILE);
@ -77,7 +130,7 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
this.bindings = this.initializeBindings(this.directive); this.bindings = this.initializeBindings(this.directive);
this.linkFn = this.compileTemplate(this.directive); this.linkFn = this.compileTemplate(this.directive);
// We ask for the Angular 1 scope from the Angular 2 injector, since // We ask for the Angular 1 scope from the Angular 2+ injector, since
// we will put the new component scope onto the new injector for each component // we will put the new component scope onto the new injector for each component
const $parentScope = injector.get($SCOPE); const $parentScope = injector.get($SCOPE);
// QUESTION 1: Should we create an isolated scope if the scope is only true? // QUESTION 1: Should we create an isolated scope if the scope is only true?

View File

@ -6,25 +6,141 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Injector, NgModule, NgZone} from '@angular/core'; import {Injector, NgModule, NgZone, Testability} from '@angular/core';
import * as angular from '../angular_js'; import * as angular from '../angular_js';
import {controllerKey} from '../util'; import {controllerKey} from '../util';
import {angular1Providers, setTempInjectorRef} from './angular1_providers'; import {angular1Providers, setTempInjectorRef} from './angular1_providers';
import {$INJECTOR, INJECTOR_KEY, UPGRADE_MODULE_NAME} from './constants'; import {$$TESTABILITY, $DELEGATE, $INJECTOR, $PROVIDE, $ROOT_SCOPE, INJECTOR_KEY, UPGRADE_MODULE_NAME} from './constants';
/** /**
* The Ng1Module contains providers for the Ng1Adapter and all the core Angular 1 services; * @whatItDoes
* and also holds the `bootstrapNg1()` method fo bootstrapping an upgraded Angular 1 app. *
* *Part of the [upgrade/static](/docs/ts/latest/api/#!?query=upgrade%2Fstatic)
* library for hybrid upgrade apps that support AoT compilation*
*
* Allows Angular 1 and Angular 2+ components to be used together inside a hybrid upgrade
* application, which supports AoT compilation.
*
* Specifically, the classes and functions in the `upgrade/static` module allow the following:
* 1. Creation of an Angular 2+ directive that wraps and exposes an Angular 1 component so
* that it can be used in an Angular 2 template. See {@link UpgradeComponent}.
* 2. Creation of an Angular 1 directive that wraps and exposes an Angular 2+ component so
* that it can be used in an Angular 1 template. See {@link downgradeComponent}.
* 3. Creation of an Angular 2+ root injector provider that wraps and exposes an Angular 1
* service so that it can be injected into an Angular 2+ context. See
* {@link UpgradeModule#upgrading-an-angular-1-service Upgrading an Angular 1 service} below.
* 4. Creation of an Angular 1 service that wraps and exposes an Angular 2+ injectable
* so that it can be injected into an Angular 1 context. See {@link downgradeInjectable}.
* 3. Bootstrapping of a hybrid Angular application which contains both of the frameworks
* coexisting in a single application. See the
* {@link UpgradeModule#example example} below.
*
* ## Mental Model
*
* When reasoning about how a hybrid application works it is useful to have a mental model which
* describes what is happening and explains what is happening at the lowest level.
*
* 1. There are two independent frameworks running in a single application, each framework treats
* the other as a black box.
* 2. Each DOM element on the page is owned exactly by one framework. Whichever framework
* instantiated the element is the owner. Each framework only updates/interacts with its own
* DOM elements and ignores others.
* 3. Angular 1 directives always execute inside the Angular 1 framework codebase regardless of
* where they are instantiated.
* 4. Angular 2+ components always execute inside the Angular 2+ framework codebase regardless of
* where they are instantiated.
* 5. An Angular 1 component can be "upgraded"" to an Angular 2+ component. This is achieved by
* defining an Angular 2+ directive, which bootstraps the Angular 1 component at its location
* in the DOM. See {@link UpgradeComponent}.
* 6. An Angular 2+ component can be "downgraded"" to an Angular 1 component. This is achieved by
* defining an Angular 1 directive, which bootstraps the Angular 2+ component at its location
* in the DOM. See {@link downgradeComponent}.
* 7. Whenever an "upgraded"/"downgraded" component is instantiated the host element is owned by
* the framework doing the instantiation. The other framework then instantiates and owns the
* view for that component.
* a. This implies that the component bindings will always follow the semantics of the
* instantiation framework.
* b. The DOM attributes are parsed by the framework that owns the current template. So
* attributes
* in Angular 1 templates must use kebab-case, while Angular 1 templates must use camelCase.
* c. However the template binding syntax will always use the Angular 2+ style, e.g. square
* brackets (`[...]`) for property binding.
* 8. Angular 1 is always bootstrapped first and owns the root component.
* 9. The new application is running in an Angular 2+ zone, and therefore it no longer needs calls
* to
* `$apply()`.
*
* @howToUse
*
* `import {UpgradeModule} from '@angular/upgrade/static';`
*
* ## Example
* Import the {@link UpgradeModule} into your top level {@link NgModule Angular 2+ `NgModule`}.
*
* {@example upgrade/static/ts/module.ts region='ng2-module'}
*
* Then bootstrap the hybrid upgrade app's module, get hold of the {@link UpgradeModule} instance
* and use it to bootstrap the top level [Angular 1
* module](https://docs.angularjs.org/api/ng/type/angular.Module).
*
* {@example upgrade/static/ts/module.ts region='bootstrap'}
*
*
* ## Upgrading an Angular 1 service
*
* There is no specific API for upgrading an Angular 1 service. Instead you should just follow the
* following recipe:
*
* Let's say you have an Angular 1 service:
*
* {@example upgrade/static/ts/module.ts region="ng1-title-case-service"}
*
* Then you should define an Angular 2+ provider to be included in your {@link NgModule} `providers`
* property.
*
* {@example upgrade/static/ts/module.ts region="upgrade-ng1-service"}
*
* Then you can use the "upgraded" Angular 1 service by injecting it into an Angular 2 component
* or service.
*
* {@example upgrade/static/ts/module.ts region="use-ng1-upgraded-service"}
*
* @description
*
* This class is an `NgModule`, which you import to provide Angular 1 core services,
* and has an instance method used to bootstrap the hybrid upgrade application.
*
* ## Core Angular 1 services
* Importing this {@link NgModule} will add providers for the core
* [Angular 1 services](https://docs.angularjs.org/api/ng/service) to the root injector.
*
* ## Bootstrap
* The runtime instance of this class contains a {@link UpgradeModule#bootstrap `bootstrap()`}
* method, which you use to bootstrap the top level Angular 1 module onto an element in the
* DOM for the hybrid upgrade app.
*
* It also contains properties to access the {@link UpgradeModule#injector root injector}, the
* bootstrap {@link NgZone} and the
* [Angular 1 $injector](https://docs.angularjs.org/api/auto/service/$injector).
*
* @experimental * @experimental
*/ */
@NgModule({providers: angular1Providers}) @NgModule({providers: angular1Providers})
export class UpgradeModule { export class UpgradeModule {
public $injector: angular.IInjectorService; /**
* The Angular 1 `$injector` for the upgrade application.
*/
public $injector: any /*angular.IInjectorService*/;
constructor(public injector: Injector, public ngZone: NgZone) {} constructor(
/** The root {@link Injector} for the upgrade application. */
public injector: Injector,
/** The bootstrap zone for the upgrade application */
public ngZone: NgZone) {}
/** /**
* Bootstrap an Angular 1 application from this NgModule * Bootstrap an Angular 1 application from this NgModule
@ -32,7 +148,8 @@ export class UpgradeModule {
* @param [modules] the Angular 1 modules to bootstrap for this application * @param [modules] the Angular 1 modules to bootstrap for this application
* @param [config] optional extra Angular 1 bootstrap configuration * @param [config] optional extra Angular 1 bootstrap configuration
*/ */
bootstrap(element: Element, modules: string[] = [], config?: angular.IAngularBootstrapConfig) { bootstrap(
element: Element, modules: string[] = [], config?: any /*angular.IAngularBootstrapConfig*/) {
// Create an ng1 module to bootstrap // Create an ng1 module to bootstrap
const upgradeModule = const upgradeModule =
angular angular
@ -40,6 +157,35 @@ export class UpgradeModule {
.value(INJECTOR_KEY, this.injector) .value(INJECTOR_KEY, this.injector)
.config([
$PROVIDE, $INJECTOR,
($provide: angular.IProvideService, $injector: angular.IInjectorService) => {
if ($injector.has($$TESTABILITY)) {
$provide.decorator($$TESTABILITY, [
$DELEGATE,
(testabilityDelegate: angular.ITestabilityService) => {
const originalWhenStable: Function = testabilityDelegate.whenStable;
const injector = this.injector;
// Cannot use arrow function below because we need the context
const newWhenStable = function(callback: Function) {
originalWhenStable.call(this, function() {
const ng2Testability: Testability = injector.get(Testability);
if (ng2Testability.isStable()) {
callback.apply(this, arguments);
} else {
ng2Testability.whenStable(newWhenStable.bind(this, callback));
}
});
};
testabilityDelegate.whenStable = newWhenStable;
return testabilityDelegate;
}
]);
}
}
])
.run([ .run([
$INJECTOR, $INJECTOR,
($injector: angular.IInjectorService) => { ($injector: angular.IInjectorService) => {
@ -59,7 +205,22 @@ export class UpgradeModule {
} }
]); ]);
// Make sure resumeBootstrap() only exists if the current bootstrap is deferred
const windowAngular = (window as any /** TODO #???? */)['angular'];
windowAngular.resumeBootstrap = undefined;
// Bootstrap the angular 1 application inside our zone // Bootstrap the angular 1 application inside our zone
this.ngZone.run(() => { angular.bootstrap(element, [upgradeModule.name], config); }); this.ngZone.run(() => { angular.bootstrap(element, [upgradeModule.name], config); });
// Patch resumeBootstrap() to run inside the ngZone
if (windowAngular.resumeBootstrap) {
const originalResumeBootstrap: () => void = windowAngular.resumeBootstrap;
const ngZone = this.ngZone;
windowAngular.resumeBootstrap = function() {
let args = arguments;
windowAngular.resumeBootstrap = originalResumeBootstrap;
ngZone.run(() => { windowAngular.resumeBootstrap.apply(this, args); });
};
}
} }
} }

View File

@ -355,14 +355,14 @@ export class UpgradeAdapter {
function(testabilityDelegate: angular.ITestabilityService) { function(testabilityDelegate: angular.ITestabilityService) {
const originalWhenStable: Function = testabilityDelegate.whenStable; const originalWhenStable: Function = testabilityDelegate.whenStable;
const newWhenStable = (callback: Function): void => { // Cannot use arrow function below because we need the context
const whenStableContext: any = this; const newWhenStable = function(callback: Function) {
originalWhenStable.call(this, function() { originalWhenStable.call(this, function() {
const ng2Testability: Testability = moduleRef.injector.get(Testability); const ng2Testability: Testability = moduleRef.injector.get(Testability);
if (ng2Testability.isStable()) { if (ng2Testability.isStable()) {
callback.apply(this, arguments); callback.apply(this, arguments);
} else { } else {
ng2Testability.whenStable(newWhenStable.bind(whenStableContext, callback)); ng2Testability.whenStable(newWhenStable.bind(this, callback));
} }
}); });
}; };
@ -433,8 +433,9 @@ export class UpgradeAdapter {
if (windowAngular.resumeBootstrap) { if (windowAngular.resumeBootstrap) {
const originalResumeBootstrap: () => void = windowAngular.resumeBootstrap; const originalResumeBootstrap: () => void = windowAngular.resumeBootstrap;
windowAngular.resumeBootstrap = function() { windowAngular.resumeBootstrap = function() {
let args = arguments;
windowAngular.resumeBootstrap = originalResumeBootstrap; windowAngular.resumeBootstrap = originalResumeBootstrap;
windowAngular.resumeBootstrap.apply(this, arguments); ngZone.run(() => { windowAngular.resumeBootstrap.apply(this, args); });
resolve(); resolve();
}; };
} else { } else {

View File

@ -49,7 +49,8 @@ export class UpgradeNg1ComponentAdapterBuilder {
], ],
ngOnInit: function() { /* needs to be here for ng2 to properly detect it */ }, ngOnInit: function() { /* needs to be here for ng2 to properly detect it */ },
ngOnChanges: function() { /* needs to be here for ng2 to properly detect it */ }, ngOnChanges: function() { /* needs to be here for ng2 to properly detect it */ },
ngDoCheck: function() { /* needs to be here for ng2 to properly detect it */ } ngDoCheck: function() { /* needs to be here for ng2 to properly detect it */ },
ngOnDestroy: function() { /* needs to be here for ng2 to properly detect it */ },
}); });
} }
@ -262,16 +263,18 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
for (const name in changes) { const ng1Changes: any = {};
if ((<Object>changes).hasOwnProperty(name)) { Object.keys(changes).forEach(name => {
const change: SimpleChange = changes[name]; const change: SimpleChange = changes[name];
this.setComponentProperty(name, change.currentValue); this.setComponentProperty(name, change.currentValue);
} ng1Changes[this.propertyMap[name]] = change;
});
if (this.destinationObj.$onChanges) {
this.destinationObj.$onChanges(ng1Changes);
} }
} }
ngDoCheck(): number { ngDoCheck() {
const count = 0;
const destinationObj = this.destinationObj; const destinationObj = this.destinationObj;
const lastValues = this.checkLastValues; const lastValues = this.checkLastValues;
const checkProperties = this.checkProperties; const checkProperties = this.checkProperties;
@ -287,7 +290,15 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
} }
} }
} }
return count; if (this.destinationObj.$doCheck && this.directive.controller) {
this.destinationObj.$doCheck();
}
}
ngOnDestroy() {
if (this.destinationObj.$onDestroy && this.directive.controller) {
this.destinationObj.$onDestroy();
}
} }
setComponentProperty(name: string, value: any) { setComponentProperty(name: string, value: any) {

View File

@ -7,6 +7,7 @@
*/ */
import {NgModule, Testability, destroyPlatform} from '@angular/core'; import {NgModule, Testability, destroyPlatform} from '@angular/core';
import {NgZone} from '@angular/core/src/zone/ng_zone';
import {fakeAsync, tick} from '@angular/core/testing'; import {fakeAsync, tick} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
@ -28,7 +29,11 @@ export function main() {
it('should handle deferred bootstrap', fakeAsync(() => { it('should handle deferred bootstrap', fakeAsync(() => {
let applicationRunning = false; let applicationRunning = false;
const ng1Module = angular.module('ng1', []).run(() => { applicationRunning = true; }); let stayedInTheZone: boolean;
const ng1Module = angular.module('ng1', []).run(() => {
applicationRunning = true;
stayedInTheZone = NgZone.isInAngularZone();
});
const element = html('<div></div>'); const element = html('<div></div>');
window.name = 'NG_DEFER_BOOTSTRAP!' + window.name; window.name = 'NG_DEFER_BOOTSTRAP!' + window.name;
@ -40,6 +45,7 @@ export function main() {
expect(applicationRunning).toEqual(false); expect(applicationRunning).toEqual(false);
tick(100); tick(100);
expect(applicationRunning).toEqual(true); expect(applicationRunning).toEqual(true);
expect(stayedInTheZone).toEqual(true);
})); }));
it('should wait for ng2 testability', fakeAsync(() => { it('should wait for ng2 testability', fakeAsync(() => {

View File

@ -922,6 +922,127 @@ export function main() {
}); });
})); }));
it('should call $doCheck of components', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module = angular.module('ng1', []);
const valueToFind = '$doCheck';
let spy = jasmine.createSpy('doCheck');
const ng1 = {
bindings: {},
template: '{{$ctrl.value}}',
controller: Class({
constructor: function() {},
$doCheck: function() {
this.value = valueToFind;
spy();
}
})
};
ng1Module.component('ng1', ng1);
const Ng2 = Component({selector: 'ng2', template: '<ng1></ng1>'}).Class({
constructor: function() {}
});
const Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
const element = html(`<div><ng2></ng2></div>`);
adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(multiTrim(document.body.textContent)).toEqual(valueToFind);
expect(spy).toHaveBeenCalled();
let count = spy.calls.count();
setTimeout(() => {
expect(spy.calls.count()).toBeGreaterThan(count);
ref.dispose();
}, 100);
});
}));
it('should call $onChanges of components', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module = angular.module('ng1', []);
const valueToFind = '$onChanges init';
const valueToChange = '$onChanges changed';
const ng1 = {
bindings: {val: '<'},
template: '{{$ctrl.value}}',
controller: Class({
constructor: function() {},
$onChanges: function(changes: any) { this.value = changes.val.currentValue; }
})
};
ng1Module.component('ng1', ng1);
const Ng2 = Component({selector: 'ng2', template: '<ng1 [val]="val"></ng1>'}).Class({
constructor: function() { this.val = valueToFind; },
ngOnInit: function() { setTimeout(() => { this.val = valueToChange; }, 100); }
});
const Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
const element = html(`<div><ng2></ng2></div>`);
adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(multiTrim(document.body.textContent)).toEqual(valueToFind);
setTimeout(() => {
expect(multiTrim(document.body.textContent)).toEqual(valueToChange);
ref.dispose();
}, 200);
});
}));
it('should call $onDestroy of components', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module = angular.module('ng1', []);
let spy = jasmine.createSpy('$onDestroy');
const ng1 = {
bindings: {},
template: '<div>ng1</div>',
controller: function($rootScope: any) { this.$onDestroy = function() { spy(); }; }
};
ng1Module.component('ng1', ng1);
const Ng2 = Component({selector: 'ng2', template: '<ng1></ng1>'}).Class({
constructor: function() {}
});
const Ng2Module = NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule],
schemas: [NO_ERRORS_SCHEMA],
}).Class({constructor: function() {}});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
const element = html(`<div ng-if="!destroy"><ng2></ng2></div>`);
adapter.bootstrap(element, ['ng1']).ready((ref) => {
(<any>ref.ng1RootScope).destroy = false;
setTimeout(() => {
(<any>ref.ng1RootScope).destroy = true;
setTimeout(() => {
expect(spy).toHaveBeenCalled();
ref.dispose();
}, 100);
}, 100);
});
}));
it('should bind input properties (<) of components', async(() => { it('should bind input properties (<) of components', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module = angular.module('ng1', []); const ng1Module = angular.module('ng1', []);

View File

@ -7,15 +7,18 @@
*/ */
import {Component, Input, NgModule} from '@angular/core'; import {Component, Input, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule, DomSanitizer, SafeStyle} from '@angular/platform-browser';
import {TableCell, emptyTable} from '../util'; import {TableCell, emptyTable} from '../util';
let trustedEmptyColor: SafeStyle;
let trustedGreyColor: SafeStyle;
@Component({ @Component({
selector: 'largetable', selector: 'largetable',
template: `<table><tbody> template: `<table><tbody>
<tr *ngFor="let row of data; trackBy: trackByIndex"> <tr *ngFor="let row of data; trackBy: trackByIndex">
<td *ngFor="let cell of row; trackBy: trackByIndex" [style.backgroundColor]="cell.row % 2 ? '' : 'grey'"> <td *ngFor="let cell of row; trackBy: trackByIndex" [style.backgroundColor]="getColor(cell.row)">
{{cell.value}} {{cell.value}}
</td> </td>
</tr> </tr>
@ -26,8 +29,14 @@ export class TableComponent {
data: TableCell[][] = emptyTable; data: TableCell[][] = emptyTable;
trackByIndex(index: number, item: any) { return index; } trackByIndex(index: number, item: any) { return index; }
getColor(row: number) { return row % 2 ? trustedEmptyColor : trustedGreyColor; }
} }
@NgModule({imports: [BrowserModule], bootstrap: [TableComponent], declarations: [TableComponent]}) @NgModule({imports: [BrowserModule], bootstrap: [TableComponent], declarations: [TableComponent]})
export class AppModule { export class AppModule {
constructor(sanitizer: DomSanitizer) {
trustedEmptyColor = sanitizer.bypassSecurityTrustStyle('');
trustedGreyColor = sanitizer.bypassSecurityTrustStyle('grey');
}
} }

View File

@ -7,20 +7,28 @@
*/ */
import {Component, NgModule} from '@angular/core'; import {Component, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule, DomSanitizer, SafeStyle} from '@angular/platform-browser';
import {TreeNode, emptyTree} from '../util'; import {TreeNode, emptyTree} from '../util';
let trustedEmptyColor: SafeStyle;
let trustedGreyColor: SafeStyle;
@Component({ @Component({
selector: 'tree', selector: 'tree',
inputs: ['data'], inputs: ['data'],
template: template:
`<span [style.backgroundColor]="data.depth % 2 ? '' : 'grey'"> {{data.value}} </span><tree *ngIf='data.right != null' [data]='data.right'></tree><tree *ngIf='data.left != null' [data]='data.left'></tree>` `<span [style.backgroundColor]="bgColor"> {{data.value}} </span><tree *ngIf='data.right != null' [data]='data.right'></tree><tree *ngIf='data.left != null' [data]='data.left'></tree>`
}) })
export class TreeComponent { export class TreeComponent {
data: TreeNode = emptyTree; data: TreeNode = emptyTree;
get bgColor() { return this.data.depth % 2 ? trustedEmptyColor : trustedGreyColor; }
} }
@NgModule({imports: [BrowserModule], bootstrap: [TreeComponent], declarations: [TreeComponent]}) @NgModule({imports: [BrowserModule], bootstrap: [TreeComponent], declarations: [TreeComponent]})
export class AppModule { export class AppModule {
constructor(sanitizer: DomSanitizer) {
trustedEmptyColor = sanitizer.bypassSecurityTrustStyle('');
trustedGreyColor = sanitizer.bypassSecurityTrustStyle('grey');
}
} }

View File

@ -7,14 +7,16 @@
*/ */
import {Component, Input, NgModule} from '@angular/core'; import {Component, Input, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule, DomSanitizer, SafeStyle} from '@angular/platform-browser';
import {TreeNode, emptyTree, maxDepth} from '../util'; import {TreeNode, emptyTree, maxDepth} from '../util';
let trustedEmptyColor: SafeStyle;
let trustedGreyColor: SafeStyle;
function createTreeComponent(level: number, isLeaf: boolean) { function createTreeComponent(level: number, isLeaf: boolean) {
const nextTreeEl = `tree${level+1}`; const nextTreeEl = `tree${level+1}`;
let template = let template = `<span [style.backgroundColor]="bgColor"> {{data.value}} </span>`;
`<span [style.backgroundColor]="data.depth % 2 ? '' : 'grey'"> {{data.value}} </span>`;
if (!isLeaf) { if (!isLeaf) {
template += template +=
`<${nextTreeEl} [data]='data.right'></${nextTreeEl}><${nextTreeEl} [data]='data.left'></${nextTreeEl}>`; `<${nextTreeEl} [data]='data.right'></${nextTreeEl}><${nextTreeEl} [data]='data.left'></${nextTreeEl}>`;
@ -24,6 +26,7 @@ function createTreeComponent(level: number, isLeaf: boolean) {
class TreeComponent { class TreeComponent {
@Input() @Input()
data: TreeNode; data: TreeNode;
get bgColor() { return this.data.depth % 2 ? trustedEmptyColor : trustedGreyColor; }
} }
return TreeComponent; return TreeComponent;
@ -43,6 +46,10 @@ function createModule(): any {
@NgModule({imports: [BrowserModule], bootstrap: [RootTreeComponent], declarations: [components]}) @NgModule({imports: [BrowserModule], bootstrap: [RootTreeComponent], declarations: [components]})
class AppModule { class AppModule {
constructor(sanitizer: DomSanitizer) {
trustedEmptyColor = sanitizer.bypassSecurityTrustStyle('');
trustedGreyColor = sanitizer.bypassSecurityTrustStyle('grey');
}
} }
return AppModule; return AppModule;

View File

@ -40,6 +40,29 @@ describe('WebWorkers Animations', function() {
browser.wait(() => boxElm.getSize().then(sizes => sizes['width'] > 750), 1000); browser.wait(() => boxElm.getSize().then(sizes => sizes['width'] > 750), 1000);
}); });
it('should cancel the animation midway and continue from where it left off', () => {
browser.ignoreSynchronization = true;
browser.get(URL);
waitForBootstrap();
const elem = element(by.css(selector + ' .box'));
const btn = element(by.css(selector + ' button'));
const getWidth = () => elem.getSize().then((sizes: any) => sizes['width']);
btn.click();
browser.sleep(250);
btn.click();
expect(getWidth()).toBeLessThan(600);
browser.sleep(500);
expect(getWidth()).toBeLessThan(50);
});
function waitForBootstrap() { function waitForBootstrap() {
browser.wait(protractor.until.elementLocated(by.css(selector + ' .box')), 5000) browser.wait(protractor.until.elementLocated(by.css(selector + ' .box')), 5000)
.then(() => {}, () => { .then(() => {}, () => {

View File

@ -20,7 +20,8 @@
"lib": ["es5", "dom", "es2015.promise", "es2015.collection", "es2015.iterable"], "lib": ["es5", "dom", "es2015.promise", "es2015.collection", "es2015.iterable"],
"skipDefaultLibCheck": true, "skipDefaultLibCheck": true,
"skipLibCheck": true, "skipLibCheck": true,
"target": "es5" "target": "es5",
"types": ["angularjs"]
}, },
"exclude": [ "exclude": [
"angular1_router", "angular1_router",

View File

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

View File

@ -749,7 +749,7 @@ export declare class RenderComponentType {
/** @experimental */ /** @experimental */
export declare abstract class Renderer { export declare abstract class Renderer {
abstract animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number, easing: string): AnimationPlayer; abstract animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number, easing: string, previousPlayers?: AnimationPlayer[]): AnimationPlayer;
abstract attachViewAfter(node: any, viewRootNodes: any[]): void; abstract attachViewAfter(node: any, viewRootNodes: any[]): void;
abstract createElement(parentElement: any, name: string, debugInfo?: RenderDebugInfo): any; abstract createElement(parentElement: any, name: string, debugInfo?: RenderDebugInfo): any;
abstract createTemplateAnchor(parentElement: any, debugInfo?: RenderDebugInfo): any; abstract createTemplateAnchor(parentElement: any, debugInfo?: RenderDebugInfo): any;

View File

@ -1,6 +1,6 @@
/** @experimental */ /** @experimental */
export declare abstract class AnimationDriver { export declare abstract class AnimationDriver {
abstract animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number, easing: string): AnimationPlayer; abstract animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number, easing: string, previousPlayers?: AnimationPlayer[]): AnimationPlayer;
static NOOP: AnimationDriver; static NOOP: AnimationDriver;
} }

View File

@ -1,5 +1,9 @@
/** @experimental */ /** @experimental */
export declare function downgradeComponent(info: ComponentInfo): angular.IInjectable; export declare function downgradeComponent(info: {
component: Type<any>;
inputs?: string[];
outputs?: string[];
}): any;
/** @experimental */ /** @experimental */
export declare function downgradeInjectable(token: any): (string | ((i: Injector) => any))[]; export declare function downgradeInjectable(token: any): (string | ((i: Injector) => any))[];
@ -15,9 +19,11 @@ export declare class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnD
/** @experimental */ /** @experimental */
export declare class UpgradeModule { export declare class UpgradeModule {
$injector: angular.IInjectorService; $injector: any;
injector: Injector; injector: Injector;
ngZone: NgZone; ngZone: NgZone;
constructor(injector: Injector, ngZone: NgZone); constructor(
bootstrap(element: Element, modules?: string[], config?: angular.IAngularBootstrapConfig): void; injector: Injector,
ngZone: NgZone);
bootstrap(element: Element, modules?: string[], config?: any): void;
} }