Compare commits
283 Commits
10.0.0-rc.
...
9.1.4
Author | SHA1 | Date | |
---|---|---|---|
9b961a410a | |||
420c73c722 | |||
62930aac7b | |||
d7433316b0 | |||
c440165384 | |||
0ce96f1d78 | |||
d3deaf9a99 | |||
5c88a9fcfe | |||
0bd7517c73 | |||
d669429bd2 | |||
6a9b2c9a67 | |||
0f389fa5ae | |||
2a27f69522 | |||
180a32894e | |||
00724dcdbb | |||
cc71116ba6 | |||
ddfcf082ca | |||
c7e4f5eb4d | |||
b3fe9017f7 | |||
d3a77ea91a | |||
1b5a931d63 | |||
d840503d35 | |||
b49a734f0d | |||
695f83529d | |||
f898c9ab57 | |||
5337e138e3 | |||
e33047454f | |||
858fa45556 | |||
b97211ee2b | |||
25d4238371 | |||
7743c43529 | |||
54883cb477 | |||
91cef8cfc7 | |||
d40bcce84e | |||
7a18fb2448 | |||
ec39bdcc15 | |||
b7070b0ad6 | |||
aa94cd505c | |||
cc6ccf28a6 | |||
e1071615c6 | |||
8bd5374cfd | |||
b9b9cc2ba8 | |||
a6e10ef869 | |||
9724169bf4 | |||
c0ed57db76 | |||
0bd50e2e50 | |||
0ceb27041f | |||
ec2affe104 | |||
c590e8ca7a | |||
254b9ea44c | |||
2a53f47159 | |||
722d9397b0 | |||
03de31a78e | |||
b22c5a953d | |||
24222e0c1f | |||
95f45e8070 | |||
18be33a9d1 | |||
a22d4f6c98 | |||
5ae8473c6b | |||
fd7c39e3cf | |||
d85d91df66 | |||
15930d21c7 | |||
61a7f98b98 | |||
c3c7bf6509 | |||
b2e7ce47ec | |||
94e518e3c7 | |||
0fa5ac8d0d | |||
f2fca3e243 | |||
5bab49828d | |||
db4e93d0ca | |||
479a59be43 | |||
52aab63dd9 | |||
506beeddc1 | |||
0075078179 | |||
bb7edc52aa | |||
ed2b0e945e | |||
da159bde83 | |||
06a9809e32 | |||
1e4fb74ec8 | |||
797c306306 | |||
972fc06135 | |||
a9117061d0 | |||
fe1d9bacc3 | |||
08b8b51486 | |||
1d4af3f734 | |||
609d81c65e | |||
af30efddc5 | |||
15115f6179 | |||
eec9b6bbb5 | |||
45fd77ead1 | |||
f16587e9b7 | |||
4f9991534e | |||
51a0ed2222 | |||
a5ea100e7c | |||
0429c7f5e9 | |||
1756cced4a | |||
793a001d7c | |||
5c3774cfe6 | |||
12266b2042 | |||
e385abc83c | |||
933cbfb828 | |||
c5e725111d | |||
93993864b1 | |||
26f49151e7 | |||
4b3f9ac739 | |||
80604d3a76 | |||
e615a10371 | |||
c8f2ca2349 | |||
a67afcc932 | |||
fd7698253e | |||
32de025dce | |||
a4e1768a74 | |||
b336871303 | |||
92fa6399f7 | |||
3992341d34 | |||
8c559ef104 | |||
0bac2b062c | |||
58d028178f | |||
e06512b22f | |||
603b0944d5 | |||
918e628f9b | |||
468cf69c55 | |||
c8f3fa9f3e | |||
78136cc3a7 | |||
66724fd159 | |||
8e7f9033a3 | |||
c8f9092364 | |||
bc995835b9 | |||
1bfa908ab3 | |||
2479f7d7ef | |||
333b8679ad | |||
5da621d3dd | |||
cbed582a1a | |||
d5aa6b5bd6 | |||
33eee43263 | |||
72053b0f27 | |||
d20ef47b16 | |||
bdc05aef64 | |||
798d959bee | |||
3570aaa363 | |||
421b6a97d6 | |||
b28a5f6eef | |||
52ab9397a0 | |||
b99e539b0e | |||
fd2ef74a31 | |||
cbc25bbf31 | |||
7385b2cf25 | |||
0daa48800e | |||
ffa4e11db1 | |||
41a41e741d | |||
c5384aee58 | |||
b02c950aac | |||
2eaa304aba | |||
9faf2e4a96 | |||
0e7a89a391 | |||
af4269471d | |||
915a6c17b9 | |||
93b32d3023 | |||
bd82b34f79 | |||
c626e90ab0 | |||
c6bbb0d8e5 | |||
f48a065db0 | |||
aeb6d0d0f7 | |||
655c9e386b | |||
2ad20b953f | |||
7059eaf2d2 | |||
939fda410f | |||
35ab6acab3 | |||
f4ee067cb8 | |||
068e28381d | |||
dff52ecb11 | |||
56af303dc3 | |||
136596ddb4 | |||
e515c913d9 | |||
31eaf78035 | |||
b0d680daa0 | |||
93cbef26c7 | |||
e2b221b8fa | |||
7a33c456d3 | |||
61db817eed | |||
4894220acf | |||
74b7a8eaf5 | |||
bfa55162de | |||
776f991c1a | |||
d9d1a1e682 | |||
867ff14a4a | |||
879457ca05 | |||
efbe4ad28a | |||
64f72c0600 | |||
81c05bfa01 | |||
56abcf088d | |||
6818432cd0 | |||
79bfb037df | |||
0fce20d79d | |||
9f486c31d2 | |||
d36066dd6c | |||
a631b99c69 | |||
78211c42ea | |||
5ff5a11509 | |||
e80047e897 | |||
eee5eeae76 | |||
c3ce1903b6 | |||
21da0346c7 | |||
af2d74fde9 | |||
8de62a9811 | |||
26e28453d6 | |||
8ea30b662f | |||
cb0a2a055d | |||
14ae3c0a21 | |||
90cae34c05 | |||
7bb35889fa | |||
03a3c753bb | |||
74b1464dff | |||
ff523c9ec9 | |||
392ef93c2b | |||
29dcb5bf68 | |||
de594ca221 | |||
3545a49a79 | |||
f4681b6e40 | |||
fb16557381 | |||
61c9da5542 | |||
dbb1eb0ffe | |||
002b81c0b0 | |||
beb3f19dc0 | |||
e8f3c47b03 | |||
c58e6ba13a | |||
61e5ab4703 | |||
10385c27da | |||
0ab244f39e | |||
eafa5260db | |||
15cffa210c | |||
df8e45a193 | |||
bb8744db94 | |||
43bad87ae1 | |||
c3b297a318 | |||
6ea232eb3c | |||
0baab0f3db | |||
e53b686375 | |||
5c28af0e74 | |||
60adc35d30 | |||
024cc3b84a | |||
ac17142001 | |||
8d7066223f | |||
0cb3c04128 | |||
2649794e65 | |||
bf426c5f0d | |||
aa6b9f0382 | |||
822b420a11 | |||
294c6297d7 | |||
58fc65d198 | |||
b6bd8d7572 | |||
b08168bb90 | |||
407fa42679 | |||
aef432384a | |||
fb70083339 | |||
c9c2408176 | |||
e066bddfe9 | |||
447a600477 | |||
70f9bfff43 | |||
57c02b044c | |||
6defe962c8 | |||
267bcb3e9c | |||
b0b66881b4 | |||
9ff8d78bcd | |||
563b707497 | |||
5357e643b3 | |||
f71d132f7c | |||
ba3edda230 | |||
0767d37c07 | |||
8ba24578bc | |||
133a97ad67 | |||
4e67a3ab3f | |||
377f0010fc | |||
6e09129e4c | |||
d80e51a6b1 | |||
feb66b00da | |||
cb19eac105 | |||
6e0564ade6 | |||
05eeb7d279 | |||
2ce5fa3cce | |||
e140cdcb34 | |||
14b2db1d43 | |||
2afc7e982e |
7
.bazelrc
7
.bazelrc
@ -33,11 +33,6 @@ build --incompatible_strict_action_env
|
||||
run --incompatible_strict_action_env
|
||||
test --incompatible_strict_action_env
|
||||
|
||||
# Do not build runfile trees by default. If an execution strategy relies on runfile
|
||||
# symlink teee, the tree is created on-demand. See: https://github.com/bazelbuild/bazel/issues/6627
|
||||
# and https://github.com/bazelbuild/bazel/commit/03246077f948f2790a83520e7dccc2625650e6df
|
||||
build --nobuild_runfile_links
|
||||
|
||||
###############################
|
||||
# Release support #
|
||||
# Turn on these settings with #
|
||||
@ -47,7 +42,7 @@ build --nobuild_runfile_links
|
||||
# Releases should always be stamped with version control info
|
||||
# This command assumes node on the path and is a workaround for
|
||||
# https://github.com/bazelbuild/bazel/issues/4802
|
||||
build:release --workspace_status_command="yarn -s ng-dev release build-env-stamp"
|
||||
build:release --workspace_status_command="node ./tools/bazel_stamp_vars.js"
|
||||
build:release --stamp
|
||||
|
||||
###############################
|
||||
|
@ -12,8 +12,8 @@ We use this as a symmetric AES encryption key to encrypt tokens like
|
||||
a GitHub token that enables publishing snapshots.
|
||||
|
||||
To create the github_token file, we take this approach:
|
||||
- Find the angular-builds:token in the internal pw database
|
||||
- Find the angular-builds:token in http://valentine
|
||||
- Go inside the CircleCI default docker image so you use the same version of openssl as we will at runtime: `docker run --rm -it circleci/node:10.12`
|
||||
- echo "https://[token]:@github.com" > credentials
|
||||
- openssl aes-256-cbc -e -in credentials -out .circleci/github_token -k $KEY
|
||||
- If needed, base64-encode the result so you can copy-paste it out of docker: `base64 github_token`
|
||||
- If needed, base64-encode the result so you can copy-paste it out of docker: `base64 github_token`
|
@ -22,18 +22,18 @@ version: 2.1
|
||||
# **NOTE 1 **: If you change the cache key prefix, also sync the cache_key_fallback to match.
|
||||
# **NOTE 2 **: Keep the static part of the cache key as prefix to enable correct fallbacks.
|
||||
# See https://circleci.com/docs/2.0/caching/#restoring-cache for how prefixes work in CircleCI.
|
||||
var_3: &cache_key v7-angular-node-12-{{ checksum ".bazelversion" }}-{{ checksum "yarn.lock" }}-{{ checksum "WORKSPACE" }}-{{ checksum "packages/bazel/package.bzl" }}-{{ checksum "aio/yarn.lock" }}
|
||||
var_3: &cache_key v6-angular-node-12-{{ checksum ".bazelversion" }}-{{ checksum "yarn.lock" }}-{{ checksum "WORKSPACE" }}-{{ checksum "packages/bazel/package.bzl" }}-{{ checksum "aio/yarn.lock" }}
|
||||
# We invalidate the cache if the Bazel version changes because otherwise the `bazelisk` cache
|
||||
# folder will contain all previously used versions and ultimately cause the cache restoring to
|
||||
# be slower due to its growing size.
|
||||
var_4: &cache_key_fallback v7-angular-node-12-{{ checksum ".bazelversion" }}
|
||||
var_3_win: &cache_key_win v7-angular-win-node-12-{{ checksum ".bazelversion" }}-{{ checksum "yarn.lock" }}-{{ checksum "WORKSPACE" }}-{{ checksum "packages/bazel/package.bzl" }}-{{ checksum "aio/yarn.lock" }}
|
||||
var_4_win: &cache_key_win_fallback v7-angular-win-node-12-{{ checksum ".bazelversion" }}
|
||||
var_4: &cache_key_fallback v6-angular-node-12-{{ checksum ".bazelversion" }}
|
||||
var_3_win: &cache_key_win v6-angular-win-node-12-{{ checksum ".bazelversion" }}-{{ checksum "yarn.lock" }}-{{ checksum "WORKSPACE" }}-{{ checksum "packages/bazel/package.bzl" }}-{{ checksum "aio/yarn.lock" }}
|
||||
var_4_win: &cache_key_win_fallback v6-angular-win-node-12-{{ checksum ".bazelversion" }}
|
||||
|
||||
# Cache key for the `components-repo-unit-tests` job. **Note** when updating the SHA in the
|
||||
# cache keys also update the SHA for the "COMPONENTS_REPO_COMMIT" environment variable.
|
||||
var_5: &components_repo_unit_tests_cache_key v7-angular-components-189d98e8b01b33974328255f085de04251d61567
|
||||
var_6: &components_repo_unit_tests_cache_key_fallback v7-angular-components-
|
||||
var_5: &components_repo_unit_tests_cache_key v6-angular-components-598db096e668aa7e9debd56eedfd127b7a55e371
|
||||
var_6: &components_repo_unit_tests_cache_key_fallback v6-angular-components-
|
||||
|
||||
# Workspace initially persisted by the `setup` job, and then enhanced by `build-npm-packages` and
|
||||
# `build-ivy-npm-packages`.
|
||||
@ -272,6 +272,12 @@ jobs:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
|
||||
- run: 'yarn bazel:format -mode=check ||
|
||||
(echo "BUILD files not formatted. Please run ''yarn bazel:format''" ; exit 1)'
|
||||
# Run the skylark linter to check our Bazel rules
|
||||
- run: 'yarn bazel:lint ||
|
||||
(echo -e "\n.bzl files have lint errors. Please run ''yarn bazel:lint-fix''"; exit 1)'
|
||||
|
||||
- run: yarn -s tslint
|
||||
- run: yarn -s ng-dev format changed $CI_GIT_BASE_REVISION --check
|
||||
- run: yarn -s ts-circular-deps:check
|
||||
@ -384,6 +390,10 @@ jobs:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
- install_chrome_libs
|
||||
# Compile dependencies to ivy
|
||||
# Running `ngcc` here (instead of implicitly via `ng build`) allows us to take advantage of
|
||||
# the parallel, async mode speed-up (~20-25s on CI).
|
||||
- run: yarn --cwd aio ngcc --properties es2015
|
||||
# Build aio
|
||||
- run: yarn --cwd aio build --progress=false
|
||||
# Lint the code
|
||||
|
131
.circleci/env.sh
131
.circleci/env.sh
@ -5,76 +5,87 @@ readonly projectDir=$(realpath "$(dirname ${BASH_SOURCE[0]})/..")
|
||||
readonly envHelpersPath="$projectDir/.circleci/env-helpers.inc.sh";
|
||||
readonly bashEnvCachePath="$projectDir/.circleci/bash_env_cache";
|
||||
|
||||
# Load helpers and make them available everywhere (through `$BASH_ENV`).
|
||||
source $envHelpersPath;
|
||||
echo "source $envHelpersPath;" >> $BASH_ENV;
|
||||
if [ -f $bashEnvCachePath ]; then
|
||||
# Since a bash env cache is present, load this into the $BASH_ENV
|
||||
cat "$bashEnvCachePath" >> $BASH_ENV;
|
||||
echo "BASH environment loaded from cached value at $bashEnvCachePath";
|
||||
else
|
||||
# Since no bash env cache is present, build out $BASH_ENV values.
|
||||
|
||||
# Load helpers and make them available everywhere (through `$BASH_ENV`).
|
||||
source $envHelpersPath;
|
||||
echo "source $envHelpersPath;" >> $BASH_ENV;
|
||||
|
||||
|
||||
####################################################################################################
|
||||
# Define PUBLIC environment variables for CircleCI.
|
||||
####################################################################################################
|
||||
# See https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables for more info.
|
||||
####################################################################################################
|
||||
setPublicVar CI "$CI"
|
||||
setPublicVar PROJECT_ROOT "$projectDir";
|
||||
setPublicVar CI_AIO_MIN_PWA_SCORE "95";
|
||||
# This is the branch being built; e.g. `pull/12345` for PR builds.
|
||||
setPublicVar CI_BRANCH "$CIRCLE_BRANCH";
|
||||
setPublicVar CI_BUILD_URL "$CIRCLE_BUILD_URL";
|
||||
setPublicVar CI_COMMIT "$CIRCLE_SHA1";
|
||||
# `CI_COMMIT_RANGE` is only used on push builds (a.k.a. non-PR, non-scheduled builds and rerun
|
||||
# workflows of such builds).
|
||||
setPublicVar CI_GIT_BASE_REVISION "${CIRCLE_GIT_BASE_REVISION}";
|
||||
setPublicVar CI_GIT_REVISION "${CIRCLE_GIT_REVISION}";
|
||||
setPublicVar CI_COMMIT_RANGE "$CIRCLE_GIT_BASE_REVISION..$CIRCLE_GIT_REVISION";
|
||||
setPublicVar CI_PULL_REQUEST "${CIRCLE_PR_NUMBER:-false}";
|
||||
setPublicVar CI_REPO_NAME "$CIRCLE_PROJECT_REPONAME";
|
||||
setPublicVar CI_REPO_OWNER "$CIRCLE_PROJECT_USERNAME";
|
||||
setPublicVar CI_PR_REPONAME "$CIRCLE_PR_REPONAME";
|
||||
setPublicVar CI_PR_USERNAME "$CIRCLE_PR_USERNAME";
|
||||
####################################################################################################
|
||||
# Define PUBLIC environment variables for CircleCI.
|
||||
####################################################################################################
|
||||
# See https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables for more info.
|
||||
####################################################################################################
|
||||
setPublicVar CI "$CI"
|
||||
setPublicVar PROJECT_ROOT "$projectDir";
|
||||
setPublicVar CI_AIO_MIN_PWA_SCORE "95";
|
||||
# This is the branch being built; e.g. `pull/12345` for PR builds.
|
||||
setPublicVar CI_BRANCH "$CIRCLE_BRANCH";
|
||||
setPublicVar CI_BUILD_URL "$CIRCLE_BUILD_URL";
|
||||
setPublicVar CI_COMMIT "$CIRCLE_SHA1";
|
||||
# `CI_COMMIT_RANGE` is only used on push builds (a.k.a. non-PR, non-scheduled builds and rerun
|
||||
# workflows of such builds).
|
||||
setPublicVar CI_GIT_BASE_REVISION "${CIRCLE_GIT_BASE_REVISION}";
|
||||
setPublicVar CI_GIT_REVISION "${CIRCLE_GIT_REVISION}";
|
||||
setPublicVar CI_COMMIT_RANGE "$CIRCLE_GIT_BASE_REVISION..$CIRCLE_GIT_REVISION";
|
||||
setPublicVar CI_PULL_REQUEST "${CIRCLE_PR_NUMBER:-false}";
|
||||
setPublicVar CI_REPO_NAME "$CIRCLE_PROJECT_REPONAME";
|
||||
setPublicVar CI_REPO_OWNER "$CIRCLE_PROJECT_USERNAME";
|
||||
setPublicVar CI_PR_REPONAME "$CIRCLE_PR_REPONAME";
|
||||
setPublicVar CI_PR_USERNAME "$CIRCLE_PR_USERNAME";
|
||||
|
||||
|
||||
####################################################################################################
|
||||
# Define "lazy" PUBLIC environment variables for CircleCI.
|
||||
# (I.e. functions to set an environment variable when called.)
|
||||
####################################################################################################
|
||||
createPublicVarSetter CI_STABLE_BRANCH "\$(npm info @angular/core dist-tags.latest | sed -r 's/^\\s*([0-9]+\\.[0-9]+)\\.[0-9]+.*$/\\1.x/')";
|
||||
####################################################################################################
|
||||
# Define "lazy" PUBLIC environment variables for CircleCI.
|
||||
# (I.e. functions to set an environment variable when called.)
|
||||
####################################################################################################
|
||||
createPublicVarSetter CI_STABLE_BRANCH "\$(npm info @angular/core dist-tags.latest | sed -r 's/^\\s*([0-9]+\\.[0-9]+)\\.[0-9]+.*$/\\1.x/')";
|
||||
|
||||
|
||||
####################################################################################################
|
||||
# Define SECRET environment variables for CircleCI.
|
||||
####################################################################################################
|
||||
setSecretVar CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN "$AIO_DEPLOY_TOKEN";
|
||||
setSecretVar CI_SECRET_PAYLOAD_FIREBASE_TOKEN "$ANGULAR_PAYLOAD_TOKEN";
|
||||
####################################################################################################
|
||||
# Define SECRET environment variables for CircleCI.
|
||||
####################################################################################################
|
||||
setSecretVar CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN "$AIO_DEPLOY_TOKEN";
|
||||
setSecretVar CI_SECRET_PAYLOAD_FIREBASE_TOKEN "$ANGULAR_PAYLOAD_TOKEN";
|
||||
|
||||
|
||||
####################################################################################################
|
||||
# Define SauceLabs environment variables for CircleCI.
|
||||
####################################################################################################
|
||||
setPublicVar SAUCE_USERNAME "angular-framework";
|
||||
setSecretVar SAUCE_ACCESS_KEY "0c731274ed5f-cbc9-16f4-021a-9835e39f";
|
||||
# TODO(josephperrott): Remove environment variables once all saucelabs tests are via bazel method.
|
||||
setPublicVar SAUCE_LOG_FILE /tmp/angular/sauce-connect.log
|
||||
setPublicVar SAUCE_READY_FILE /tmp/angular/sauce-connect-ready-file.lock
|
||||
setPublicVar SAUCE_PID_FILE /tmp/angular/sauce-connect-pid-file.lock
|
||||
setPublicVar SAUCE_TUNNEL_IDENTIFIER "angular-framework-${CIRCLE_BUILD_NUM}-${CIRCLE_NODE_INDEX}"
|
||||
# Amount of seconds we wait for sauceconnect to establish a tunnel instance. In order to not
|
||||
# acquire CircleCI instances for too long if sauceconnect failed, we need a connect timeout.
|
||||
setPublicVar SAUCE_READY_FILE_TIMEOUT 120
|
||||
####################################################################################################
|
||||
# Define SauceLabs environment variables for CircleCI.
|
||||
####################################################################################################
|
||||
setPublicVar SAUCE_USERNAME "angular-framework";
|
||||
setSecretVar SAUCE_ACCESS_KEY "0c731274ed5f-cbc9-16f4-021a-9835e39f";
|
||||
# TODO(josephperrott): Remove environment variables once all saucelabs tests are via bazel method.
|
||||
setPublicVar SAUCE_LOG_FILE /tmp/angular/sauce-connect.log
|
||||
setPublicVar SAUCE_READY_FILE /tmp/angular/sauce-connect-ready-file.lock
|
||||
setPublicVar SAUCE_PID_FILE /tmp/angular/sauce-connect-pid-file.lock
|
||||
setPublicVar SAUCE_TUNNEL_IDENTIFIER "angular-framework-${CIRCLE_BUILD_NUM}-${CIRCLE_NODE_INDEX}"
|
||||
# Amount of seconds we wait for sauceconnect to establish a tunnel instance. In order to not
|
||||
# acquire CircleCI instances for too long if sauceconnect failed, we need a connect timeout.
|
||||
setPublicVar SAUCE_READY_FILE_TIMEOUT 120
|
||||
|
||||
|
||||
####################################################################################################
|
||||
# Define environment variables for the `angular/components` repo unit tests job.
|
||||
####################################################################################################
|
||||
# We specifically use a directory within "/tmp" here because we want the cloned repo to be
|
||||
# completely isolated from angular/angular in order to avoid any bad interactions between
|
||||
# their separate build setups. **NOTE**: When updating the temporary directory, also update
|
||||
# the `save_cache` path configuration in `config.yml`
|
||||
setPublicVar COMPONENTS_REPO_TMP_DIR "/tmp/angular-components-repo"
|
||||
setPublicVar COMPONENTS_REPO_URL "https://github.com/angular/components.git"
|
||||
setPublicVar COMPONENTS_REPO_BRANCH "master"
|
||||
# **NOTE**: When updating the commit SHA, also update the cache key in the CircleCI `config.yml`.
|
||||
setPublicVar COMPONENTS_REPO_COMMIT "189d98e8b01b33974328255f085de04251d61567"
|
||||
####################################################################################################
|
||||
# Define environment variables for the `angular/components` repo unit tests job.
|
||||
####################################################################################################
|
||||
# We specifically use a directory within "/tmp" here because we want the cloned repo to be
|
||||
# completely isolated from angular/angular in order to avoid any bad interactions between
|
||||
# their separate build setups. **NOTE**: When updating the temporary directory, also update
|
||||
# the `save_cache` path configuration in `config.yml`
|
||||
setPublicVar COMPONENTS_REPO_TMP_DIR "/tmp/angular-components-repo"
|
||||
setPublicVar COMPONENTS_REPO_URL "https://github.com/angular/components.git"
|
||||
setPublicVar COMPONENTS_REPO_BRANCH "master"
|
||||
# **NOTE**: When updating the commit SHA, also update the cache key in the CircleCI `config.yml`.
|
||||
setPublicVar COMPONENTS_REPO_COMMIT "598db096e668aa7e9debd56eedfd127b7a55e371"
|
||||
|
||||
# Save the created BASH_ENV into the bash env cache file.
|
||||
cat "$BASH_ENV" >> $bashEnvCachePath;
|
||||
fi
|
||||
|
||||
|
||||
####################################################################################################
|
||||
|
69
.dev-infra.json
Normal file
69
.dev-infra.json
Normal file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"commitMessage": {
|
||||
"maxLength": 120,
|
||||
"minBodyLength": 100,
|
||||
"types": [
|
||||
"build",
|
||||
"ci",
|
||||
"docs",
|
||||
"feat",
|
||||
"fix",
|
||||
"perf",
|
||||
"refactor",
|
||||
"release",
|
||||
"style",
|
||||
"test"
|
||||
],
|
||||
"scopes": [
|
||||
"animations",
|
||||
"bazel",
|
||||
"benchpress",
|
||||
"changelog",
|
||||
"common",
|
||||
"compiler",
|
||||
"compiler-cli",
|
||||
"core",
|
||||
"dev-infra",
|
||||
"docs-infra",
|
||||
"elements",
|
||||
"forms",
|
||||
"http",
|
||||
"language-service",
|
||||
"localize",
|
||||
"ngcc",
|
||||
"packaging",
|
||||
"platform-browser",
|
||||
"platform-browser-dynamic",
|
||||
"platform-server",
|
||||
"platform-webworker",
|
||||
"platform-webworker-dynamic",
|
||||
"router",
|
||||
"service-worker",
|
||||
"upgrade",
|
||||
"ve",
|
||||
"zone.js"
|
||||
]
|
||||
},
|
||||
"format": {
|
||||
"matchers": [
|
||||
"dev-infra/**/*.{js,ts}",
|
||||
"packages/**/*.{js,ts}",
|
||||
"!packages/zone.js",
|
||||
"!packages/common/locales/**/*.{js,ts}",
|
||||
"!packages/common/src/i18n/available_locales.ts",
|
||||
"!packages/common/src/i18n/currencies.ts",
|
||||
"!packages/common/src/i18n/locale_en.ts",
|
||||
"modules/benchmarks/**/*.{js,ts}",
|
||||
"modules/playground/**/*.{js,ts}",
|
||||
"tools/**/*.{js,ts}",
|
||||
"!tools/gulp-tasks/cldr/extract.js",
|
||||
"!tools/public_api_guard/**/*.d.ts",
|
||||
"!tools/ts-api-guardian/test/fixtures/**",
|
||||
"./*.{js,ts}",
|
||||
"!**/node_modules/**",
|
||||
"!**/dist/**",
|
||||
"!**/built/**",
|
||||
"!shims_for_IE.js"
|
||||
]
|
||||
}
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
import {MergeConfig} from '../dev-infra/pr/merge/config';
|
||||
|
||||
// The configuration for `ng-dev commit-message` commands.
|
||||
const commitMessage = {
|
||||
'maxLength': 120,
|
||||
'minBodyLength': 100,
|
||||
'types': [
|
||||
'build',
|
||||
'ci',
|
||||
'docs',
|
||||
'feat',
|
||||
'fix',
|
||||
'perf',
|
||||
'refactor',
|
||||
'release',
|
||||
'style',
|
||||
'test',
|
||||
],
|
||||
'scopes': [
|
||||
'animations',
|
||||
'bazel',
|
||||
'benchpress',
|
||||
'changelog',
|
||||
'common',
|
||||
'compiler',
|
||||
'compiler-cli',
|
||||
'core',
|
||||
'dev-infra',
|
||||
'docs-infra',
|
||||
'elements',
|
||||
'forms',
|
||||
'http',
|
||||
'language-service',
|
||||
'localize',
|
||||
'ngcc',
|
||||
'packaging',
|
||||
'platform-browser',
|
||||
'platform-browser-dynamic',
|
||||
'platform-server',
|
||||
'platform-webworker',
|
||||
'platform-webworker-dynamic',
|
||||
'router',
|
||||
'service-worker',
|
||||
'upgrade',
|
||||
've',
|
||||
'zone.js',
|
||||
]
|
||||
};
|
||||
|
||||
// The configuration for `ng-dev format` commands.
|
||||
const format = {
|
||||
'clang-format': {
|
||||
'matchers': [
|
||||
'dev-infra/**/*.{js,ts}',
|
||||
'packages/**/*.{js,ts}',
|
||||
'!packages/zone.js',
|
||||
'!packages/common/locales/**/*.{js,ts}',
|
||||
'!packages/common/src/i18n/available_locales.ts',
|
||||
'!packages/common/src/i18n/currencies.ts',
|
||||
'!packages/common/src/i18n/locale_en.ts',
|
||||
'modules/benchmarks/**/*.{js,ts}',
|
||||
'modules/playground/**/*.{js,ts}',
|
||||
'tools/**/*.{js,ts}',
|
||||
'!tools/gulp-tasks/cldr/extract.js',
|
||||
'!tools/public_api_guard/**/*.d.ts',
|
||||
'!tools/ts-api-guardian/test/fixtures/**',
|
||||
'*.{js,ts}',
|
||||
'!**/node_modules/**',
|
||||
'!**/dist/**',
|
||||
'!**/built/**',
|
||||
'!shims_for_IE.js',
|
||||
]
|
||||
},
|
||||
'buildifier': true
|
||||
};
|
||||
|
||||
/** Github metadata information for `ng-dev` commands. */
|
||||
const github = {
|
||||
owner: 'angular',
|
||||
name: 'angular',
|
||||
};
|
||||
|
||||
// Configuration for the `ng-dev pr merge` command. The command can be used
|
||||
// for merging upstream pull requests into branches based on a PR target label.
|
||||
const merge = () => {
|
||||
// TODO: resume dynamically determining patch branch
|
||||
const patch = '10.0.x';
|
||||
const config: MergeConfig = {
|
||||
githubApiMerge: false,
|
||||
claSignedLabel: 'cla: yes',
|
||||
mergeReadyLabel: /^PR action: merge(-assistance)?/,
|
||||
commitMessageFixupLabel: 'commit message fixup',
|
||||
labels: [
|
||||
{
|
||||
pattern: 'PR target: master-only',
|
||||
branches: ['master'],
|
||||
},
|
||||
{
|
||||
pattern: 'PR target: patch-only',
|
||||
branches: [patch],
|
||||
},
|
||||
{
|
||||
pattern: 'PR target: master & patch',
|
||||
branches: ['master', patch],
|
||||
},
|
||||
],
|
||||
requiredBaseCommits: {
|
||||
// PRs that target either `master` or the patch branch, need to be rebased
|
||||
// on top of the latest commit message validation fix.
|
||||
'master': '4341743b4a6d7e23c6f944aa9e34166b701369a1',
|
||||
[patch]: '4341743b4a6d7e23c6f944aa9e34166b701369a1'
|
||||
},
|
||||
};
|
||||
return config;
|
||||
};
|
||||
|
||||
// Export function to build ng-dev configuration object.
|
||||
module.exports = {
|
||||
commitMessage,
|
||||
format,
|
||||
github,
|
||||
merge,
|
||||
};
|
@ -351,7 +351,6 @@ groups:
|
||||
users:
|
||||
- alxhub
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- kara
|
||||
- mhevery
|
||||
- pkozlowski-opensource
|
||||
@ -367,6 +366,7 @@ groups:
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
'packages/common/http/**',
|
||||
'packages/http/**',
|
||||
'packages/examples/http/**',
|
||||
'aio/content/guide/http.md',
|
||||
'aio/content/examples/http/**',
|
||||
@ -494,8 +494,6 @@ groups:
|
||||
'packages/router/**',
|
||||
'packages/examples/router/**',
|
||||
'aio/content/guide/router.md',
|
||||
'aio/content/guide/router-tutorial.md',
|
||||
'aio/content/examples/router-tutorial/**',
|
||||
'aio/content/examples/router/**',
|
||||
'aio/content/images/guide/router/**'
|
||||
])
|
||||
@ -507,7 +505,7 @@ groups:
|
||||
# =========================================================
|
||||
# Framework: Service Worker
|
||||
# =========================================================
|
||||
fw-service-worker:
|
||||
fw-server-worker:
|
||||
conditions:
|
||||
- *can-be-global-approved
|
||||
- *can-be-global-docs-approved
|
||||
@ -569,7 +567,7 @@ groups:
|
||||
- *can-be-global-approved
|
||||
- *can-be-global-docs-approved
|
||||
- >
|
||||
contains_any_globs(files.exclude('packages/compiler-cli/**'), [
|
||||
contains_any_globs(files, [
|
||||
'**/testing/**',
|
||||
'aio/content/guide/testing.md',
|
||||
'aio/content/examples/testing/**',
|
||||
@ -648,6 +646,7 @@ groups:
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
'packages/bazel/**',
|
||||
'aio/content/guide/bazel.md'
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
@ -777,10 +776,10 @@ groups:
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- aikidave
|
||||
- IgorMinar
|
||||
- StephenFluin
|
||||
|
||||
|
||||
# =========================================================
|
||||
# Docs: Observables
|
||||
# =========================================================
|
||||
@ -947,12 +946,11 @@ groups:
|
||||
conditions:
|
||||
- *can-be-global-approved
|
||||
- >
|
||||
contains_any_globs(files.exclude("CHANGELOG.md"), [
|
||||
contains_any_globs(files, [
|
||||
'*',
|
||||
'.circleci/**',
|
||||
'.devcontainer/**',
|
||||
'.github/**',
|
||||
'.ng-dev/**',
|
||||
'.vscode/**',
|
||||
'.yarn/**',
|
||||
'dev-infra/**',
|
||||
@ -1020,7 +1018,6 @@ groups:
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
'goldens/public-api/**',
|
||||
'CHANGELOG.md',
|
||||
'docs/NAMING.md',
|
||||
'aio/content/guide/glossary.md',
|
||||
'aio/content/guide/styleguide.md',
|
||||
@ -1030,7 +1027,6 @@ groups:
|
||||
reviewers:
|
||||
users:
|
||||
- IgorMinar
|
||||
- kara
|
||||
|
||||
|
||||
# ================================================
|
||||
|
499
CHANGELOG.md
499
CHANGELOG.md
@ -1,368 +1,3 @@
|
||||
<a name="10.0.0-rc.2"></a>
|
||||
# [10.0.0-rc.2](https://github.com/angular/angular/compare/10.0.0-rc.0...10.0.0-rc.2) (2020-06-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** reenable decorator downleveling for Angular npm packages ([#37317](https://github.com/angular/angular/issues/37317)) ([d16a7f3](https://github.com/angular/angular/commit/d16a7f3)), closes [#37221](https://github.com/angular/angular/issues/37221) [#37221](https://github.com/angular/angular/issues/37221)
|
||||
|
||||
|
||||
Note: the 10.0.0-rc.1 release on npm accidentally glitched-out midway, so we cut 10.0.0-rc.2 instead. oops :-)
|
||||
|
||||
<a name="10.0.0-rc.0"></a>
|
||||
# [10.0.0-rc.0](https://github.com/angular/angular/compare/10.0.0-next.9...10.0.0-rc.0) (2020-05-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** disable tsickle pass when producing APF packages ([#37221](https://github.com/angular/angular/issues/37221)) ([a1001f2](https://github.com/angular/angular/commit/a1001f2))
|
||||
* **elements:** capture input properties set before upgrading the element ([#36114](https://github.com/angular/angular/issues/36114)) ([2fc5ae5](https://github.com/angular/angular/commit/2fc5ae5)), closes [#30848](https://github.com/angular/angular/issues/30848) [#31416](https://github.com/angular/angular/issues/31416)
|
||||
* **elements:** correctly handle getting/setting properties before connecting the element ([#36114](https://github.com/angular/angular/issues/36114)) ([327980b](https://github.com/angular/angular/commit/327980b)), closes [/github.com/angular/angular/pull/31416/files#r300326698](https://github.com//github.com/angular/angular/pull/31416/files/issues/r300326698)
|
||||
* **elements:** do not break when the constructor of an Angular Element is not called ([#36114](https://github.com/angular/angular/issues/36114)) ([89b44d1](https://github.com/angular/angular/commit/89b44d1))
|
||||
* **ngcc:** identifier ModuleWithProviders functions in IIFE wrapped classes ([#37206](https://github.com/angular/angular/issues/37206)) ([97e1399](https://github.com/angular/angular/commit/97e1399)), closes [#37189](https://github.com/angular/angular/issues/37189)
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **core:** Angular npm packages no longer contain jsdoc comments
|
||||
to support Closure Compiler's advanced optimizations
|
||||
|
||||
The support for Closure Compiler in Angular packages has been
|
||||
experimental and broken for quite some time.
|
||||
|
||||
As of TS3.9, Closure is unusable with the JavaScript emit. Please follow
|
||||
https://github.com/microsoft/TypeScript/issues/38374 for more
|
||||
information and updates.
|
||||
|
||||
If you used Closure Compiler with Angular in the past, you will likely
|
||||
be better off consuming Angular packages built from sources directly
|
||||
rather than consuming the version we publish on npm,
|
||||
which is primarily optimized for Webpack/Rollup + Terser build pipeline.
|
||||
|
||||
As a temporary workaround, you might consider using your current build
|
||||
pipeline with Closure flag `--compilation_level=SIMPLE`. This flag
|
||||
will ensure that your build pipeline produces buildable and
|
||||
runnable artifacts, at the cost of increased payload size due to
|
||||
advanced optimizations being disabled.
|
||||
|
||||
If you were affected by this change, please help us understand your
|
||||
needs by leaving a comment on https://github.com/angular/angular/issues/37234.
|
||||
|
||||
|
||||
|
||||
<a name="9.1.9"></a>
|
||||
## [9.1.9](https://github.com/angular/angular/compare/9.1.8...9.1.9) (2020-05-20)
|
||||
|
||||
This release contains a re-submit of the following 3 commits with fixes for TS 3.8.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **elements:** capture input properties set before upgrading the element ([#36114](https://github.com/angular/angular/issues/36114)) ([#37226](https://github.com/angular/angular/issues/37226)) ([a33cb2d](https://github.com/angular/angular/commit/a33cb2d)), closes [#30848](https://github.com/angular/angular/issues/30848) [#31416](https://github.com/angular/angular/issues/31416)
|
||||
* **elements:** correctly handle getting/setting properties before connecting the element ([#36114](https://github.com/angular/angular/issues/36114)) ([#37226](https://github.com/angular/angular/issues/37226)) ([6ac0042](https://github.com/angular/angular/commit/6ac0042)), closes [/github.com/angular/angular/pull/31416/files#r300326698](https://github.com//github.com/angular/angular/pull/31416/files/issues/r300326698)
|
||||
* **elements:** do not break when the constructor of an Angular Element is not called ([#36114](https://github.com/angular/angular/issues/36114)) ([#37226](https://github.com/angular/angular/issues/37226)) ([1465372](https://github.com/angular/angular/commit/1465372))
|
||||
|
||||
|
||||
|
||||
<a name="9.1.8"></a>
|
||||
## [9.1.8](https://github.com/angular/angular/compare/9.1.6...9.1.8) (2020-05-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** Host classes should not be fed back into `@Input` ([#35889](https://github.com/angular/angular/issues/35889)) ([f872b69](https://github.com/angular/angular/commit/f872b69)), closes [#35383](https://github.com/angular/angular/issues/35383)
|
||||
* **core:** inheritance delegate ctor regex updated to work on minified code ([#36962](https://github.com/angular/angular/issues/36962)) ([e3d3395](https://github.com/angular/angular/commit/e3d3395))
|
||||
* **elements:** capture input properties set before upgrading the element ([#36114](https://github.com/angular/angular/issues/36114)) ([1c8f179](https://github.com/angular/angular/commit/1c8f179)), closes [#30848](https://github.com/angular/angular/issues/30848) [#31416](https://github.com/angular/angular/issues/31416)
|
||||
* **elements:** correctly handle getting/setting properties before connecting the element ([#36114](https://github.com/angular/angular/issues/36114)) ([363f14c](https://github.com/angular/angular/commit/363f14c)), closes [/github.com/angular/angular/pull/31416/files#r300326698](https://github.com//github.com/angular/angular/pull/31416/files/issues/r300326698)
|
||||
* **elements:** do not break when the constructor of an Angular Element is not called ([#36114](https://github.com/angular/angular/issues/36114)) ([87b9f08](https://github.com/angular/angular/commit/87b9f08))
|
||||
* **router:** update type for routerLink to include null and undefined ([#37018](https://github.com/angular/angular/issues/37018)) ([7de7871](https://github.com/angular/angular/commit/7de7871)), closes [#13380](https://github.com/angular/angular/issues/13380) [#36544](https://github.com/angular/angular/issues/36544)
|
||||
|
||||
|
||||
<a name="10.0.0-next.9"></a>
|
||||
# [10.0.0-next.9](https://github.com/angular/angular/compare/10.0.0-next.8...10.0.0-next.9) (2020-05-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler-cli:** ensure LogicalFileSystem maintains case in paths ([#37008](https://github.com/angular/angular/issues/37008)) ([3dfc770](https://github.com/angular/angular/commit/3dfc770)), closes [#36992](https://github.com/angular/angular/issues/36992) [#36993](https://github.com/angular/angular/issues/36993) [#37000](https://github.com/angular/angular/issues/37000)
|
||||
* **router:** update type for routerLink to include null and undefined ([#37018](https://github.com/angular/angular/issues/37018)) ([ef9f8df](https://github.com/angular/angular/commit/ef9f8df)), closes [#13380](https://github.com/angular/angular/issues/13380) [#36544](https://github.com/angular/angular/issues/36544)
|
||||
|
||||
|
||||
|
||||
<a name="10.0.0-next.8"></a>
|
||||
# [10.0.0-next.8](https://github.com/angular/angular/compare/10.0.0-next.7...10.0.0-next.8) (2020-05-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler-cli:** compute the correct target output for `$localize` messages ([#36989](https://github.com/angular/angular/issues/36989)) ([4e1b5e4](https://github.com/angular/angular/commit/4e1b5e4))
|
||||
* **core:** Host classes should not be fed back into `@Input` ([#35889](https://github.com/angular/angular/issues/35889)) ([cda2530](https://github.com/angular/angular/commit/cda2530)), closes [#35383](https://github.com/angular/angular/issues/35383)
|
||||
* **core:** inheritance delegate ctor regex updated to work on minified code ([#36962](https://github.com/angular/angular/issues/36962)) ([ea971f7](https://github.com/angular/angular/commit/ea971f7))
|
||||
* **language-service:** use empty statement as parent of type node ([#36989](https://github.com/angular/angular/issues/36989)) ([a32cbed](https://github.com/angular/angular/commit/a32cbed))
|
||||
* **ngcc:** `viaModule` should be `null` for local imports ([#36989](https://github.com/angular/angular/issues/36989)) ([d268d2a](https://github.com/angular/angular/commit/d268d2a))
|
||||
* **ngcc:** ensure reflection hosts can handle TS 3.9 IIFE wrapped classes ([#36989](https://github.com/angular/angular/issues/36989)) ([d7440c4](https://github.com/angular/angular/commit/d7440c4))
|
||||
* **ngcc:** ensure rendering formatters work with IIFE wrapped classes ([#36989](https://github.com/angular/angular/issues/36989)) ([c8ee390](https://github.com/angular/angular/commit/c8ee390))
|
||||
* **ngcc:** support `defineProperty()` re-exports in CommonJS and UMD ([#36989](https://github.com/angular/angular/issues/36989)) ([91092f6](https://github.com/angular/angular/commit/91092f6))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* remove support for TypeScript 3.8 ([#37129](https://github.com/angular/angular/issues/37129)) ([6466fb2](https://github.com/angular/angular/commit/6466fb2))
|
||||
* **platform-server:** use absolute URLs from Location for HTTP ([#37071](https://github.com/angular/angular/issues/37071)) ([9edea0b](https://github.com/angular/angular/commit/9edea0b))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* TypeScript 3.8 is no longer supported, please update to TypeScript 3.9.
|
||||
|
||||
|
||||
|
||||
<a name="10.0.0-next.7"></a>
|
||||
# [10.0.0-next.7](https://github.com/angular/angular/compare/10.0.0-next.6...10.0.0-next.7) (2020-05-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** correct "development mode" console message ([#36571](https://github.com/angular/angular/issues/36571)) ([8d8e419](https://github.com/angular/angular/commit/8d8e419)), closes [#36570](https://github.com/angular/angular/issues/36570)
|
||||
* add aikidave as reviewer for DOCS: Marketing ([#37014](https://github.com/angular/angular/issues/37014)) ([286fbf4](https://github.com/angular/angular/commit/286fbf4))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler:** add name spans for property reads and method calls ([#36826](https://github.com/angular/angular/issues/36826)) ([eb34aa5](https://github.com/angular/angular/commit/eb34aa5))
|
||||
* **language-service:** [ivy] wrap ngtsc to handle typecheck files ([#36930](https://github.com/angular/angular/issues/36930)) ([1142c37](https://github.com/angular/angular/commit/1142c37))
|
||||
* **core** make generic mandatory for ModuleWithProviders ([#36892](https://github.com/angular/angular/issues/36892)) ([20cc3ab](https://github.com/angular/angular/commit/20cc3ab))
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **core:** make generic mandatory for ModuleWithProviders
|
||||
|
||||
A generic type parameter has always been required for the `ModuleWithProviders` pattern to work with Ivy, but prior to this commit, View Engine allowed the generic type to be omitted (though support was officially deprecated).
|
||||
If you're using `ModuleWithProviders` without a generic type in your application code, a v10 migration will update your code for you.
|
||||
|
||||
However, if you are using View Engine and also depending on a library that omits the generic type, you will now get a build time error similar to:
|
||||
|
||||
```
|
||||
error TS2314: Generic type 'ModuleWithProviders<T>' requires 1 type argument(s).
|
||||
```
|
||||
|
||||
In this case, ngcc won't help you (because it's Ivy-only) and the migration only covers application code.
|
||||
You should contact the library author to fix their library to provide a type parameter when they use this class.
|
||||
|
||||
As a workaround, we suggest setting `skipLibChecks` to false in your tsconfig or updating your app to use Ivy.
|
||||
|
||||
|
||||
<a name="9.1.7"></a>
|
||||
## [9.1.7](https://github.com/angular/angular/compare/9.1.6...9.1.7) (2020-05-13)
|
||||
|
||||
This release contains various API docs improvements.
|
||||
|
||||
|
||||
<a name="9.1.6"></a>
|
||||
## [9.1.6](https://github.com/angular/angular/compare/9.1.5...9.1.6) (2020-05-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler-cli**: Revert "fix(compiler-cli): fix case-sensitivity issues in NgtscCompilerHost (#36968)" (#37003)
|
||||
|
||||
|
||||
|
||||
<a name="10.0.0-next.6"></a>
|
||||
# [10.0.0-next.6](https://github.com/angular/angular/compare/10.0.0-next.5...10.0.0-next.6) (2020-05-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bazel:** ng_package rule should update "package.json" of ts_library targets ([#36944](https://github.com/angular/angular/issues/36944)) ([d5293d2](https://github.com/angular/angular/commit/d5293d2))
|
||||
* **compiler:** remove outdated and invalid warning for unresolved DI parameters ([#36985](https://github.com/angular/angular/issues/36985)) ([d0280a0](https://github.com/angular/angular/commit/d0280a0))
|
||||
* **compiler:** switch to 'referencedFiles' for shim generation ([#36211](https://github.com/angular/angular/issues/36211)) ([4213e8d](https://github.com/angular/angular/commit/4213e8d))
|
||||
* **compiler-cli:** `isCaseSensitive()` returns correct value ([#36859](https://github.com/angular/angular/issues/36859)) ([fc4741f](https://github.com/angular/angular/commit/fc4741f))
|
||||
* **compiler-cli:** don't try to tag non-ts files as shims ([#36987](https://github.com/angular/angular/issues/36987)) ([42d1091](https://github.com/angular/angular/commit/42d1091))
|
||||
* **compiler-cli:** ensure `getRootDirs()` handles case-sensitivity ([#36859](https://github.com/angular/angular/issues/36859)) ([3f3e9b7](https://github.com/angular/angular/commit/3f3e9b7))
|
||||
* **compiler-cli:** ensure `MockFileSystem` handles case-sensitivity ([#36859](https://github.com/angular/angular/issues/36859)) ([26eacd4](https://github.com/angular/angular/commit/26eacd4))
|
||||
* **compiler-cli:** ensure LogicalFileSystem handles case-sensitivity ([#36859](https://github.com/angular/angular/issues/36859)) ([53a8459](https://github.com/angular/angular/commit/53a8459))
|
||||
* **compiler-cli:** fix bug tracking indirect NgModule dependencies ([#36211](https://github.com/angular/angular/issues/36211)) ([bab90a7](https://github.com/angular/angular/commit/bab90a7))
|
||||
* **compiler-cli:** fix case-sensitivity issues in NgtscCompilerHost ([#36859](https://github.com/angular/angular/issues/36859)) ([0ec0ff3](https://github.com/angular/angular/commit/0ec0ff3))
|
||||
* **compiler-cli:** normalize mock Windows file paths correctly ([#36859](https://github.com/angular/angular/issues/36859)) ([b682bd1](https://github.com/angular/angular/commit/b682bd1))
|
||||
* **compiler-cli:** use CompilerHost to ensure canonical file paths ([#36859](https://github.com/angular/angular/issues/36859)) ([a10c126](https://github.com/angular/angular/commit/a10c126))
|
||||
* **core:** handle pluralize functions that expect a number ([#36901](https://github.com/angular/angular/issues/36901)) ([2ff4b35](https://github.com/angular/angular/commit/2ff4b35)), closes [#36888](https://github.com/angular/angular/issues/36888)
|
||||
* **core:** properly get root nodes from embedded views with <ng-content> ([#36051](https://github.com/angular/angular/issues/36051)) ([e30e132](https://github.com/angular/angular/commit/e30e132)), closes [#35967](https://github.com/angular/angular/issues/35967)
|
||||
* **forms:** handle numeric values properly in the validator ([#36157](https://github.com/angular/angular/issues/36157)) ([88a235d](https://github.com/angular/angular/commit/88a235d)), closes [#35591](https://github.com/angular/angular/issues/35591)
|
||||
* **forms:** number input fires valueChanges twice ([#36087](https://github.com/angular/angular/issues/36087)) ([97d6d90](https://github.com/angular/angular/commit/97d6d90)), closes [#12540](https://github.com/angular/angular/issues/12540)
|
||||
* **localize:** ensure `getLocation()` works ([#36853](https://github.com/angular/angular/issues/36853)) ([70b25a3](https://github.com/angular/angular/commit/70b25a3))
|
||||
* **ngcc:** support ModuleWithProviders functions that delegate ([#36948](https://github.com/angular/angular/issues/36948)) ([fafa50d](https://github.com/angular/angular/commit/fafa50d)), closes [#36892](https://github.com/angular/angular/issues/36892)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
|
||||
* **bazel:** simplify ng_package by dropping esm5 and fesm5 ([#36944](https://github.com/angular/angular/issues/36944)) ([9dbb30f](https://github.com/angular/angular/commit/9dbb30f))
|
||||
* **compiler-cli:** report error if undecorated class with Angular features is discovered ([#36921](https://github.com/angular/angular/issues/36921)) ([4c92cf4](https://github.com/angular/angular/commit/4c92cf4))
|
||||
* **core:** undecorated-classes-with-decorated-fields migration should handle classes with lifecycle hooks ([#36921](https://github.com/angular/angular/issues/36921)) ([c6ecdc9](https://github.com/angular/angular/commit/c6ecdc9))
|
||||
* **ngcc:** support for new APF where `module` points to esm2015 output ([#36944](https://github.com/angular/angular/issues/36944)) ([c98a4d6](https://github.com/angular/angular/commit/c98a4d6))
|
||||
* **language-service:** [ivy] Parse Angular compiler options ([#36922](https://github.com/angular/angular/issues/36922)) ([dbd0f8e](https://github.com/angular/angular/commit/dbd0f8e))
|
||||
* remove TypeScript 3.6 and 3.7 support ([#36329](https://github.com/angular/angular/issues/36329)) ([fbd281c](https://github.com/angular/angular/commit/fbd281c))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **compiler-cli:** perform template type-checking incrementally ([#36211](https://github.com/angular/angular/issues/36211)) ([ecffc35](https://github.com/angular/angular/commit/ecffc35))
|
||||
* **compiler-cli:** split Ivy template type-checking into multiple files ([#36211](https://github.com/angular/angular/issues/36211)) ([b861e9c](https://github.com/angular/angular/commit/b861e9c))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* TypeScript versions 3.6 and 3.7 are no longer supported, please update to TypeScript 3.8
|
||||
|
||||
* **forms:** Number inputs no longer listen to the `change` event.
|
||||
|
||||
Tests which trigger `change` events need to be updated to trigger `input` events instead.
|
||||
|
||||
The `change` event was in place to support IE9, as we found that `input` events were not fired with backspace or cut actions. If you need to maintain IE9 support, you will need to add a change event listener to number inputs and call the `onChange` method of `NumberValueAccessor` manually.
|
||||
|
||||
Lastly, old versions of WebDriver would synthetically trigger the `change` event on `WebElement.clear` and `WebElement.sendKeys`. If you are using an old version of WebDriver, you may need to update tests to ensure `input` events are triggered. For example, you could use `element.sendKeys(Keys.chord(Keys.CONTROL, "a"), Keys.BACK_SPACE);` in place of `element.clear()`.
|
||||
* **forms:** The `minLength` and `maxLength` validators now verify that the form control's value has a
|
||||
numeric `length` property, and only validate for length if that's the case.
|
||||
|
||||
Previously, falsey values without the length property (such as `0` or
|
||||
`false` values) were triggering validation errors. If your code relies on
|
||||
the old behavior, you can include other validators such as [min][1] or
|
||||
[requiredTrue][2] to the list of validators for a particular field.
|
||||
|
||||
[1]: https://angular.io/api/forms/Validators#min
|
||||
[2]: https://angular.io/api/forms/Validators#requiredTrue
|
||||
* **bazel:** esm5 and fesm5 format is no longer distributed in
|
||||
Angular's npm packages e.g. @angular/core
|
||||
|
||||
If you are not using Angular CLI to build your application or library,
|
||||
and you need to be able to build es5 artifacts, then you will need to
|
||||
downlevel the distributed Angular code to es5 on your own.
|
||||
|
||||
Angular CLI will automatically downlevel the code to es5 if differential
|
||||
loading is enabled in the Angular project, so no action is required from
|
||||
Angular CLI users.
|
||||
|
||||
|
||||
<a name="9.1.5"></a>
|
||||
## [9.1.5](https://github.com/angular/angular/compare/9.1.4...9.1.5) (2020-05-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler-cli:** `isCaseSensitive()` returns correct value ([#36968](https://github.com/angular/angular/issues/36968)) ([4becc1b](https://github.com/angular/angular/commit/4becc1b))
|
||||
* **compiler-cli:** ensure `getRootDirs()` handles case-sensitivity ([#36968](https://github.com/angular/angular/issues/36968)) ([5bddeea](https://github.com/angular/angular/commit/5bddeea))
|
||||
* **compiler-cli:** ensure `MockFileSystem` handles case-sensitivity ([#36968](https://github.com/angular/angular/issues/36968)) ([b6c042d](https://github.com/angular/angular/commit/b6c042d))
|
||||
* **compiler-cli:** ensure LogicalFileSystem handles case-sensitivity ([#36968](https://github.com/angular/angular/issues/36968)) ([65337fb](https://github.com/angular/angular/commit/65337fb))
|
||||
* **compiler-cli:** fix case-sensitivity issues in NgtscCompilerHost ([#36968](https://github.com/angular/angular/issues/36968)) ([4abd603](https://github.com/angular/angular/commit/4abd603))
|
||||
* **compiler-cli:** normalize mock Windows file paths correctly ([#36968](https://github.com/angular/angular/issues/36968)) ([654868f](https://github.com/angular/angular/commit/654868f))
|
||||
* **compiler-cli:** use CompilerHost to ensure canonical file paths ([#36968](https://github.com/angular/angular/issues/36968)) ([7e9d5f5](https://github.com/angular/angular/commit/7e9d5f5))
|
||||
* **core:** handle pluralize functions that expect a number ([#36901](https://github.com/angular/angular/issues/36901)) ([e5317d5](https://github.com/angular/angular/commit/e5317d5)), closes [#36888](https://github.com/angular/angular/issues/36888)
|
||||
* **core:** properly get root nodes from embedded views with <ng-content> ([#36051](https://github.com/angular/angular/issues/36051)) ([a576852](https://github.com/angular/angular/commit/a576852)), closes [#35967](https://github.com/angular/angular/issues/35967)
|
||||
* **core:** Refresh transplanted views at insertion point only ([#35968](https://github.com/angular/angular/issues/35968)) ([c8c2272](https://github.com/angular/angular/commit/c8c2272)), closes [#35400](https://github.com/angular/angular/issues/35400) [#21324](https://github.com/angular/angular/issues/21324)
|
||||
* **localize:** ensure `getLocation()` works ([#36920](https://github.com/angular/angular/issues/36920)) ([701016d](https://github.com/angular/angular/commit/701016d))
|
||||
* **ngcc:** do not run in parallel mode if there are less than 3 CPU cores ([#36626](https://github.com/angular/angular/issues/36626)) ([3800455](https://github.com/angular/angular/commit/3800455))
|
||||
* **ngcc:** give up re-spawing crashed worker process after 3 attempts ([#36626](https://github.com/angular/angular/issues/36626)) ([1863733](https://github.com/angular/angular/commit/1863733))
|
||||
* **ngcc:** handle `ENOMEM` errors in worker processes ([#36626](https://github.com/angular/angular/issues/36626)) ([901b980](https://github.com/angular/angular/commit/901b980))
|
||||
* **ngcc:** support ModuleWithProviders functions that delegate ([#36948](https://github.com/angular/angular/issues/36948)) ([9d13ee0](https://github.com/angular/angular/commit/9d13ee0)), closes [#36892](https://github.com/angular/angular/issues/36892)
|
||||
* **ngcc:** support recovering when a worker process crashes ([#36626](https://github.com/angular/angular/issues/36626)) ([f30307a](https://github.com/angular/angular/commit/f30307a)), closes [#36278](https://github.com/angular/angular/issues/36278)
|
||||
* **ngcc:** partially support TS 3.9 wrapped ES2015 classes ([#36884](https://github.com/angular/angular/issues/36884)) ([ebb4733](https://github.com/angular/angular/commit/ebb4733))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **ngcc:** only compute basePaths in TargetedEntryPointFinder when needed ([#36881](https://github.com/angular/angular/issues/36881)) ([5ea51b2](https://github.com/angular/angular/commit/5ea51b2)), closes [#36874](https://github.com/angular/angular/issues/36874)
|
||||
* **ngcc:** speed up the `getBasePaths()` computation ([#36881](https://github.com/angular/angular/issues/36881)) ([b6d0e21](https://github.com/angular/angular/commit/b6d0e21))
|
||||
|
||||
|
||||
|
||||
<a name="10.0.0-next.5"></a>
|
||||
# [10.0.0-next.5](https://github.com/angular/angular/compare/10.0.0-next.4...10.0.0-next.5) (2020-05-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** log error instead of warning for unknown properties and elements ([#36399](https://github.com/angular/angular/issues/36399)) ([9d9d46f](https://github.com/angular/angular/commit/9d9d46f)), closes [#35699](https://github.com/angular/angular/issues/35699)
|
||||
* **core:** Refresh transplanted views at insertion point only ([#35968](https://github.com/angular/angular/issues/35968)) ([1786586](https://github.com/angular/angular/commit/1786586)), closes [#35400](https://github.com/angular/angular/issues/35400) [#21324](https://github.com/angular/angular/issues/21324)
|
||||
* **ngcc:** do not run in parallel mode if there are less than 3 CPU cores ([#36626](https://github.com/angular/angular/issues/36626)) ([4c63241](https://github.com/angular/angular/commit/4c63241))
|
||||
* **ngcc:** give up re-spawning crashed worker process after 3 attempts ([#36626](https://github.com/angular/angular/issues/36626)) ([793cb32](https://github.com/angular/angular/commit/793cb32))
|
||||
* **ngcc:** handle `ENOMEM` errors in worker processes ([#36626](https://github.com/angular/angular/issues/36626)) ([4779c4b](https://github.com/angular/angular/commit/4779c4b))
|
||||
* **ngcc:** provide a unique exit code for timeouts ([#36838](https://github.com/angular/angular/issues/36838)) ([d805526](https://github.com/angular/angular/commit/d805526))
|
||||
* **ngcc:** support recovering when a worker process crashes ([#36626](https://github.com/angular/angular/issues/36626)) ([966598c](https://github.com/angular/angular/commit/966598c)), closes [#36278](https://github.com/angular/angular/issues/36278)
|
||||
* **ngcc:** support TS 3.9 wrapped ES2015 classes ([#36884](https://github.com/angular/angular/issues/36884)) ([db4c59d](https://github.com/angular/angular/commit/db4c59d))
|
||||
* **router:** cancel navigation when at least one resolver completes with no "next" emission ([#24621](https://github.com/angular/angular/issues/24621)) ([d9c4840](https://github.com/angular/angular/commit/d9c4840)), closes [#24195](https://github.com/angular/angular/issues/24195)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **common:** remove WrappedValue from AsyncPipe ([#36633](https://github.com/angular/angular/issues/36633)) ([49be32c](https://github.com/angular/angular/commit/49be32c)), closes [#29927](https://github.com/angular/angular/issues/29927)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **localize:** support merging multiple translation files ([#36792](https://github.com/angular/angular/issues/36792)) ([72f534f](https://github.com/angular/angular/commit/72f534f))
|
||||
* **ngcc:** allow async locking timeouts to be configured ([#36838](https://github.com/angular/angular/issues/36838)) ([38f805c](https://github.com/angular/angular/commit/38f805c))
|
||||
* **ngcc:** support marking an in-progress task as unprocessed ([#36626](https://github.com/angular/angular/issues/36626)) ([4665c35](https://github.com/angular/angular/commit/4665c35))
|
||||
* **ngcc:** support reverting a file written by `FileWriter` ([#36626](https://github.com/angular/angular/issues/36626)) ([772ccf0](https://github.com/angular/angular/commit/772ccf0))
|
||||
* **service-worker:** include `CacheQueryOptions` options in ngsw-config ([#34663](https://github.com/angular/angular/issues/34663)) ([dc9f4b9](https://github.com/angular/angular/commit/dc9f4b9)), closes [#28443](https://github.com/angular/angular/issues/28443)
|
||||
* **service-worker:** use `ignoreVary: true` when retrieving responses from cache ([#34663](https://github.com/angular/angular/issues/34663)) ([ee35e22](https://github.com/angular/angular/commit/ee35e22)), closes [#36638](https://github.com/angular/angular/issues/36638)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **ngcc:** only compute basePaths in TargetedEntryPointFinder when needed ([#36881](https://github.com/angular/angular/issues/36881)) ([ec6b9cc](https://github.com/angular/angular/commit/ec6b9cc)), closes [#36874](https://github.com/angular/angular/issues/36874)
|
||||
* **ngcc:** speed up the `getBasePaths()` computation ([#36881](https://github.com/angular/angular/issues/36881)) ([e037840](https://github.com/angular/angular/commit/e037840))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **core:** Warnings about unknown elements are now logged as errors. This won't break your app, but it may trip up tools that expect nothing to be logged via `console.error`.
|
||||
* **router:** Any resolver which return EMPTY will cancel navigation.
|
||||
If you want to allow the navigation to continue, you will need to update the resolvers to emit
|
||||
some value, (i.e. defaultIfEmpty(...), of(...), etc).
|
||||
* **service-worker:** Previously, [Vary](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary)
|
||||
headers would be taken into account when retrieving resources from the
|
||||
cache, completely preventing the retrieval of cached assets (due to
|
||||
ServiceWorker implementation details) and leading to unpredictable
|
||||
behavior due to inconsistent/buggy implementations in different
|
||||
browsers.
|
||||
|
||||
Now, `Vary` headers are ignored when retrieving resources from the
|
||||
ServiceWorker caches, which can result in resources being retrieved even
|
||||
when their headers are different. If your application needs to
|
||||
differentiate its responses based on request headers, please make sure
|
||||
the Angular ServiceWorker is [configured](https://angular.io/guide/service-worker-config)
|
||||
to avoid caching the affected resources.
|
||||
* **common:** This change could result in ExpressionChangedAfterItHasBeenChecked errors that
|
||||
were not detected before. The error could previously have gone undetected
|
||||
because two WrappedValues are considered "equal" in all cases for the purposes
|
||||
of the check, even if their respective unwrapped values are not.
|
||||
|
||||
Additionally, `[val]=(observable | async).someProperty` will no longer
|
||||
trigger change detection if the value of `someProperty` is identical to
|
||||
the value in the previous emit. If you need to force change detection,
|
||||
either update the binding to use an object whose reference changes or
|
||||
subscribe to the observable and call markForCheck as needed.
|
||||
|
||||
|
||||
|
||||
<a name="10.0.0-next.4"></a>
|
||||
# [10.0.0-next.4](https://github.com/angular/angular/compare/10.0.0-next.3...10.0.0-next.4) (2020-04-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** normalize line endings in ICU expansions ([#36741](https://github.com/angular/angular/issues/36741)) ([70dd27f](https://github.com/angular/angular/commit/70dd27f)), closes [#36725](https://github.com/angular/angular/issues/36725)
|
||||
* **core:** attempt to recover from user errors during creation ([#36381](https://github.com/angular/angular/issues/36381)) ([3d82aa7](https://github.com/angular/angular/commit/3d82aa7)), closes [#31221](https://github.com/angular/angular/issues/31221)
|
||||
* **core:** handle synthetic props in Directive host bindings correctly ([#35568](https://github.com/angular/angular/issues/35568)) ([f27deea](https://github.com/angular/angular/commit/f27deea)), closes [#35501](https://github.com/angular/angular/issues/35501)
|
||||
* **language-service:** disable update the `[@angular](https://github.com/angular)/core` module ([#36783](https://github.com/angular/angular/issues/36783)) ([dd049ca](https://github.com/angular/angular/commit/dd049ca))
|
||||
* **localize:** include legacy ids when describing messages ([#36761](https://github.com/angular/angular/issues/36761)) ([47f9867](https://github.com/angular/angular/commit/47f9867))
|
||||
* **ngcc:** recognize enum declarations emitted in JavaScript ([#36550](https://github.com/angular/angular/issues/36550)) ([89c5890](https://github.com/angular/angular/commit/89c5890)), closes [#35584](https://github.com/angular/angular/issues/35584)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **router:** allow CanLoad guard to return UrlTree ([#36610](https://github.com/angular/angular/issues/36610)) ([00e6cb1](https://github.com/angular/angular/commit/00e6cb1)), closes [#26521](https://github.com/angular/angular/issues/26521) [#28306](https://github.com/angular/angular/issues/28306)
|
||||
|
||||
|
||||
|
||||
<a name="9.1.4"></a>
|
||||
## [9.1.4](https://github.com/angular/angular/compare/9.1.3...9.1.4) (2020-04-29)
|
||||
|
||||
@ -377,42 +12,6 @@ subscribe to the observable and call markForCheck as needed.
|
||||
|
||||
|
||||
|
||||
<a name="10.0.0-next.3"></a>
|
||||
# [10.0.0-next.3](https://github.com/angular/angular/compare/10.0.0-next.2...10.0.0-next.3) (2020-04-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **common:** format day-periods that cross midnight ([#36611](https://github.com/angular/angular/issues/36611)) ([c6e5fc4](https://github.com/angular/angular/commit/c6e5fc4)), closes [#36566](https://github.com/angular/angular/issues/36566)
|
||||
* **compiler:** avoid generating i18n attributes in plain form ([#36422](https://github.com/angular/angular/issues/36422)) ([88b0985](https://github.com/angular/angular/commit/88b0985))
|
||||
* **core:** do not use unbound attributes as inputs to structural directives ([#36441](https://github.com/angular/angular/issues/36441)) ([acf6075](https://github.com/angular/angular/commit/acf6075))
|
||||
* **core:** handle empty translations correctly ([#36499](https://github.com/angular/angular/issues/36499)) ([b1f1d3f](https://github.com/angular/angular/commit/b1f1d3f)), closes [#36476](https://github.com/angular/angular/issues/36476)
|
||||
* **core:** missing-injectable migration should not migrate `@NgModule` classes ([#36369](https://github.com/angular/angular/issues/36369)) ([28995db](https://github.com/angular/angular/commit/28995db)), closes [#35700](https://github.com/angular/angular/issues/35700)
|
||||
* **core:** pipes injecting viewProviders when used on a component host node ([#36512](https://github.com/angular/angular/issues/36512)) ([81d23b3](https://github.com/angular/angular/commit/81d23b3)), closes [#36146](https://github.com/angular/angular/issues/36146)
|
||||
* **core:** prevent unknown property check for AOT-compiled components ([#36072](https://github.com/angular/angular/issues/36072)) ([4a9f0be](https://github.com/angular/angular/commit/4a9f0be)), closes [#35945](https://github.com/angular/angular/issues/35945)
|
||||
* **core:** properly identify modules affected by overrides in TestBed ([#36649](https://github.com/angular/angular/issues/36649)) ([942b986](https://github.com/angular/angular/commit/942b986)), closes [#36619](https://github.com/angular/angular/issues/36619)
|
||||
* **language-service:** properly evaluate types in comparable expressions ([#36529](https://github.com/angular/angular/issues/36529)) ([8be0972](https://github.com/angular/angular/commit/8be0972))
|
||||
* **ngcc:** display unlocker process output in sync mode ([#36637](https://github.com/angular/angular/issues/36637)) ([cabf997](https://github.com/angular/angular/commit/cabf997)), closes [/github.com/nodejs/node/issues/3596#issuecomment-250890218](https://github.com//github.com/nodejs/node/issues/3596/issues/issuecomment-250890218)
|
||||
* **ngcc:** do not use cached file-system ([#36687](https://github.com/angular/angular/issues/36687)) ([0c2ed4c](https://github.com/angular/angular/commit/0c2ed4c)), closes [/github.com/angular/angular-cli/issues/16860#issuecomment-614694269](https://github.com//github.com/angular/angular-cli/issues/16860/issues/issuecomment-614694269)
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **common:** format day-periods that cross midnight
|
||||
|
||||
When formatting a time with the `b` or `B` format codes, the rendered
|
||||
string was not correctly handling day periods that spanned midnight.
|
||||
Instead the logic was falling back to the default case of `AM`.
|
||||
|
||||
Now the logic has been updated so that it matches times that are within
|
||||
a day period that spans midnight, so it will now render the correct
|
||||
output, such as `at night` in the case of English.
|
||||
|
||||
Applications that are using either `formatDate()` or `DatePipe` and any
|
||||
of the `b` or `B` format codes will be affected by this change.
|
||||
|
||||
|
||||
|
||||
<a name="9.1.3"></a>
|
||||
## [9.1.3](https://github.com/angular/angular/compare/9.1.2...9.1.3) (2020-04-22)
|
||||
|
||||
@ -432,32 +31,6 @@ subscribe to the observable and call markForCheck as needed.
|
||||
|
||||
|
||||
|
||||
<a name="10.0.0-next.2"></a>
|
||||
# [10.0.0-next.2](https://github.com/angular/angular/compare/10.0.0-next.1...10.0.0-next.2) (2020-04-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **common:** `locales/global/*.js` are not ES5 compliant ([#36342](https://github.com/angular/angular/issues/36342)) ([078b0be](https://github.com/angular/angular/commit/078b0be)), closes [angular/angular-cli#16394](https://github.com/angular/angular-cli/issues/16394)
|
||||
* **compiler:** handle type references to namespaced symbols correctly ([#36106](https://github.com/angular/angular/issues/36106)) ([4aa4e6f](https://github.com/angular/angular/commit/4aa4e6f)), closes [#36006](https://github.com/angular/angular/issues/36006)
|
||||
* **core:** undecorated-classes-with-decorated-fields migration should avoid error if base class has no value declaration ([#36543](https://github.com/angular/angular/issues/36543)) ([ca67748](https://github.com/angular/angular/commit/ca67748)), closes [#36522](https://github.com/angular/angular/issues/36522)
|
||||
* **ngcc:** correctly detect external files from nested `node_modules/` ([#36559](https://github.com/angular/angular/issues/36559)) ([6ab43d7](https://github.com/angular/angular/commit/6ab43d7)), closes [#36526](https://github.com/angular/angular/issues/36526)
|
||||
* **ngcc:** display output from the unlocker process on Windows ([#36569](https://github.com/angular/angular/issues/36569)) ([e041ac6](https://github.com/angular/angular/commit/e041ac6))
|
||||
* **ngcc:** do not spawn unlocker processes on cluster workers ([#36569](https://github.com/angular/angular/issues/36569)) ([66effde](https://github.com/angular/angular/commit/66effde)), closes [#35861](https://github.com/angular/angular/issues/35861)
|
||||
* **ngcc:** do not warn if `paths` mapping does not exist ([#36525](https://github.com/angular/angular/issues/36525)) ([717df13](https://github.com/angular/angular/commit/717df13)), closes [#36518](https://github.com/angular/angular/issues/36518)
|
||||
* **ngcc:** force ngcc to exit on error ([#36622](https://github.com/angular/angular/issues/36622)) ([663b768](https://github.com/angular/angular/commit/663b768)), closes [#36616](https://github.com/angular/angular/issues/36616)
|
||||
* **router:** pass correct component to canDeactivate checks when using two or more sibling router-outlets ([#36302](https://github.com/angular/angular/issues/36302)) ([80e6c07](https://github.com/angular/angular/commit/80e6c07)), closes [#34614](https://github.com/angular/angular/issues/34614)
|
||||
* **upgrade:** update $locationShim to handle Location changes before initialization ([#36498](https://github.com/angular/angular/issues/36498)) ([0cc53fb](https://github.com/angular/angular/commit/0cc53fb)), closes [#36492](https://github.com/angular/angular/issues/36492)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **ngcc:** only load if it is needed ([#36486](https://github.com/angular/angular/issues/36486)) ([3bedfda](https://github.com/angular/angular/commit/3bedfda))
|
||||
* **ngcc:** read dependencies from entry-point manifest ([#36486](https://github.com/angular/angular/issues/36486)) ([a185efb](https://github.com/angular/angular/commit/a185efb)), closes [#issuecomment-608401834](https://github.com/angular/angular/issues/issuecomment-608401834)
|
||||
* **ngcc:** reduce the size of the entry-point manifest file ([#36486](https://github.com/angular/angular/issues/36486)) ([ec0ce60](https://github.com/angular/angular/commit/ec0ce60))
|
||||
|
||||
|
||||
|
||||
<a name="9.1.2"></a>
|
||||
## [9.1.2](https://github.com/angular/angular/compare/9.1.1...9.1.2) (2020-04-15)
|
||||
|
||||
@ -474,77 +47,14 @@ subscribe to the observable and call markForCheck as needed.
|
||||
* **router:** pass correct component to canDeactivate checks when using two or more sibling router-outlets ([#36302](https://github.com/angular/angular/issues/36302)) ([8e7f903](https://github.com/angular/angular/commit/8e7f903)), closes [#34614](https://github.com/angular/angular/issues/34614)
|
||||
* **upgrade:** update $locationShim to handle Location changes before initialization ([#36498](https://github.com/angular/angular/issues/36498)) ([a67afcc](https://github.com/angular/angular/commit/a67afcc)), closes [#36492](https://github.com/angular/angular/issues/36492)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
* **ngcc:** only load if it is needed ([#36486](https://github.com/angular/angular/issues/36486)) ([e06512b](https://github.com/angular/angular/commit/e06512b)) * **ngcc:** read dependencies from entry-point manifest ([#36486](https://github.com/angular/angular/issues/36486)) ([918e628](https://github.com/angular/angular/commit/918e628)), closes [#issuecomment-608401834](https://github.com/angular/angular/issues/issuecomment-608401834)
|
||||
|
||||
* **ngcc:** only load if it is needed ([#36486](https://github.com/angular/angular/issues/36486)) ([e06512b](https://github.com/angular/angular/commit/e06512b))
|
||||
* **ngcc:** read dependencies from entry-point manifest ([#36486](https://github.com/angular/angular/issues/36486)) ([918e628](https://github.com/angular/angular/commit/918e628)), closes [#issuecomment-608401834](https://github.com/angular/angular/issues/issuecomment-608401834)
|
||||
* **ngcc:** reduce the size of the entry-point manifest file ([#36486](https://github.com/angular/angular/issues/36486)) ([603b094](https://github.com/angular/angular/commit/603b094))
|
||||
|
||||
|
||||
<a name="10.0.0-next.1"></a>
|
||||
# [10.0.0-next.1](https://github.com/angular/angular/compare/10.0.0-next.0...10.0.0-next.1) (2020-04-08)
|
||||
|
||||
This release contains various API docs improvements.
|
||||
|
||||
<a name="10.0.0-next.0"></a>
|
||||
# [10.0.0-next.0](https://github.com/angular/angular/compare/9.1.0-rc.0...10.0.0-next.0) (2020-04-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **common:** let `KeyValuePipe` accept type unions with `null` ([#36093](https://github.com/angular/angular/issues/36093)) ([d783519](https://github.com/angular/angular/commit/d783519)), closes [#35743](https://github.com/angular/angular/issues/35743)
|
||||
* **compiler:** avoid undefined expressions in holey array ([#36343](https://github.com/angular/angular/issues/36343)) ([5516802](https://github.com/angular/angular/commit/5516802))
|
||||
* **compiler:** record correct end of expression ([#34690](https://github.com/angular/angular/issues/34690)) ([df890d7](https://github.com/angular/angular/commit/df890d7)), closes [#33477](https://github.com/angular/angular/issues/33477)
|
||||
* **compiler:** resolve enum values in binary operations ([#36461](https://github.com/angular/angular/issues/36461)) ([64022f5](https://github.com/angular/angular/commit/64022f5)), closes [#35584](https://github.com/angular/angular/issues/35584)
|
||||
* **compiler-cli:** pass real source spans where they are empty ([#31805](https://github.com/angular/angular/issues/31805)) ([e893c5a](https://github.com/angular/angular/commit/e893c5a))
|
||||
* **core:** avoid migration error when non-existent symbol is imported ([#36367](https://github.com/angular/angular/issues/36367)) ([d43c306](https://github.com/angular/angular/commit/d43c306)), closes [#36346](https://github.com/angular/angular/issues/36346)
|
||||
* **core:** ngOnDestroy on multi providers called with incorrect context ([#35840](https://github.com/angular/angular/issues/35840)) ([95fc3d4](https://github.com/angular/angular/commit/95fc3d4)), closes [#35231](https://github.com/angular/angular/issues/35231)
|
||||
* **core:** run `APP_INITIALIZER`s before accessing `LOCALE_ID` token in Ivy TestBed ([#36237](https://github.com/angular/angular/issues/36237)) ([1649743](https://github.com/angular/angular/commit/1649743)), closes [#36230](https://github.com/angular/angular/issues/36230)
|
||||
* **core:** undecorated-classes-with-decorated-fields migration does not decorate derived classes ([#35339](https://github.com/angular/angular/issues/35339)) ([32eafef](https://github.com/angular/angular/commit/32eafef)), closes [#34376](https://github.com/angular/angular/issues/34376)
|
||||
* **core:** workaround Terser inlining bug ([#36200](https://github.com/angular/angular/issues/36200)) ([0ce8ad3](https://github.com/angular/angular/commit/0ce8ad3))
|
||||
* **elements:** correctly handle setting inputs to `undefined` ([#36140](https://github.com/angular/angular/issues/36140)) ([9ba46d9](https://github.com/angular/angular/commit/9ba46d9))
|
||||
* **elements:** correctly set `SimpleChange#firstChange` for pre-existing inputs ([#36140](https://github.com/angular/angular/issues/36140)) ([b14ac96](https://github.com/angular/angular/commit/b14ac96)), closes [#36130](https://github.com/angular/angular/issues/36130)
|
||||
* **language-service:** infer type of elements of array-like objects ([#36312](https://github.com/angular/angular/issues/36312)) ([fe2b692](https://github.com/angular/angular/commit/fe2b692)), closes [#36191](https://github.com/angular/angular/issues/36191)
|
||||
* **language-service:** use the `HtmlAst` to get the span of HTML tag ([#36371](https://github.com/angular/angular/issues/36371)) ([81195a2](https://github.com/angular/angular/commit/81195a2))
|
||||
* **localize:** allow ICU expansion case to start with any character except `}` ([#36123](https://github.com/angular/angular/issues/36123)) ([fced8ee](https://github.com/angular/angular/commit/fced8ee)), closes [#31586](https://github.com/angular/angular/issues/31586)
|
||||
* **ngcc:** add process title ([#36448](https://github.com/angular/angular/issues/36448)) ([76a8cd5](https://github.com/angular/angular/commit/76a8cd5)), closes [/github.com/angular/angular/issues/36414#issuecomment-609644282](https://github.com//github.com/angular/angular/issues/36414/issues/issuecomment-609644282)
|
||||
* **ngcc:** allow ngcc configuration to match pre-release versions of packages ([#36370](https://github.com/angular/angular/issues/36370)) ([326240e](https://github.com/angular/angular/commit/326240e))
|
||||
* **ngcc:** correctly detect imported TypeScript helpers ([#36284](https://github.com/angular/angular/issues/36284)) ([ca25c95](https://github.com/angular/angular/commit/ca25c95)), closes [#36089](https://github.com/angular/angular/issues/36089)
|
||||
* **ngcc:** correctly identify relative Windows-style import paths ([#36372](https://github.com/angular/angular/issues/36372)) ([aecf9de](https://github.com/angular/angular/commit/aecf9de))
|
||||
* **ngcc:** correctly identify the package path of secondary entry-points ([#36249](https://github.com/angular/angular/issues/36249)) ([995cd15](https://github.com/angular/angular/commit/995cd15)), closes [#35747](https://github.com/angular/angular/issues/35747)
|
||||
* **ngcc:** detect non-emitted, non-imported TypeScript helpers ([#36418](https://github.com/angular/angular/issues/36418)) ([5fa7b8b](https://github.com/angular/angular/commit/5fa7b8b))
|
||||
* **ngcc:** do not spawn more processes than intended in parallel mode ([#36280](https://github.com/angular/angular/issues/36280)) ([5cee709](https://github.com/angular/angular/commit/5cee709)), closes [#35719](https://github.com/angular/angular/issues/35719) [#36278](https://github.com/angular/angular/issues/36278) [/github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/main.ts#L429](https://github.com//github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/main.ts/issues/L429) [/github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts#L108](https://github.com//github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts/issues/L108) [/github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts#L110](https://github.com//github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts/issues/L110) [/github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts#L199](https://github.com//github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts/issues/L199)
|
||||
* **ngcc:** do not write entry-point manifest outside node_modules ([#36299](https://github.com/angular/angular/issues/36299)) ([c6dd900](https://github.com/angular/angular/commit/c6dd900)), closes [#36296](https://github.com/angular/angular/issues/36296)
|
||||
* **ngcc:** don't crash on cyclic source-map references ([#36452](https://github.com/angular/angular/issues/36452)) ([ee70a18](https://github.com/angular/angular/commit/ee70a18)), closes [#35727](https://github.com/angular/angular/issues/35727) [#35757](https://github.com/angular/angular/issues/35757)
|
||||
* **ngcc:** handle bad path mappings when finding entry-points ([#36331](https://github.com/angular/angular/issues/36331)) ([cc4b813](https://github.com/angular/angular/commit/cc4b813)), closes [#36313](https://github.com/angular/angular/issues/36313) [#36283](https://github.com/angular/angular/issues/36283)
|
||||
* **ngcc:** handle entry-points within container folders ([#36305](https://github.com/angular/angular/issues/36305)) ([38ad1d9](https://github.com/angular/angular/commit/38ad1d9)), closes [#35756](https://github.com/angular/angular/issues/35756) [#36216](https://github.com/angular/angular/issues/36216)
|
||||
* **ngcc:** sniff `main` property for ESM5 format ([#36396](https://github.com/angular/angular/issues/36396)) ([2463548](https://github.com/angular/angular/commit/2463548)), closes [#35788](https://github.com/angular/angular/issues/35788)
|
||||
* **ngcc:** support ignoring deep-imports via package config ([#36423](https://github.com/angular/angular/issues/36423)) ([f9fb833](https://github.com/angular/angular/commit/f9fb833)), closes [#35750](https://github.com/angular/angular/issues/35750)
|
||||
* **ngcc:** support simple `browser` property in entry-points ([#36396](https://github.com/angular/angular/issues/36396)) ([6b3aa60](https://github.com/angular/angular/commit/6b3aa60)), closes [#36062](https://github.com/angular/angular/issues/36062)
|
||||
* **ngcc:** use path-mappings from tsconfig in dependency resolution ([#36180](https://github.com/angular/angular/issues/36180)) ([380de1e](https://github.com/angular/angular/commit/380de1e)), closes [#36119](https://github.com/angular/angular/issues/36119)
|
||||
* **ngcc:** use preserve whitespaces from tsconfig if provided ([#36189](https://github.com/angular/angular/issues/36189)) ([b8e9a30](https://github.com/angular/angular/commit/b8e9a30)), closes [#35871](https://github.com/angular/angular/issues/35871)
|
||||
* **platform-server:** update `xhr2` dependency ([#36366](https://github.com/angular/angular/issues/36366)) ([b59bc0e](https://github.com/angular/angular/commit/b59bc0e)), closes [#36358](https://github.com/angular/angular/issues/36358)
|
||||
* **router:** allow UrlMatcher to return null ([#36402](https://github.com/angular/angular/issues/36402)) ([568e9df](https://github.com/angular/angular/commit/568e9df)), closes [#29824](https://github.com/angular/angular/issues/29824)
|
||||
* **router:** state data missing in routerLink ([#36462](https://github.com/angular/angular/issues/36462)) ([e0415db](https://github.com/angular/angular/commit/e0415db)), closes [#33173](https://github.com/angular/angular/issues/33173)
|
||||
* **service-worker:** by default register the SW after 30s even the app never stabilizes ([#35870](https://github.com/angular/angular/issues/35870)) ([29e8a64](https://github.com/angular/angular/commit/29e8a64)), closes [#34464](https://github.com/angular/angular/issues/34464)
|
||||
* **service-worker:** prevent SW registration strategies from affecting app stabilization ([#35870](https://github.com/angular/angular/issues/35870)) ([2d7c95f](https://github.com/angular/angular/commit/2d7c95f))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler:** add dependency info and ng-content selectors to metadata ([#35695](https://github.com/angular/angular/issues/35695)) ([32ce8b1](https://github.com/angular/angular/commit/32ce8b1))
|
||||
* **compiler:** Propagate value span of ExpressionBinding to ParsedProperty ([#36133](https://github.com/angular/angular/issues/36133)) ([d714b95](https://github.com/angular/angular/commit/d714b95))
|
||||
* **core:** undecorated-classes migration should handle derived abstract classes ([#35339](https://github.com/angular/angular/issues/35339)) ([c24ad56](https://github.com/angular/angular/commit/c24ad56))
|
||||
* **service-worker:** support timeout in `registerWhenStable` SW registration strategy ([#35870](https://github.com/angular/angular/issues/35870)) ([00efacf](https://github.com/angular/angular/commit/00efacf)), closes [#34464](https://github.com/angular/angular/issues/34464)
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **router:** UrlMatcher's type now reflects that it could always return
|
||||
null.
|
||||
|
||||
If you implemented your own Router or Recognizer class, please update it to
|
||||
handle matcher returning null.
|
||||
|
||||
|
||||
|
||||
<a name="9.1.1"></a>
|
||||
## [9.1.1](https://github.com/angular/angular/compare/9.1.0...9.1.1) (2020-04-07)
|
||||
@ -580,6 +90,7 @@ This release contains various API docs improvements.
|
||||
* **router:** state data missing in routerLink ([#36462](https://github.com/angular/angular/issues/36462)) ([0e7a89a](https://github.com/angular/angular/commit/0e7a89a)), closes [#33173](https://github.com/angular/angular/issues/33173)
|
||||
|
||||
|
||||
|
||||
<a name="9.1.0"></a>
|
||||
# [9.1.0](https://github.com/angular/angular/compare/9.0.0...9.1.0) (2020-03-25)
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Image metadata and config
|
||||
FROM debian:buster
|
||||
FROM debian:stretch
|
||||
|
||||
LABEL name="angular.io PR preview" \
|
||||
description="This image implements the PR preview functionality for angular.io." \
|
||||
@ -37,9 +37,9 @@ ARG TEST_AIO_NGINX_PORT_HTTPS=4433
|
||||
ARG AIO_SIGNIFICANT_FILES_PATTERN='^(?:aio|packages)/(?!.*[._]spec\\.[jt]s$)'
|
||||
ARG TEST_AIO_SIGNIFICANT_FILES_PATTERN=$AIO_SIGNIFICANT_FILES_PATTERN
|
||||
ARG AIO_TRUSTED_PR_LABEL="aio: preview"
|
||||
ARG TEST_AIO_TRUSTED_PR_LABEL=$AIO_TRUSTED_PR_LABEL
|
||||
ARG TEST_AIO_TRUSTED_PR_LABEL="aio: preview"
|
||||
ARG AIO_PREVIEW_SERVER_HOSTNAME=preview.localhost
|
||||
ARG TEST_AIO_PREVIEW_SERVER_HOSTNAME=$AIO_PREVIEW_SERVER_HOSTNAME
|
||||
ARG TEST_AIO_PREVIEW_SERVER_HOSTNAME=preview.localhost
|
||||
ARG AIO_ARTIFACT_MAX_SIZE=26214400
|
||||
ARG TEST_AIO_ARTIFACT_MAX_SIZE=200
|
||||
ARG AIO_PREVIEW_SERVER_PORT=3000
|
||||
@ -72,29 +72,24 @@ RUN mkdir /var/log/aio
|
||||
|
||||
|
||||
# Add extra package sources
|
||||
RUN apt-get update -y && apt-get install -y curl=7.64.0-4+deb10u1
|
||||
RUN curl --silent --show-error --location https://deb.nodesource.com/setup_12.x | bash -
|
||||
RUN apt-get update -y && apt-get install -y curl
|
||||
RUN curl --silent --show-error --location https://deb.nodesource.com/setup_10.x | bash -
|
||||
RUN curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
RUN echo "deb http://ftp.debian.org/debian stretch-backports main" | tee /etc/apt/sources.list.d/backports.list
|
||||
|
||||
|
||||
# Install packages
|
||||
# NOTE: Some packages (such as `nginx`, `nodejs`, `openssl`) make older versions unavailable on the
|
||||
# repositories, so we cannot pin to specific versions for these packages :(
|
||||
# See for example:
|
||||
# - https://github.com/nodesource/distributions/issues/33
|
||||
# - https://askubuntu.com/questions/715104/how-can-i-downgrade-openssl-via-apt-get
|
||||
RUN apt-get update -y && apt-get install -y \
|
||||
cron=3.0pl1-134+deb10u1 \
|
||||
dnsmasq=2.80-1 \
|
||||
nano=3.2-3 \
|
||||
nginx \
|
||||
nodejs \
|
||||
openssl \
|
||||
rsyslog=8.1901.0-1 \
|
||||
vim=2:8.1.0875-5 \
|
||||
yarn=1.22.4-1
|
||||
RUN yarn global add pm2@4.4.0
|
||||
cron=3.0pl1-128+deb9u1 \
|
||||
dnsmasq=2.76-5+deb9u2 \
|
||||
nano=2.7.4-1 \
|
||||
nginx=1.10.3-1+deb9u2 \
|
||||
nodejs=10.15.3-1nodesource1 \
|
||||
openssl=1.1.0j-1~deb9u1 \
|
||||
rsyslog=8.24.0-1 \
|
||||
yarn=1.15.2-1
|
||||
RUN yarn global add pm2@3.5.0
|
||||
|
||||
|
||||
# Set up log rotation
|
||||
@ -167,7 +162,8 @@ RUN find $AIO_SCRIPTS_SH_DIR -maxdepth 1 -type f -printf "%P\n" \
|
||||
|
||||
# Set up the Node.js scripts
|
||||
COPY scripts-js/ $AIO_SCRIPTS_JS_DIR/
|
||||
RUN yarn --cwd="$AIO_SCRIPTS_JS_DIR/" install --production --frozen-lockfile
|
||||
WORKDIR $AIO_SCRIPTS_JS_DIR/
|
||||
RUN yarn install --production --frozen-lockfile
|
||||
|
||||
|
||||
# Set up health check
|
||||
|
@ -35,7 +35,6 @@ export class BuildCleaner {
|
||||
]);
|
||||
} catch (error) {
|
||||
this.logger.error('ERROR:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import * as express from 'express';
|
||||
import {promisify} from 'util';
|
||||
import {PreviewServerError} from './preview-error';
|
||||
|
||||
/**
|
||||
@ -12,7 +13,7 @@ export async function respondWithError(res: express.Response, err: any): Promise
|
||||
}
|
||||
|
||||
res.status(err.status);
|
||||
return new Promise(resolve => res.end(err.message, resolve));
|
||||
await promisify(res.end.bind(res))(err.message);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -93,7 +93,7 @@ class Helper {
|
||||
return fs.readFileSync(absFilePath, 'utf8');
|
||||
}
|
||||
|
||||
public runCmd(cmd: string, opts: cp.ExecOptions = {}): Promise<CmdResult> {
|
||||
public runCmd(cmd: string, opts: cp.ExecFileOptions = {}): Promise<CmdResult> {
|
||||
return new Promise(resolve => {
|
||||
const proc = cp.exec(cmd, opts, (err, stdout, stderr) => resolve({success: !err, err, stdout, stderr}));
|
||||
this.createCleanUpFn(() => proc.kill());
|
||||
@ -101,7 +101,7 @@ class Helper {
|
||||
}
|
||||
|
||||
public runForAllSupportedSchemes(suiteFactory: TestSuiteFactory): void {
|
||||
Object.entries(this.portPerScheme).forEach(([scheme, port]) => suiteFactory(scheme, port));
|
||||
Object.keys(this.portPerScheme).forEach(scheme => suiteFactory(scheme, this.portPerScheme[scheme]));
|
||||
}
|
||||
|
||||
public verifyResponse(status: number, regex: string | RegExp = /^/): VerifyCmdResultFn {
|
||||
|
@ -15,7 +15,7 @@ describe(`nginx`, () => {
|
||||
afterEach(() => h.cleanUp());
|
||||
|
||||
|
||||
it('should redirect HTTP to HTTPS', async () => {
|
||||
it('should redirect HTTP to HTTPS', done => {
|
||||
const httpHost = `${AIO_NGINX_HOSTNAME}:${AIO_NGINX_PORT_HTTP}`;
|
||||
const httpsHost = `${AIO_NGINX_HOSTNAME}:${AIO_NGINX_PORT_HTTPS}`;
|
||||
const urlMap = {
|
||||
@ -24,15 +24,16 @@ describe(`nginx`, () => {
|
||||
[`http://foo.${httpHost}/`]: `https://foo.${httpsHost}/`,
|
||||
};
|
||||
|
||||
const verifyRedirection = async (fromUrl: string, toUrl: string) => {
|
||||
const result = await h.runCmd(`curl -i ${fromUrl}`);
|
||||
const verifyRedirection = (httpUrl: string) => h.runCmd(`curl -i ${httpUrl}`).then(result => {
|
||||
h.verifyResponse(307)(result);
|
||||
|
||||
const headers = result.stdout.split(/(?:\r?\n){2,}/)[0];
|
||||
expect(headers).toContain(`Location: ${toUrl}`);
|
||||
};
|
||||
expect(headers).toContain(`Location: ${urlMap[httpUrl]}`);
|
||||
});
|
||||
|
||||
await Promise.all(Object.entries(urlMap).map(urls => verifyRedirection(...urls)));
|
||||
Promise.
|
||||
all(Object.keys(urlMap).map(verifyRedirection)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
@ -61,15 +62,15 @@ describe(`nginx`, () => {
|
||||
});
|
||||
|
||||
|
||||
it('should return /index.html', async () => {
|
||||
it('should return /index.html', done => {
|
||||
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
|
||||
await Promise.all([
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)),
|
||||
]);
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
@ -89,11 +90,12 @@ describe(`nginx`, () => {
|
||||
});
|
||||
|
||||
|
||||
it('should return /foo/bar.js', async () => {
|
||||
it('should return /foo/bar.js', done => {
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
|
||||
|
||||
await h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo/bar.js`).
|
||||
then(h.verifyResponse(200, bodyRegex));
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo/bar.js`).
|
||||
then(h.verifyResponse(200, bodyRegex)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
@ -109,46 +111,47 @@ describe(`nginx`, () => {
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 403 for directories', async () => {
|
||||
await Promise.all([
|
||||
it('should respond with 403 for directories', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo/`).then(h.verifyResponse(403)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo`).then(h.verifyResponse(403)),
|
||||
]);
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths to files', async () => {
|
||||
await h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo/baz.css`).
|
||||
then(h.verifyResponse(404));
|
||||
it('should respond with 404 for unknown paths to files', done => {
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo/baz.css`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should rewrite to \'index.html\' for unknown paths that don\'t look like files', async () => {
|
||||
it('should rewrite to \'index.html\' for unknown paths that don\'t look like files', done => {
|
||||
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
|
||||
await Promise.all([
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${origin}/foo/baz`).then(h.verifyResponse(200, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${origin}/foo/baz/`).then(h.verifyResponse(200, bodyRegex)),
|
||||
]);
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown PRs/SHAs', async () => {
|
||||
it('should respond with 404 for unknown PRs/SHAs', done => {
|
||||
const otherPr = 54321;
|
||||
const otherShortSha = computeShortSha('8'.repeat(40));
|
||||
|
||||
await Promise.all([
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}9-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${otherPr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}9.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${otherShortSha}.${host}`).then(h.verifyResponse(404)),
|
||||
]);
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the subdomain format is wrong', async () => {
|
||||
await Promise.all([
|
||||
it('should respond with 404 if the subdomain format is wrong', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://xpr${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://prx${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://xx${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
@ -157,25 +160,26 @@ describe(`nginx`, () => {
|
||||
h.runCmd(`curl -iL ${scheme}://${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}_${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
]);
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', async () => {
|
||||
await h.runCmd(`curl -iL ${scheme}://pr0${pr}-${shortSha9}.${host}`).
|
||||
then(h.verifyResponse(404));
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
h.runCmd(`curl -iL ${scheme}://pr0${pr}-${shortSha9}.${host}`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not trim the zeros)', async () => {
|
||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||
const bodyRegex9 = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
const bodyRegex0 = new RegExp(`^PR: ${pr} | SHA: ${sha0} | File: /index\\.html$`);
|
||||
|
||||
await Promise.all([
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-0${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}`).then(h.verifyResponse(200, bodyRegex9)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha0}.${host}`).then(h.verifyResponse(200, bodyRegex0)),
|
||||
]);
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
@ -227,23 +231,23 @@ describe(`nginx`, () => {
|
||||
|
||||
describe(`${host}/health-check`, () => {
|
||||
|
||||
it('should respond with 200', async () => {
|
||||
await Promise.all([
|
||||
it('should respond with 200', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check`).then(h.verifyResponse(200)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check/`).then(h.verifyResponse(200)),
|
||||
]);
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the path does not match exactly', async () => {
|
||||
await Promise.all([
|
||||
it('should respond with 404 if the path does not match exactly', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check/foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check-foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-checknfoo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo/health-check`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo-health-check`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foonhealth-check`).then(h.verifyResponse(404)),
|
||||
]);
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
@ -287,28 +291,29 @@ describe(`nginx`, () => {
|
||||
|
||||
describe(`${host}/circle-build`, () => {
|
||||
|
||||
it('should disallow non-POST requests', async () => {
|
||||
it('should disallow non-POST requests', done => {
|
||||
const url = `${scheme}://${host}/circle-build`;
|
||||
|
||||
await Promise.all([
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse(405)),
|
||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(405)),
|
||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(405)),
|
||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(405)),
|
||||
]);
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should pass requests through to the preview server', async () => {
|
||||
await h.runCmd(`curl -iLX POST ${scheme}://${host}/circle-build`).
|
||||
then(h.verifyResponse(400, /Incorrect body content. Expected JSON/));
|
||||
it('should pass requests through to the preview server', done => {
|
||||
h.runCmd(`curl -iLX POST ${scheme}://${host}/circle-build`).
|
||||
then(h.verifyResponse(400, /Incorrect body content. Expected JSON/)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', async () => {
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
const cmdPrefix = `curl -iLX POST ${scheme}://${host}`;
|
||||
|
||||
await Promise.all([
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/foo/circle-build/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foo-circle-build/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/fooncircle-build/`).then(h.verifyResponse(404)),
|
||||
@ -317,7 +322,7 @@ describe(`nginx`, () => {
|
||||
h.runCmd(`${cmdPrefix}/circle-buildnfoo/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/circle-build/pr`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/circle-build/42`).then(h.verifyResponse(404)),
|
||||
]);
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
@ -327,33 +332,38 @@ describe(`nginx`, () => {
|
||||
const url = `${scheme}://${host}/pr-updated`;
|
||||
|
||||
|
||||
it('should disallow non-POST requests', async () => {
|
||||
await Promise.all([
|
||||
it('should disallow non-POST requests', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse(405)),
|
||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(405)),
|
||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(405)),
|
||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(405)),
|
||||
]);
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should pass requests through to the preview server', async () => {
|
||||
await h.runCmd(`curl -iLX POST --header "Content-Type: application/json" ${url}`).
|
||||
then(h.verifyResponse(400, /Missing or empty 'number' field/));
|
||||
it('should pass requests through to the preview server', done => {
|
||||
const cmdPrefix = `curl -iLX POST --header "Content-Type: application/json"`;
|
||||
|
||||
const cmd1 = `${cmdPrefix} ${url}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(cmd1).then(h.verifyResponse(400, /Missing or empty 'number' field/)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', async () => {
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
const cmdPrefix = `curl -iLX POST ${scheme}://${host}`;
|
||||
|
||||
await Promise.all([
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/foo/pr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foo-pr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foonpr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updated/foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updated-foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updatednfoo`).then(h.verifyResponse(404)),
|
||||
]);
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
@ -364,7 +374,7 @@ describe(`nginx`, () => {
|
||||
beforeEach(() => {
|
||||
['index.html', 'foo.js', 'foo/index.html'].forEach(relFilePath => {
|
||||
const absFilePath = path.join(AIO_BUILDS_DIR, relFilePath);
|
||||
h.writeFile(absFilePath, {content: `File: /${relFilePath}`});
|
||||
return h.writeFile(absFilePath, {content: `File: /${relFilePath}`});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -105,8 +105,8 @@ describe('preview-server', () => {
|
||||
|
||||
|
||||
describe(`${host}/circle-build`, () => {
|
||||
const curl = makeCurl(`${host}/circle-build`);
|
||||
|
||||
const curl = makeCurl(`${host}/circle-build`);
|
||||
|
||||
it('should disallow non-POST requests', async () => {
|
||||
const bodyRegex = /^Unknown resource/;
|
||||
@ -189,7 +189,8 @@ describe('preview-server', () => {
|
||||
});
|
||||
|
||||
it('should respond with 201 if a new public build is created', async () => {
|
||||
await curl(payload(BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).then(h.verifyResponse(201));
|
||||
await curl(payload(BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER))
|
||||
.then(h.verifyResponse(201));
|
||||
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER }).toExistAsABuild();
|
||||
});
|
||||
|
||||
@ -198,7 +199,7 @@ describe('preview-server', () => {
|
||||
expect({ prNum: PrNums.TRUST_CHECK_UNTRUSTED, isPublic: false }).toExistAsABuild();
|
||||
});
|
||||
|
||||
[true, false].forEach(isPublic => {
|
||||
[true].forEach(isPublic => {
|
||||
const build = isPublic ? BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER : BuildNums.TRUST_CHECK_UNTRUSTED;
|
||||
const prNum = isPublic ? PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER : PrNums.TRUST_CHECK_UNTRUSTED;
|
||||
const label = isPublic ? 'public' : 'non-public';
|
||||
@ -363,23 +364,23 @@ describe('preview-server', () => {
|
||||
|
||||
describe(`${host}/health-check`, () => {
|
||||
|
||||
it('should respond with 200', async () => {
|
||||
await Promise.all([
|
||||
it('should respond with 200', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${host}/health-check`).then(h.verifyResponse(200)),
|
||||
h.runCmd(`curl -iL ${host}/health-check/`).then(h.verifyResponse(200)),
|
||||
]);
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the path does not match exactly', async () => {
|
||||
await Promise.all([
|
||||
it('should respond with 404 if the path does not match exactly', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${host}/health-check/foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${host}/health-check-foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${host}/health-checknfoo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${host}/foo/health-check`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${host}/foo-health-check`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${host}/foonhealth-check`).then(h.verifyResponse(404)),
|
||||
]);
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
@ -425,18 +426,18 @@ describe('preview-server', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', async () => {
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
const mockPayload = JSON.stringify({number: 1}); // MockExternalApiFlags.TRUST_CHECK_ACTIVE_TRUSTED_USER });
|
||||
const cmdPrefix = `curl -iLX POST --data "${mockPayload}" ${host}`;
|
||||
|
||||
await Promise.all([
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/foo/pr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foo-pr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foonpr-updated`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updated/foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updated-foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/pr-updatednfoo`).then(h.verifyResponse(404)),
|
||||
]);
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
@ -550,10 +551,10 @@ describe('preview-server', () => {
|
||||
|
||||
describe(`${host}/*`, () => {
|
||||
|
||||
it('should respond with 404 for requests to unknown URLs', async () => {
|
||||
it('should respond with 404 for requests to unknown URLs', done => {
|
||||
const bodyRegex = /^Unknown resource/;
|
||||
|
||||
await Promise.all([
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${host}/index.html`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${host}/`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
@ -561,7 +562,7 @@ describe('preview-server', () => {
|
||||
h.runCmd(`curl -iLX POST ${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PATCH ${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iLX DELETE ${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
]);
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -14,41 +14,42 @@
|
||||
"predev": "yarn build || true",
|
||||
"dev": "run-p ~~build-watch ~~test-watch",
|
||||
"lint": "tslint --project tsconfig.json",
|
||||
"pretest": "run-s build lint",
|
||||
"pretest": "yarn build",
|
||||
"test": "yarn ~~test-only",
|
||||
"pretest-watch": "yarn pretest",
|
||||
"test-watch": "yarn ~~test-watch",
|
||||
"~~build": "tsc",
|
||||
"~~build-watch": "yarn ~~build --watch",
|
||||
"pre~~test-only": "yarn lint",
|
||||
"~~test-only": "node dist/test",
|
||||
"~~test-watch": "nodemon --delay 1 --exec \"yarn ~~test-only\" --watch dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.19.0",
|
||||
"delete-empty": "^3.0.0",
|
||||
"express": "^4.17.1",
|
||||
"jasmine": "^3.5.0",
|
||||
"nock": "^12.0.3",
|
||||
"node-fetch": "^2.6.0",
|
||||
"shelljs": "^0.8.4",
|
||||
"source-map-support": "^0.5.19",
|
||||
"tar-stream": "^2.1.2",
|
||||
"tslib": "^1.11.1"
|
||||
"body-parser": "^1.18.3",
|
||||
"delete-empty": "^2.0.0",
|
||||
"express": "^4.16.3",
|
||||
"jasmine": "^3.2.0",
|
||||
"nock": "^9.6.1",
|
||||
"node-fetch": "^2.2.0",
|
||||
"shelljs": "^0.8.2",
|
||||
"source-map-support": "^0.5.9",
|
||||
"tar-stream": "^1.6.1",
|
||||
"tslib": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/body-parser": "^1.19.0",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/jasmine": "^3.5.10",
|
||||
"@types/nock": "^11.1.0",
|
||||
"@types/node": "^13.13.2",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/shelljs": "^0.8.7",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"nodemon": "^2.0.3",
|
||||
"@types/body-parser": "^1.17.0",
|
||||
"@types/express": "^4.16.0",
|
||||
"@types/jasmine": "^2.8.8",
|
||||
"@types/nock": "^9.3.0",
|
||||
"@types/node": "^10.9.2",
|
||||
"@types/node-fetch": "^2.1.2",
|
||||
"@types/shelljs": "^0.8.0",
|
||||
"@types/supertest": "^2.0.5",
|
||||
"nodemon": "^1.18.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"supertest": "^4.0.2",
|
||||
"tslint": "^6.1.1",
|
||||
"supertest": "^3.1.0",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-jasmine-noSkipOrFocus": "^1.0.9",
|
||||
"typescript": "^3.8.3"
|
||||
"typescript": "^3.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ const EXISTING_DOWNLOADS = [
|
||||
'20-1234567-build.zip',
|
||||
];
|
||||
const OPEN_PRS = [10, 40];
|
||||
const ANY_DATE = jasmine.any(String);
|
||||
|
||||
// Tests
|
||||
describe('BuildCleaner', () => {
|
||||
@ -76,18 +77,22 @@ describe('BuildCleaner', () => {
|
||||
let cleanerRemoveUnnecessaryDownloadsSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
cleanerGetExistingBuildNumbersSpy = spyOn(cleaner, 'getExistingBuildNumbers').and.resolveTo(EXISTING_BUILDS);
|
||||
cleanerGetOpenPrNumbersSpy = spyOn(cleaner, 'getOpenPrNumbers').and.resolveTo(OPEN_PRS);
|
||||
cleanerGetExistingDownloadsSpy = spyOn(cleaner, 'getExistingDownloads').and.resolveTo(EXISTING_DOWNLOADS);
|
||||
cleanerGetExistingBuildNumbersSpy = spyOn(cleaner, 'getExistingBuildNumbers')
|
||||
.and.callFake(() => Promise.resolve(EXISTING_BUILDS));
|
||||
cleanerGetOpenPrNumbersSpy = spyOn(cleaner, 'getOpenPrNumbers')
|
||||
.and.callFake(() => Promise.resolve(OPEN_PRS));
|
||||
cleanerGetExistingDownloadsSpy = spyOn(cleaner, 'getExistingDownloads')
|
||||
.and.callFake(() => Promise.resolve(EXISTING_DOWNLOADS));
|
||||
|
||||
cleanerRemoveUnnecessaryBuildsSpy = spyOn(cleaner, 'removeUnnecessaryBuilds');
|
||||
cleanerRemoveUnnecessaryDownloadsSpy = spyOn(cleaner, 'removeUnnecessaryDownloads');
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', async () => {
|
||||
const promise = cleaner.cleanUp();
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
|
||||
// Do not complete the test and release the spies synchronously, to avoid running the actual implementations.
|
||||
await promise;
|
||||
@ -125,32 +130,52 @@ describe('BuildCleaner', () => {
|
||||
|
||||
|
||||
it('should reject if \'getOpenPrNumbers()\' rejects', async () => {
|
||||
cleanerGetOpenPrNumbersSpy.and.rejectWith('Test');
|
||||
await expectAsync(cleaner.cleanUp()).toBeRejectedWith('Test');
|
||||
try {
|
||||
cleanerGetOpenPrNumbersSpy.and.callFake(() => Promise.reject('Test'));
|
||||
await cleaner.cleanUp();
|
||||
} catch (err) {
|
||||
expect(err).toBe('Test');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it('should reject if \'getExistingBuildNumbers()\' rejects', async () => {
|
||||
cleanerGetExistingBuildNumbersSpy.and.rejectWith('Test');
|
||||
await expectAsync(cleaner.cleanUp()).toBeRejectedWith('Test');
|
||||
try {
|
||||
cleanerGetExistingBuildNumbersSpy.and.callFake(() => Promise.reject('Test'));
|
||||
await cleaner.cleanUp();
|
||||
} catch (err) {
|
||||
expect(err).toBe('Test');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it('should reject if \'getExistingDownloads()\' rejects', async () => {
|
||||
cleanerGetExistingDownloadsSpy.and.rejectWith('Test');
|
||||
await expectAsync(cleaner.cleanUp()).toBeRejectedWith('Test');
|
||||
try {
|
||||
cleanerGetExistingDownloadsSpy.and.callFake(() => Promise.reject('Test'));
|
||||
await cleaner.cleanUp();
|
||||
} catch (err) {
|
||||
expect(err).toBe('Test');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it('should reject if \'removeUnnecessaryBuilds()\' rejects', async () => {
|
||||
cleanerRemoveUnnecessaryBuildsSpy.and.rejectWith('Test');
|
||||
await expectAsync(cleaner.cleanUp()).toBeRejectedWith('Test');
|
||||
try {
|
||||
cleanerRemoveUnnecessaryBuildsSpy.and.callFake(() => Promise.reject('Test'));
|
||||
await cleaner.cleanUp();
|
||||
} catch (err) {
|
||||
expect(err).toBe('Test');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it('should reject if \'removeUnnecessaryDownloads()\' rejects', async () => {
|
||||
cleanerRemoveUnnecessaryDownloadsSpy.and.rejectWith('Test');
|
||||
await expectAsync(cleaner.cleanUp()).toBeRejectedWith('Test');
|
||||
try {
|
||||
cleanerRemoveUnnecessaryDownloadsSpy.and.callFake(() => Promise.reject('Test'));
|
||||
await cleaner.cleanUp();
|
||||
} catch (err) {
|
||||
expect(err).toBe('Test');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@ -162,15 +187,13 @@ describe('BuildCleaner', () => {
|
||||
let promise: Promise<number[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
fsReaddirSpy = spyOn(fs, 'readdir').and.callFake(
|
||||
((_: string, cb: typeof readdirCb) => readdirCb = cb) as unknown as typeof fs.readdir,
|
||||
);
|
||||
fsReaddirSpy = spyOn(fs, 'readdir').and.callFake((_: string, cb: typeof readdirCb) => readdirCb = cb);
|
||||
promise = cleaner.getExistingBuildNumbers();
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
@ -180,27 +203,43 @@ describe('BuildCleaner', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should reject if an error occurs while getting the files', async () => {
|
||||
it('should reject if an error occurs while getting the files', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
readdirCb('Test');
|
||||
await expectAsync(promise).toBeRejectedWith('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the returned files (as numbers)', async () => {
|
||||
it('should resolve with the returned files (as numbers)', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toEqual([12, 34, 56]);
|
||||
done();
|
||||
});
|
||||
|
||||
readdirCb(null, ['12', '34', '56']);
|
||||
await expectAsync(promise).toBeResolvedTo([12, 34, 56]);
|
||||
});
|
||||
|
||||
|
||||
it('should remove `HIDDEN_DIR_PREFIX` from the filenames', async () => {
|
||||
it('should remove `HIDDEN_DIR_PREFIX` from the filenames', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toEqual([12, 34, 56]);
|
||||
done();
|
||||
});
|
||||
|
||||
readdirCb(null, [`${HIDDEN_DIR_PREFIX}12`, '34', `${HIDDEN_DIR_PREFIX}56`]);
|
||||
await expectAsync(promise).toBeResolvedTo([12, 34, 56]);
|
||||
});
|
||||
|
||||
|
||||
it('should ignore files with non-numeric (or zero) names', async () => {
|
||||
it('should ignore files with non-numeric (or zero) names', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toEqual([12, 34, 56]);
|
||||
done();
|
||||
});
|
||||
|
||||
readdirCb(null, ['12', 'foo', '34', 'bar', '56', '000']);
|
||||
await expectAsync(promise).toBeResolvedTo([12, 34, 56]);
|
||||
});
|
||||
|
||||
});
|
||||
@ -220,7 +259,7 @@ describe('BuildCleaner', () => {
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
@ -229,15 +268,31 @@ describe('BuildCleaner', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should reject if an error occurs while fetching PRs', async () => {
|
||||
it('should reject if an error occurs while fetching PRs', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
prDeferred.reject('Test');
|
||||
await expectAsync(promise).toBeRejectedWith('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the numbers of the fetched PRs', async () => {
|
||||
it('should resolve with the numbers of the fetched PRs', done => {
|
||||
promise.then(prNumbers => {
|
||||
expect(prNumbers).toEqual([1, 2, 3]);
|
||||
done();
|
||||
});
|
||||
|
||||
prDeferred.resolve([{id: 0, number: 1}, {id: 1, number: 2}, {id: 2, number: 3}]);
|
||||
await expectAsync(promise).toBeResolvedTo([1, 2, 3]);
|
||||
});
|
||||
|
||||
|
||||
it('should log the number of open PRs', () => {
|
||||
promise.then(prNumbers => {
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith(
|
||||
ANY_DATE, 'BuildCleaner: ', `Open pull requests: ${prNumbers}`);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@ -249,15 +304,13 @@ describe('BuildCleaner', () => {
|
||||
let promise: Promise<string[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
fsReaddirSpy = spyOn(fs, 'readdir').and.callFake(
|
||||
((_: string, cb: typeof readdirCb) => readdirCb = cb) as unknown as typeof fs.readdir,
|
||||
);
|
||||
fsReaddirSpy = spyOn(fs, 'readdir').and.callFake((_: string, cb: typeof readdirCb) => readdirCb = cb);
|
||||
promise = cleaner.getExistingDownloads();
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
@ -267,21 +320,33 @@ describe('BuildCleaner', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should reject if an error occurs while getting the files', async () => {
|
||||
it('should reject if an error occurs while getting the files', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
readdirCb('Test');
|
||||
await expectAsync(promise).toBeRejectedWith('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the returned file names', async () => {
|
||||
it('should resolve with the returned file names', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toEqual(EXISTING_DOWNLOADS);
|
||||
done();
|
||||
});
|
||||
|
||||
readdirCb(null, EXISTING_DOWNLOADS);
|
||||
await expectAsync(promise).toBeResolvedTo(EXISTING_DOWNLOADS);
|
||||
});
|
||||
|
||||
|
||||
it('should ignore files that do not match the artifactPath', async () => {
|
||||
it('should ignore files that do not match the artifactPath', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toEqual(['10-ABCDEF-build.zip', '30-FFFFFFF-build.zip']);
|
||||
done();
|
||||
});
|
||||
|
||||
readdirCb(null, ['10-ABCDEF-build.zip', '20-AAAAAAA-otherfile.zip', '30-FFFFFFF-build.zip']);
|
||||
await expectAsync(promise).toBeResolvedTo(['10-ABCDEF-build.zip', '30-FFFFFFF-build.zip']);
|
||||
});
|
||||
|
||||
});
|
||||
@ -299,7 +364,7 @@ describe('BuildCleaner', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should test if the directory exists (and return if it does not)', () => {
|
||||
it('should test if the directory exists (and return if is does not)', () => {
|
||||
shellTestSpy.and.returnValue(false);
|
||||
cleaner.removeDir('/foo/bar');
|
||||
|
||||
@ -316,19 +381,22 @@ describe('BuildCleaner', () => {
|
||||
|
||||
|
||||
it('should make the directory and its content writable before removing', () => {
|
||||
shellRmSpy.and.callFake(() => expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a+w', '/foo/bar'));
|
||||
cleaner.removeDir('/foo/bar');
|
||||
|
||||
expect(shellChmodSpy).toHaveBeenCalledBefore(shellRmSpy);
|
||||
expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a+w', '/foo/bar');
|
||||
expect(shellRmSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should catch errors and log them', () => {
|
||||
shellRmSpy.and.throwError('Test');
|
||||
shellRmSpy.and.callFake(() => {
|
||||
// tslint:disable-next-line: no-string-throw
|
||||
throw 'Test';
|
||||
});
|
||||
|
||||
cleaner.removeDir('/foo/bar');
|
||||
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('ERROR: Unable to remove \'/foo/bar\' due to:', new Error('Test'));
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('ERROR: Unable to remove \'/foo/bar\' due to:', 'Test');
|
||||
});
|
||||
|
||||
});
|
||||
@ -381,7 +449,7 @@ describe('BuildCleaner', () => {
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(0);
|
||||
cleanerRemoveDirSpy.calls.reset();
|
||||
|
||||
cleaner.removeUnnecessaryBuilds([1, 2, 3, 4], []);
|
||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], []);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(8);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/1'));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/2'));
|
||||
|
@ -45,15 +45,25 @@ describe('CircleCIApi', () => {
|
||||
const errorMessage = 'Invalid request';
|
||||
const request = nock(BASE_URL).get(`/${buildNum}?circle-token=${TOKEN}`);
|
||||
|
||||
request.replyWithError(errorMessage);
|
||||
await expectAsync(api.getBuildInfo(buildNum)).toBeRejectedWithError(
|
||||
try {
|
||||
request.replyWithError(errorMessage);
|
||||
await api.getBuildInfo(buildNum);
|
||||
throw new Error('Exception Expected');
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
`CircleCI build info request failed ` +
|
||||
`(request to ${BASE_URL}/${buildNum}?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
|
||||
}
|
||||
|
||||
request.reply(404, errorMessage);
|
||||
await expectAsync(api.getBuildInfo(buildNum)).toBeRejectedWithError(
|
||||
try {
|
||||
request.reply(404, errorMessage);
|
||||
await api.getBuildInfo(buildNum);
|
||||
throw new Error('Exception Expected');
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
`CircleCI build info request failed ` +
|
||||
`(request to ${BASE_URL}/${buildNum}?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -68,7 +78,8 @@ describe('CircleCIApi', () => {
|
||||
.get(`/${buildNum}/artifacts?circle-token=${TOKEN}`)
|
||||
.reply(200, [artifact0, artifact1, artifact2]);
|
||||
|
||||
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/1')).toBeResolvedTo('https://url/1');
|
||||
const artifactUrl = await api.getBuildArtifactUrl(buildNum, 'some/path/1');
|
||||
expect(artifactUrl).toEqual('https://url/1');
|
||||
request.done();
|
||||
});
|
||||
|
||||
@ -79,15 +90,25 @@ describe('CircleCIApi', () => {
|
||||
const errorMessage = 'Invalid request';
|
||||
const request = nock(BASE_URL).get(`/${buildNum}/artifacts?circle-token=${TOKEN}`);
|
||||
|
||||
request.replyWithError(errorMessage);
|
||||
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/1')).toBeRejectedWithError(
|
||||
try {
|
||||
request.replyWithError(errorMessage);
|
||||
await api.getBuildArtifactUrl(buildNum, 'some/path/1');
|
||||
throw new Error('Exception Expected');
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
`CircleCI artifact URL request failed ` +
|
||||
`(request to ${BASE_URL}/${buildNum}/artifacts?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
|
||||
}
|
||||
|
||||
request.reply(404, errorMessage);
|
||||
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/1')).toBeRejectedWithError(
|
||||
try {
|
||||
request.reply(404, errorMessage);
|
||||
await api.getBuildArtifactUrl(buildNum, 'some/path/1');
|
||||
throw new Error('Exception Expected');
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
`CircleCI artifact URL request failed ` +
|
||||
`(request to ${BASE_URL}/${buildNum}/artifacts?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw an error if the response does not contain the specified artifact', async () => {
|
||||
@ -100,9 +121,14 @@ describe('CircleCIApi', () => {
|
||||
.get(`/${buildNum}/artifacts?circle-token=${TOKEN}`)
|
||||
.reply(200, [artifact0, artifact1, artifact2]);
|
||||
|
||||
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/3')).toBeRejectedWithError(
|
||||
try {
|
||||
await api.getBuildArtifactUrl(buildNum, 'some/path/3');
|
||||
throw new Error('Exception Expected');
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
`CircleCI artifact URL request failed ` +
|
||||
`(Missing artifact (some/path/3) for CircleCI build: ${buildNum})`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -118,7 +118,7 @@ describe('GithubApi', () => {
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect((api as any).getPaginated()).toBeInstanceOf(Promise);
|
||||
expect((api as any).getPaginated()).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
@ -131,30 +131,45 @@ describe('GithubApi', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should reject if the request fails', async () => {
|
||||
const responsePromise = (api as any).getPaginated('/foo/bar');
|
||||
it('should reject if the request fails', done => {
|
||||
(api as any).getPaginated('/foo/bar').catch((err: any) => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
deferreds[0].reject('Test');
|
||||
|
||||
await expectAsync(responsePromise).toBeRejectedWith('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the returned items', async () => {
|
||||
it('should resolve with the returned items', done => {
|
||||
const items = [{id: 1}, {id: 2}];
|
||||
const responsePromise = (api as any).getPaginated('/foo/bar');
|
||||
deferreds[0].resolve(items);
|
||||
|
||||
await expectAsync(responsePromise).toBeResolvedTo(items);
|
||||
(api as any).getPaginated('/foo/bar').then((data: any) => {
|
||||
expect(data).toEqual(items);
|
||||
done();
|
||||
});
|
||||
|
||||
deferreds[0].resolve(items);
|
||||
});
|
||||
|
||||
|
||||
it('should iteratively call \'get()\' to fetch all items', async () => {
|
||||
it('should iteratively call \'get()\' to fetch all items', done => {
|
||||
// Create an array or 250 objects.
|
||||
const allItems = new Array(250).fill(null).map((_, i) => ({id: i}));
|
||||
const allItems = '.'.repeat(250).split('').map((_, i) => ({id: i}));
|
||||
const apiGetSpy = api.get as jasmine.Spy;
|
||||
const paramsForPage = (page: number) => ({baz: 'qux', page, per_page: 100});
|
||||
|
||||
const responsePromise = (api as any).getPaginated('/foo/bar', {baz: 'qux'});
|
||||
(api as any).getPaginated('/foo/bar', {baz: 'qux'}).then((data: any) => {
|
||||
const paramsForPage = (page: number) => ({baz: 'qux', page, per_page: 100});
|
||||
|
||||
expect(apiGetSpy).toHaveBeenCalledTimes(3);
|
||||
expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(1)]);
|
||||
expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(2)]);
|
||||
expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(3)]);
|
||||
|
||||
expect(data).toEqual(allItems);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
deferreds[0].resolve(allItems.slice(0, 100));
|
||||
setTimeout(() => {
|
||||
@ -163,13 +178,6 @@ describe('GithubApi', () => {
|
||||
deferreds[2].resolve(allItems.slice(200));
|
||||
}, 0);
|
||||
}, 0);
|
||||
|
||||
await expectAsync(responsePromise).toBeResolvedTo(allItems);
|
||||
|
||||
expect(apiGetSpy).toHaveBeenCalledTimes(3);
|
||||
expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(1)]);
|
||||
expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(2)]);
|
||||
expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(3)]);
|
||||
});
|
||||
|
||||
});
|
||||
@ -209,8 +217,8 @@ describe('GithubApi', () => {
|
||||
|
||||
describe('request()', () => {
|
||||
it('should return a promise', () => {
|
||||
nock('https://api.github.com').get('/').reply(200);
|
||||
expect((api as any).request()).toBeInstanceOf(Promise);
|
||||
nock('https://api.github.com').get('').reply(200);
|
||||
expect((api as any).request()).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
@ -239,8 +247,9 @@ describe('GithubApi', () => {
|
||||
nock('https://api.github.com')
|
||||
.intercept('/path', 'method')
|
||||
.replyWithError('Test');
|
||||
|
||||
await expectAsync((api as any).request('method', '/path')).toBeRejectedWithError('Test');
|
||||
let message = 'Failed to reject error';
|
||||
await (api as any).request('method', '/path').catch((err: any) => message = err.message);
|
||||
expect(message).toEqual('Test');
|
||||
});
|
||||
|
||||
|
||||
@ -254,69 +263,81 @@ describe('GithubApi', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should reject if response statusCode is <200', async () => {
|
||||
it('should reject if response statusCode is <200', done => {
|
||||
const requestHandler = nock('https://api.github.com')
|
||||
.intercept('/path', 'method')
|
||||
.reply(199);
|
||||
const responsePromise = (api as any).request('method', '/path');
|
||||
|
||||
await expectAsync(responsePromise).toBeRejectedWith(jasmine.stringMatching('failed'));
|
||||
await expectAsync(responsePromise).toBeRejectedWith(jasmine.stringMatching('status: 199'));
|
||||
|
||||
(api as any).request('method', '/path')
|
||||
.catch((err: string) => {
|
||||
expect(err).toContain('failed');
|
||||
expect(err).toContain('status: 199');
|
||||
done();
|
||||
});
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
|
||||
it('should reject if response statusCode is >=400', async () => {
|
||||
it('should reject if response statusCode is >=400', done => {
|
||||
const requestHandler = nock('https://api.github.com')
|
||||
.intercept('/path', 'method')
|
||||
.reply(400);
|
||||
const responsePromise = (api as any).request('method', '/path');
|
||||
|
||||
await expectAsync(responsePromise).toBeRejectedWith(jasmine.stringMatching('failed'));
|
||||
await expectAsync(responsePromise).toBeRejectedWith(jasmine.stringMatching('status: 400'));
|
||||
|
||||
(api as any).request('method', '/path')
|
||||
.catch((err: string) => {
|
||||
expect(err).toContain('failed');
|
||||
expect(err).toContain('status: 400');
|
||||
done();
|
||||
});
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
|
||||
it('should include the response text in the rejection message', async () => {
|
||||
it('should include the response text in the rejection message', done => {
|
||||
const requestHandler = nock('https://api.github.com')
|
||||
.intercept('/path', 'method')
|
||||
.reply(500, 'Test');
|
||||
const responsePromise = (api as any).request('method', '/path');
|
||||
|
||||
await expectAsync(responsePromise).toBeRejectedWith(jasmine.stringMatching('Test'));
|
||||
|
||||
(api as any).request('method', '/path')
|
||||
.catch((err: string) => {
|
||||
expect(err).toContain('Test');
|
||||
done();
|
||||
});
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
|
||||
it('should resolve if returned statusCode is >=200 and <400', async () => {
|
||||
it('should resolve if returned statusCode is >=200 and <400', done => {
|
||||
const requestHandler = nock('https://api.github.com')
|
||||
.intercept('/path', 'method')
|
||||
.reply(200);
|
||||
|
||||
await expectAsync((api as any).request('method', '/path')).toBeResolved();
|
||||
(api as any).request('method', '/path').then(done);
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
|
||||
it('should parse the response body into an object using \'JSON.parse\'', async () => {
|
||||
it('should parse the response body into an object using \'JSON.parse\'', done => {
|
||||
const requestHandler = nock('https://api.github.com')
|
||||
.intercept('/path', 'method')
|
||||
.reply(300, '{"foo": "bar"}');
|
||||
|
||||
await expectAsync((api as any).request('method', '/path')).toBeResolvedTo({foo: 'bar'});
|
||||
(api as any).request('method', '/path').then((data: any) => {
|
||||
expect(data).toEqual({foo: 'bar'});
|
||||
done();
|
||||
});
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
it('should reject if the response text is malformed JSON', async () => {
|
||||
it('should reject if the response text is malformed JSON', done => {
|
||||
const requestHandler = nock('https://api.github.com')
|
||||
.intercept('/path', 'method')
|
||||
.reply(300, '}');
|
||||
|
||||
await expectAsync((api as any).request('method', '/path')).toBeRejectedWithError(SyntaxError);
|
||||
(api as any).request('method', '/path').catch((err: any) => {
|
||||
expect(err).toEqual(jasmine.any(SyntaxError));
|
||||
done();
|
||||
});
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Imports
|
||||
import {GithubApi} from '../../lib/common/github-api';
|
||||
import {GithubPullRequests, PullRequest} from '../../lib/common/github-pull-requests';
|
||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
|
||||
// Tests
|
||||
describe('GithubPullRequests', () => {
|
||||
@ -47,21 +47,27 @@ describe('GithubPullRequests', () => {
|
||||
|
||||
|
||||
it('should make a POST request to Github with the correct pathname, params and data', () => {
|
||||
githubApi.post.and.resolveTo();
|
||||
githubApi.post.and.callFake(() => Promise.resolve());
|
||||
prs.addComment(42, 'body');
|
||||
expect(githubApi.post).toHaveBeenCalledWith('/repos/foo/bar/issues/42/comments', null, {body: 'body'});
|
||||
});
|
||||
|
||||
|
||||
it('should reject if the request fails', async () => {
|
||||
githubApi.post.and.rejectWith('Test');
|
||||
await expectAsync(prs.addComment(42, 'body')).toBeRejectedWith('Test');
|
||||
it('should reject if the request fails', done => {
|
||||
githubApi.post.and.callFake(() => Promise.reject('Test'));
|
||||
prs.addComment(42, 'body').catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the data from the Github POST', async () => {
|
||||
githubApi.post.and.resolveTo('Test');
|
||||
await expectAsync(prs.addComment(42, 'body')).toBeResolvedTo('Test');
|
||||
it('should resolve with the data from the Github POST', done => {
|
||||
githubApi.post.and.callFake(() => Promise.resolve('Test'));
|
||||
prs.addComment(42, 'body').then(data => {
|
||||
expect(data).toBe('Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@ -81,11 +87,13 @@ describe('GithubPullRequests', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the data returned from GitHub', async () => {
|
||||
it('should resolve with the data returned from GitHub', done => {
|
||||
const expected: any = {number: 42};
|
||||
githubApi.get.and.resolveTo(expected);
|
||||
|
||||
await expectAsync(prs.fetch(42)).toBeResolvedTo(expected);
|
||||
githubApi.get.and.callFake(() => Promise.resolve(expected));
|
||||
prs.fetch(42).then(data => {
|
||||
expect(data).toEqual(expected);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@ -117,14 +125,9 @@ describe('GithubPullRequests', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should forward the value returned by \'getPaginated()\'', async () => {
|
||||
const mockPrs: PullRequest[] = [
|
||||
{number: 1, user: {login: 'foo'}, labels: []},
|
||||
{number: 2, user: {login: 'bar'}, labels: []},
|
||||
];
|
||||
|
||||
githubApi.getPaginated.and.resolveTo(mockPrs);
|
||||
expect(await prs.fetchAll()).toBe(mockPrs);
|
||||
it('should forward the value returned by \'getPaginated()\'', () => {
|
||||
githubApi.getPaginated.and.returnValue('Test');
|
||||
expect(prs.fetchAll() as any).toBe('Test');
|
||||
});
|
||||
|
||||
});
|
||||
@ -144,11 +147,13 @@ describe('GithubPullRequests', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the data returned from GitHub', async () => {
|
||||
it('should resolve with the data returned from GitHub', done => {
|
||||
const expected: any = [{sha: 'ABCDE', filename: 'a/b/c'}, {sha: '12345', filename: 'x/y/z'}];
|
||||
githubApi.getPaginated.and.resolveTo(expected);
|
||||
|
||||
await expectAsync(prs.fetchFiles(42)).toBeResolvedTo(expected);
|
||||
githubApi.getPaginated.and.callFake(() => Promise.resolve(expected));
|
||||
prs.fetchFiles(42).then(data => {
|
||||
expect(data).toEqual(expected);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {GithubApi} from '../../lib/common/github-api';
|
||||
import {GithubTeams, Team} from '../../lib/common/github-teams';
|
||||
import {GithubTeams} from '../../lib/common/github-teams';
|
||||
|
||||
// Tests
|
||||
describe('GithubTeams', () => {
|
||||
@ -16,7 +16,6 @@ describe('GithubTeams', () => {
|
||||
expect(() => new GithubTeams(githubApi, '')).
|
||||
toThrowError('Missing or empty required parameter \'githubOrg\'!');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@ -34,14 +33,9 @@ describe('GithubTeams', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should forward the value returned by \'getPaginated()\'', async () => {
|
||||
const mockTeams: Team[] = [
|
||||
{id: 1, slug: 'foo'},
|
||||
{id: 2, slug: 'bar'},
|
||||
];
|
||||
|
||||
githubApi.getPaginated.and.resolveTo(mockTeams);
|
||||
expect(await teams.fetchAll()).toBe(mockTeams);
|
||||
it('should forward the value returned by \'getPaginated()\'', () => {
|
||||
githubApi.getPaginated.and.returnValue('Test');
|
||||
expect(teams.fetchAll() as any).toBe('Test');
|
||||
});
|
||||
|
||||
});
|
||||
@ -56,77 +50,100 @@ describe('GithubTeams', () => {
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
githubApi.get.and.resolveTo();
|
||||
githubApi.get.and.callFake(() => Promise.resolve());
|
||||
const promise = teams.isMemberById('user', [1]);
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if called with an empty array', async () => {
|
||||
await expectAsync(teams.isMemberById('user', [])).toBeResolvedTo(false);
|
||||
expect(githubApi.get).not.toHaveBeenCalled();
|
||||
it('should resolve with false if called with an empty array', done => {
|
||||
teams.isMemberById('user', []).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
expect(githubApi.get).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should call \'get()\' with the correct pathname', async () => {
|
||||
githubApi.get.and.resolveTo();
|
||||
await teams.isMemberById('user', [1]);
|
||||
|
||||
expect(githubApi.get).toHaveBeenCalledWith('/teams/1/memberships/user');
|
||||
it('should call \'get()\' with the correct pathname', done => {
|
||||
githubApi.get.and.callFake(() => Promise.resolve());
|
||||
teams.isMemberById('user', [1]).then(() => {
|
||||
expect(githubApi.get).toHaveBeenCalledWith('/teams/1/memberships/user');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if \'get()\' rejects', async () => {
|
||||
githubApi.get.and.rejectWith(null);
|
||||
|
||||
await expectAsync(teams.isMemberById('user', [1])).toBeResolvedTo(false);
|
||||
expect(githubApi.get).toHaveBeenCalled();
|
||||
it('should resolve with false if \'get()\' rejects', done => {
|
||||
githubApi.get.and.callFake(() => Promise.reject(null));
|
||||
teams.isMemberById('user', [1]).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
expect(githubApi.get).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if the membership is not active', async () => {
|
||||
githubApi.get.and.resolveTo({state: 'pending'});
|
||||
|
||||
await expectAsync(teams.isMemberById('user', [1])).toBeResolvedTo(false);
|
||||
expect(githubApi.get).toHaveBeenCalled();
|
||||
it('should resolve with false if the membership is not active', done => {
|
||||
githubApi.get.and.callFake(() => Promise.resolve({state: 'pending'}));
|
||||
teams.isMemberById('user', [1]).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
expect(githubApi.get).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with true if the membership is active', async () => {
|
||||
githubApi.get.and.resolveTo({state: 'active'});
|
||||
await expectAsync(teams.isMemberById('user', [1])).toBeResolvedTo(true);
|
||||
it('should resolve with true if the membership is active', done => {
|
||||
githubApi.get.and.callFake(() => Promise.resolve({state: 'active'}));
|
||||
teams.isMemberById('user', [1]).then(isMember => {
|
||||
expect(isMember).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should sequentially call \'get()\' until an active membership is found', async () => {
|
||||
githubApi.get.
|
||||
withArgs('/teams/1/memberships/user').and.resolveTo({state: 'pending'}).
|
||||
withArgs('/teams/2/memberships/user').and.rejectWith(null).
|
||||
withArgs('/teams/3/memberships/user').and.resolveTo({state: 'active'});
|
||||
it('should sequentially call \'get()\' until an active membership is found', done => {
|
||||
const trainedResponses: {[pathname: string]: Promise<{state: string}>} = {
|
||||
'/teams/1/memberships/user': Promise.resolve({state: 'pending'}),
|
||||
'/teams/2/memberships/user': Promise.reject(null),
|
||||
'/teams/3/memberships/user': Promise.resolve({state: 'active'}),
|
||||
};
|
||||
githubApi.get.and.callFake((pathname: string) => trainedResponses[pathname]);
|
||||
|
||||
await expectAsync(teams.isMemberById('user', [1, 2, 3, 4])).toBeResolvedTo(true);
|
||||
teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => {
|
||||
expect(isMember).toBe(true);
|
||||
|
||||
expect(githubApi.get).toHaveBeenCalledTimes(3);
|
||||
expect(githubApi.get.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
|
||||
expect(githubApi.get.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
|
||||
expect(githubApi.get.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
|
||||
expect(githubApi.get).toHaveBeenCalledTimes(3);
|
||||
expect(githubApi.get.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
|
||||
expect(githubApi.get.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
|
||||
expect(githubApi.get.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if no active membership is found', async () => {
|
||||
githubApi.get.
|
||||
withArgs('/teams/1/memberships/user').and.resolveTo({state: 'pending'}).
|
||||
withArgs('/teams/2/memberships/user').and.rejectWith(null).
|
||||
withArgs('/teams/3/memberships/user').and.resolveTo({state: 'not active'}).
|
||||
withArgs('/teams/4/memberships/user').and.rejectWith(null);
|
||||
it('should resolve with false if no active membership is found', done => {
|
||||
const trainedResponses: {[pathname: string]: Promise<{state: string}>} = {
|
||||
'/teams/1/memberships/user': Promise.resolve({state: 'pending'}),
|
||||
'/teams/2/memberships/user': Promise.reject(null),
|
||||
'/teams/3/memberships/user': Promise.resolve({state: 'not active'}),
|
||||
'/teams/4/memberships/user': Promise.reject(null),
|
||||
};
|
||||
githubApi.get.and.callFake((pathname: string) => trainedResponses[pathname]);
|
||||
|
||||
await expectAsync(teams.isMemberById('user', [1, 2, 3, 4])).toBeResolvedTo(false);
|
||||
teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
|
||||
expect(githubApi.get).toHaveBeenCalledTimes(4);
|
||||
expect(githubApi.get.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
|
||||
expect(githubApi.get.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
|
||||
expect(githubApi.get.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
|
||||
expect(githubApi.get.calls.argsFor(3)[0]).toBe('/teams/4/memberships/user');
|
||||
expect(githubApi.get).toHaveBeenCalledTimes(4);
|
||||
expect(githubApi.get.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
|
||||
expect(githubApi.get.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
|
||||
expect(githubApi.get.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
|
||||
expect(githubApi.get.calls.argsFor(3)[0]).toBe('/teams/4/memberships/user');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@ -140,13 +157,14 @@ describe('GithubTeams', () => {
|
||||
beforeEach(() => {
|
||||
teams = new GithubTeams(githubApi, 'foo');
|
||||
|
||||
teamsFetchAllSpy = spyOn(teams, 'fetchAll').and.resolveTo([{id: 1, slug: 'team1'}, {id: 2, slug: 'team2'}]);
|
||||
const mockResponse = Promise.resolve([{id: 1, slug: 'team1'}, {id: 2, slug: 'team2'}]);
|
||||
teamsFetchAllSpy = spyOn(teams, 'fetchAll').and.returnValue(mockResponse);
|
||||
teamsIsMemberByIdSpy = spyOn(teams, 'isMemberById');
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(teams.isMemberBySlug('user', ['team-slug'])).toBeInstanceOf(Promise);
|
||||
expect(teams.isMemberBySlug('user', ['team-slug'])).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
@ -156,46 +174,55 @@ describe('GithubTeams', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if \'fetchAll()\' rejects', async () => {
|
||||
teamsFetchAllSpy.and.rejectWith(null);
|
||||
await expectAsync(teams.isMemberBySlug('user', ['team-slug'])).toBeResolvedTo(false);
|
||||
it('should resolve with false if \'fetchAll()\' rejects', done => {
|
||||
teamsFetchAllSpy.and.callFake(() => Promise.reject(null));
|
||||
teams.isMemberBySlug('user', ['team-slug']).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should call \'isMemberById()\' with the correct params if no team is found', async () => {
|
||||
await teams.isMemberBySlug('user', ['no-match']);
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('user', []);
|
||||
it('should call \'isMemberById()\' with the correct params if no team is found', done => {
|
||||
teams.isMemberBySlug('user', ['no-match']).then(() => {
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('user', []);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should call \'isMemberById()\' with the correct params if teams are found', async () => {
|
||||
await teams.isMemberBySlug('userA', ['team1']);
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('userA', [1]);
|
||||
it('should call \'isMemberById()\' with the correct params if teams are found', done => {
|
||||
const spy = teamsIsMemberByIdSpy;
|
||||
|
||||
await teams.isMemberBySlug('userB', ['team2']);
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('userB', [2]);
|
||||
|
||||
await teams.isMemberBySlug('userC', ['team1', 'team2']);
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('userC', [1, 2]);
|
||||
Promise.all([
|
||||
teams.isMemberBySlug('user', ['team1']).then(() => expect(spy).toHaveBeenCalledWith('user', [1])),
|
||||
teams.isMemberBySlug('user', ['team2']).then(() => expect(spy).toHaveBeenCalledWith('user', [2])),
|
||||
teams.isMemberBySlug('user', ['team1', 'team2']).then(() => expect(spy).toHaveBeenCalledWith('user', [1, 2])),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if \'isMemberById()\' rejects', async () => {
|
||||
teamsIsMemberByIdSpy.and.rejectWith(null);
|
||||
|
||||
await expectAsync(teams.isMemberBySlug('user', ['team1'])).toBeResolvedTo(false);
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalled();
|
||||
it('should resolve with false if \'isMemberById()\' rejects', done => {
|
||||
teamsIsMemberByIdSpy.and.callFake(() => Promise.reject(null));
|
||||
teams.isMemberBySlug('user', ['team1']).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the value \'isMemberById()\' resolves with', async () => {
|
||||
teamsIsMemberByIdSpy.and.resolveTo(true);
|
||||
await expectAsync(teams.isMemberBySlug('userA', ['team1'])).toBeResolvedTo(true);
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('userA', [1]);
|
||||
|
||||
teamsIsMemberByIdSpy.and.resolveTo(false);
|
||||
await expectAsync(teams.isMemberBySlug('userB', ['team1'])).toBeResolvedTo(false);
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('userB', [1]);
|
||||
teamsIsMemberByIdSpy.and.callFake(() => Promise.resolve(true));
|
||||
const isMember1 = await teams.isMemberBySlug('user', ['team1']);
|
||||
expect(isMember1).toBe(true);
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('user', [1]);
|
||||
|
||||
teamsIsMemberByIdSpy.and.callFake(() => Promise.resolve(false));
|
||||
const isMember2 = await teams.isMemberBySlug('user', ['team1']);
|
||||
expect(isMember2).toBe(false);
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('user', [1]);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -9,8 +9,7 @@ import {Logger} from '../../lib/common/utils';
|
||||
import {BuildCreator} from '../../lib/preview-server/build-creator';
|
||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
|
||||
import {PreviewServerError} from '../../lib/preview-server/preview-error';
|
||||
import {customAsyncMatchers} from './jasmine-custom-async-matchers';
|
||||
|
||||
import {expectToBePreviewServerError} from './helpers';
|
||||
|
||||
// Tests
|
||||
describe('BuildCreator', () => {
|
||||
@ -25,7 +24,6 @@ describe('BuildCreator', () => {
|
||||
const publicShaDir = path.join(publicPrDir, shortSha);
|
||||
let bc: BuildCreator;
|
||||
|
||||
beforeEach(() => jasmine.addAsyncMatchers(customAsyncMatchers));
|
||||
beforeEach(() => bc = new BuildCreator(buildsDir));
|
||||
|
||||
|
||||
@ -37,8 +35,8 @@ describe('BuildCreator', () => {
|
||||
|
||||
|
||||
it('should extend EventEmitter', () => {
|
||||
expect(bc).toBeInstanceOf(BuildCreator);
|
||||
expect(bc).toBeInstanceOf(EventEmitter);
|
||||
expect(bc).toEqual(jasmine.any(BuildCreator));
|
||||
expect(bc).toEqual(jasmine.any(EventEmitter));
|
||||
|
||||
expect(Object.getPrototypeOf(bc)).toBe(BuildCreator.prototype);
|
||||
});
|
||||
@ -69,43 +67,47 @@ describe('BuildCreator', () => {
|
||||
const shaDir = isPublic ? publicShaDir : hiddenShaDir;
|
||||
|
||||
|
||||
it('should return a promise', async () => {
|
||||
it('should return a promise', done => {
|
||||
const promise = bc.create(pr, sha, archive, isPublic);
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `extractArchive()`.
|
||||
|
||||
// Do not complete the test (and release the spies) synchronously to avoid running the actual
|
||||
// `extractArchive()`.
|
||||
await promise;
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should update the PR\'s visibility first if necessary', async () => {
|
||||
await bc.create(pr, sha, archive, isPublic);
|
||||
it('should update the PR\'s visibility first if necessary', done => {
|
||||
bcUpdatePrVisibilitySpy.and.callFake(() => expect(shellMkdirSpy).not.toHaveBeenCalled());
|
||||
|
||||
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledBefore(shellMkdirSpy);
|
||||
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(pr, isPublic);
|
||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||
bc.create(pr, sha, archive, isPublic).
|
||||
then(() => {
|
||||
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(pr, isPublic);
|
||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should create the build directory (and any parent directories)', async () => {
|
||||
await bc.create(pr, sha, archive, isPublic);
|
||||
expect(shellMkdirSpy).toHaveBeenCalledWith('-p', shaDir);
|
||||
it('should create the build directory (and any parent directories)', done => {
|
||||
bc.create(pr, sha, archive, isPublic).
|
||||
then(() => expect(shellMkdirSpy).toHaveBeenCalledWith('-p', shaDir)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should extract the archive contents into the build directory', async () => {
|
||||
await bc.create(pr, sha, archive, isPublic);
|
||||
expect(bcExtractArchiveSpy).toHaveBeenCalledWith(archive, shaDir);
|
||||
it('should extract the archive contents into the build directory', done => {
|
||||
bc.create(pr, sha, archive, isPublic).
|
||||
then(() => expect(bcExtractArchiveSpy).toHaveBeenCalledWith(archive, shaDir)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should emit a CreatedBuildEvent on success', async () => {
|
||||
it('should emit a CreatedBuildEvent on success', done => {
|
||||
let emitted = false;
|
||||
|
||||
bcEmitSpy.and.callFake((type: string, evt: CreatedBuildEvent) => {
|
||||
expect(type).toBe(CreatedBuildEvent.type);
|
||||
expect(evt).toBeInstanceOf(CreatedBuildEvent);
|
||||
expect(evt).toEqual(jasmine.any(CreatedBuildEvent));
|
||||
expect(evt.pr).toBe(+pr);
|
||||
expect(evt.sha).toBe(shortSha);
|
||||
expect(evt.isPublic).toBe(isPublic);
|
||||
@ -113,108 +115,130 @@ describe('BuildCreator', () => {
|
||||
emitted = true;
|
||||
});
|
||||
|
||||
await bc.create(pr, sha, archive, isPublic);
|
||||
expect(emitted).toBe(true);
|
||||
bc.create(pr, sha, archive, isPublic).
|
||||
then(() => expect(emitted).toBe(true)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe('on error', () => {
|
||||
let existsValues: {[dir: string]: boolean};
|
||||
|
||||
beforeEach(() => {
|
||||
bcExistsSpy.and.returnValue(false);
|
||||
existsValues = {
|
||||
[prDir]: false,
|
||||
[shaDir]: false,
|
||||
};
|
||||
|
||||
bcExistsSpy.and.callFake((dir: string) => existsValues[dir]);
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if changing the PR\'s visibility fails', async () => {
|
||||
it('should abort and skip further operations if changing the PR\'s visibility fails', done => {
|
||||
const mockError = new PreviewServerError(543, 'Test');
|
||||
bcUpdatePrVisibilitySpy.and.rejectWith(mockError);
|
||||
bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject(mockError));
|
||||
|
||||
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejectedWith(mockError);
|
||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||
expect(err).toBe(mockError);
|
||||
|
||||
expect(bcExistsSpy).not.toHaveBeenCalled();
|
||||
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
expect(bcExistsSpy).not.toHaveBeenCalled();
|
||||
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if the build does already exist', async () => {
|
||||
bcExistsSpy.withArgs(shaDir).and.returnValue(true);
|
||||
|
||||
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejectedWithPreviewServerError(
|
||||
409, `Request to overwrite existing ${isPublic ? '' : 'non-'}public directory: ${shaDir}`);
|
||||
|
||||
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
it('should abort and skip further operations if the build does already exist', done => {
|
||||
existsValues[shaDir] = true;
|
||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||
const publicOrNot = isPublic ? 'public' : 'non-public';
|
||||
expectToBePreviewServerError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
|
||||
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should detect existing build directory after visibility change', async () => {
|
||||
bcUpdatePrVisibilitySpy.and.callFake(() => bcExistsSpy.and.returnValue(true));
|
||||
it('should detect existing build directory after visibility change', done => {
|
||||
bcUpdatePrVisibilitySpy.and.callFake(() => existsValues[prDir] = existsValues[shaDir] = true);
|
||||
|
||||
expect(bcExistsSpy(prDir)).toBe(false);
|
||||
expect(bcExistsSpy(shaDir)).toBe(false);
|
||||
|
||||
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejectedWithPreviewServerError(
|
||||
409, `Request to overwrite existing ${isPublic ? '' : 'non-'}public directory: ${shaDir}`);
|
||||
|
||||
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||
const publicOrNot = isPublic ? 'public' : 'non-public';
|
||||
expectToBePreviewServerError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
|
||||
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to create the directories', async () => {
|
||||
it('should abort and skip further operations if it fails to create the directories', done => {
|
||||
shellMkdirSpy.and.throwError('');
|
||||
|
||||
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejected();
|
||||
|
||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
bc.create(pr, sha, archive, isPublic).catch(() => {
|
||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to extract the archive', async () => {
|
||||
it('should abort and skip further operations if it fails to extract the archive', done => {
|
||||
bcExtractArchiveSpy.and.throwError('');
|
||||
bc.create(pr, sha, archive, isPublic).catch(() => {
|
||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should delete the PR directory (for new PR)', done => {
|
||||
bcExtractArchiveSpy.and.throwError('');
|
||||
bc.create(pr, sha, archive, isPublic).catch(() => {
|
||||
expect(shellRmSpy).toHaveBeenCalledWith('-rf', prDir);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should delete the SHA directory (for existing PR)', done => {
|
||||
existsValues[prDir] = true;
|
||||
bcExtractArchiveSpy.and.throwError('');
|
||||
|
||||
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejected();
|
||||
|
||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
bc.create(pr, sha, archive, isPublic).catch(() => {
|
||||
expect(shellRmSpy).toHaveBeenCalledWith('-rf', shaDir);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should delete the PR directory (for new PR)', async () => {
|
||||
bcExtractArchiveSpy.and.throwError('');
|
||||
|
||||
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejected();
|
||||
expect(shellRmSpy).toHaveBeenCalledWith('-rf', prDir);
|
||||
});
|
||||
|
||||
|
||||
it('should delete the SHA directory (for existing PR)', async () => {
|
||||
bcExistsSpy.withArgs(prDir).and.returnValue(true);
|
||||
bcExtractArchiveSpy.and.throwError('');
|
||||
|
||||
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejected();
|
||||
expect(shellRmSpy).toHaveBeenCalledWith('-rf', shaDir);
|
||||
});
|
||||
|
||||
|
||||
it('should reject with an PreviewServerError', async () => {
|
||||
it('should reject with an PreviewServerError', done => {
|
||||
// tslint:disable-next-line: no-string-throw
|
||||
shellMkdirSpy.and.callFake(() => { throw 'Test'; });
|
||||
|
||||
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejectedWithPreviewServerError(
|
||||
500, `Error while creating preview at: ${shaDir}\nTest`);
|
||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||
expectToBePreviewServerError(err, 500, `Error while creating preview at: ${shaDir}\nTest`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should pass PreviewServerError instances unmodified', async () => {
|
||||
it('should pass PreviewServerError instances unmodified', done => {
|
||||
shellMkdirSpy.and.callFake(() => { throw new PreviewServerError(543, 'Test'); });
|
||||
await expectAsync(bc.create(pr, sha, archive, isPublic)).toBeRejectedWithPreviewServerError(543, 'Test');
|
||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||
expectToBePreviewServerError(err, 543, 'Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@ -241,12 +265,12 @@ describe('BuildCreator', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', async () => {
|
||||
it('should return a promise', done => {
|
||||
const promise = bc.updatePrVisibility(pr, true);
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `extractArchive()`.
|
||||
|
||||
// Do not complete the test (and release the spies) synchronously to avoid running the actual `extractArchive()`.
|
||||
await promise;
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
@ -255,53 +279,58 @@ describe('BuildCreator', () => {
|
||||
const newPrDir = makePublic ? publicPrDir : hiddenPrDir;
|
||||
|
||||
|
||||
it('should rename the directory', async () => {
|
||||
await bc.updatePrVisibility(pr, makePublic);
|
||||
expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir);
|
||||
it('should rename the directory', done => {
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(() => expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe('when the visibility is updated', () => {
|
||||
|
||||
it('should resolve to true', async () => {
|
||||
await expectAsync(bc.updatePrVisibility(pr, makePublic)).toBeResolvedTo(true);
|
||||
it('should resolve to true', done => {
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(result => expect(result).toBe(true)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should rename the directory', async () => {
|
||||
await bc.updatePrVisibility(pr, makePublic);
|
||||
expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir);
|
||||
it('should rename the directory', done => {
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(() => expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should emit a ChangedPrVisibilityEvent on success', async () => {
|
||||
it('should emit a ChangedPrVisibilityEvent on success', done => {
|
||||
let emitted = false;
|
||||
|
||||
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
||||
expect(type).toBe(ChangedPrVisibilityEvent.type);
|
||||
expect(evt).toBeInstanceOf(ChangedPrVisibilityEvent);
|
||||
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
|
||||
expect(evt.pr).toBe(+pr);
|
||||
expect(evt.shas).toBeInstanceOf(Array);
|
||||
expect(evt.shas).toEqual(jasmine.any(Array));
|
||||
expect(evt.isPublic).toBe(makePublic);
|
||||
|
||||
emitted = true;
|
||||
});
|
||||
|
||||
await bc.updatePrVisibility(pr, makePublic);
|
||||
expect(emitted).toBe(true);
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(() => expect(emitted).toBe(true)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should include all shas in the emitted event', async () => {
|
||||
it('should include all shas in the emitted event', done => {
|
||||
const shas = ['foo', 'bar', 'baz'];
|
||||
let emitted = false;
|
||||
|
||||
bcListShasByDate.and.resolveTo(shas);
|
||||
bcListShasByDate.and.callFake(() => Promise.resolve(shas));
|
||||
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
||||
expect(bcListShasByDate).toHaveBeenCalledWith(newPrDir);
|
||||
|
||||
expect(type).toBe(ChangedPrVisibilityEvent.type);
|
||||
expect(evt).toBeInstanceOf(ChangedPrVisibilityEvent);
|
||||
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
|
||||
expect(evt.pr).toBe(+pr);
|
||||
expect(evt.shas).toBe(shas);
|
||||
expect(evt.isPublic).toBe(makePublic);
|
||||
@ -309,82 +338,94 @@ describe('BuildCreator', () => {
|
||||
emitted = true;
|
||||
});
|
||||
|
||||
await bc.updatePrVisibility(pr, makePublic);
|
||||
expect(emitted).toBe(true);
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(() => expect(emitted).toBe(true)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should do nothing if the visibility is already up-to-date', async () => {
|
||||
it('should do nothing if the visibility is already up-to-date', done => {
|
||||
bcExistsSpy.and.callFake((dir: string) => dir === newPrDir);
|
||||
|
||||
await expectAsync(bc.updatePrVisibility(pr, makePublic)).toBeResolvedTo(false);
|
||||
|
||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(result => {
|
||||
expect(result).toBe(false);
|
||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should do nothing if the PR directory does not exist', async () => {
|
||||
it('should do nothing if the PR directory does not exist', done => {
|
||||
bcExistsSpy.and.returnValue(false);
|
||||
|
||||
await expectAsync(bc.updatePrVisibility(pr, makePublic)).toBeResolvedTo(false);
|
||||
|
||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(result => {
|
||||
expect(result).toBe(false);
|
||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe('on error', () => {
|
||||
|
||||
it('should abort and skip further operations if both directories exist', async () => {
|
||||
it('should abort and skip further operations if both directories exist', done => {
|
||||
bcExistsSpy.and.returnValue(true);
|
||||
|
||||
await expectAsync(bc.updatePrVisibility(pr, makePublic)).toBeRejectedWithPreviewServerError(
|
||||
409, `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
|
||||
|
||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||
expectToBePreviewServerError(err, 409,
|
||||
`Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
|
||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to rename the directory', async () => {
|
||||
it('should abort and skip further operations if it fails to rename the directory', done => {
|
||||
shellMvSpy.and.throwError('');
|
||||
|
||||
await expectAsync(bc.updatePrVisibility(pr, makePublic)).toBeRejected();
|
||||
|
||||
expect(shellMvSpy).toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
bc.updatePrVisibility(pr, makePublic).catch(() => {
|
||||
expect(shellMvSpy).toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to list the SHAs', async () => {
|
||||
it('should abort and skip further operations if it fails to list the SHAs', done => {
|
||||
bcListShasByDate.and.throwError('');
|
||||
|
||||
await expectAsync(bc.updatePrVisibility(pr, makePublic)).toBeRejected();
|
||||
|
||||
expect(shellMvSpy).toHaveBeenCalled();
|
||||
expect(bcListShasByDate).toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
bc.updatePrVisibility(pr, makePublic).catch(() => {
|
||||
expect(shellMvSpy).toHaveBeenCalled();
|
||||
expect(bcListShasByDate).toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should reject with an PreviewServerError', async () => {
|
||||
it('should reject with an PreviewServerError', done => {
|
||||
// tslint:disable-next-line: no-string-throw
|
||||
shellMvSpy.and.callFake(() => { throw 'Test'; });
|
||||
await expectAsync(bc.updatePrVisibility(pr, makePublic)).toBeRejectedWithPreviewServerError(
|
||||
500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`);
|
||||
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||
expectToBePreviewServerError(err, 500,
|
||||
`Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should pass PreviewServerError instances unmodified', async () => {
|
||||
it('should pass PreviewServerError instances unmodified', done => {
|
||||
shellMvSpy.and.callFake(() => { throw new PreviewServerError(543, 'Test'); });
|
||||
await expectAsync(bc.updatePrVisibility(pr, makePublic)).toBeRejectedWithPreviewServerError(543, 'Test');
|
||||
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||
expectToBePreviewServerError(err, 543, 'Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@ -402,14 +443,12 @@ describe('BuildCreator', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
fsAccessCbs = [];
|
||||
fsAccessSpy = spyOn(fs, 'access').and.callFake(
|
||||
((_: string, cb: (v?: any) => void) => fsAccessCbs.push(cb)) as unknown as typeof fs.access,
|
||||
);
|
||||
fsAccessSpy = spyOn(fs, 'access').and.callFake((_: string, cb: (v?: any) => void) => fsAccessCbs.push(cb));
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect((bc as any).exists('foo')).toBeInstanceOf(Promise);
|
||||
expect((bc as any).exists('foo')).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
@ -419,29 +458,25 @@ describe('BuildCreator', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with \'true\' if \'fs.access()\' succeeds', async () => {
|
||||
const existsPromises = [
|
||||
(bc as any).exists('foo'),
|
||||
(bc as any).exists('bar'),
|
||||
];
|
||||
it('should resolve with \'true\' if \'fs.access()\' succeeds', done => {
|
||||
Promise.
|
||||
all([(bc as any).exists('foo'), (bc as any).exists('bar')]).
|
||||
then(results => expect(results).toEqual([true, true])).
|
||||
then(done);
|
||||
|
||||
fsAccessCbs[0]();
|
||||
fsAccessCbs[1](null);
|
||||
|
||||
await expectAsync(Promise.all(existsPromises)).toBeResolvedTo([true, true]);
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with \'false\' if \'fs.access()\' errors', async () => {
|
||||
const existsPromises = [
|
||||
(bc as any).exists('foo'),
|
||||
(bc as any).exists('bar'),
|
||||
];
|
||||
it('should resolve with \'false\' if \'fs.access()\' errors', done => {
|
||||
Promise.
|
||||
all([(bc as any).exists('foo'), (bc as any).exists('bar')]).
|
||||
then(results => expect(results).toEqual([false, false])).
|
||||
then(done);
|
||||
|
||||
fsAccessCbs[0]('Error');
|
||||
fsAccessCbs[1](new Error());
|
||||
|
||||
await expectAsync(Promise.all(existsPromises)).toBeResolvedTo([false, false]);
|
||||
});
|
||||
|
||||
});
|
||||
@ -460,15 +495,12 @@ describe('BuildCreator', () => {
|
||||
consoleWarnSpy = spyOn(Logger.prototype, 'warn');
|
||||
shellChmodSpy = spyOn(shell, 'chmod');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
cpExecSpy = spyOn(cp, 'exec').and.callFake(
|
||||
((_: string, cb: (...args: any[]) => void) =>
|
||||
cpExecCbs.push(cb)) as unknown as typeof cp.exec,
|
||||
);
|
||||
cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: (...args: any[]) => void) => cpExecCbs.push(cb));
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect((bc as any).extractArchive('foo', 'bar')).toBeInstanceOf(Promise);
|
||||
expect((bc as any).extractArchive('foo', 'bar')).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
@ -480,68 +512,78 @@ describe('BuildCreator', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should log (as a warning) any stderr output if extracting succeeded', async () => {
|
||||
const extractPromise = (bc as any).extractArchive('foo', 'bar');
|
||||
it('should log (as a warning) any stderr output if extracting succeeded', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').
|
||||
then(() => expect(consoleWarnSpy).toHaveBeenCalledWith('This is stderr')).
|
||||
then(done);
|
||||
|
||||
cpExecCbs[0](null, 'This is stdout', 'This is stderr');
|
||||
|
||||
await expectAsync(extractPromise).toBeResolved();
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith('This is stderr');
|
||||
});
|
||||
|
||||
|
||||
it('should make the build directory non-writable', async () => {
|
||||
const extractPromise = (bc as any).extractArchive('foo', 'bar');
|
||||
cpExecCbs[0]();
|
||||
it('should make the build directory non-writable', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').
|
||||
then(() => expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a-w', 'bar')).
|
||||
then(done);
|
||||
|
||||
await expectAsync(extractPromise).toBeResolved();
|
||||
expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a-w', 'bar');
|
||||
cpExecCbs[0]();
|
||||
});
|
||||
|
||||
|
||||
it('should delete the build artifact file on success', async () => {
|
||||
const extractPromise = (bc as any).extractArchive('input/file', 'output/dir');
|
||||
cpExecCbs[0]();
|
||||
it('should delete the build artifact file on success', done => {
|
||||
(bc as any).extractArchive('input/file', 'output/dir').
|
||||
then(() => expect(shellRmSpy).toHaveBeenCalledWith('-f', 'input/file')).
|
||||
then(done);
|
||||
|
||||
await expectAsync(extractPromise).toBeResolved();
|
||||
expect(shellRmSpy).toHaveBeenCalledWith('-f', 'input/file');
|
||||
cpExecCbs[0]();
|
||||
});
|
||||
|
||||
|
||||
describe('on error', () => {
|
||||
|
||||
it('should abort and skip further operations if it fails to extract the archive', async () => {
|
||||
const extractPromise = (bc as any).extractArchive('foo', 'bar');
|
||||
it('should abort and skip further operations if it fails to extract the archive', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').catch((err: any) => {
|
||||
expect(shellChmodSpy).not.toHaveBeenCalled();
|
||||
expect(shellRmSpy).not.toHaveBeenCalled();
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
cpExecCbs[0]('Test');
|
||||
|
||||
await expectAsync(extractPromise).toBeRejectedWith('Test');
|
||||
expect(shellChmodSpy).not.toHaveBeenCalled();
|
||||
expect(shellRmSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to make non-writable', async () => {
|
||||
// tslint:disable-next-line: no-string-throw
|
||||
shellChmodSpy.and.callFake(() => { throw 'Test'; });
|
||||
it('should abort and skip further operations if it fails to make non-writable', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').catch((err: any) => {
|
||||
expect(shellChmodSpy).toHaveBeenCalled();
|
||||
expect(shellRmSpy).not.toHaveBeenCalled();
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
shellChmodSpy.and.callFake(() => {
|
||||
// tslint:disable-next-line: no-string-throw
|
||||
throw 'Test';
|
||||
});
|
||||
|
||||
const extractPromise = (bc as any).extractArchive('foo', 'bar');
|
||||
cpExecCbs[0]();
|
||||
|
||||
await expectAsync(extractPromise).toBeRejectedWith('Test');
|
||||
expect(shellChmodSpy).toHaveBeenCalled();
|
||||
expect(shellRmSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should abort and reject if it fails to remove the build artifact file', async () => {
|
||||
// tslint:disable-next-line: no-string-throw
|
||||
shellRmSpy.and.callFake(() => { throw 'Test'; });
|
||||
it('should abort and reject if it fails to remove the build artifact file', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').catch((err: any) => {
|
||||
expect(shellChmodSpy).toHaveBeenCalled();
|
||||
expect(shellRmSpy).toHaveBeenCalled();
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
shellRmSpy.and.callFake(() => {
|
||||
// tslint:disable-next-line: no-string-throw
|
||||
throw 'Test';
|
||||
});
|
||||
|
||||
const extractPromise = (bc as any).extractArchive('foo', 'bar');
|
||||
cpExecCbs[0]();
|
||||
|
||||
await expectAsync(extractPromise).toBeRejectedWith('Test');
|
||||
expect(shellChmodSpy).toHaveBeenCalled();
|
||||
expect(shellRmSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
@ -558,54 +600,62 @@ describe('BuildCreator', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
shellLsSpy = spyOn(shell, 'ls').and.returnValue([] as unknown as shell.ShellArray);
|
||||
shellLsSpy = spyOn(shell, 'ls').and.returnValue([]);
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', async () => {
|
||||
it('should return a promise', done => {
|
||||
const promise = (bc as any).listShasByDate('input/dir');
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `ls()`.
|
||||
|
||||
// Do not complete the test (and release the spies) synchronously to avoid running the actual `ls()`.
|
||||
await promise;
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should `ls()` files with their metadata', async () => {
|
||||
await (bc as any).listShasByDate('input/dir');
|
||||
expect(shellLsSpy).toHaveBeenCalledWith('-l', 'input/dir');
|
||||
it('should `ls()` files with their metadata', done => {
|
||||
(bc as any).listShasByDate('input/dir').
|
||||
then(() => expect(shellLsSpy).toHaveBeenCalledWith('-l', 'input/dir')).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject if listing files fails', async () => {
|
||||
shellLsSpy.and.rejectWith('Test');
|
||||
await expectAsync((bc as any).listShasByDate('input/dir')).toBeRejectedWith('Test');
|
||||
it('should reject if listing files fails', done => {
|
||||
shellLsSpy.and.callFake(() => Promise.reject('Test'));
|
||||
(bc as any).listShasByDate('input/dir').catch((err: string) => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should return the filenames', async () => {
|
||||
shellLsSpy.and.resolveTo([
|
||||
it('should return the filenames', done => {
|
||||
shellLsSpy.and.callFake(() => Promise.resolve([
|
||||
lsResult('foo', 100),
|
||||
lsResult('bar', 200),
|
||||
lsResult('baz', 300),
|
||||
]);
|
||||
]));
|
||||
|
||||
await expectAsync((bc as any).listShasByDate('input/dir')).toBeResolvedTo(['foo', 'bar', 'baz']);
|
||||
(bc as any).listShasByDate('input/dir').
|
||||
then((shas: string[]) => expect(shas).toEqual(['foo', 'bar', 'baz'])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should sort by date', async () => {
|
||||
shellLsSpy.and.resolveTo([
|
||||
it('should sort by date', done => {
|
||||
shellLsSpy.and.callFake(() => Promise.resolve([
|
||||
lsResult('foo', 300),
|
||||
lsResult('bar', 100),
|
||||
lsResult('baz', 200),
|
||||
]);
|
||||
]));
|
||||
|
||||
await expectAsync((bc as any).listShasByDate('input/dir')).toBeResolvedTo(['bar', 'baz', 'foo']);
|
||||
(bc as any).listShasByDate('input/dir').
|
||||
then((shas: string[]) => expect(shas).toEqual(['bar', 'baz', 'foo'])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should not break with ShellJS\' custom `sort()` method', async () => {
|
||||
it('should not break with ShellJS\' custom `sort()` method', done => {
|
||||
const mockArray = [
|
||||
lsResult('foo', 300),
|
||||
lsResult('bar', 100),
|
||||
@ -613,21 +663,26 @@ describe('BuildCreator', () => {
|
||||
];
|
||||
mockArray.sort = jasmine.createSpy('sort');
|
||||
|
||||
shellLsSpy.and.resolveTo(mockArray);
|
||||
|
||||
await expectAsync((bc as any).listShasByDate('input/dir')).toBeResolvedTo(['bar', 'baz', 'foo']);
|
||||
expect(mockArray.sort).not.toHaveBeenCalled();
|
||||
shellLsSpy.and.callFake(() => Promise.resolve(mockArray));
|
||||
(bc as any).listShasByDate('input/dir').
|
||||
then((shas: string[]) => {
|
||||
expect(shas).toEqual(['bar', 'baz', 'foo']);
|
||||
expect(mockArray.sort).not.toHaveBeenCalled();
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should only include directories', async () => {
|
||||
shellLsSpy.and.resolveTo([
|
||||
it('should only include directories', done => {
|
||||
shellLsSpy.and.callFake(() => Promise.resolve([
|
||||
lsResult('foo', 100),
|
||||
lsResult('bar', 200, false),
|
||||
lsResult('baz', 300),
|
||||
]);
|
||||
]));
|
||||
|
||||
await expectAsync((bc as any).listShasByDate('input/dir')).toBeResolvedTo(['foo', 'baz']);
|
||||
(bc as any).listShasByDate('input/dir').
|
||||
then((shas: string[]) => expect(shas).toEqual(['foo', 'baz'])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -32,18 +32,18 @@ describe('BuildRetriever', () => {
|
||||
};
|
||||
|
||||
api = new CircleCiApi('ORG', 'REPO', 'TOKEN');
|
||||
spyOn(api, 'getBuildInfo').and.resolveTo(BUILD_INFO);
|
||||
getBuildArtifactUrlSpy = spyOn(api, 'getBuildArtifactUrl').and.resolveTo(BASE_URL + ARTIFACT_PATH);
|
||||
spyOn(api, 'getBuildInfo').and.callFake(() => Promise.resolve(BUILD_INFO));
|
||||
getBuildArtifactUrlSpy = spyOn(api, 'getBuildArtifactUrl')
|
||||
.and.callFake(() => Promise.resolve(BASE_URL + ARTIFACT_PATH));
|
||||
|
||||
WRITEFILE_RESULT = undefined;
|
||||
writeFileSpy = spyOn(fs, 'writeFile').and.callFake(
|
||||
((_path: string, _buffer: Buffer, callback: fs.NoParamCallback) =>
|
||||
callback(WRITEFILE_RESULT)) as typeof fs.writeFile,
|
||||
(_path: string, _buffer: Buffer, callback: (err?: any) => {}) => callback(WRITEFILE_RESULT),
|
||||
);
|
||||
|
||||
EXISTS_RESULT = false;
|
||||
existsSpy = spyOn(fs, 'exists').and.callFake(
|
||||
((_path, callback) => callback(EXISTS_RESULT)) as typeof fs.exists,
|
||||
(_path: string, callback: (exists: boolean) => {}) => callback(EXISTS_RESULT),
|
||||
);
|
||||
});
|
||||
|
||||
@ -56,7 +56,6 @@ describe('BuildRetriever', () => {
|
||||
expect(() => new BuildRetriever(api, -1, DOWNLOAD_DIR))
|
||||
.toThrowError(`Invalid parameter "downloadSizeLimit" should be a number greater than 0.`);
|
||||
});
|
||||
|
||||
it('should fail if the "downloadDir" is missing', () => {
|
||||
expect(() => new BuildRetriever(api, MAX_DOWNLOAD_SIZE, ''))
|
||||
.toThrowError(`Missing or empty required parameter 'downloadDir'!`);
|
||||
@ -73,10 +72,14 @@ describe('BuildRetriever', () => {
|
||||
});
|
||||
|
||||
it('should error if it is not possible to extract the PR number from the branch', async () => {
|
||||
BUILD_INFO.branch = 'master';
|
||||
const retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR);
|
||||
|
||||
await expectAsync(retriever.getGithubInfo(12345)).toBeRejectedWithError('No PR found in branch field: master');
|
||||
try {
|
||||
BUILD_INFO.branch = 'master';
|
||||
await retriever.getGithubInfo(12345);
|
||||
throw new Error('Exception Expected');
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual('No PR found in branch field: master');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -107,10 +110,12 @@ describe('BuildRetriever', () => {
|
||||
it('should fail if the artifact is too large', async () => {
|
||||
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS);
|
||||
retriever = new BuildRetriever(api, 10, DOWNLOAD_DIR);
|
||||
|
||||
await expectAsync(retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH)).
|
||||
toBeRejectedWith(jasmine.objectContaining({status: 413}));
|
||||
|
||||
try {
|
||||
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
||||
throw new Error('Exception Expected');
|
||||
} catch (error) {
|
||||
expect(error.status).toEqual(413);
|
||||
}
|
||||
artifactRequest.done();
|
||||
});
|
||||
|
||||
@ -138,40 +143,50 @@ describe('BuildRetriever', () => {
|
||||
artifactRequest.done();
|
||||
});
|
||||
|
||||
it('should fail if the CircleCI API fails', async () => {
|
||||
getBuildArtifactUrlSpy.and.rejectWith('getBuildArtifactUrl failed');
|
||||
await expectAsync(retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH)).
|
||||
toBeRejectedWithError('CircleCI artifact download failed (getBuildArtifactUrl failed)');
|
||||
it('should fail if the CircleCI API fails', async () => {
|
||||
try {
|
||||
getBuildArtifactUrlSpy.and.callFake(() => Promise.reject('getBuildArtifactUrl failed'));
|
||||
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
||||
throw new Error('Exception Expected');
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual('CircleCI artifact download failed (getBuildArtifactUrl failed)');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail if the URL fetch errors', async () => {
|
||||
it('should fail if the URL fetch errors', async () => {
|
||||
// create a new handler that errors
|
||||
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).replyWithError('Artifact Request Failed');
|
||||
|
||||
await expectAsync(retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH)).toBeRejectedWithError(
|
||||
'CircleCI artifact download failed ' +
|
||||
try {
|
||||
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
||||
throw new Error('Exception Expected');
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual('CircleCI artifact download failed ' +
|
||||
'(request to http://test.com/some/path/build.zip failed, reason: Artifact Request Failed)');
|
||||
|
||||
}
|
||||
artifactRequest.done();
|
||||
});
|
||||
|
||||
it('should fail if the URL fetch 404s', async () => {
|
||||
it('should fail if the URL fetch 404s', async () => {
|
||||
// create a new handler that errors
|
||||
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(404, 'No such artifact');
|
||||
|
||||
await expectAsync(retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH)).
|
||||
toBeRejectedWithError('CircleCI artifact download failed (Error 404 - Not Found)');
|
||||
|
||||
try {
|
||||
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
||||
throw new Error('Exception Expected');
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual('CircleCI artifact download failed (Error 404 - Not Found)');
|
||||
}
|
||||
artifactRequest.done();
|
||||
});
|
||||
|
||||
it('should fail if file write fails', async () => {
|
||||
it('should fail if file write fails', async () => {
|
||||
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS);
|
||||
WRITEFILE_RESULT = 'Test Error';
|
||||
|
||||
await expectAsync(retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH)).
|
||||
toBeRejectedWithError('CircleCI artifact download failed (Test Error)');
|
||||
|
||||
try {
|
||||
WRITEFILE_RESULT = 'Test Error';
|
||||
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
||||
throw new Error('Exception Expected');
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual('CircleCI artifact download failed (Test Error)');
|
||||
}
|
||||
artifactRequest.done();
|
||||
});
|
||||
});
|
||||
|
@ -51,10 +51,7 @@ describe('BuildVerifier', () => {
|
||||
describe('getSignificantFilesChanged', () => {
|
||||
it('should return false if none of the fetched files match the given pattern', async () => {
|
||||
const fetchFilesSpy = spyOn(prs, 'fetchFiles');
|
||||
fetchFilesSpy.and.resolveTo([
|
||||
{filename: 'a/b/c', sha: 'a1'},
|
||||
{filename: 'd/e/f', sha: 'b2'},
|
||||
]);
|
||||
fetchFilesSpy.and.callFake(() => Promise.resolve([{filename: 'a/b/c'}, {filename: 'd/e/f'}]));
|
||||
expect(await bv.getSignificantFilesChanged(777, /^x/)).toEqual(false);
|
||||
expect(fetchFilesSpy).toHaveBeenCalledWith(777);
|
||||
|
||||
@ -81,30 +78,37 @@ describe('BuildVerifier', () => {
|
||||
user: {login: 'username'},
|
||||
};
|
||||
|
||||
prsFetchSpy = spyOn(GithubPullRequests.prototype, 'fetch').and.resolveTo(mockPrInfo);
|
||||
teamsIsMemberBySlugSpy = spyOn(GithubTeams.prototype, 'isMemberBySlug').and.resolveTo(true);
|
||||
prsFetchSpy = spyOn(GithubPullRequests.prototype, 'fetch').
|
||||
and.callFake(() => Promise.resolve(mockPrInfo));
|
||||
|
||||
teamsIsMemberBySlugSpy = spyOn(GithubTeams.prototype, 'isMemberBySlug').
|
||||
and.callFake(() => Promise.resolve(true));
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', async () => {
|
||||
it('should return a promise', done => {
|
||||
const promise = bv.getPrIsTrusted(pr);
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `GithubTeams#isMemberBySlug()`.
|
||||
|
||||
// Do not complete the test (and release the spies) synchronously to avoid running the actual
|
||||
// `GithubTeams#isMemberBySlug()`.
|
||||
await promise;
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should fetch the corresponding PR', async () => {
|
||||
await bv.getPrIsTrusted(pr);
|
||||
expect(prsFetchSpy).toHaveBeenCalledWith(pr);
|
||||
it('should fetch the corresponding PR', done => {
|
||||
bv.getPrIsTrusted(pr).then(() => {
|
||||
expect(prsFetchSpy).toHaveBeenCalledWith(pr);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if fetching the PR errors', async () => {
|
||||
prsFetchSpy.and.rejectWith('Test');
|
||||
await expectAsync(bv.getPrIsTrusted(pr)).toBeRejectedWith('Test');
|
||||
it('should fail if fetching the PR errors', done => {
|
||||
prsFetchSpy.and.callFake(() => Promise.reject('Test'));
|
||||
bv.getPrIsTrusted(pr).catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -113,14 +117,19 @@ describe('BuildVerifier', () => {
|
||||
beforeEach(() => mockPrInfo.labels.push({name: 'trusted: pr-label'}));
|
||||
|
||||
|
||||
it('should resolve to true', async () => {
|
||||
await expectAsync(bv.getPrIsTrusted(pr)).toBeResolvedTo(true);
|
||||
it('should resolve to true', done => {
|
||||
bv.getPrIsTrusted(pr).then(isTrusted => {
|
||||
expect(isTrusted).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not try to verify the author\'s membership status', async () => {
|
||||
await expectAsync(bv.getPrIsTrusted(pr));
|
||||
expect(teamsIsMemberBySlugSpy).not.toHaveBeenCalled();
|
||||
it('should not try to verify the author\'s membership status', done => {
|
||||
bv.getPrIsTrusted(pr).then(() => {
|
||||
expect(teamsIsMemberBySlugSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@ -128,27 +137,40 @@ describe('BuildVerifier', () => {
|
||||
|
||||
describe('when the PR does not have the "trusted PR" label', () => {
|
||||
|
||||
it('should verify the PR author\'s membership in the specified teams', async () => {
|
||||
await bv.getPrIsTrusted(pr);
|
||||
expect(teamsIsMemberBySlugSpy).toHaveBeenCalledWith('username', ['team1', 'team2']);
|
||||
it('should verify the PR author\'s membership in the specified teams', done => {
|
||||
bv.getPrIsTrusted(pr).then(() => {
|
||||
expect(teamsIsMemberBySlugSpy).toHaveBeenCalledWith('username', ['team1', 'team2']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if verifying membership errors', async () => {
|
||||
teamsIsMemberBySlugSpy.and.rejectWith('Test');
|
||||
await expectAsync(bv.getPrIsTrusted(pr)).toBeRejectedWith('Test');
|
||||
it('should fail if verifying membership errors', done => {
|
||||
teamsIsMemberBySlugSpy.and.callFake(() => Promise.reject('Test'));
|
||||
bv.getPrIsTrusted(pr).catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve to true if the PR\'s author is a member', async () => {
|
||||
teamsIsMemberBySlugSpy.and.resolveTo(true);
|
||||
await expectAsync(bv.getPrIsTrusted(pr)).toBeResolvedTo(true);
|
||||
it('should resolve to true if the PR\'s author is a member', done => {
|
||||
teamsIsMemberBySlugSpy.and.callFake(() => Promise.resolve(true));
|
||||
|
||||
bv.getPrIsTrusted(pr).then(isTrusted => {
|
||||
expect(isTrusted).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve to false if the PR\'s author is not a member', async () => {
|
||||
teamsIsMemberBySlugSpy.and.resolveTo(false);
|
||||
await expectAsync(bv.getPrIsTrusted(pr)).toBeResolvedTo(false);
|
||||
it('should resolve to false if the PR\'s author is not a member', done => {
|
||||
teamsIsMemberBySlugSpy.and.callFake(() => Promise.resolve(false));
|
||||
|
||||
bv.getPrIsTrusted(pr).then(isTrusted => {
|
||||
expect(isTrusted).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -0,0 +1,11 @@
|
||||
import {PreviewServerError} from '../../lib/preview-server/preview-error';
|
||||
|
||||
export const expectToBePreviewServerError = (actual: PreviewServerError, status?: number, message?: string) => {
|
||||
expect(actual).toEqual(jasmine.any(PreviewServerError));
|
||||
if (status != null) {
|
||||
expect(actual.status).toBe(status);
|
||||
}
|
||||
if (message != null) {
|
||||
expect(actual.message).toBe(message);
|
||||
}
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
declare module jasmine {
|
||||
interface AsyncMatchers {
|
||||
toBeRejectedWithPreviewServerError(status: number, message?: string | RegExp): Promise<void>;
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
import {PreviewServerError} from '../../lib/preview-server/preview-error';
|
||||
|
||||
|
||||
// Matchers
|
||||
const toBeRejectedWithPreviewServerError: jasmine.CustomAsyncMatcherFactory = () => {
|
||||
return {
|
||||
async compare(actualPromise: Promise<never>, expectedStatus: number, expectedMessage?: string | RegExp) {
|
||||
if (!(actualPromise instanceof Promise)) {
|
||||
throw new Error(`Expected '${toBeRejectedWithPreviewServerError.name}()' to be called on a promise.`);
|
||||
}
|
||||
|
||||
try {
|
||||
await actualPromise;
|
||||
|
||||
return {
|
||||
pass: false,
|
||||
message: `Expected a promise to be rejected with a '${PreviewServerError.name}', but it was resolved.`,
|
||||
};
|
||||
} catch (actualError) {
|
||||
const actualPrintValue = stringify(actualError);
|
||||
const expectedPrintValue =
|
||||
stringify(new PreviewServerError(expectedStatus, expectedMessage && `${expectedMessage}`));
|
||||
|
||||
const pass = errorMatches(actualError, expectedStatus, expectedMessage);
|
||||
const message =
|
||||
`Expected a promise ${pass ? 'not ' : ''}to be rejected with ${expectedPrintValue}, but is was` +
|
||||
`${pass ? '' : ` rejected with ${actualPrintValue}`}.`;
|
||||
|
||||
return {pass, message};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Helpers
|
||||
function errorMatches(actualErr: unknown, expectedStatus: number, expectedMsg?: string | RegExp): boolean {
|
||||
if (!(actualErr instanceof PreviewServerError)) return false;
|
||||
if (actualErr.status !== expectedStatus) return false;
|
||||
return messageMatches(actualErr.message, expectedMsg);
|
||||
}
|
||||
|
||||
function messageMatches(actualMsg: string, expectedMsg?: string | RegExp): boolean {
|
||||
if (typeof expectedMsg === 'undefined') return true;
|
||||
if (typeof expectedMsg === 'string') return actualMsg === expectedMsg;
|
||||
return expectedMsg.test(actualMsg);
|
||||
}
|
||||
|
||||
function stringify(value: unknown): string {
|
||||
if (value instanceof PreviewServerError) {
|
||||
return `${PreviewServerError.name}(${value.status}${value.message ? `, ${value.message}` : ''})`;
|
||||
}
|
||||
|
||||
return jasmine.pp(value);
|
||||
}
|
||||
};
|
||||
|
||||
// Exports
|
||||
export const customAsyncMatchers: jasmine.CustomAsyncMatcherFactories = {
|
||||
toBeRejectedWithPreviewServerError,
|
||||
};
|
@ -9,8 +9,8 @@ describe('PreviewServerError', () => {
|
||||
|
||||
|
||||
it('should extend Error', () => {
|
||||
expect(err).toBeInstanceOf(PreviewServerError);
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
expect(err).toEqual(jasmine.any(PreviewServerError));
|
||||
expect(err).toEqual(jasmine.any(Error));
|
||||
|
||||
expect(Object.getPrototypeOf(err)).toBe(PreviewServerError.prototype);
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
// Imports
|
||||
import * as express from 'express';
|
||||
import * as http from 'http';
|
||||
import * as supertest from 'supertest';
|
||||
import {CircleCiApi} from '../../lib/common/circle-ci-api';
|
||||
@ -133,7 +134,7 @@ describe('PreviewServerFactory', () => {
|
||||
const buildCreator = jasmine.any(BuildCreator);
|
||||
expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(buildRetriever, buildVerifier, buildCreator, defaultConfig);
|
||||
|
||||
const middleware = usfCreateMiddlewareSpy.calls.mostRecent().returnValue;
|
||||
const middleware: express.Express = usfCreateMiddlewareSpy.calls.mostRecent().returnValue;
|
||||
expect(httpCreateServerSpy).toHaveBeenCalledWith(middleware);
|
||||
});
|
||||
|
||||
@ -229,7 +230,7 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
expect(prsAddCommentSpy).toHaveBeenCalledTimes(2);
|
||||
expect(prs).toBe(allCalls[1].object);
|
||||
expect(prs).toBeInstanceOf(GithubPullRequests);
|
||||
expect(prs).toEqual(jasmine.any(GithubPullRequests));
|
||||
expect(prs.repoSlug).toBe('organisation/repo');
|
||||
});
|
||||
|
||||
@ -301,8 +302,9 @@ describe('PreviewServerFactory', () => {
|
||||
let bvGetSignificantFilesChangedSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
bvGetPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted').and.resolveTo(true);
|
||||
bvGetSignificantFilesChangedSpy = spyOn(buildVerifier, 'getSignificantFilesChanged').and.resolveTo(true);
|
||||
bvGetPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted').and.returnValue(Promise.resolve(true));
|
||||
bvGetSignificantFilesChangedSpy = spyOn(buildVerifier, 'getSignificantFilesChanged').
|
||||
and.returnValue(Promise.resolve(true));
|
||||
});
|
||||
|
||||
|
||||
@ -329,7 +331,7 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
|
||||
it('should respond appropriately if the PR did not touch any significant files', async () => {
|
||||
bvGetSignificantFilesChangedSpy.and.resolveTo(false);
|
||||
bvGetSignificantFilesChangedSpy.and.returnValue(Promise.resolve(false));
|
||||
|
||||
const expectedResponse = {canHavePublicPreview: false, reason: 'No significant files touched.'};
|
||||
const expectedLog = `PR:${pr} - Cannot have a public preview, because it did not touch any significant files.`;
|
||||
@ -343,7 +345,7 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
|
||||
it('should respond appropriately if the PR is not automatically verifiable as "trusted"', async () => {
|
||||
bvGetPrIsTrustedSpy.and.resolveTo(false);
|
||||
bvGetPrIsTrustedSpy.and.returnValue(Promise.resolve(false));
|
||||
|
||||
const expectedResponse = {canHavePublicPreview: false, reason: 'Not automatically verifiable as "trusted".'};
|
||||
const expectedLog =
|
||||
@ -370,7 +372,7 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
|
||||
it('should respond with error if `getSignificantFilesChanged()` fails', async () => {
|
||||
bvGetSignificantFilesChangedSpy.and.rejectWith('getSignificantFilesChanged error');
|
||||
bvGetSignificantFilesChangedSpy.and.callFake(() => Promise.reject('getSignificantFilesChanged error'));
|
||||
|
||||
await agent.get(url).expect(500, 'getSignificantFilesChanged error');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Previewability check error', 'getSignificantFilesChanged error');
|
||||
@ -378,10 +380,11 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
|
||||
it('should respond with error if `getPrIsTrusted()` fails', async () => {
|
||||
bvGetPrIsTrustedSpy.and.throwError('getPrIsTrusted error');
|
||||
const error = new Error('getPrIsTrusted error');
|
||||
bvGetPrIsTrustedSpy.and.callFake(() => { throw error; });
|
||||
|
||||
await agent.get(url).expect(500, 'getPrIsTrusted error');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Previewability check error', new Error('getPrIsTrusted error'));
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Previewability check error', error);
|
||||
});
|
||||
|
||||
});
|
||||
@ -494,7 +497,7 @@ describe('PreviewServerFactory', () => {
|
||||
// Note it is important to put the `reject` into `and.callFake`;
|
||||
// If you just `and.returnValue` the rejected promise
|
||||
// then you get an "unhandled rejection" message in the console.
|
||||
getGithubInfoSpy.and.rejectWith('Test Error');
|
||||
getGithubInfoSpy.and.callFake(() => Promise.reject('Test Error'));
|
||||
await agent.post(URL).send(BASIC_PAYLOAD).expect(500, 'Test Error');
|
||||
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
|
||||
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
|
||||
@ -515,7 +518,7 @@ describe('PreviewServerFactory', () => {
|
||||
});
|
||||
|
||||
it('should fail if the artifact fetch request fails', async () => {
|
||||
downloadBuildArtifactSpy.and.rejectWith('Test Error');
|
||||
downloadBuildArtifactSpy.and.callFake(() => Promise.reject('Test Error'));
|
||||
await agent.post(URL).send(BASIC_PAYLOAD).expect(500, 'Test Error');
|
||||
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
|
||||
expect(downloadBuildArtifactSpy).toHaveBeenCalled();
|
||||
@ -524,7 +527,7 @@ describe('PreviewServerFactory', () => {
|
||||
});
|
||||
|
||||
it('should fail if verifying the PR fails', async () => {
|
||||
getPrIsTrustedSpy.and.rejectWith('Test Error');
|
||||
getPrIsTrustedSpy.and.callFake(() => Promise.reject('Test Error'));
|
||||
await agent.post(URL).send(BASIC_PAYLOAD).expect(500, 'Test Error');
|
||||
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
|
||||
expect(downloadBuildArtifactSpy).toHaveBeenCalled();
|
||||
@ -533,7 +536,7 @@ describe('PreviewServerFactory', () => {
|
||||
});
|
||||
|
||||
it('should fail if creating the preview build fails', async () => {
|
||||
createBuildSpy.and.rejectWith('Test Error');
|
||||
createBuildSpy.and.callFake(() => Promise.reject('Test Error'));
|
||||
await agent.post(URL).send(BASIC_PAYLOAD).expect(500, 'Test Error');
|
||||
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
|
||||
expect(downloadBuildArtifactSpy).toHaveBeenCalled();
|
||||
@ -602,7 +605,7 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
|
||||
it('should propagate errors from BuildVerifier', async () => {
|
||||
bvGetPrIsTrustedSpy.and.rejectWith('Test');
|
||||
bvGetPrIsTrustedSpy.and.callFake(() => Promise.reject('Test'));
|
||||
|
||||
await createRequest(+pr).expect(500, 'Test');
|
||||
|
||||
@ -612,9 +615,7 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
|
||||
it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', async () => {
|
||||
bvGetPrIsTrustedSpy.
|
||||
withArgs(24).and.resolveTo(false).
|
||||
withArgs(42).and.resolveTo(true);
|
||||
bvGetPrIsTrustedSpy.and.callFake((pr2: number) => Promise.resolve(pr2 === 42));
|
||||
|
||||
await createRequest(24);
|
||||
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(24, false);
|
||||
@ -625,7 +626,7 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
|
||||
it('should propagate errors from BuildCreator', async () => {
|
||||
bcUpdatePrVisibilitySpy.and.rejectWith('Test');
|
||||
bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject('Test'));
|
||||
await createRequest(+pr).expect(500, 'Test');
|
||||
});
|
||||
|
||||
@ -633,9 +634,7 @@ describe('PreviewServerFactory', () => {
|
||||
describe('on success', () => {
|
||||
|
||||
it('should respond with 200 (action: undefined)', async () => {
|
||||
bvGetPrIsTrustedSpy.
|
||||
withArgs(2).and.resolveTo(false).
|
||||
withArgs(4).and.resolveTo(true);
|
||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
|
||||
const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200]));
|
||||
await Promise.all(reqs);
|
||||
@ -643,9 +642,7 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
|
||||
it('should respond with 200 (action: labeled)', async () => {
|
||||
bvGetPrIsTrustedSpy.
|
||||
withArgs(2).and.resolveTo(false).
|
||||
withArgs(4).and.resolveTo(true);
|
||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
|
||||
const reqs = [4, 2].map(num => createRequest(num, 'labeled').expect(200, http.STATUS_CODES[200]));
|
||||
await Promise.all(reqs);
|
||||
@ -653,9 +650,7 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
|
||||
it('should respond with 200 (action: unlabeled)', async () => {
|
||||
bvGetPrIsTrustedSpy.
|
||||
withArgs(2).and.resolveTo(false).
|
||||
withArgs(4).and.resolveTo(true);
|
||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
|
||||
const reqs = [4, 2].map(num => createRequest(num, 'unlabeled').expect(200, http.STATUS_CODES[200]));
|
||||
await Promise.all(reqs);
|
||||
|
@ -39,7 +39,7 @@ describe('preview-server/utils', () => {
|
||||
throwRequestError(505, 'ERROR MESSAGE', request);
|
||||
} catch (error) {
|
||||
caught = true;
|
||||
expect(error).toBeInstanceOf(PreviewServerError);
|
||||
expect(error).toEqual(jasmine.any(PreviewServerError));
|
||||
expect(error.status).toEqual(505);
|
||||
expect(error.message).toEqual(`ERROR MESSAGE in request: POST some.domain.com/path "The request body"`);
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -8,32 +8,10 @@ exitCode=0
|
||||
|
||||
|
||||
# Helpers
|
||||
function checkCert {
|
||||
local certPath=$1
|
||||
|
||||
if [[ ! -f "$certPath" ]]; then
|
||||
echo "Certificate '$certPath' does not exist. Skipping expiration check..."
|
||||
return
|
||||
fi
|
||||
|
||||
openssl x509 -checkend 0 -in "$certPath" -noout > /dev/null
|
||||
reportStatus "Certificate '$certPath'"
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo " [WARN]"
|
||||
echo " If you did not provide the certificate explicitly, try running the"
|
||||
echo " 'docker build' command again with the '--no-cache' option to generate"
|
||||
echo " a new self-signed certificate."
|
||||
fi
|
||||
}
|
||||
|
||||
function reportStatus {
|
||||
local lastExitCode=$?
|
||||
|
||||
echo "$1: $([[ $lastExitCode -eq 0 ]] && echo OK || echo NOT OK)"
|
||||
[[ $lastExitCode -eq 0 ]] || exitCode=1
|
||||
|
||||
return $lastExitCode
|
||||
}
|
||||
|
||||
|
||||
@ -50,16 +28,6 @@ for s in ${services[@]}; do
|
||||
done
|
||||
|
||||
|
||||
# Check SSL/TLS certificates expiration
|
||||
certs=(
|
||||
"$AIO_LOCALCERTS_DIR/$AIO_DOMAIN_NAME.crt"
|
||||
"$TEST_AIO_LOCALCERTS_DIR/$TEST_AIO_DOMAIN_NAME.crt"
|
||||
)
|
||||
for c in ${certs[@]}; do
|
||||
checkCert $c
|
||||
done
|
||||
|
||||
|
||||
# Check servers
|
||||
origins=(
|
||||
http://$AIO_PREVIEW_SERVER_HOSTNAME:$AIO_PREVIEW_SERVER_PORT
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
## Create `aio-builds` persistent disk (if not already exists)
|
||||
- Follow instructions [here](https://cloud.google.com/compute/docs/disks/add-persistent-disk#create_disk).
|
||||
- `sudo mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/disk/by-id/google-aio-builds`
|
||||
- `sudo mkfs.ext4 -F -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/disk/by-id/google-aio-builds`
|
||||
|
||||
|
||||
## Mount disk
|
||||
@ -14,7 +14,7 @@
|
||||
|
||||
## Mount disk on boot
|
||||
- Run:
|
||||
```sh
|
||||
echo UUID=`sudo blkid -s UUID -o value /dev/disk/by-id/google-aio-builds` \
|
||||
/mnt/disks/aio-builds ext4 defaults,discard,nofail 0 2 | sudo tee -a /etc/fstab
|
||||
```
|
||||
echo UUID=`sudo blkid -s UUID -o value /dev/disk/by-id/google-aio-builds` \
|
||||
/mnt/disks/aio-builds ext4 discard,defaults,nofail 0 2 | sudo tee -a /etc/fstab
|
||||
```
|
||||
|
@ -1,11 +1,10 @@
|
||||
# VM setup - Create docker image
|
||||
|
||||
|
||||
## Install git, Node.js and yarn
|
||||
- `sudo apt-get update`
|
||||
- `sudo apt-get install -y git`
|
||||
- Install the latest stable version of [Node.js](https://nodejs.org/en/download).
|
||||
- Install the latest stable version of [yarn](https://classic.yarnpkg.com/en/docs/install).
|
||||
## Install node and yarn
|
||||
- Install [nvm](https://github.com/creationix/nvm#installation).
|
||||
- Install node.js: `nvm install 8`
|
||||
- Install yarn: `npm -g install yarn`
|
||||
|
||||
|
||||
## Checkout repository
|
||||
@ -17,11 +16,7 @@
|
||||
- You can overwrite the default environment variables inside the image, by passing new values using
|
||||
`--build-arg`.
|
||||
|
||||
**Note 1:** The script has to execute docker commands with `sudo`.
|
||||
|
||||
**Note 2:**
|
||||
The script has to execute `yarn` commands, so make sure `yarn` is on the `PATH` when invoking the
|
||||
script.
|
||||
**Note:** The script has to execute docker commands with `sudo`.
|
||||
|
||||
|
||||
## Example
|
||||
@ -31,7 +26,7 @@ The following commands would create a docker image from GitHub repo `foo/bar` to
|
||||
|
||||
- `git clone https://github.com/foo/bar.git foobar`
|
||||
- Run:
|
||||
```sh
|
||||
```
|
||||
./foobar/aio-builds-setup/scripts/create-image.sh foobar-builds \
|
||||
--build-arg AIO_REPO_SLUG=foo/bar \
|
||||
--build-arg AIO_DOMAIN_NAME=foobar-builds.io \
|
||||
|
@ -3,17 +3,24 @@
|
||||
|
||||
## Install docker
|
||||
|
||||
Official installation instructions: https://docs.docker.com/engine/install
|
||||
Example:
|
||||
_Debian (jessie):_
|
||||
- `sudo apt-get update`
|
||||
- `sudo apt-get install -y apt-transport-https ca-certificates curl git software-properties-common`
|
||||
- `curl -fsSL https://apt.dockerproject.org/gpg | sudo apt-key add -`
|
||||
- `apt-key fingerprint 58118E89F3A912897C070ADBF76221572C52609D`
|
||||
- `sudo add-apt-repository "deb https://apt.dockerproject.org/repo/ debian-$(lsb_release -cs) main"`
|
||||
- `sudo apt-get update`
|
||||
- `sudo apt-get -y install docker-engine`
|
||||
|
||||
_Debian (buster):_
|
||||
_Ubuntu (16.04):_
|
||||
- `sudo apt-get update`
|
||||
- `sudo apt-get install -y apt-transport-https ca-certificates curl gnupg-agent software-properties-common`
|
||||
- `curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -`
|
||||
- `sudo apt-key fingerprint 0EBFCD88`
|
||||
- `sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"`
|
||||
- `sudo apt-get install -y curl git linux-image-extra-$(uname -r) linux-image-extra-virtual`
|
||||
- `sudo apt-get install -y apt-transport-https ca-certificates`
|
||||
- `curl -fsSL https://yum.dockerproject.org/gpg | sudo apt-key add -`
|
||||
- `apt-key fingerprint 58118E89F3A912897C070ADBF76221572C52609D`
|
||||
- `sudo add-apt-repository "deb https://apt.dockerproject.org/repo/ ubuntu-$(lsb_release -cs) main"`
|
||||
- `sudo apt-get update`
|
||||
- `sudo apt-get -y install docker-ce docker-ce-cli containerd.io`
|
||||
- `sudo apt-get -y install docker-engine`
|
||||
|
||||
|
||||
## Start the docker
|
||||
|
@ -8,16 +8,16 @@ VM host to update the preview server based on changes in the source code.
|
||||
|
||||
The script will pull the latest changes from the origin's master branch and examine if there have
|
||||
been any changes in files inside the preview server source code directory (see below). If there are,
|
||||
it will create a new image and verify that it works as expected. Finally, it will stop and remove
|
||||
it will create a new image and verify that is works as expected. Finally, it will stop and remove
|
||||
the old docker container and image, create a new container based on the new image and start it.
|
||||
|
||||
The script assumes that the preview server source code is in the repository's
|
||||
`aio/aio-builds-setup/` directory and expects the following inputs:
|
||||
|
||||
- **$1**: `HOST_REPO_DIR`
|
||||
- **$2**: `HOST_SECRETS_DIR`
|
||||
- **$3**: `HOST_BUILDS_DIR`
|
||||
- **$4**: `HOST_LOCALCERTS_DIR`
|
||||
- **$2**: `HOST_LOCALCERTS_DIR`
|
||||
- **$3**: `HOST_SECRETS_DIR`
|
||||
- **$4**: `HOST_BUILDS_DIR`
|
||||
- **$5**: `HOST_LOGS_DIR`
|
||||
|
||||
See [here](vm-setup--create-host-dirs-and-files.md) for more info on what each input directory is
|
||||
@ -25,38 +25,28 @@ used for.
|
||||
|
||||
**Note 1:** The script has to execute docker commands with `sudo`.
|
||||
|
||||
**Note 2:**
|
||||
The script has to execute `yarn` commands, so make sure `yarn` is on the `PATH` when invoking the
|
||||
script.
|
||||
|
||||
**Note 3:** Make sure the user that executes the script has access to update the repository.
|
||||
**Note 2:** Make sure the user that executes the script has access to update the repository
|
||||
|
||||
|
||||
## Run the script manually
|
||||
You may choose to manually run the script, when necessary. Example:
|
||||
|
||||
```sh
|
||||
```
|
||||
update-preview-server.sh \
|
||||
/path/to/repo \
|
||||
/path/to/localcerts \
|
||||
/path/to/secrets \
|
||||
/path/to/builds \
|
||||
/path/to/localcerts \
|
||||
/path/to/logs
|
||||
```
|
||||
|
||||
|
||||
## Run the script automatically
|
||||
You may choose to automatically trigger the script, e.g. using a cronjob. For example, the following
|
||||
cronjob entry would run the script every 30 minutes, update the preview server (if necessary) and
|
||||
log its output to `update-preview-server.log` (assuming the user has the necessary permissions):
|
||||
cronjob entry would run the script every hour and update the preview server (assuming the user has
|
||||
the necessary permissions):
|
||||
|
||||
```
|
||||
# Periodically check for changes and update the preview server (if necessary)
|
||||
*/30 * * * * /path/to/update-preview-server.sh /path/to/repo /path/to/secrets /path/to/builds /path/to/localcerts /path/to/logs >> /path/to/update-preview-server.log 2>&1
|
||||
*/30 * * * * /path/to/update-preview-server.sh /path/to/repo /path/to/localcerts /path/to/secrets /path/to/builds /path/to/logs
|
||||
```
|
||||
|
||||
**Note:**
|
||||
Keep in mind that cron jobs run in non-interactive, non-login shells. This means that the execution
|
||||
context might be different compared to when running the same commands from an interactive, login
|
||||
shell. For example, `.bashrc` files are normally _not_ sourced automatically in cron jobs. See
|
||||
[here](http://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html) for more info.
|
||||
|
@ -7,14 +7,13 @@ echo -e "\n\n[`date`] - Updating the preview server..."
|
||||
|
||||
# Input
|
||||
readonly HOST_REPO_DIR=$1
|
||||
readonly HOST_SECRETS_DIR=$2
|
||||
readonly HOST_BUILDS_DIR=$3
|
||||
readonly HOST_LOCALCERTS_DIR=$4
|
||||
readonly HOST_LOCALCERTS_DIR=$2
|
||||
readonly HOST_SECRETS_DIR=$3
|
||||
readonly HOST_BUILDS_DIR=$4
|
||||
readonly HOST_LOGS_DIR=$5
|
||||
|
||||
# Constants
|
||||
readonly PROVISIONAL_TAG=provisional
|
||||
readonly PROVISIONAL_IMAGE_NAME=aio-builds:$PROVISIONAL_TAG
|
||||
readonly PROVISIONAL_IMAGE_NAME=aio-builds:provisional
|
||||
readonly LATEST_IMAGE_NAME=aio-builds:latest
|
||||
readonly CONTAINER_NAME=aio
|
||||
|
||||
@ -31,7 +30,7 @@ readonly CONTAINER_NAME=aio
|
||||
# Do not update the server unless files inside `aio-builds-setup/` have changed
|
||||
# or the last attempt failed (identified by the provisional image still being around).
|
||||
readonly relevantChangedFilesCount=$(git diff --name-only $lastDeployedCommit...HEAD | grep -P "^aio/aio-builds-setup/" | wc -l)
|
||||
readonly lastAttemptFailed=$(sudo docker image ls | grep "$PROVISIONAL_TAG" >> /dev/fd/3 && echo "true" || echo "false")
|
||||
readonly lastAttemptFailed=$(sudo docker rmi "$PROVISIONAL_IMAGE_NAME" >> /dev/fd/3 && echo "true" || echo "false")
|
||||
if [[ $relevantChangedFilesCount -eq 0 ]] && [[ "$lastAttemptFailed" != "true" ]]; then
|
||||
echo "Skipping update because no relevant files have been touched."
|
||||
exit 0
|
||||
@ -61,9 +60,9 @@ readonly CONTAINER_NAME=aio
|
||||
--publish 80:80 \
|
||||
--publish 443:443 \
|
||||
--restart unless-stopped \
|
||||
--volume $HOST_LOCALCERTS_DIR:/etc/ssl/localcerts:ro \
|
||||
--volume $HOST_SECRETS_DIR:/aio-secrets:ro \
|
||||
--volume $HOST_BUILDS_DIR:/var/www/aio-builds \
|
||||
--volume $HOST_LOCALCERTS_DIR:/etc/ssl/localcerts:ro \
|
||||
--volume $HOST_LOGS_DIR:/var/log/aio \
|
||||
"$LATEST_IMAGE_NAME"
|
||||
|
||||
|
@ -109,3 +109,9 @@ Options that specify files can be given as absolute paths, or as paths relative
|
||||
The [ng generate](cli/generate) and [ng add](cli/add) commands take as an argument the artifact or library to be generated or added to the current project.
|
||||
In addition to any general options, each artifact or library defines its own options in a *schematic*.
|
||||
Schematic options are supplied to the command in the same format as immediate command options.
|
||||
|
||||
|
||||
### Building with Bazel
|
||||
|
||||
Optionally, you can configure the Angular CLI to use [Bazel](https://docs.bazel.build) as the build tool. For more information, see [Building with Bazel](guide/bazel).
|
||||
|
||||
|
@ -3,7 +3,6 @@ import {
|
||||
AfterContentInit,
|
||||
AfterViewChecked,
|
||||
AfterViewInit,
|
||||
Directive,
|
||||
DoCheck,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
@ -16,8 +15,7 @@ import { LoggerService } from './logger.service';
|
||||
let nextId = 1;
|
||||
|
||||
// #docregion ngOnInit
|
||||
@Directive()
|
||||
export class PeekABooDirective implements OnInit {
|
||||
export class PeekABoo implements OnInit {
|
||||
constructor(private logger: LoggerService) { }
|
||||
|
||||
// implement OnInit's `ngOnInit` method
|
||||
@ -36,7 +34,7 @@ export class PeekABooDirective implements OnInit {
|
||||
})
|
||||
// Don't HAVE to mention the Lifecycle Hook interfaces
|
||||
// unless we want typing and tool support.
|
||||
export class PeekABooComponent extends PeekABooDirective implements
|
||||
export class PeekABooComponent extends PeekABoo implements
|
||||
OnChanges, OnInit, DoCheck,
|
||||
AfterContentInit, AfterContentChecked,
|
||||
AfterViewInit, AfterViewChecked,
|
||||
|
@ -23,7 +23,7 @@ export class GreetingModule {
|
||||
// #enddocregion ctor
|
||||
|
||||
// #docregion for-root
|
||||
static forRoot(config: UserServiceConfig): ModuleWithProviders<GreetingModule> {
|
||||
static forRoot(config: UserServiceConfig): ModuleWithProviders {
|
||||
return {
|
||||
ngModule: GreetingModule,
|
||||
providers: [
|
||||
|
@ -1,42 +0,0 @@
|
||||
import { browser, element, by } from 'protractor';
|
||||
|
||||
describe('Router basic tutorial e2e tests', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
browser.get('');
|
||||
});
|
||||
|
||||
it('should display Angular Router Sample', () => {
|
||||
expect(element(by.css('h1')).getText()).toBe('Angular Router Sample');
|
||||
});
|
||||
|
||||
it('should display Crisis Center button', () => {
|
||||
expect(element.all(by.css('a')).get(0).getText()).toBe('Crisis Center');
|
||||
});
|
||||
|
||||
it('should display Heroes button', () => {
|
||||
expect(element.all(by.css('a')).get(1).getText()).toBe('Heroes');
|
||||
});
|
||||
|
||||
it('should display HEROES', () => {
|
||||
expect(element(by.css('h3')).getText()).toBe('HEROES');
|
||||
});
|
||||
|
||||
it('should change to display crisis list component', async () => {
|
||||
const crisisButton = element.all(by.css('a')).get(0);
|
||||
await crisisButton.click();
|
||||
expect(element(by.css('h3')).getText()).toBe('CRISIS CENTER');
|
||||
});
|
||||
|
||||
it('should change to display heroes component', async () => {
|
||||
const heroesButton = element.all(by.css('a')).get(1);
|
||||
await heroesButton.click();
|
||||
expect(element(by.css('h3')).getText()).toBe('HEROES');
|
||||
});
|
||||
|
||||
it('should use wildcard route', async () => {
|
||||
browser.get('/fake-page');
|
||||
expect(browser.getCurrentUrl()).toContain('fake-page');
|
||||
expect(element(by.css('h2')).getText()).toBe('Page Not Found');
|
||||
});
|
||||
});
|
@ -1,34 +0,0 @@
|
||||
.button {
|
||||
box-shadow: inset 0px 1px 0px 0px #ffffff;
|
||||
background: linear-gradient(to bottom, #ffffff 5%, #f6f6f6 100%);
|
||||
background-color: #ffffff;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #dcdcdc;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
color: #666666;
|
||||
font-family: Arial;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
padding: 6px 24px;
|
||||
text-decoration: none;
|
||||
text-shadow: 0px 1px 0px #ffffff;
|
||||
outline: 0;
|
||||
}
|
||||
.activebutton {
|
||||
box-shadow: inset 0px 1px 0px 0px #dcecfb;
|
||||
background: linear-gradient(to bottom, #bddbfa 5%, #80b5ea 100%);
|
||||
background-color: #bddbfa;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #84bbf3;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
color: #ffffff;
|
||||
font-family: Arial;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
padding: 6px 24px;
|
||||
text-decoration: none;
|
||||
text-shadow: 0px 1px 0px #528ecc;
|
||||
outline: 0;
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
<!-- #docplaster -->
|
||||
<!-- #docregion setup -->
|
||||
<h1>Angular Router Sample</h1>
|
||||
<!-- #enddocregion setup-->
|
||||
<!-- #docregion routeractivelink -->
|
||||
<nav>
|
||||
<a class="button" routerLink="/crisis-list" routerLinkActive="activebutton">Crisis Center</a> |
|
||||
<a class="button" routerLink="/heroes-list" routerLinkActive="activebutton">Heroes</a>
|
||||
</nav>
|
||||
<!-- #enddocregion routeractivelink-->
|
||||
<!-- #docregion router-outlet -->
|
||||
<router-outlet></router-outlet>
|
||||
<!-- #enddocregion router-outlet -->
|
||||
|
||||
<div style="display: none;">
|
||||
<!-- This HTML represents the initial state for the tutorial. It is not intended to appear in the app. -->
|
||||
<!-- #docregion setup, components -->
|
||||
<app-crisis-list></app-crisis-list>
|
||||
<app-heroes-list></app-heroes-list>
|
||||
<!-- #enddocregion setup, components -->
|
||||
|
||||
<!-- This HTML snippet is for when the user first adds the routerlink navigation. -->
|
||||
<!-- #docregion nav -->
|
||||
<nav>
|
||||
<a class="button" routerLink="/crisis-list">Crisis Center</a> |
|
||||
<a class="button" routerLink="/heroes-list">Heroes</a>
|
||||
</nav>
|
||||
<!-- #enddocregion nav-->
|
||||
|
||||
|
||||
</div>
|
@ -1,10 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'angular-router-sample';
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
// #docplaster
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
// #docregion router-import
|
||||
import { RouterModule } from '@angular/router';
|
||||
// #enddocregion router-import
|
||||
import { AppComponent } from './app.component';
|
||||
import { CrisisListComponent } from './crisis-list/crisis-list.component';
|
||||
import { HeroesListComponent } from './heroes-list/heroes-list.component';
|
||||
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
CrisisListComponent,
|
||||
HeroesListComponent,
|
||||
PageNotFoundComponent
|
||||
],
|
||||
// #docplaster
|
||||
// #docregion import-basic, import-redirect, import-wildcard
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule.forRoot([
|
||||
{path: 'crisis-list', component: CrisisListComponent},
|
||||
{path: 'heroes-list', component: HeroesListComponent},
|
||||
// #enddocregion import-basic
|
||||
{path: '', redirectTo: '/heroes-list', pathMatch: 'full'},
|
||||
// #enddocregion import-redirect
|
||||
{path: '**', component: PageNotFoundComponent}
|
||||
// #enddocregion import-wildcard
|
||||
// #docregion import-basic, import-redirect, import-wildcard
|
||||
]),
|
||||
],
|
||||
// #enddocregion import-basic, import-redirect, import-wildcard
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
@ -1,2 +0,0 @@
|
||||
<h3>CRISIS CENTER</h3>
|
||||
<p>Get your crisis here</p>
|
@ -1,10 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-crisis-list',
|
||||
templateUrl: './crisis-list.component.html',
|
||||
styleUrls: ['./crisis-list.component.css']
|
||||
})
|
||||
export class CrisisListComponent {
|
||||
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
<h3>HEROES</h3>
|
||||
<p>Get your heroes here</p>
|
@ -1,10 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-heroes-list',
|
||||
templateUrl: './heroes-list.component.html',
|
||||
styleUrls: ['./heroes-list.component.css']
|
||||
})
|
||||
export class HeroesListComponent {
|
||||
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
<h2>Page Not Found</h2>
|
||||
<p>We couldn't find that page! Not even with x-ray vision.</p>
|
@ -1,10 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page-not-found',
|
||||
templateUrl: './page-not-found.component.html',
|
||||
styleUrls: ['./page-not-found.component.css']
|
||||
})
|
||||
export class PageNotFoundComponent {
|
||||
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Angular Router Sample</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
@ -1,12 +0,0 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"description": "Router",
|
||||
"files":[
|
||||
"!**/*.d.ts",
|
||||
"!**/*.js",
|
||||
"!**/*.[0-9].*"
|
||||
],
|
||||
"tags": ["router-tutorial"]
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
// #docregion
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router';
|
||||
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@ -12,20 +12,21 @@ export class AuthGuard implements CanActivate {
|
||||
|
||||
canActivate(
|
||||
next: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot): true|UrlTree {
|
||||
state: RouterStateSnapshot): boolean {
|
||||
let url: string = state.url;
|
||||
|
||||
return this.checkLogin(url);
|
||||
}
|
||||
|
||||
checkLogin(url: string): true|UrlTree {
|
||||
checkLogin(url: string): boolean {
|
||||
if (this.authService.isLoggedIn) { return true; }
|
||||
|
||||
// Store the attempted URL for redirecting
|
||||
this.authService.redirectUrl = url;
|
||||
|
||||
// Redirect to the login page
|
||||
return this.router.parseUrl('/login');
|
||||
// Navigate to the login page with extras
|
||||
this.router.navigate(['/login']);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// #enddocregion
|
||||
|
@ -4,8 +4,7 @@ import {
|
||||
CanActivate, Router,
|
||||
ActivatedRouteSnapshot,
|
||||
RouterStateSnapshot,
|
||||
CanActivateChild,
|
||||
UrlTree
|
||||
CanActivateChild
|
||||
} from '@angular/router';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@ -17,7 +16,7 @@ export class AuthGuard implements CanActivate, CanActivateChild {
|
||||
|
||||
canActivate(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot): true|UrlTree {
|
||||
state: RouterStateSnapshot): boolean {
|
||||
let url: string = state.url;
|
||||
|
||||
return this.checkLogin(url);
|
||||
@ -25,19 +24,20 @@ export class AuthGuard implements CanActivate, CanActivateChild {
|
||||
|
||||
canActivateChild(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot): true|UrlTree {
|
||||
state: RouterStateSnapshot): boolean {
|
||||
return this.canActivate(route, state);
|
||||
}
|
||||
|
||||
// #enddocregion can-activate-child
|
||||
checkLogin(url: string): true|UrlTree {
|
||||
checkLogin(url: string): boolean {
|
||||
if (this.authService.isLoggedIn) { return true; }
|
||||
|
||||
// Store the attempted URL for redirecting
|
||||
this.authService.redirectUrl = url;
|
||||
|
||||
// Redirect to the login page
|
||||
return this.router.parseUrl('/login');
|
||||
// Navigate to the login page
|
||||
this.router.navigate(['/login']);
|
||||
return false;
|
||||
}
|
||||
// #docregion can-activate-child
|
||||
}
|
||||
|
@ -6,8 +6,7 @@ import {
|
||||
ActivatedRouteSnapshot,
|
||||
RouterStateSnapshot,
|
||||
CanActivateChild,
|
||||
NavigationExtras,
|
||||
UrlTree
|
||||
NavigationExtras
|
||||
} from '@angular/router';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@ -17,17 +16,17 @@ import { AuthService } from './auth.service';
|
||||
export class AuthGuard implements CanActivate, CanActivateChild {
|
||||
constructor(private authService: AuthService, private router: Router) {}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree {
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
|
||||
let url: string = state.url;
|
||||
|
||||
return this.checkLogin(url);
|
||||
}
|
||||
|
||||
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree {
|
||||
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
|
||||
return this.canActivate(route, state);
|
||||
}
|
||||
|
||||
checkLogin(url: string): true|UrlTree {
|
||||
checkLogin(url: string): boolean {
|
||||
if (this.authService.isLoggedIn) { return true; }
|
||||
|
||||
// Store the attempted URL for redirecting
|
||||
@ -43,7 +42,8 @@ export class AuthGuard implements CanActivate, CanActivateChild {
|
||||
fragment: 'anchor'
|
||||
};
|
||||
|
||||
// Redirect to the login page with extras
|
||||
return this.router.createUrlTree(['/login'], navigationExtras);
|
||||
// Navigate to the login page with extras
|
||||
this.router.navigate(['/login'], navigationExtras);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
// #docregion
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs';
|
||||
import {terser} from 'rollup-plugin-terser'
|
||||
import uglify from 'rollup-plugin-uglify'
|
||||
|
||||
//paths are relative to the execution path
|
||||
export default {
|
||||
@ -17,6 +17,6 @@ export default {
|
||||
commonjs({
|
||||
include: ['node_modules/rxjs/**']
|
||||
}),
|
||||
terser()
|
||||
uglify()
|
||||
]
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ to provide semantic meaning where it might otherwise be missing.
|
||||
Use [attribute binding](guide/template-syntax#attribute-binding) template syntax to control the values of accessibility-related attributes.
|
||||
|
||||
When binding to ARIA attributes in Angular, you must use the `attr.` prefix, as the ARIA
|
||||
specification depends specifically on HTML attributes rather than properties of DOM elements.
|
||||
specification depends specifically on HTML attributes rather than properties on DOM elements.
|
||||
|
||||
```html
|
||||
<!-- Use attr. when binding to an ARIA attribute -->
|
||||
@ -44,7 +44,7 @@ NOTE:
|
||||
|
||||
By convention, HTML attributes use lowercase names (`tabindex`), while properties use camelCase names (`tabIndex`).
|
||||
|
||||
See the [Template Syntax](guide/template-syntax#html-attribute-vs-dom-property) guide for more background on the difference between attributes and properties.
|
||||
See the [Template Syntax](https://angular.io/guide/template-syntax#html-attribute-vs-dom-property) guide for more background on the difference between attributes and properties.
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -8,7 +8,7 @@ This guide explains how to specify metadata and apply available compiler options
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
<a href="https://www.youtube.com/watch?v=anphffaCZrQ">Watch Alex Rickabaugh explain the Angular compiler</a> at AngularConnect 2019.
|
||||
<a href="https://www.youtube.com/watch?v=kW9cJsvcsGo">Watch compiler author Tobias Bosch explain the Angular compiler</a> at AngularConnect 2016.
|
||||
|
||||
</div>
|
||||
|
||||
@ -43,13 +43,33 @@ Here are some reasons you might want to use AOT.
|
||||
|
||||
Angular offers two ways to compile your application:
|
||||
|
||||
* **_Just-in-Time_ (JIT)**, which compiles your app in the browser at runtime. This was the default until Angular 8.
|
||||
* **_Ahead-of-Time_ (AOT)**, which compiles your app and libraries at build time. This is the default since Angular 9.
|
||||
* **_Just-in-Time_ (JIT)**, which compiles your app in the browser at runtime.
|
||||
* **_Ahead-of-Time_ (AOT)**, which compiles your app at build time.
|
||||
|
||||
When you run the [`ng build`](cli/build) (build only) or [`ng serve`](cli/serve) (build and serve locally) CLI commands, the type of compilation (JIT or AOT) depends on the value of the `aot` property in your build configuration specified in `angular.json`. By default, `aot` is set to `true` for new CLI apps.
|
||||
JIT compilation is the default when you run the [`ng build`](cli/build) (build only) or [`ng serve`](cli/serve) (build and serve locally) CLI commands:
|
||||
|
||||
<code-example language="sh" class="code-shell">
|
||||
ng build
|
||||
ng serve
|
||||
</code-example>
|
||||
|
||||
{@a compile}
|
||||
|
||||
For AOT compilation, include the `--aot` option with the `ng build` or `ng serve` command:
|
||||
|
||||
<code-example language="sh" class="code-shell">
|
||||
ng build --aot
|
||||
ng serve --aot
|
||||
</code-example>
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
The `ng build` command with the `--prod` meta-flag (`ng build --prod`) compiles with AOT by default.
|
||||
|
||||
See the [CLI command reference](cli) and [Building and serving Angular apps](guide/build) for more information.
|
||||
|
||||
</div>
|
||||
|
||||
## How AOT works
|
||||
|
||||
The Angular AOT compiler extracts **metadata** to interpret the parts of the application that Angular is supposed to manage.
|
||||
@ -542,7 +562,6 @@ It does not, however, rewrite the `.d.ts` file, so TypeScript doesn't recognize
|
||||
|
||||
|
||||
{@a binding-expression-validation}
|
||||
|
||||
## Phase 3: Template type checking
|
||||
|
||||
One of the Angular compiler's most helpful features is the ability to type-check expressions within templates, and catch any errors before they cause crashes at runtime.
|
||||
@ -560,7 +579,7 @@ As a result, templates that previously compiled under View Engine can fail type
|
||||
This stricter type checking is not enabled by default in version 9, but can be enabled by setting the `strictTemplates` configuration option.
|
||||
We do expect to make strict type checking the default in the future.
|
||||
|
||||
For more information about type-checking options, and about improvements to template type checking in version 9 and above, see [Template type checking](guide/template-typecheck).
|
||||
<!-- For more information about type-checking options, and about improvements to template type checking in version 9 and above, see [Template type checking](guide/template-type-checking). -->
|
||||
|
||||
</div>
|
||||
|
||||
@ -619,7 +638,16 @@ For example, to avoid `Object is possibly 'undefined'` error in the template abo
|
||||
|
||||
Using `*ngIf` allows the TypeScript compiler to infer that the `person` used in the binding expression will never be `undefined`.
|
||||
|
||||
For more information about input type narrowing, see [Input setter coercion](guide/template-typecheck#input-setter-coercion) and [Improving template type checking for custom directives](guide/structural-directives#directive-type-checks).
|
||||
#### Custom `ngIf` like directives
|
||||
|
||||
Directives that behave like `*ngIf` can declare that they want the same treatment by including a static member marker that is a signal to the template compiler to treat them like `*ngIf`. This static member for `*ngIf` is:
|
||||
|
||||
```typescript
|
||||
public static ngIfUseIfTypeGuard: void;
|
||||
```
|
||||
|
||||
This declares that the input property `ngIf` of the `NgIf` directive should be treated as a guard to the use of its template, implying that the template will only be instantiated if the `ngIf` input property is true.
|
||||
|
||||
|
||||
### Non-null type assertion operator
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Introduction to components and templates
|
||||
|
||||
A *component* controls a patch of screen called a [*view*](guide/glossary#view "Definition of view").
|
||||
For example, individual components define and control each of the following views from the [Tour of Heroes tutorial](tutorial):
|
||||
A *component* controls a patch of screen called a *view*.
|
||||
For example, individual components define and control each of the following views from the [Tutorial](tutorial):
|
||||
|
||||
* The app root with the navigation links.
|
||||
* The list of heroes.
|
||||
|
@ -52,7 +52,7 @@ For some platforms and applications, you might also want to use the PWA (Progres
|
||||
|
||||
## Support for the development cycle
|
||||
|
||||
The **Development Workflow** section describes the tools and processes you use to compile, test, and deploy Angular applications.
|
||||
The **Development Workflow** section describes the tools and processes you use to compile, test, and and deploy Angular applications.
|
||||
|
||||
* [CLI Command Reference](cli): The Angular CLI is a command-line tool that you use to create projects, generate application and library code, and perform a variety of ongoing development tasks such as testing, bundling, and deployment.
|
||||
|
||||
|
122
aio/content/guide/bazel.md
Normal file
122
aio/content/guide/bazel.md
Normal file
@ -0,0 +1,122 @@
|
||||
# Building with Bazel
|
||||
|
||||
This guide explains how to build and test Angular apps with Bazel.
|
||||
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
This guide assumes you are already familiar with developing and building Angular applications using the [CLI](cli).
|
||||
|
||||
It describes features which are part of Angular Labs, and are not considered a stable, supported API.
|
||||
|
||||
</div>
|
||||
|
||||
## Using Bazel with the Angular CLI
|
||||
|
||||
The `@angular/bazel` package provides a builder that allows Angular CLI to use Bazel as the build tool.
|
||||
|
||||
To opt-in an existing application, run
|
||||
|
||||
```sh
|
||||
ng add @angular/bazel
|
||||
```
|
||||
|
||||
To use Bazel in a new application, first install `@angular/bazel` globally
|
||||
|
||||
```sh
|
||||
npm install -g @angular/bazel
|
||||
```
|
||||
|
||||
then create the new application with
|
||||
|
||||
```sh
|
||||
ng new --collection=@angular/bazel
|
||||
```
|
||||
|
||||
Now when you use Angular CLI build commands such as `ng build` and `ng serve`,
|
||||
Bazel is used behind the scenes.
|
||||
Outputs from Bazel appear in the `dist/bin` folder.
|
||||
|
||||
> The command-line output includes extra logging from Bazel.
|
||||
> We plan to reduce this in the future.
|
||||
|
||||
### Removing Bazel
|
||||
|
||||
If you need to opt-out from using Bazel, you can restore the backup files:
|
||||
|
||||
- `/angular.json.bak` replaces `/angular.json`
|
||||
|
||||
## Advanced configuration
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Editing the Bazel configuration may prevent you opting out of Bazel.
|
||||
Custom behaviors driven by Bazel won't be available in other Builders.
|
||||
|
||||
This section assumes you are familiar with [Bazel](https://docs.bazel.build).
|
||||
|
||||
</div>
|
||||
|
||||
You can manually adjust the Bazel configuration to:
|
||||
|
||||
* customize the build steps
|
||||
* parallellize the build for scale and incrementality
|
||||
|
||||
Create the initial Bazel configuration files by running the following command:
|
||||
|
||||
```sh
|
||||
ng build --leaveBazelFilesOnDisk
|
||||
```
|
||||
|
||||
Now you'll find new files in the Angular workspace:
|
||||
|
||||
* `/WORKSPACE` tells Bazel how to download external dependencies.
|
||||
* `/BUILD.bazel` and `/src/BUILD.bazel` tell Bazel about your source code.
|
||||
|
||||
You can find a full-featured example with custom Bazel configurations at https://github.com/bazelbuild/rules_nodejs/tree/master/examples/angular.
|
||||
|
||||
Documentation for using Bazel for frontend projects is linked from https://docs.bazel.build/versions/master/bazel-and-javascript.html.
|
||||
|
||||
|
||||
|
||||
## Running Bazel directly
|
||||
|
||||
In some cases you'll want to bypass the Angular CLI builder, and run the Bazel CLI directly.
|
||||
The Bazel tool is managed by the `@bazel/bazelisk` package (similar to how Node.js can be managed by `nvm`).
|
||||
You can install it globally to get the `bazelisk` command in your path, or use `$(npm bin)/bazelisk` in place of bazelisk below.
|
||||
|
||||
The common commands in Bazel are:
|
||||
|
||||
* `bazelisk build [targets]`: Compile the default output artifacts of the given targets.
|
||||
* `bazelisk test [targets]`: For whichever `*_test` targets are found in the patterns, run the tests.
|
||||
* `bazelisk run [target]`: Compile the program represented by target, and then run it.
|
||||
|
||||
To repeat the command any time the inputs change (watch mode), replace `bazelisk` with `ibazel` in these commands.
|
||||
|
||||
The output locations are printed in the output.
|
||||
|
||||
Full documentation for the Bazel CLI is at https://docs.bazel.build/versions/master/command-line-reference.html.
|
||||
|
||||
|
||||
## Querying the build graph
|
||||
|
||||
Because Bazel constructs a graph out of your targets, you can find lots of useful information.
|
||||
|
||||
Using the graphviz optional dependency, you'll have a program `dot`, which you can use with `bazel query`:
|
||||
|
||||
```bash
|
||||
$ bazel query --output=graph ... | dot -Tpng > graph.png
|
||||
```
|
||||
|
||||
See https://docs.bazel.build/versions/master/query-how-to.html for more details on `bazel query`.
|
||||
|
||||
|
||||
## Customizing `BUILD.bazel` files
|
||||
|
||||
"Rules" are like plugins for Bazel. Many rule sets are available. This guide documents the ones maintained by the Angular team at Google.
|
||||
|
||||
Rules are used in `BUILD.bazel` files, which are markers for the packages in your workspace. Each `BUILD.bazel` file declares a separate package to Bazel, though you can have more coarse-grained distributions so that the packages you publish (for example, to `npm`) can be made up of many Bazel packages.
|
||||
|
||||
In the `BUILD.bazel` file, each rule must first be imported, using the `load` statement. Then the rule is called with some attributes, and the result of calling the rule is that you've declared to Bazel how it can derive some outputs given some inputs and dependencies. Then later, when you run a `bazel` command line, Bazel loads all the rules you've declared to determine an absolute ordering of what needs to be run. Note that only the rules needed to produce the requested output will actually be executed.
|
||||
|
||||
A list of common rules for frontend development is documented in the README at https://github.com/bazelbuild/rules_nodejs/.
|
@ -34,7 +34,7 @@ Angular supports most recent browsers. This includes the following specific vers
|
||||
</td>
|
||||
|
||||
<td>
|
||||
latest and extended support release (ESR)
|
||||
latest
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -53,18 +53,16 @@ Angular supports most recent browsers. This includes the following specific vers
|
||||
IE
|
||||
</td>
|
||||
<td>
|
||||
<div> 11, 10*, 9* ("compatibility view" mode not supported) </div>
|
||||
<div>*deprecated in v10, see the {@link guide/deprecations#ie-9-10-and-mobile deprecations guide}.</div>
|
||||
11, 10, 9 ("compatibility view" mode not supported)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr>
|
||||
<td>
|
||||
IE Mobile*
|
||||
IE Mobile
|
||||
</td>
|
||||
<td>
|
||||
11
|
||||
<div>*deprecated in v10, see the {@link guide/deprecations#ie-9-10-and-mobile deprecations guide}.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -55,7 +55,7 @@ This method is for development and testing only, and is not a supported or secur
|
||||
|
||||
### Automatic deployment with the CLI
|
||||
|
||||
The Angular CLI command `ng deploy` (introduced in version 8.3.0) executes the `deploy` [CLI builder](guide/cli-builder) associated with your project. A number of third-party builders implement deployment capabilities to different platforms. You can add any of them to your project by running `ng add [package name]`.
|
||||
The Angular CLI command `ng deploy` (introduced in version 8.3.0) executes the `deploy` [CLI builder](https://angular.io/guide/cli-builder) associated with your project. A number of third-party builders implement deployment capabilities to different platforms. You can add any of them to your project by running `ng add [package name]`.
|
||||
|
||||
When you add a package with deployment capability, it'll automatically update your workspace configuration (`angular.json` file) with a `deploy` section for the selected project. You can then use the `ng deploy` command to deploy that project.
|
||||
|
||||
@ -304,7 +304,7 @@ In addition to build optimizations, Angular also has a runtime production mode.
|
||||
|
||||
<code-example format="nocode">
|
||||
|
||||
Angular is running in development mode. Call enableProdMode() to enable production mode.
|
||||
Angular is running in the development mode. Call enableProdMode() to enable the production mode.
|
||||
|
||||
</code-example>
|
||||
|
||||
|
@ -35,24 +35,24 @@ v9 - v12
|
||||
|
||||
| Area | API or Feature | May be removed in |
|
||||
| ----------------------------- | --------------------------------------------------------------------------- | ----------------- |
|
||||
| `@angular/bazel` | [`Bazel builder and schematics`](#bazelbuilder) | v10 |
|
||||
| `@angular/common` | [`ReflectiveInjector`](#reflectiveinjector) | <!--v8--> v11 |
|
||||
| `@angular/common` | [`ReflectiveInjector`](#reflectiveinjector) | <!--v8--> v10 |
|
||||
| `@angular/common` | [`CurrencyPipe` - `DEFAULT_CURRENCY_CODE`](api/common/CurrencyPipe#currency-code-deprecation) | <!--v9--> v11 |
|
||||
| `@angular/core` | [`CollectionChangeRecord`](#core) | <!--v7--> v11 |
|
||||
| `@angular/core` | [`DefaultIterableDiffer`](#core) | <!--v7--> v11 |
|
||||
| `@angular/core` | [`ReflectiveKey`](#core) | <!--v8--> v11 |
|
||||
| `@angular/core` | [`RenderComponentType`](#core) | <!--v7--> v11 |
|
||||
| `@angular/core` | [`ViewEncapsulation.Native`](#core) | <!--v6--> v11 |
|
||||
| `@angular/core` | [`WrappedValue`](#core) | <!--v10--> v12 |
|
||||
| `@angular/forms` | [`ngModel` with reactive forms](#ngmodel-reactive) | <!--v6--> v11 |
|
||||
| `@angular/router` | [`preserveQueryParams`](#router) | <!--v7--> v11 |
|
||||
| `@angular/upgrade` | [`@angular/upgrade`](#upgrade) | <!--v8--> v11 |
|
||||
| `@angular/upgrade` | [`getAngularLib`](#upgrade-static) | <!--v8--> v11 |
|
||||
| `@angular/upgrade` | [`setAngularLib`](#upgrade-static) | <!--v8--> v11 |
|
||||
| `@angular/platform-webworker` | [All entry points](api/platform-webworker) | <!--v8--> v11 |
|
||||
| template syntax | [`<template`>](#template-tag) | <!--v7--> v11 |
|
||||
| polyfills | [reflect-metadata](#reflect-metadata) | <!--v8--> v11 |
|
||||
| npm package format | [`esm5` and `fesm5` entry-points in @angular/* npm packages](guide/deprecations#esm5-fesm5) | <!-- v9 --> v11 |
|
||||
| `@angular/core` | [`CollectionChangeRecord`](#core) | <!--v7--> v10 |
|
||||
| `@angular/core` | [`DefaultIterableDiffer`](#core) | <!--v7--> v10 |
|
||||
| `@angular/core` | [`ReflectiveKey`](#core) | <!--v8--> v10 |
|
||||
| `@angular/core` | [`RenderComponentType`](#core) | <!--v7--> v10 |
|
||||
| `@angular/core` | [`ViewEncapsulation.Native`](#core) | <!--v6--> v10 |
|
||||
| `@angular/core` | [`ModuleWithProviders` without a generic](#moduleWithProviders) | <!--v9--> v10 |
|
||||
| `@angular/core` | [Undecorated base classes that use Angular features](#undecorated-base-classes) | <!--v9--> v10 |
|
||||
| `@angular/forms` | [`ngModel` with reactive forms](#ngmodel-reactive) | <!--v6--> v10 |
|
||||
| `@angular/router` | [`preserveQueryParams`](#router) | <!--v7--> v10 |
|
||||
| `@angular/upgrade` | [`@angular/upgrade`](#upgrade) | <!--v8--> v10 |
|
||||
| `@angular/upgrade` | [`getAngularLib`](#upgrade-static) | <!--v8--> v10 |
|
||||
| `@angular/upgrade` | [`setAngularLib`](#upgrade-static) | <!--v8--> v10 |
|
||||
| `@angular/platform-webworker` | [All entry points](api/platform-webworker) | <!--v8--> v10 |
|
||||
| template syntax | [`<template`>](#template-tag) | <!--v7--> v10 |
|
||||
| polyfills | [reflect-metadata](#reflect-metadata) | <!--v8--> v10 |
|
||||
| npm package format | [`esm5` and `fesm5` entry-points in @angular/* npm packages](guide/deprecations#esm5-fesm5) | <!-- v9 --> v10 |
|
||||
| `@angular/core` | [`defineInjectable`](#core) | <!--v8--> v11 |
|
||||
| `@angular/core` | [`entryComponents`](api/core/NgModule#entryComponents) | <!--v9--> v11 |
|
||||
| `@angular/core` | [`ANALYZE_FOR_ENTRY_COMPONENTS`](api/core/ANALYZE_FOR_ENTRY_COMPONENTS) | <!--v9--> v11 |
|
||||
@ -60,7 +60,6 @@ v9 - v12
|
||||
| `@angular/core/testing` | [`TestBed.get`](#testing) | <!--v9--> v12 |
|
||||
| `@angular/router` | [`ActivatedRoute` params and `queryParams` properties](#activatedroute-props) | unspecified |
|
||||
| template syntax | [`/deep/`, `>>>`, and `::ng-deep`](#deep-component-style-selector) | <!--v7--> unspecified |
|
||||
| browser support | [`IE 9 and 10, IE mobile`](#ie-9-10-and-mobile) | <!--v10--> v11 |
|
||||
|
||||
|
||||
|
||||
@ -97,7 +96,9 @@ Tip: In the [API reference section](api) of this doc site, deprecated APIs are i
|
||||
| [`defineInjectable`](api/core/defineInjectable) | `ɵɵdefineInjectable` | v8 | Used only in generated code. No source code should depend on this API. |
|
||||
| [`entryComponents`](api/core/NgModule#entryComponents) | none | v9 | See [`entryComponents`](#entryComponents) |
|
||||
| [`ANALYZE_FOR_ENTRY_COMPONENTS`](api/core/ANALYZE_FOR_ENTRY_COMPONENTS) | none | v9 | See [`ANALYZE_FOR_ENTRY_COMPONENTS`](#entryComponents) |
|
||||
| [`WrappedValue`](api/core/WrappedValue) | none | v10 | See [removing `WrappedValue`](#wrapped-value) |
|
||||
| `ModuleWithProviders` without a generic | `ModuleWithProviders` with a generic | v9 | See [`ModuleWithProviders` section](#moduleWithProviders) |
|
||||
| Undecorated base classes that use Angular features | Base classes with `@Directive()` decorator that use Angular features | v9 | See [undecorated base classes section](#undecorated-base-classes) |
|
||||
|
||||
|
||||
|
||||
|
||||
@ -161,11 +162,7 @@ Tip: In the [API reference section](api) of this doc site, deprecated APIs are i
|
||||
|
||||
This section lists all of the currently-deprecated features, which includes template syntax, configuration options, and any other deprecations not listed in the [Deprecated APIs](#deprecated-apis) section above. It also includes deprecated API usage scenarios or API combinations, to augment the information above.
|
||||
|
||||
{@a bazelbuilder}
|
||||
### Bazel builder and schematics
|
||||
|
||||
Bazel builder and schematics were introduced in Angular Labs to let users try out Bazel without having to manage Bazel version and BUILD files.
|
||||
This feature has been deprecated. For more information, please refer to the [migration doc](https://github.com/angular/angular/blob/master/packages/bazel/src/schematics/README.md).
|
||||
|
||||
{@a wtf}
|
||||
### Web Tracing Framework integration
|
||||
@ -379,6 +376,60 @@ However, in practice, Angular simply ignores two-way bindings to template variab
|
||||
<option *ngFor="let optionName of options" [value]="optionName"></option>
|
||||
```
|
||||
|
||||
{@a undecorated-base-classes}
|
||||
### Undecorated base classes using Angular features
|
||||
|
||||
As of version 9, it's deprecated to have an undecorated base class that:
|
||||
|
||||
- uses Angular features
|
||||
- is extended by a directive or component
|
||||
|
||||
Angular lifecycle hooks or any of the following Angular field decorators are considered Angular features:
|
||||
|
||||
- `@Input()`
|
||||
- `@Output()`
|
||||
- `@HostBinding()`
|
||||
- `@HostListener()`
|
||||
- `@ViewChild()` / `@ViewChildren()`
|
||||
- `@ContentChild()` / `@ContentChildren()`
|
||||
|
||||
For example, the following case is deprecated because the base class uses `@Input()` and does not have a class-level decorator:
|
||||
|
||||
```ts
|
||||
class Base {
|
||||
@Input()
|
||||
foo: string;
|
||||
}
|
||||
|
||||
@Directive(...)
|
||||
class Dir extends Base {
|
||||
ngOnChanges(): void {
|
||||
// notified when bindings to [foo] are updated
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In a future version of Angular, this code will start to throw an error.
|
||||
To fix this example, add a selectorless `@Directive()` decorator to the base class:
|
||||
|
||||
```ts
|
||||
@Directive()
|
||||
class Base {
|
||||
@Input()
|
||||
foo: string;
|
||||
}
|
||||
|
||||
@Directive(...)
|
||||
class Dir extends Base {
|
||||
ngOnChanges(): void {
|
||||
// notified when bindings to [foo] are updated
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In version 9, the CLI has an automated migration that will update your code for you when `ng update` is run.
|
||||
See [the dedicated migration guide](guide/migration-undecorated-classes) for more information about the change and more examples.
|
||||
|
||||
|
||||
|
||||
{@a binding-to-innertext}
|
||||
@ -463,54 +514,13 @@ export class MyModule {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
{@a ie-9-10-and-mobile}
|
||||
### IE 9, 10, and IE mobile support
|
||||
|
||||
Support for IE 9 and 10 has been deprecated, as well as support for IE Mobile. These will be dropped in a future version.
|
||||
Supporting outdated browsers like these increases bundle size, code complexity, and test load, and also requires time and effort that could be spent on improvements to the framework.
|
||||
For example, fixing issues can be more difficult, as a straightforward fix for modern browsers could break old ones that have quirks due to not receiving updates from vendors.
|
||||
|
||||
The final decision was made on three key points:
|
||||
* __Vendor support__: Microsoft dropped support of IE 9 and 10 on 1/12/16, meaning they no longer provide security updates or technical support. Additionally, Microsoft dropped support for of Windows 10 Mobile in December 2019.
|
||||
* __Usage statistics__: We looked at usage trends for IE 9 and 10 (as well as IE Mobile) from various sources and all indicated that usage percentages were extremely small (fractions of 1%).
|
||||
* __Feedback from partners__: We also reached out to some of our Angular customers and none expressed concern about dropping IE 9, 10, nor IE Mobile support.
|
||||
|
||||
|
||||
{@a wrapped-value}
|
||||
### `WrappedValue`
|
||||
|
||||
The purpose of `WrappedValue` is to allow the same object instance to be treated as different for the purposes of change detection.
|
||||
It is commonly used with the `async` pipe in the case where the `Observable` produces the same instance of the value.
|
||||
|
||||
Given that this use case is relatively rare and special handling impacts application performance, we have deprecated it in v10.
|
||||
No replacement is planned for this deprecation.
|
||||
|
||||
If you rely on the behavior that the same object instance should cause change detection, you have two options:
|
||||
- Clone the resulting value so that it has a new identity.
|
||||
- Explicitly call [`ChangeDetectorRef.detectChanges()`](api/core/ChangeDetectorRef#detectchanges) to force the update.
|
||||
|
||||
{@a removed}
|
||||
## Removed APIs
|
||||
|
||||
The following APIs have been removed starting with version 10.0.0*:
|
||||
|
||||
| Package | API | Replacement | Notes |
|
||||
| ---------------- | -------------- | ----------- | ----- |
|
||||
| `@angular/core` | Undecorated base classes that use Angular features | Add Angular decorator | See [migration guide](guide/migration-undecorated-classes) for more info |
|
||||
| `@angular/core` | `ModuleWithProviders` without a generic | `ModuleWithProviders` with a generic | See [migration guide](guide/migration-module-with-providers) for more info |
|
||||
| `@angular/core` | Style Sanitization | no action needed | See [style sanitization API removal](#style-sanitization) for more info
|
||||
|
||||
*To see APIs removed in version 9, check out this guide on the [version 9 docs site](https://v9.angular.io/guide/deprecations#removed).
|
||||
|
||||
|
||||
{@a esm5-fesm5}
|
||||
### `esm5` and `fesm5` code formats in @angular/* npm packages
|
||||
|
||||
As of Angular v8, the CLI primarily consumes the `fesm2015` variant of the code distributed via `@angular/*` npm packages.
|
||||
This renders the `esm5` and `fesm5` distributions obsolete and unnecessary, adding bloat to the package size and slowing down npm installations.
|
||||
|
||||
This removal has no impact on CLI users, unless they modified their build configuration to explicitly consume these code distributions.
|
||||
The future removal of this distribution will have no impact on CLI users, unless they modified their build configuration to explicitly consume these code distributions.
|
||||
|
||||
Any application still relying on the `esm5` and `fesm5` as the input to its build system will need to ensure that the build pipeline is capable of accepting JavaScript code conforming to ECMAScript 2015 (ES2015) language specification.
|
||||
|
||||
@ -518,7 +528,7 @@ Note that this change doesn't make existing libraries distributed in this format
|
||||
The CLI will fall back and consume libraries in less desirable formats if others are not available.
|
||||
However, we do recommend that libraries ship their code in ES2015 format in order to make builds faster and build output smaller.
|
||||
|
||||
In practical terms, the `package.json` of all `@angular` packages has changed in the following way:
|
||||
In practical terms, the `package.json` of all `@angular` packages will change in the following way:
|
||||
|
||||
**Before**:
|
||||
```
|
||||
@ -552,18 +562,92 @@ In practical terms, the `package.json` of all `@angular` packages has changed in
|
||||
|
||||
For more information about the npm package format, see the [Angular Package Format spec](https://goo.gl/jB3GVv).
|
||||
|
||||
|
||||
|
||||
{@a removed}
|
||||
## Removed APIs
|
||||
|
||||
The following APIs have been removed starting with version 10.0.0*:
|
||||
The following APIs have been removed starting with version 9.0.0*:
|
||||
|
||||
| Package | API | Replacement | Notes |
|
||||
| ---------------- | -------------- | ----------- | ----- |
|
||||
| `@angular/core` | Undecorated base classes that use Angular features | Add Angular decorator | See [migration guide](guide/migration-undecorated-classes) for more info |
|
||||
| `@angular/core` | `ModuleWithProviders` without a generic | `ModuleWithProviders` with a generic | See [migration guide](guide/migration-module-with-providers) for more info |
|
||||
| `@angular/core` | [`Renderer`](https://v8.angular.io/api/core/Renderer) | [`Renderer2`](https://angular.io/api/core/Renderer2) | [Migration guide](guide/migration-renderer) |
|
||||
| `@angular/core` | [`RootRenderer`](https://v8.angular.io/api/core/RootRenderer) | [`RendererFactory2`](https://angular.io/api/core/RendererFactory2) | none |
|
||||
| `@angular/core` | [`RenderComponentType`](https://v8.angular.io/api/core/RenderComponentType) | [`RendererType2`](https://angular.io/api/core/RendererType2) | none |
|
||||
| `@angular/core` | [`WtfScopeFn`](https://v8.angular.io/api/core/WtfScopeFn) | none | v8 | See [Web Tracing Framework](#wtf) |
|
||||
| `@angular/core` | [`wtfCreateScope`](https://v8.angular.io/api/core/wtfCreateScope) | none | v8 | See [Web Tracing Framework](#wtf) |
|
||||
| `@angular/core` | [`wtfStartTimeRange`](https://v8.angular.io/api/core/wtfStartTimeRange) | none | v8 | See [Web Tracing Framework](#wtf) |
|
||||
| `@angular/core` | [`wtfEndTimeRange`](https://v8.angular.io/api/core/wtfEndTimeRange) | none | v8 | See [Web Tracing Framework](#wtf) |
|
||||
| `@angular/core` | [`wtfLeave`](https://v8.angular.io/api/core/wtfLeave) | none | v8 | See [Web Tracing Framework](#wtf) |
|
||||
| `@angular/common` | `DeprecatedI18NPipesModule` | [`CommonModule`](api/common/CommonModule#pipes) | none |
|
||||
| `@angular/common` | `DeprecatedCurrencyPipe` | [`CurrencyPipe`](api/common/CurrencyPipe) | none |
|
||||
| `@angular/common` | `DeprecatedDatePipe` | [`DatePipe`](api/common/DatePipe) | none |
|
||||
| `@angular/common` | `DeprecatedDecimalPipe` | [`DecimalPipe`](api/common/DecimalPipe) | none |
|
||||
| `@angular/common` | `DeprecatedPercentPipe` | [`PercentPipe`](api/common/PercentPipe) | none |
|
||||
| `@angular/forms` | [`NgFormSelectorWarning`](https://v8.angular.io/api/forms/NgFormSelectorWarning) | none | none |
|
||||
| `@angular/forms` | `ngForm` element selector | `ng-form` element selector | none |
|
||||
| `@angular/service-worker` | `versionedFiles` | `files` | In the service worker configuration file `ngsw-config.json`, replace `versionedFiles` with `files`. See [Service Worker Configuration](guide/service-worker-config#assetgroups). |
|
||||
|
||||
*To see APIs removed in version 9, check out this guide on the [version 9 docs site](https://v9.angular.io/guide/deprecations#removed).
|
||||
*To see APIs removed in version 8, check out this guide on the [version 8 docs site](https://v8.angular.io/guide/deprecations#removed).
|
||||
|
||||
{@a style-sanitization}
|
||||
### Style Sanitization for `[style]` and `[style.prop]` bindings
|
||||
Angular used to sanitize `[style]` and `[style.prop]` bindings to prevent malicious code from being inserted through `javascript:` expressions in CSS `url()` entries. However, most modern browsers no longer support the usage of these expressions, so sanitization was only maintained for the sake of IE 6 and 7. Given that Angular does not support either IE 6 or 7 and sanitization has a performance cost, we will no longer sanitize style bindings as of version 10 of Angular.
|
||||
|
||||
<!-- The following anchor is used by redirects from the removed API pages. Do not change or remove. -->
|
||||
{@a http}
|
||||
### @angular/http
|
||||
|
||||
<!--
|
||||
Deprecation announced in version 5
|
||||
https://blog.angular.io/version-5-0-0-of-angular-now-available-37e414935ced)
|
||||
-->
|
||||
|
||||
|
||||
The entire [`@angular/http`](http://v7.angular.io/api/http) package has been removed. Use [`@angular/common/http`](api/common/http) instead.
|
||||
|
||||
The new API is a smaller, easier, and more powerful way to make HTTP requests in Angular.
|
||||
The new API simplifies the default ergonomics: There is no need to map by invoking the `.json()` method.
|
||||
It also supports typed return values and interceptors.
|
||||
|
||||
To update your apps:
|
||||
* Replace `HttpModule` with [`HttpClientModule`](api/common/http/HttpClientModule) (from [`@angular/common/http`](api/common/http)) in each of your modules.
|
||||
* Replace the `Http` service with the [`HttpClient`](api/common/http/HttpClient) service.
|
||||
* Remove any `map(res => res.json())` calls. They are no longer needed.
|
||||
|
||||
For more information about using `@angular/common/http`, see the [HttpClient guide](guide/http "HTTP Client guide").
|
||||
|
||||
|
||||
| `@angular/http` | Closest replacement in `@angular/common/http` |
|
||||
| ------------- | ------------------------------------------- |
|
||||
| `BaseRequestOptions` | [`HttpRequest`](/api/common/http/HttpRequest) |
|
||||
| `BaseResponseOptions` | [`HttpResponse`](/api/common/http/HttpResponse) |
|
||||
| `BrowserXhr` | |
|
||||
| `Connection` | [`HttpBackend`](/api/common/http/HttpBackend) |
|
||||
| `ConnectionBackend` | [`HttpBackend`](/api/common/http/HttpBackend) |
|
||||
| `CookieXSRFStrategy` | [`HttpClientXsrfModule`](/api/common/http/HttpClientXsrfModule) |
|
||||
| `Headers` | [`HttpHeaders`](/api/common/http/HttpHeaders) |
|
||||
| `Http` | [`HttpClient`](/api/common/http/HttpClient) |
|
||||
| `HttpModule` | [`HttpClientModule`](/api/common/http/HttpClientModule) |
|
||||
| `Jsonp` | [`HttpClient`](/api/common/http/HttpClient) |
|
||||
| `JSONPBackend` | [`JsonpClientBackend`](/api/common/http/JsonpClientBackend) |
|
||||
| `JSONPConnection` | [`JsonpClientBackend`](/api/common/http/JsonpClientBackend) |
|
||||
| `JsonpModule` | [`HttpClientJsonpModule`](/api/common/http/HttpClientJsonpModule) |
|
||||
| `QueryEncoder` | [`HttpUrlEncodingCodec`](/api/common/http/HttpUrlEncodingCodec) |
|
||||
| `ReadyState` | [`HttpBackend`](/api/common/http/HttpBackend) |
|
||||
| `Request` | [`HttpRequest`](/api/common/http/HttpRequest) |
|
||||
| `RequestMethod` | [`HttpClient`](/api/common/http/HttpClient) |
|
||||
| `RequestOptions` | [`HttpRequest`](/api/common/http/HttpRequest) |
|
||||
| `RequestOptionsArgs` | [`HttpRequest`](/api/common/http/HttpRequest) |
|
||||
| `Response` | [`HttpResponse`](/api/common/http/HttpResponse) |
|
||||
| `ResponseContentType` | [`HttpClient`](/api/common/http/HttpClient) |
|
||||
| `ResponseOptions` | [`HttpResponse`](/api/common/http/HttpResponse) |
|
||||
| `ResponseOptionsArgs` | [`HttpResponse`](/api/common/http/HttpResponse) |
|
||||
| `ResponseType` | [`HttpClient`](/api/common/http/HttpClient) |
|
||||
| `URLSearchParams` | [`HttpParams`](/api/common/http/HttpParams) |
|
||||
| `XHRBackend` | [`HttpXhrBackend`](/api/common/http/HttpXhrBackend) |
|
||||
| `XHRConnection` | [`HttpXhrBackend`](/api/common/http/HttpXhrBackend) |
|
||||
| `XSRFStrategy` | [`HttpClientXsrfModule`](/api/common/http/HttpClientXsrfModule) |
|
||||
|
||||
|
||||
| `@angular/http/testing` | Closest replacement in `@angular/common/http/testing` |
|
||||
| --------------------- | ------------------------------------------- |
|
||||
| `MockBackend` | [`HttpTestingController`](/api/common/http/testing/HttpTestingController) |
|
||||
| `MockConnection` | [`HttpTestingController`](/api/common/http/testing/HttpTestingController) |
|
||||
|
@ -1010,21 +1010,21 @@ Callouts use the same _urgency levels_ that alerts do.
|
||||
<div class="callout is-critical">
|
||||
<header>A critical point</header>
|
||||
|
||||
**Pitchfork hoodie semiotics**, roof party pop-up _paleo_ messenger messenger bag cred Carles tousled Truffaut yr. Semiotics viral freegan VHS, Shoreditch disrupt McSweeney's. Intelligentsia kale chips Vice four dollar toast
|
||||
**Pitchfork hoodie semiotics**, roof party pop-up _paleo_ messenger messenger bag cred Carles tousled Truffaut yr. Semiotics viral freegan VHS, Shoreditch disrupt McSweeney's. Intelligentsia kale chips Vice four dollar toast, Schlitz crucifix
|
||||
|
||||
</div>
|
||||
|
||||
<div class="callout is-important">
|
||||
<header>An important point</header>
|
||||
|
||||
**Pitchfork hoodie semiotics**, roof party pop-up _paleo_ messenger bag cred Carles tousled Truffaut yr. Semiotics viral freegan VHS, Shoreditch disrupt McSweeney's. Intelligentsia kale chips Vice four dollar toast
|
||||
**Pitchfork hoodie semiotics**, roof party pop-up _paleo_ messenger bag cred Carles tousled Truffaut yr. Semiotics viral freegan VHS, Shoreditch disrupt McSweeney's. Intelligentsia kale chips Vice four dollar toast, Schlitz crucifix
|
||||
|
||||
</div>
|
||||
|
||||
<div class="callout is-helpful">
|
||||
<header>A helpful or informational point</header>
|
||||
|
||||
**Pitchfork hoodie semiotics**, roof party pop-up _paleo_ messenger bag cred Carles tousled Truffaut yr. Semiotics viral freegan VHS, Shoreditch disrupt McSweeney's. Intelligentsia kale chips Vice four dollar toast
|
||||
**Pitchfork hoodie semiotics**, roof party pop-up _paleo_ messenger bag cred Carles tousled Truffaut yr. Semiotics viral freegan VHS, Shoreditch disrupt McSweeney's. Intelligentsia kale chips Vice four dollar toast, Schlitz crucifix
|
||||
|
||||
</div>
|
||||
|
||||
@ -1033,7 +1033,7 @@ Here is the markup for the first of these callouts.
|
||||
<div class="callout is-critical">
|
||||
<header>A critical point</header>
|
||||
|
||||
**Pitchfork hoodie semiotics**, roof party pop-up _paleo_ messenger bag cred Carles tousled Truffaut yr. Semiotics viral freegan VHS, Shoreditch disrupt McSweeney's. Intelligentsia kale chips Vice four dollar toast
|
||||
**Pitchfork hoodie semiotics**, roof party pop-up _paleo_ messenger bag cred Carles tousled Truffaut yr. Semiotics viral freegan VHS, Shoreditch disrupt McSweeney's. Intelligentsia kale chips Vice four dollar toast, Schlitz crucifix
|
||||
|
||||
</div>
|
||||
```
|
||||
|
@ -1,53 +1,42 @@
|
||||
# Building dynamic forms
|
||||
# Dynamic forms
|
||||
|
||||
Many forms, such as questionaires, can be very similar to one another in format and intent.
|
||||
To make it faster and easier to generate different versions of such a form,
|
||||
you can create a *dynamic form template* based on metadata that describes the business object model.
|
||||
You can then use the template to generate new forms automatically, according to changes in the data model.
|
||||
{@a top}
|
||||
|
||||
The technique is particularly useful when you have a type of form whose content must
|
||||
change frequently to meet rapidly changing business and regulatory requirements.
|
||||
A typical use case is a questionaire. You might need to get input from users in different contexts.
|
||||
The format and style of the forms a user sees should remain constant, while the actual questions you need to ask vary with the context.
|
||||
Building handcrafted forms can be costly and time-consuming,
|
||||
especially if you need a great number of them, they're similar to each other, and they change frequently
|
||||
to meet rapidly changing business and regulatory requirements.
|
||||
|
||||
In this tutorial you will build a dynamic form that presents a basic questionaire.
|
||||
You will build an online application for heroes seeking employment.
|
||||
The agency is constantly tinkering with the application process, but by using the dynamic form
|
||||
you can create the new forms on the fly without changing the application code.
|
||||
It may be more economical to create the forms dynamically, based on
|
||||
metadata that describes the business object model.
|
||||
|
||||
The tutorial walks you through the following steps.
|
||||
This cookbook shows you how to use `formGroup` to dynamically
|
||||
render a simple form with different control types and validation.
|
||||
It's a primitive start.
|
||||
It might evolve to support a much richer variety of questions, more graceful rendering, and superior user experience.
|
||||
All such greatness has humble beginnings.
|
||||
|
||||
1. Enable reactive forms for a project.
|
||||
2. Establish a data model to represent form controls.
|
||||
3. Populate the model with sample data.
|
||||
4. Develop a component to create form controls dynamically.
|
||||
|
||||
The form you create uses input validation and styling to improve the user experience.
|
||||
It has a Submit button that is only enabled when all user input is valid, and flags invalid input with color coding and error messages.
|
||||
|
||||
The basic version can evolve to support a richer variety of questions, more graceful rendering, and superior user experience.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
The example in this cookbook is a dynamic form to build an
|
||||
online application experience for heroes seeking employment.
|
||||
The agency is constantly tinkering with the application process.
|
||||
You can create the forms on the fly *without changing the application code*.
|
||||
{@a toc}
|
||||
|
||||
See the <live-example name="dynamic-form"></live-example>.
|
||||
|
||||
</div>
|
||||
{@a bootstrap}
|
||||
|
||||
## Prerequisites
|
||||
## Bootstrap
|
||||
|
||||
Before doing this tutorial, you should have a basic understanding to the following.
|
||||
Start by creating an `NgModule` called `AppModule`.
|
||||
|
||||
* [TypeScript](https://www.typescriptlang.org/docs/home.html "The TypeScript language") and HTML5 programming.
|
||||
This cookbook uses [reactive forms](guide/reactive-forms).
|
||||
|
||||
* Fundamental concepts of [Angular app design](guide/architecture "Introduction to Angular app-design concepts").
|
||||
Reactive forms belongs to a different `NgModule` called `ReactiveFormsModule`,
|
||||
so in order to access any reactive forms directives, you have to import
|
||||
`ReactiveFormsModule` from the `@angular/forms` library.
|
||||
|
||||
* Basic knowledge of [reactive forms](guide/reactive-forms "Reactive forms guide").
|
||||
Bootstrap the `AppModule` in `main.ts`.
|
||||
|
||||
## Enable reactive forms for your project
|
||||
|
||||
Dynamic forms are based on reactive forms. To give the application access reactive forms directives, the [root module](guide/bootstrapping "Learn about bootstrapping an app from the root module.") imports `ReactiveFormsModule` from the `@angular/forms` library.
|
||||
|
||||
The following code from the example shows the setup in the root module.
|
||||
|
||||
<code-tabs>
|
||||
|
||||
@ -61,99 +50,59 @@ The following code from the example shows the setup in the root module.
|
||||
|
||||
</code-tabs>
|
||||
|
||||
|
||||
{@a object-model}
|
||||
|
||||
## Create a form object model
|
||||
## Question model
|
||||
|
||||
A dynamic form requires an object model that can describe all scenarios needed by the form functionality.
|
||||
The example hero-application form is a set of questions—that is, each control in the form must ask a question and accept an answer.
|
||||
The next step is to define an object model that can describe all scenarios needed by the form functionality.
|
||||
The hero application process involves a form with a lot of questions.
|
||||
The _question_ is the most fundamental object in the model.
|
||||
|
||||
The data model for this type of form must represent a question.
|
||||
The example includes the `DynamicFormQuestionComponent`, which defines a question as the fundamental object in the model.
|
||||
The following `QuestionBase` is a fundamental question class.
|
||||
|
||||
The following `QuestionBase` is a base class for a set of controls that can represent the question and its answer in the form.
|
||||
|
||||
<code-example path="dynamic-form/src/app/question-base.ts" header="src/app/question-base.ts">
|
||||
|
||||
</code-example>
|
||||
|
||||
### Define control classes
|
||||
|
||||
From this base, the example derives two new classes, `TextboxQuestion` and `DropdownQuestion`,
|
||||
that represent different control types.
|
||||
When you create the form template in the next step, you will instantiate these specific question types in order to render the appropriate controls dynamically.
|
||||
|
||||
* The `TextboxQuestion` control type presents a question and allows users to enter input.
|
||||
From this base you can derive two new classes in `TextboxQuestion` and `DropdownQuestion`
|
||||
that represent textbox and dropdown questions.
|
||||
The idea is that the form will be bound to specific question types and render the
|
||||
appropriate controls dynamically.
|
||||
|
||||
<code-example path="dynamic-form/src/app/question-textbox.ts" header="src/app/question-textbox.ts"></code-example>
|
||||
`TextboxQuestion` supports multiple HTML5 types such as text, email, and url
|
||||
via the `type` property.
|
||||
|
||||
The `TextboxQuestion` control type will be represented in a form template using an `<input>` element.
|
||||
The `type` attribute of the element will be defined based on the `type` field specified in the `options` argument (for example `text`, `email`, `url`).
|
||||
|
||||
* The `DropdownQuestion` control presents a list of choices in a select box.
|
||||
<code-example path="dynamic-form/src/app/question-textbox.ts" header="src/app/question-textbox.ts"></code-example>
|
||||
|
||||
<code-example path="dynamic-form/src/app/question-dropdown.ts" header="src/app/question-dropdown.ts"></code-example>
|
||||
|
||||
### Compose form groups
|
||||
|
||||
A dynamic form uses a service to create grouped sets of input controls, based on the form model.
|
||||
The following `QuestionControlService` collects a set of `FormGroup` instances that consume the metadata from the question model. You can specify default values and validation rules.
|
||||
`DropdownQuestion` presents a list of choices in a select box.
|
||||
|
||||
|
||||
<code-example path="dynamic-form/src/app/question-dropdown.ts" header="src/app/question-dropdown.ts"></code-example>
|
||||
|
||||
|
||||
|
||||
Next is `QuestionControlService`, a simple service for transforming the questions to a `FormGroup`.
|
||||
In a nutshell, the form group consumes the metadata from the question model and
|
||||
allows you to specify default values and validation rules.
|
||||
|
||||
|
||||
<code-example path="dynamic-form/src/app/question-control.service.ts" header="src/app/question-control.service.ts"></code-example>
|
||||
|
||||
{@a form-component}
|
||||
|
||||
## Compose dynamic form contents
|
||||
|
||||
The dynamic form itself will be represented by a container component, which you will add in a later step.
|
||||
Each question is represented in the form component's template by an `<app-question>` tag, which matches an instance of `DynamicFormQuestionComponent`.
|
||||
|
||||
The `DynamicFormQuestionComponent` is responsible for rendering the details of an individual question based on values in the data-bound question object.
|
||||
The form relies on a [`[formGroup]` directive](api/forms/FormGroupDirective "API reference") to connect the template HTML to the underlying control objects.
|
||||
The `DynamicFormQuestionComponent` creates form groups and populates them with controls defined in the question model, specifying display and validation rules.
|
||||
|
||||
<code-tabs>
|
||||
|
||||
<code-pane header="dynamic-form-question.component.html" path="dynamic-form/src/app/dynamic-form-question.component.html">
|
||||
|
||||
</code-pane>
|
||||
|
||||
<code-pane header="dynamic-form-question.component.ts" path="dynamic-form/src/app/dynamic-form-question.component.ts">
|
||||
|
||||
</code-pane>
|
||||
|
||||
</code-tabs>
|
||||
|
||||
The goal of the `DynamicFormQuestionComponent` is to present question types defined in your model.
|
||||
You only have two types of questions at this point but you can imagine many more.
|
||||
The `ngSwitch` statement in the template determines which type of question to display.
|
||||
The switch uses directives with the [`formControlName`](api/forms/FormControlName "FormControlName directive API reference") and [`formGroup`](api/forms/FormGroupDirective "FormGroupDirective API reference") selectors. Both directives are defined in `ReactiveFormsModule`.
|
||||
|
||||
{@a questionnaire-data}
|
||||
|
||||
### Supply data
|
||||
|
||||
Another service is needed to supply a specific set of questions from which to build an individual form.
|
||||
For this exercise you will create the `QuestionService` to supply this array of questions from the hard-coded sample data.
|
||||
In a real-world app, the service might fetch data from a backend system.
|
||||
The key point, however, is that you control the hero job-application questions entirely through the objects returned from `QuestionService`.
|
||||
To maintain the questionnaire as requirements change, you only need to add, update, and remove objects from the `questions` array.
|
||||
## Question form components
|
||||
Now that you have defined the complete model you are ready
|
||||
to create components to represent the dynamic form.
|
||||
|
||||
|
||||
The `QuestionService` supplies a set of questions in the form of an array bound to `@Input()` questions.
|
||||
|
||||
<code-example path="dynamic-form/src/app/question.service.ts" header="src/app/question.service.ts">
|
||||
|
||||
</code-example>
|
||||
|
||||
|
||||
{@a dynamic-template}
|
||||
|
||||
## Create a dynamic form template
|
||||
|
||||
The `DynamicFormComponent` component is the entry point and the main container for the form, which is represented using the `<app-dynamic-form>` in a template.
|
||||
|
||||
The `DynamicFormComponent` component presents a list of questions by binding each one to an `<app-question>` element that matches the `DynamicFormQuestionComponent`.
|
||||
`DynamicFormComponent` is the entry point and the main container for the form.
|
||||
|
||||
<code-tabs>
|
||||
|
||||
@ -167,45 +116,90 @@ The `DynamicFormComponent` component presents a list of questions by binding eac
|
||||
|
||||
</code-tabs>
|
||||
|
||||
### Display the form
|
||||
|
||||
To display an instance of the dynamic form, the `AppComponent` shell template passes the `questions` array returned by the `QuestionService` to the form container component, `<app-dynamic-form>`.
|
||||
|
||||
It presents a list of questions, each bound to a `<app-question>` component element.
|
||||
The `<app-question>` tag matches the `DynamicFormQuestionComponent`,
|
||||
the component responsible for rendering the details of each _individual_
|
||||
question based on values in the data-bound question object.
|
||||
|
||||
|
||||
<code-tabs>
|
||||
|
||||
<code-pane header="dynamic-form-question.component.html" path="dynamic-form/src/app/dynamic-form-question.component.html">
|
||||
|
||||
</code-pane>
|
||||
|
||||
<code-pane header="dynamic-form-question.component.ts" path="dynamic-form/src/app/dynamic-form-question.component.ts">
|
||||
|
||||
</code-pane>
|
||||
|
||||
</code-tabs>
|
||||
|
||||
|
||||
|
||||
Notice this component can present any type of question in your model.
|
||||
You only have two types of questions at this point but you can imagine many more.
|
||||
The `ngSwitch` determines which type of question to display.
|
||||
|
||||
In both components you're relying on Angular's **formGroup** to connect the template HTML to the
|
||||
underlying control objects, populated from the question model with display and validation rules.
|
||||
|
||||
`formControlName` and `formGroup` are directives defined in
|
||||
`ReactiveFormsModule`. The templates can access these directives
|
||||
directly since you imported `ReactiveFormsModule` from `AppModule`.
|
||||
{@a questionnaire-data}
|
||||
|
||||
## Questionnaire data
|
||||
|
||||
`DynamicFormComponent` expects the list of questions in the form of an array bound to `@Input() questions`.
|
||||
|
||||
The set of questions you've defined for the job application is returned from the `QuestionService`.
|
||||
In a real app you'd retrieve these questions from storage.
|
||||
|
||||
The key point is that you control the hero job application questions
|
||||
entirely through the objects returned from `QuestionService`.
|
||||
Questionnaire maintenance is a simple matter of adding, updating,
|
||||
and removing objects from the `questions` array.
|
||||
|
||||
|
||||
<code-example path="dynamic-form/src/app/question.service.ts" header="src/app/question.service.ts">
|
||||
|
||||
</code-example>
|
||||
|
||||
|
||||
|
||||
Finally, display an instance of the form in the `AppComponent` shell.
|
||||
|
||||
|
||||
<code-example path="dynamic-form/src/app/app.component.ts" header="app.component.ts">
|
||||
|
||||
</code-example>
|
||||
|
||||
The example provides a model for a job application for heroes, but there are
|
||||
no references to any specific hero question other than the objects returned by `QuestionService`.
|
||||
This separation of model and data allows you to repurpose the components for any type of survey
|
||||
{@a dynamic-template}
|
||||
|
||||
## Dynamic Template
|
||||
Although in this example you're modelling a job application for heroes, there are
|
||||
no references to any specific hero question
|
||||
outside the objects returned by `QuestionService`.
|
||||
|
||||
This is very important since it allows you to repurpose the components for any type of survey
|
||||
as long as it's compatible with the *question* object model.
|
||||
|
||||
### Ensuring valid data
|
||||
|
||||
The form template uses dynamic data binding of metadata to render the form
|
||||
The key is the dynamic data binding of metadata used to render the form
|
||||
without making any hardcoded assumptions about specific questions.
|
||||
It adds both control metadata and validation criteria dynamically.
|
||||
In addition to control metadata, you are also adding validation dynamically.
|
||||
|
||||
To ensure valid input, the *Save* button is disabled until the form is in a valid state.
|
||||
The *Save* button is disabled until the form is in a valid state.
|
||||
When the form is valid, you can click *Save* and the app renders the current form values as JSON.
|
||||
This proves that any user input is bound back to the data model.
|
||||
Saving and retrieving the data is an exercise for another time.
|
||||
|
||||
The following figure shows the final form.
|
||||
|
||||
The final form looks like this:
|
||||
|
||||
<div class="lightbox">
|
||||
<img src="generated/images/guide/dynamic-form/dynamic-form.png" alt="Dynamic-Form">
|
||||
</div>
|
||||
|
||||
## Next steps
|
||||
|
||||
* **Different types of forms and control collection**
|
||||
|
||||
This tutorial shows how to build a a questionaire, which is just one kind of dynamic form.
|
||||
The example uses `FormGroup` to collect a set of controls.
|
||||
For an example of a different type of dynamic form, see the section [Creating dynamic forms](guide/reactive-forms#creating-dynamic-forms "Create dynamic forms with arrays") in the Reactive Forms guide.
|
||||
That example also shows how to use `FormArray` instead of `FormGroup` to collect a set of controls.
|
||||
|
||||
* **Validating user input**
|
||||
|
||||
The section [Validating form input](guide/reactive-forms#validating-form-input "Basic input validation") introduces the basics of how input validation works in reactive forms.
|
||||
|
||||
The [Form validation guide](guide/form-validation "Form validation guide") covers the topic in more depth.
|
||||
[Back to top](guide/dynamic-form#top)
|
||||
|
@ -72,7 +72,7 @@ Files at the top level of `src/` support testing and running your application. S
|
||||
| `environments/` | Contains build configuration options for particular target environments. By default there is an unnamed standard development environment and a production ("prod") environment. You can define additional target environment configurations. |
|
||||
| `favicon.ico` | An icon to use for this application in the bookmark bar. |
|
||||
| `index.html` | The main HTML page that is served when someone visits your site. The CLI automatically adds all JavaScript and CSS files when building your app, so you typically don't need to add any `<script>` or` <link>` tags here manually. |
|
||||
| `main.ts` | The main entry point for your application. Compiles the application with the [JIT compiler](guide/glossary#jit) and bootstraps the application's root module (AppModule) to run in the browser. You can also use the [AOT compiler](guide/aot-compiler) without changing any code by appending the `--aot` flag to the CLI `build` and `serve` commands. |
|
||||
| `main.ts` | The main entry point for your application. Compiles the application with the [JIT compiler](https://angular.io/guide/glossary#jit) and bootstraps the application's root module (AppModule) to run in the browser. You can also use the [AOT compiler](https://angular.io/guide/aot-compiler) without changing any code by appending the `--aot` flag to the CLI `build` and `serve` commands. |
|
||||
| `polyfills.ts` | Provides polyfill scripts for browser support. |
|
||||
| `styles.sass` | Lists CSS files that supply styles for a project. The extension reflects the style preprocessor you have configured for the project. |
|
||||
| `test.ts` | The main entry point for your unit tests, with some Angular-specific configuration. You don't typically need to edit this file. |
|
||||
@ -99,7 +99,7 @@ Project-specific [TypeScript](https://www.typescriptlang.org/) configuration fil
|
||||
|
||||
| APPLICATION-SPECIFIC CONFIG FILES | PURPOSE |
|
||||
| :--------------------- | :------------------------------------------|
|
||||
| `.browserslistrc` | Configures sharing of target browsers and Node.js versions among various front-end tools. See [Browserslist on GitHub](https://github.com/browserslist/browserslist) for more information. |
|
||||
| `browserslist` | Configures sharing of target browsers and Node.js versions among various front-end tools. See [Browserslist on GitHub](https://github.com/browserslist/browserslist) for more information. |
|
||||
| `karma.conf.js` | Application-specific [Karma](https://karma-runner.github.io/2.0/config/configuration-file.html) configuration. |
|
||||
| `tsconfig.app.json` | Application-specific [TypeScript](https://www.typescriptlang.org/) configuration, including TypeScript and Angular template compiler options. See [TypeScript Configuration](guide/typescript-configuration) and [Angular Compiler Options](guide/angular-compiler-options). |
|
||||
| `tsconfig.spec.json` | [TypeScript](https://www.typescriptlang.org/) configuration for the application tests. See [TypeScript Configuration](guide/typescript-configuration). |
|
||||
@ -165,7 +165,7 @@ my-workspace/
|
||||
|
||||
## Library project files
|
||||
|
||||
When you generate a library using the CLI (with a command such as `ng generate library my-lib`), the generated files go into the projects/ folder of the workspace. For more information about creating your own libraries, see [Creating Libraries](guide/creating-libraries).
|
||||
When you generate a library using the CLI (with a command such as `ng generate library my-lib`), the generated files go into the projects/ folder of the workspace. For more information about creating your own libraries, see [Creating Libraries](https://angular.io/guide/creating-libraries).
|
||||
|
||||
Libraries (unlike applications and their associated e2e projects) have their own `package.json` configuration files.
|
||||
|
||||
|
@ -1,131 +1,132 @@
|
||||
# Validating form input
|
||||
# Form validation
|
||||
|
||||
You can improve overall data quality by validating user input for accuracy and completeness.
|
||||
This page shows how to validate user input from the UI and display useful validation messages,
|
||||
in both reactive and template-driven forms.
|
||||
Improve overall data quality by validating user input for accuracy and completeness.
|
||||
|
||||
**Prerequisites**
|
||||
|
||||
Before reading about form validation, you should have a basic understanding of the following.
|
||||
|
||||
* [TypeScript](https://www.typescriptlang.org/docs/home.html "The TypeScript language") and HTML5 programming.
|
||||
|
||||
* Fundamental concepts of [Angular app design](guide/architecture "Introduction to Angular app-design concepts").
|
||||
|
||||
* The [two types of forms that Angular supports](guide/forms-overview "Introduction to Angular forms").
|
||||
|
||||
* Basics of either [Template-driven Forms](guide/forms "Template-driven forms guide") or [Reactive Forms](guide/reactive-forms "Reactive forms guide").
|
||||
This page shows how to validate user input in the UI and display useful validation messages
|
||||
using both reactive and template-driven forms. It assumes some basic knowledge of the two
|
||||
forms modules.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Get the complete example code for the reactive and template-driven forms used here to illustrate form validation.
|
||||
Run the <live-example></live-example>.
|
||||
For the sample app that this page describes, see the <live-example></live-example>.
|
||||
|
||||
</div>
|
||||
|
||||
{@a template-driven-validation}
|
||||
<div class="alert is-helpful">
|
||||
|
||||
## Validating input in template-driven forms
|
||||
If you're new to forms, start by reviewing the [Forms](guide/forms) and
|
||||
[Reactive Forms](guide/reactive-forms) guides.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## Template-driven validation
|
||||
|
||||
To add validation to a template-driven form, you add the same validation attributes as you
|
||||
would with [native HTML form validation](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation).
|
||||
Angular uses directives to match these attributes with validator functions in the framework.
|
||||
|
||||
Every time the value of a form control changes, Angular runs validation and generates
|
||||
either a list of validation errors that results in an INVALID status, or null, which results in a VALID status.
|
||||
either a list of validation errors, which results in an INVALID status, or null, which results in a VALID status.
|
||||
|
||||
You can then inspect the control's state by exporting `ngModel` to a local template variable.
|
||||
The following example exports `NgModel` into a variable called `name`:
|
||||
|
||||
<code-example path="form-validation/src/app/template/hero-form-template.component.html" region="name-with-error-msg" header="template/hero-form-template.component.html (name)"></code-example>
|
||||
|
||||
Notice the following features illustrated by the example.
|
||||
|
||||
Note the following:
|
||||
|
||||
* The `<input>` element carries the HTML validation attributes: `required` and `minlength`. It
|
||||
also carries a custom validator directive, `forbiddenName`. For more
|
||||
information, see the [Custom validators](#custom-validators) section.
|
||||
information, see [Custom validators](guide/form-validation#custom-validators) section.
|
||||
|
||||
* `#name="ngModel"` exports `NgModel` into a local variable called `name`. `NgModel` mirrors many of the properties of its underlying
|
||||
`FormControl` instance, so you can use this in the template to check for control states such as `valid` and `dirty`. For a full list of control properties, see the [AbstractControl](api/forms/AbstractControl)
|
||||
API reference.
|
||||
|
||||
* The `*ngIf` on the `<div>` element reveals a set of nested message `divs`
|
||||
* The `*ngIf` on the `<div>` element reveals a set of nested message `divs`
|
||||
but only if the `name` is invalid and the control is either `dirty` or `touched`.
|
||||
|
||||
* Each nested `<div>` can present a custom message for one of the possible validation errors.
|
||||
* Each nested `<div>` can present a custom message for one of the possible validation errors.
|
||||
There are messages for `required`, `minlength`, and `forbiddenName`.
|
||||
|
||||
{@a dirty-or-touched}
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
To prevent the validator from displaying errors before the user has a chance to edit the form, you should check for either the `dirty` or `touched` states in a control.
|
||||
|
||||
* When the user changes the value in the watched field, the control is marked as "dirty".
|
||||
* When the user blurs the form control element, the control is marked as "touched".
|
||||
|
||||
#### Why check _dirty_ and _touched_?
|
||||
|
||||
You may not want your application to display errors before the user has a chance to edit the form.
|
||||
The checks for `dirty` and `touched` prevent errors from showing until the user
|
||||
does one of two things: changes the value,
|
||||
turning the control dirty; or blurs the form control element, setting the control to touched.
|
||||
|
||||
</div>
|
||||
|
||||
{@a reactive-form-validation}
|
||||
## Reactive form validation
|
||||
|
||||
## Validating input in reactive forms
|
||||
|
||||
In a reactive form, the source of truth is the component class.
|
||||
Instead of adding validators through attributes in the template, you add validator functions directly to the form control model in the component class.
|
||||
Angular then calls these functions whenever the value of the control changes.
|
||||
In a reactive form, the source of truth is the component class. Instead of adding validators through attributes in the template, you add validator functions directly to the form control model in the component class. Angular then calls these functions whenever the value of the control changes.
|
||||
|
||||
### Validator functions
|
||||
|
||||
Validator functions can be either synchronous or asynchronous.
|
||||
There are two types of validator functions: sync validators and async validators.
|
||||
|
||||
* **Sync validators**: Synchronous functions that take a control instance and immediately return either a set of validation errors or `null`. You can pass these in as the second argument when you instantiate a `FormControl`.
|
||||
* **Sync validators**: functions that take a control instance and immediately return either a set of validation errors or `null`. You can pass these in as the second argument when you instantiate a `FormControl`.
|
||||
|
||||
* **Async validators**: Asynchronous functions that take a control instance and return a Promise
|
||||
* **Async validators**: functions that take a control instance and return a Promise
|
||||
or Observable that later emits a set of validation errors or `null`. You can
|
||||
pass these in as the third argument when you instantiate a `FormControl`.
|
||||
|
||||
For performance reasons, Angular only runs async validators if all sync validators pass. Each must complete before errors are set.
|
||||
Note: for performance reasons, Angular only runs async validators if all sync validators pass. Each must complete before errors are set.
|
||||
|
||||
### Built-in validator functions
|
||||
### Built-in validators
|
||||
|
||||
You can choose to [write your own validator functions](#custom-validators), or you can use some of Angular's built-in validators.
|
||||
You can choose to [write your own validator functions](guide/form-validation#custom-validators), or you can use some of
|
||||
Angular's built-in validators.
|
||||
|
||||
The same built-in validators that are available as attributes in template-driven forms, such as `required` and `minlength`, are all available to use as functions from the `Validators` class.
|
||||
For a full list of built-in validators, see the [Validators](api/forms/Validators) API reference.
|
||||
The same built-in validators that are available as attributes in template-driven forms, such as `required` and `minlength`, are all available to use as functions from the `Validators` class. For a full list of built-in validators, see the [Validators](api/forms/Validators) API reference.
|
||||
|
||||
To update the hero form to be a reactive form, you can use some of the same
|
||||
built-in validators—this time, in function form, as in the following example.
|
||||
built-in validators—this time, in function form. See below:
|
||||
|
||||
{@a reactive-component-class}
|
||||
|
||||
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.1.ts" region="form-group" header="reactive/hero-form-reactive.component.ts (validator functions)"></code-example>
|
||||
|
||||
In this example, the `name` control sets up two built-in validators—`Validators.required` and `Validators.minLength(4)`—and one custom validator, `forbiddenNameValidator`. (For more details see [custom validators](#custom-validators) below.)
|
||||
Note that:
|
||||
|
||||
All of these validators are synchronous, so they are passed as the second argument. Notice that you can support multiple validators by passing the functions in as an array.
|
||||
* The name control sets up two built-in validators—`Validators.required` and `Validators.minLength(4)`—and one custom validator, `forbiddenNameValidator`. For more details see the [Custom validators](guide/form-validation#custom-validators) section in this guide.
|
||||
* As these validators are all sync validators, you pass them in as the second argument.
|
||||
* Support multiple validators by passing the functions in as an array.
|
||||
* This example adds a few getter methods. In a reactive form, you can always access any form control through the `get` method on its parent group, but sometimes it's useful to define getters as shorthands
|
||||
for the template.
|
||||
|
||||
This example also adds a few getter methods. In a reactive form, you can always access any form control through the `get` method on its parent group, but sometimes it's useful to define getters as shorthand for the template.
|
||||
|
||||
If you look at the template for the `name` input again, it is fairly similar to the template-driven example.
|
||||
If you look at the template for the name input again, it is fairly similar to the template-driven example.
|
||||
|
||||
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.html" region="name-with-error-msg" header="reactive/hero-form-reactive.component.html (name with error msg)"></code-example>
|
||||
|
||||
This form differs from the template-driven version in that it no longer exports any directives. Instead, it uses the `name` getter defined in the component class.
|
||||
Key takeaways:
|
||||
|
||||
Notice that the `required` attribute is still present in the template. Although it's not necessary for validation, it should be retained to for accessibility purposes.
|
||||
* The form no longer exports any directives, and instead uses the `name` getter defined in
|
||||
the component class.
|
||||
* The `required` attribute is still present. While it's not necessary for validation purposes,
|
||||
you may want to keep it in your template for CSS styling or accessibility reasons.
|
||||
|
||||
{@a custom-validators}
|
||||
|
||||
## Defining custom validators
|
||||
## Custom validators
|
||||
|
||||
The built-in validators don't always match the exact use case of your application, so you sometimes need to create a custom validator.
|
||||
Since the built-in validators won't always match the exact use case of your application, sometimes you'll want to create a custom validator.
|
||||
|
||||
Consider the `forbiddenNameValidator` function from previous [reactive-form examples](#reactive-component-class).
|
||||
Here's what the definition of that function looks like.
|
||||
Consider the `forbiddenNameValidator` function from previous
|
||||
[examples](guide/form-validation#reactive-component-class) in
|
||||
this guide. Here's what the definition of that function looks like:
|
||||
|
||||
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="custom-validator" header="shared/forbidden-name.directive.ts (forbiddenNameValidator)"></code-example>
|
||||
|
||||
The function is a factory that takes a regular expression to detect a _specific_ forbidden name and returns a validator function.
|
||||
The function is actually a factory that takes a regular expression to detect a _specific_ forbidden name and returns a validator function.
|
||||
|
||||
In this sample, the forbidden name is "bob", so the validator will reject any hero name containing "bob".
|
||||
Elsewhere it could reject "alice" or any name that the configuring regular expression matches.
|
||||
@ -136,57 +137,55 @@ null if the control value is valid _or_ a validation error object.
|
||||
The validation error object typically has a property whose name is the validation key, `'forbiddenName'`,
|
||||
and whose value is an arbitrary dictionary of values that you could insert into an error message, `{name}`.
|
||||
|
||||
Custom async validators are similar to sync validators, but they must instead return a Promise or observable that later emits null or a validation error object.
|
||||
In the case of an observable, the observable must complete, at which point the form uses the last value emitted for validation.
|
||||
Custom async validators are similar to sync validators, but they must instead return a Promise or Observable
|
||||
that later emits null or a validation error object. In the case of an Observable, the Observable must complete,
|
||||
at which point the form uses the last value emitted for validation.
|
||||
|
||||
{@a adding-to-reactive-forms}
|
||||
### Adding to reactive forms
|
||||
|
||||
### Adding custom validators to reactive forms
|
||||
|
||||
In reactive forms, add a custom validator by passing the function directly to the `FormControl`.
|
||||
In reactive forms, custom validators are fairly simple to add. All you have to do is pass the function directly
|
||||
to the `FormControl`.
|
||||
|
||||
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.1.ts" region="custom-validator" header="reactive/hero-form-reactive.component.ts (validator functions)"></code-example>
|
||||
|
||||
{@a adding-to-template-driven-forms}
|
||||
### Adding to template-driven forms
|
||||
|
||||
### Adding custom validators to template-driven forms
|
||||
In template-driven forms, you don't have direct access to the `FormControl` instance, so you can't pass the
|
||||
validator in like you can for reactive forms. Instead, you need to add a directive to the template.
|
||||
|
||||
In template-driven forms, add a directive to the template, where the directive wraps the validator function.
|
||||
For example, the corresponding `ForbiddenValidatorDirective` serves as a wrapper around the `forbiddenNameValidator`.
|
||||
The corresponding `ForbiddenValidatorDirective` serves as a wrapper around the `forbiddenNameValidator`.
|
||||
|
||||
Angular recognizes the directive's role in the validation process because the directive registers itself with the `NG_VALIDATORS` provider, as shown in the following example.
|
||||
`NG_VALIDATORS` is a predefined provider with an extensible collection of validators.
|
||||
Angular recognizes the directive's role in the validation process because the directive registers itself
|
||||
with the `NG_VALIDATORS` provider, a provider with an extensible collection of validators.
|
||||
|
||||
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="directive-providers" header="shared/forbidden-name.directive.ts (providers)"></code-example>
|
||||
|
||||
The directive class then implements the `Validator` interface, so that it can easily integrate
|
||||
with Angular forms.
|
||||
Here is the rest of the directive to help you get an idea of how it all
|
||||
comes together.
|
||||
with Angular forms. Here is the rest of the directive to help you get an idea of how it all
|
||||
comes together:
|
||||
|
||||
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="directive" header="shared/forbidden-name.directive.ts (directive)">
|
||||
</code-example>
|
||||
|
||||
Once the `ForbiddenValidatorDirective` is ready, you can add its selector, `appForbiddenName`, to any input element to activate it.
|
||||
For example:
|
||||
Once the `ForbiddenValidatorDirective` is ready, you can simply add its selector, `appForbiddenName`, to any input element to activate it. For example:
|
||||
|
||||
<code-example path="form-validation/src/app/template/hero-form-template.component.html" region="name-input" header="template/hero-form-template.component.html (forbidden-name-input)"></code-example>
|
||||
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Notice that the custom validation directive is instantiated with `useExisting` rather than `useClass`. The registered validator must be _this instance_ of
|
||||
You may have noticed that the custom validation directive is instantiated with `useExisting`
|
||||
rather than `useClass`. The registered validator must be _this instance_ of
|
||||
the `ForbiddenValidatorDirective`—the instance in the form with
|
||||
its `forbiddenName` property bound to “bob".
|
||||
|
||||
If you were to replace `useExisting` with `useClass`, then you’d be registering a new class instance, one that doesn’t have a `forbiddenName`.
|
||||
its `forbiddenName` property bound to “bob". If you were to replace
|
||||
`useExisting` with `useClass`, then you’d be registering a new class instance, one that
|
||||
doesn’t have a `forbiddenName`.
|
||||
|
||||
</div>
|
||||
|
||||
## Control status CSS classes
|
||||
|
||||
Angular automatically mirrors many control properties onto the form control element as CSS classes. You can use these classes to style form control elements according to the state of the form.
|
||||
The following classes are currently supported.
|
||||
Like in AngularJS, Angular automatically mirrors many control properties onto the form control element as CSS classes. You can use these classes to style form control elements according to the state of the form. The following classes are currently supported:
|
||||
|
||||
* `.ng-valid`
|
||||
* `.ng-invalid`
|
||||
@ -196,27 +195,25 @@ The following classes are currently supported.
|
||||
* `.ng-untouched`
|
||||
* `.ng-touched`
|
||||
|
||||
In the following example, the hero form uses the `.ng-valid` and `.ng-invalid` classes to
|
||||
The hero form uses the `.ng-valid` and `.ng-invalid` classes to
|
||||
set the color of each form control's border.
|
||||
|
||||
<code-example path="form-validation/src/assets/forms.css" header="forms.css (status classes)">
|
||||
|
||||
</code-example>
|
||||
|
||||
## Cross-field validation
|
||||
## Cross field validation
|
||||
This section shows how to perform cross field validation. It assumes some basic knowledge of creating custom validators.
|
||||
|
||||
A cross-field validator is a [custom validator](#custom-validators "Read about custom validators") that compares the values of different fields in a form and accepts or rejects them in combination.
|
||||
For example, you might have a form that offers mutually incompatible options, so that if the user can choose A or B, but not both.
|
||||
Some field values might also depend on others; a user might be allowed to choose B only if A is also chosen.
|
||||
<div class="alert is-helpful">
|
||||
|
||||
The following cross validation examples show how to do the following:
|
||||
If you haven't created custom validators before, start by reviewing the [custom validators section](guide/form-validation#custom-validators).
|
||||
|
||||
* Validate reactive or template-based form input based on the values of two sibling controls,
|
||||
* Show a descriptive error message after the user interacted with the form and the validation failed.
|
||||
</div>
|
||||
|
||||
The examples use cross-validation to ensure that heroes do not reveal their true identities by filling out the Hero Form. The validators do this by checking that the hero names and alter egos do not match.
|
||||
In the following section, we will make sure that our heroes do not reveal their true identities by filling out the Hero Form. We will do that by validating that the hero names and alter egos do not match.
|
||||
|
||||
### Adding cross-validation to reactive forms
|
||||
### Adding to reactive forms
|
||||
|
||||
The form has the following structure:
|
||||
|
||||
@ -228,9 +225,7 @@ const heroForm = new FormGroup({
|
||||
});
|
||||
```
|
||||
|
||||
Notice that the `name` and `alterEgo` are sibling controls.
|
||||
To evaluate both controls in a single custom validator, you must perform the validation in a common ancestor control: the `FormGroup`.
|
||||
You query the `FormGroup` for its child controls so that you can compare their values.
|
||||
Notice that the name and alterEgo are sibling controls. To evaluate both controls in a single custom validator, we should perform the validation in a common ancestor control: the `FormGroup`. That way, we can query the `FormGroup` for the child controls which will allow us to compare their values.
|
||||
|
||||
To add a validator to the `FormGroup`, pass the new validator in as the second argument on creation.
|
||||
|
||||
@ -242,73 +237,74 @@ const heroForm = new FormGroup({
|
||||
}, { validators: identityRevealedValidator });
|
||||
```
|
||||
|
||||
The validator code is as follows.
|
||||
The validator code is as follows:
|
||||
|
||||
<code-example path="form-validation/src/app/shared/identity-revealed.directive.ts" region="cross-validation-validator" header="shared/identity-revealed.directive.ts"></code-example>
|
||||
|
||||
The `identity` validator implements the `ValidatorFn` interface. It takes an Angular control object as an argument and returns either null if the form is valid, or `ValidationErrors` otherwise.
|
||||
The identity validator implements the `ValidatorFn` interface. It takes an Angular control object as an argument and returns either null if the form is valid, or `ValidationErrors` otherwise.
|
||||
|
||||
The validator retrieves the child controls by calling the `FormGroup`'s [get](api/forms/AbstractControl#get) method, then compares the values of the `name` and `alterEgo` controls.
|
||||
First we retrieve the child controls by calling the `FormGroup`'s [get](api/forms/AbstractControl#get) method. Then we simply compare the values of the `name` and `alterEgo` controls.
|
||||
|
||||
If the values do not match, the hero's identity remains secret, both are valid, and the validator returns null.
|
||||
If they do match, the hero's identity is revealed and the validator must mark the form as invalid by returning an error object.
|
||||
|
||||
To provide better user experience, the template shows an appropriate error message when the form is invalid.
|
||||
If the values do not match, the hero's identity remains secret, and we can safely return null. Otherwise, the hero's identity is revealed and we must mark the form as invalid by returning an error object.
|
||||
|
||||
Next, to provide better user experience, we show an appropriate error message when the form is invalid.
|
||||
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.html" region="cross-validation-error-message" header="reactive/hero-form-template.component.html"></code-example>
|
||||
|
||||
This `*ngIf` displays the error if the `FormGroup` has the cross validation error returned by the `identityRevealed` validator, but only if the user has finished [interacting with the form](#dirty-or-touched).
|
||||
Note that we check if:
|
||||
- the `FormGroup` has the cross validation error returned by the `identityRevealed` validator,
|
||||
- the user is yet to [interact](guide/form-validation#why-check-dirty-and-touched) with the form.
|
||||
|
||||
### Adding cross-validation to template-driven forms
|
||||
|
||||
For a template-driven form, you must create a directive to wrap the validator function.
|
||||
You provide that directive as the validator using the [`NG_VALIDATORS` token](#adding-to-template-driven-forms "Read about providing validators"), as shown in the following example.
|
||||
### Adding to template driven forms
|
||||
First we must create a directive that will wrap the validator function. We provide it as the validator using the `NG_VALIDATORS` token. If you are not sure why, or you do not fully understand the syntax, revisit the previous [section](guide/form-validation#adding-to-template-driven-forms).
|
||||
|
||||
<code-example path="form-validation/src/app/shared/identity-revealed.directive.ts" region="cross-validation-directive" header="shared/identity-revealed.directive.ts"></code-example>
|
||||
|
||||
You must add the new directive to the HTML template.
|
||||
Because the validator must be registered at the highest level in the form, the following template puts the directive on the `form` tag.
|
||||
|
||||
Next, we have to add the directive to the html template. Since the validator must be registered at the highest level in the form, we put the directive on the `form` tag.
|
||||
<code-example path="form-validation/src/app/template/hero-form-template.component.html" region="cross-validation-register-validator" header="template/hero-form-template.component.html"></code-example>
|
||||
|
||||
To provide better user experience, we show an appropriate error message when the form is invalid.
|
||||
|
||||
<code-example path="form-validation/src/app/template/hero-form-template.component.html" region="cross-validation-error-message" header="template/hero-form-template.component.html"></code-example>
|
||||
|
||||
This is the same in both template-driven and reactive forms.
|
||||
Note that we check if:
|
||||
- the form has the cross validation error returned by the `identityRevealed` validator,
|
||||
- the user is yet to [interact](guide/form-validation#why-check-dirty-and-touched) with the form.
|
||||
|
||||
## Creating asynchronous validators
|
||||
This completes the cross validation example. We managed to:
|
||||
- validate the form based on the values of two sibling controls,
|
||||
- show a descriptive error message after the user interacted with the form and the validation failed.
|
||||
|
||||
Asynchronous validators implement the `AsyncValidatorFn` and `AsyncValidator` interfaces.
|
||||
These are very similar to their synchronous counterparts, with the following differences.
|
||||
## Async Validation
|
||||
This section shows how to create asynchronous validators. It assumes some basic knowledge of creating [custom validators](guide/form-validation#custom-validators).
|
||||
|
||||
* The `validate()` functions must return a Promise or an observable,
|
||||
* The observable returned must be finite, meaning it must complete at some point.
|
||||
To convert an infinite observable into a finite one, pipe the observable through a filtering operator such as `first`, `last`, `take`, or `takeUntil`.
|
||||
### The Basics
|
||||
Just like synchronous validators have the `ValidatorFn` and `Validator` interfaces, asynchronous validators have their own counterparts: `AsyncValidatorFn` and `AsyncValidator`.
|
||||
|
||||
Asynchronous validation happens after the synchronous validation, and is performed only if the synchronous validation is successful.
|
||||
This check allows forms to avoid potentially expensive async validation processes (such as an HTTP request) if the more basic validation methods have already found invalid input.
|
||||
They are very similar with the only difference being:
|
||||
|
||||
After asynchronous validation begins, the form control enters a `pending` state. You can inspect the control's `pending` property and use it to give visual feedback about the ongoing validation operation.
|
||||
* They must return a Promise or an Observable,
|
||||
* The observable returned must be finite, meaning it must complete at some point. To convert an infinite observable into a finite one, pipe the observable through a filtering operator such as `first`, `last`, `take`, or `takeUntil`.
|
||||
|
||||
A common UI pattern is to show a spinner while the async validation is being performed. The following example shows how to achieve this in a template-driven form.
|
||||
It is important to note that the asynchronous validation happens after the synchronous validation, and is performed only if the synchronous validation is successful. This check allows forms to avoid potentially expensive async validation processes such as an HTTP request if more basic validation methods fail.
|
||||
|
||||
After asynchronous validation begins, the form control enters a `pending` state. You can inspect the control's `pending` property and use it to give visual feedback about the ongoing validation.
|
||||
|
||||
A common UI pattern is to show a spinner while the async validation is being performed. The following example presents how to achieve this with template-driven forms:
|
||||
|
||||
```html
|
||||
<input [(ngModel)]="name" #model="ngModel" appSomeAsyncValidator>
|
||||
<app-spinner *ngIf="model.pending"></app-spinner>
|
||||
```
|
||||
|
||||
### Implementing a custom async validator
|
||||
### Implementing Custom Async Validator
|
||||
In the following section, validation is performed asynchronously to ensure that our heroes pick an alter ego that is not already taken. New heroes are constantly enlisting and old heroes are leaving the service. That means that we do not have the list of available alter egos ahead of time.
|
||||
|
||||
In the following example, an async validator ensures that heroes pick an alter ego that is not already taken.
|
||||
New heroes are constantly enlisting and old heroes are leaving the service, so the list of available alter egos cannot be retrieved ahead of time.
|
||||
To validate the potential alter ego entry, the validator must initiate an asynchronous operation to consult a central database of all currently enlisted heroes.
|
||||
To validate the potential alter ego, we need to consult a central database of all currently enlisted heroes. The process is asynchronous, so we need a special validator for that.
|
||||
|
||||
The following code create the validator class, `UniqueAlterEgoValidator`, which implements the `AsyncValidator` interface.
|
||||
Let's start by creating the validator class.
|
||||
|
||||
<code-example path="form-validation/src/app/shared/alter-ego.directive.ts" region="async-validator"></code-example>
|
||||
|
||||
The constructor injects the `HeroesService`, which defines the following interface.
|
||||
As you can see, the `UniqueAlterEgoValidator` class implements the `AsyncValidator` interface. In the constructor, we inject the `HeroesService` that has the following interface:
|
||||
|
||||
```typescript
|
||||
interface HeroesService {
|
||||
@ -316,37 +312,29 @@ interface HeroesService {
|
||||
}
|
||||
```
|
||||
|
||||
In a real world application, the `HeroesService` would be responsible for making an HTTP request to the hero database to check if the alter ego is available.
|
||||
From the validator's point of view, the actual implementation of the service is not important, so the example can just code against the `HeroesService` interface.
|
||||
In a real world application, the `HeroesService` is responsible for making an HTTP request to the hero database to check if the alter ego is available. From the validator's point of view, the actual implementation of the service is not important, so we can just code against the `HeroesService` interface.
|
||||
|
||||
As the validation begins, the `UniqueAlterEgoValidator` delegates to the `HeroesService` `isAlterEgoTaken()` method with the current control value.
|
||||
At this point the control is marked as `pending` and remains in this state until the observable chain returned from the `validate()` method completes.
|
||||
As the validation begins, the `UniqueAlterEgoValidator` delegates to the `HeroesService` `isAlterEgoTaken()` method with the current control value. At this point the control is marked as `pending` and remains in this state until the observable chain returned from the `validate()` method completes.
|
||||
|
||||
The `isAlterEgoTaken()` method dispatches an HTTP request that checks if the alter ego is available, and returns `Observable<boolean>` as the result.
|
||||
The `validate()` method pipes the response through the `map` operator and transforms it into a validation result.
|
||||
The `isAlterEgoTaken()` method dispatches an HTTP request that checks if the alter ego is available, and returns `Observable<boolean>` as the result. We pipe the response through the `map` operator and transform it into a validation result. As always, we return `null` if the form is valid, and `ValidationErrors` if it is not. We make sure to handle any potential errors with the `catchError` operator.
|
||||
|
||||
The method then, like any validator, returns `null` if the form is valid, and `ValidationErrors` if it is not.
|
||||
This validator handles any potential errors with the `catchError` operator.
|
||||
In this case, the validator treats the `isAlterEgoTaken()` error as a successful validation, because failure to make a validation request does not necessarily mean that the alter ego is invalid.
|
||||
You could handle the error differently and return the `ValidationError` object instead.
|
||||
Here we decided that `isAlterEgoTaken()` error is treated as a successful validation, because failure to make a validation request does not necessarily mean that the alter ego is invalid. You could handle the error differently and return the `ValidationError` object instead.
|
||||
|
||||
After some time passes, the observable chain completes and the asynchronous validation is done.
|
||||
The `pending` flag is set to `false`, and the form validity is updated.
|
||||
After some time passes, the observable chain completes and the async validation is done. The `pending` flag is set to `false`, and the form validity is updated.
|
||||
|
||||
### Optimizing performance of async validators
|
||||
### Note on performance
|
||||
|
||||
By default, all validators run after every form value change. With synchronous validators, this does not normally have a noticeable impact on application performance.
|
||||
Async validators, however, commonly perform some kind of HTTP request to validate the control. Dispatching an HTTP request after every keystroke could put a strain on the backend API, and should be avoided if possible.
|
||||
By default, all validators are run after every form value change. With synchronous validators, this will not likely have a noticeable impact on application performance. However, it's common for async validators to perform some kind of HTTP request to validate the control. Dispatching an HTTP request after every keystroke could put a strain on the backend API, and should be avoided if possible.
|
||||
|
||||
You can delay updating the form validity by changing the `updateOn` property from `change` (default) to `submit` or `blur`.
|
||||
We can delay updating the form validity by changing the `updateOn` property from `change` (default) to `submit` or `blur`.
|
||||
|
||||
With template-driven forms, set the property in the template.
|
||||
With template-driven forms:
|
||||
|
||||
```html
|
||||
<input [(ngModel)]="name" [ngModelOptions]="{updateOn: 'blur'}">
|
||||
```
|
||||
|
||||
With reactive forms, set the property in the `FormControl` instance.
|
||||
With reactive forms:
|
||||
|
||||
```typescript
|
||||
new FormControl('', {updateOn: 'blur'});
|
||||
|
@ -214,13 +214,6 @@ Read more about component classes, templates, and views in [Introduction to Angu
|
||||
|
||||
See [workspace configuration](#cli-config)
|
||||
|
||||
{@a content-projection}
|
||||
|
||||
## content projection
|
||||
|
||||
A way to insert DOM content from outside a component into the component's view in a designated spot.
|
||||
|
||||
For more information, see [Responding to changes in content](guide/lifecycle-hooks#content-projection).
|
||||
|
||||
{@a custom-element}
|
||||
|
||||
@ -601,7 +594,7 @@ Compare to [NgModule](#ngmodule).
|
||||
## ngcc
|
||||
|
||||
Angular compatibility compiler.
|
||||
If you build your app using [Ivy](#ivy), but it depends on libraries that have not been compiled with Ivy, the CLI uses `ngcc` to automatically update the dependent libraries to use Ivy.
|
||||
If you build your app using [Ivy](#ivy), but it depends on libraries have not been compiled with Ivy, the CLI uses `ngcc` to automatically update the dependent libraries to use Ivy.
|
||||
|
||||
|
||||
{@a ngmodule}
|
||||
@ -953,19 +946,6 @@ Read more about TypeScript at [typescriptlang.org](http://www.typescriptlang.org
|
||||
|
||||
{@a U}
|
||||
|
||||
{@a unidirectional-data-flow}
|
||||
|
||||
## unidirectional data flow
|
||||
|
||||
A data flow model where the component tree is always checked for changes in one direction (parent to child), which prevents cycles in the change detection graph.
|
||||
|
||||
In practice, this means that data in Angular flows downward during change detection.
|
||||
A parent component can easily change values in its child components because the parent is checked first.
|
||||
A failure could occur, however, if a child component tries to change a value in its parent during change detection (inverting the expected data flow), because the parent component has already been rendered.
|
||||
In development mode, Angular throws the `ExpressionChangedAfterItHasBeenCheckedError` error if your app attempts to do this, rather than silently failing to render the new value.
|
||||
|
||||
To avoid this error, a [lifecycle hook](guide/lifecycle-hooks) method that seeks to make such a change should trigger a new change detection run. The new run follows the same direction as before, but succeeds in picking up the new value.
|
||||
|
||||
{@a universal}
|
||||
|
||||
## Universal
|
||||
@ -988,7 +968,7 @@ Angular renders a view under the control of one or more [directives](#directive)
|
||||
A [component](#component) class and its associated [template](#template) define a view.
|
||||
A view is specifically represented by a `ViewRef` instance associated with a component.
|
||||
A view that belongs immediately to a component is called a *host view*.
|
||||
Views are typically collected into [view hierarchies](#view-tree).
|
||||
Views are typically collected into [view hierarchies](#view-tree).
|
||||
|
||||
Properties of elements in a view can change dynamically, in response to user actions;
|
||||
the structure (number and order) of elements in a view can't.
|
||||
|
@ -234,7 +234,7 @@ export class SettingsMenu extends BaseMenu {}
|
||||
## Cannot Bind to `value` property of `<select>` with `*ngFor`
|
||||
|
||||
|
||||
### Basic example of change
|
||||
### Basic example of change
|
||||
|
||||
|
||||
```html
|
||||
@ -243,15 +243,15 @@ export class SettingsMenu extends BaseMenu {}
|
||||
</select>
|
||||
```
|
||||
|
||||
In the View Engine runtime, the above code would set the initial value of the `<select>` as expected.
|
||||
In the View Engine runtime, the above code would set the initial value of the `<select>` as expected.
|
||||
In Ivy, the initial value would not be set at all in this case.
|
||||
|
||||
|
||||
### Background
|
||||
|
||||
Prior to Ivy, directive input bindings were always executed in their own change detection pass before any DOM bindings were processed.
|
||||
Prior to Ivy, directive input bindings were always executed in their own change detection pass before any DOM bindings were processed.
|
||||
This was an implementation detail that supported the use case in question:
|
||||
|
||||
|
||||
```html
|
||||
<select [value]="someValue">
|
||||
<option *ngFor="let option of options" [value]="option"> {{ option }} <option>
|
||||
@ -259,13 +259,13 @@ This was an implementation detail that supported the use case in question:
|
||||
```
|
||||
|
||||
It happened to work because the `*ngFor` would be checked first, during the directive input binding pass, and thus create the options first.
|
||||
Then the DOM binding pass would run, which would check the `value` binding.
|
||||
At this time, it would be able to match the value against one of the existing options, and set the value of the `<select>` element in the DOM to display that option.
|
||||
Then the DOM binding pass would run, which would check the `value` binding.
|
||||
At this time, it would be able to match the value against one of the existing options, and set the value of the `<select>` element in the DOM to display that option.
|
||||
|
||||
In Ivy, bindings are checked in the order they are defined in the template, regardless of whether they are directive input bindings or DOM bindings.
|
||||
This change makes change detection easier to reason about for debugging purposes, since bindings will be checked in depth-first order as declared in the template.
|
||||
|
||||
In this case, it means that the `value` binding will be checked before the `*ngFor` is checked, as it is declared above the `*ngFor` in the template.
|
||||
In this case, it means that the `value` binding will be checked before the `*ngFor` is checked, as it is declared above the `*ngFor` in the template.
|
||||
Consequently, the value of the `<select>` element will be set before any options are created, and it won't be able to match and display the correct option in the DOM.
|
||||
|
||||
### Example of error
|
||||
@ -291,60 +291,4 @@ To fix this problem, we recommend binding to the `selected` property on the `<op
|
||||
{{ option }}
|
||||
<option>
|
||||
</select>
|
||||
```
|
||||
|
||||
{@a forward-refs-directive-inputs}
|
||||
## Forward references to directive inputs accessed through local refs are no longer supported.
|
||||
|
||||
|
||||
### Basic example of change
|
||||
|
||||
|
||||
```ts
|
||||
@Directive({
|
||||
selector: '[myDir]',
|
||||
exportAs: 'myDir'
|
||||
})
|
||||
export class MyDir {
|
||||
@Input() message: string;
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
{{ myDir.name }}
|
||||
<div myDir #myDir="myDir" [name]="myName"></div>
|
||||
```
|
||||
|
||||
In the View Engine runtime, the above code would print out the name without any errors.
|
||||
In Ivy, the `myDir.name` binding will throw an `ExpressionChangedAfterItHasBeenCheckedError`.
|
||||
|
||||
|
||||
### Background
|
||||
|
||||
In the ViewEngine runtime, directive input bindings and element bindings were executed in different stages. Angular would process the template one full time to check directive inputs only (e.g. `[name]`), then process the whole template again to check element and text bindings only (e.g.`{{ myDir.name }}`). This meant that the `name` directive input would be checked before the `myDir.name` text binding despite their relative order in the template, which some users felt to be counterintuitive.
|
||||
|
||||
In contrast, Ivy processes the template in just one pass, so that bindings are checked in the same order that they are written in the template. In this case, it means that the `myDir.name` binding will be checked before the `name` input sets the property on the directive (and thus it will be `undefined`). Since the `myDir.name` property will be set by the time the next change detection pass runs, a change detection error is thrown.
|
||||
|
||||
### Example of error
|
||||
|
||||
Assuming that the value for `myName` is `Angular`, you should see an error that looks like
|
||||
|
||||
```
|
||||
Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'Angular'.
|
||||
```
|
||||
|
||||
### Recommended fix
|
||||
|
||||
To fix this problem, we recommend either getting the information for the binding directly from the host component (e.g. the `myName` property from our example) or to move the data binding after the directive has been declared so that the initial value is available on the first pass.
|
||||
|
||||
*Before*
|
||||
```html
|
||||
{{ myDir.name }}
|
||||
<div myDir #myDir="myDir" [name]="myName"></div>
|
||||
```
|
||||
|
||||
*After*
|
||||
```html
|
||||
{{ myName }}
|
||||
<div myDir [name]="myName"></div>
|
||||
```
|
||||
```
|
@ -63,7 +63,7 @@ Please note that these constants are not meant to be used by 3rd party library o
|
||||
|
||||
* Foreign functions or foreign constants in decorator metadata aren't statically resolvable (previously, you could import a constant or function from another compilation unit, like a library, and use that constant/function in your `@NgModule` definition).
|
||||
|
||||
* Forward references to directive inputs accessed through local refs are no longer supported by default. [details](guide/ivy-compatibility-examples#forward-refs-directive-inputs)
|
||||
* Forward references to directive inputs accessed through local refs are no longer supported by default.
|
||||
|
||||
* If there is both an unbound class attribute and a `[class]` binding, the classes in the unbound attribute will also be added (previously, the class binding would overwrite classes in the unbound attribute).
|
||||
|
||||
|
@ -1,61 +1,19 @@
|
||||
# Lazy-loading feature modules
|
||||
|
||||
## High level view
|
||||
|
||||
By default, NgModules are eagerly loaded, which means that as soon as the app loads, so do all the NgModules, whether or not they are immediately necessary. For large apps with lots of routes, consider lazy loading—a design pattern that loads NgModules as needed. Lazy loading helps keep initial
|
||||
bundle sizes smaller, which in turn helps decrease load times.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
For the final sample app with two lazy-loaded modules that this page describes, see the
|
||||
<live-example></live-example>.
|
||||
|
||||
</div>
|
||||
|
||||
{@a lazy-loading}
|
||||
|
||||
## Lazy loading basics
|
||||
|
||||
This section introduces the basic procedure for configuring a lazy-loaded route.
|
||||
For a step-by-step example, see the [step-by-step setup](#step-by-step) section on this page.
|
||||
|
||||
To lazy load Angular modules, use `loadchildren` (instead of `component`) in your `AppRoutingModule` `routes` configuration as follows.
|
||||
|
||||
<code-example header="AppRoutingModule (excerpt)">
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'items',
|
||||
loadChildren: () => import('./items/items.module').then(m => m.ItemsModule)
|
||||
}
|
||||
];
|
||||
|
||||
</code-example>
|
||||
|
||||
In the lazy-loaded module's routing module, add a route for the component.
|
||||
|
||||
<code-example header="Routing module for lazy loaded module (excerpt)">
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ItemsComponent
|
||||
}
|
||||
];
|
||||
|
||||
</code-example>
|
||||
|
||||
Also be sure to remove the `ItemsModule` from the `AppModule`.
|
||||
For step-by-step instructions on lazy loading modules, continue with the following sections of this page.
|
||||
|
||||
{@a step-by-step}
|
||||
|
||||
## Step-by-step setup
|
||||
|
||||
There are two main steps to setting up a lazy-loaded feature module:
|
||||
|
||||
1. Create the feature module with the CLI, using the `--route` flag.
|
||||
1. Configure the routes.
|
||||
|
||||
### Set up an app
|
||||
## Set up an app
|
||||
|
||||
If you don’t already have an app, you can follow the steps below to
|
||||
create one with the CLI. If you already have an app, skip to
|
||||
@ -78,7 +36,7 @@ See [Keeping Up to Date](guide/updating).
|
||||
|
||||
</div>
|
||||
|
||||
### Create a feature module with routing
|
||||
## Create a feature module with routing
|
||||
|
||||
Next, you’ll need a feature module with a component to route to.
|
||||
To make one, enter the following command in the terminal, where `customers` is the name of the feature module. The path for loading the `customers` feature modules is also `customers` because it is specified with the `--route` option:
|
||||
@ -101,7 +59,7 @@ Instead, it adds the declared route, `customers` to the `routes` array declared
|
||||
Notice that the lazy-loading syntax uses `loadChildren` followed by a function that uses the browser's built-in `import('...')` syntax for dynamic imports.
|
||||
The import path is the relative path to the module.
|
||||
|
||||
#### Add another feature module
|
||||
### Add another feature module
|
||||
|
||||
Use the same command to create a second lazy-loaded feature module with routing, along with its stub component.
|
||||
|
||||
@ -118,14 +76,16 @@ The `orders` route, specified with the `--route` option, is added to the `routes
|
||||
region="routes-customers-orders">
|
||||
</code-example>
|
||||
|
||||
### Set up the UI
|
||||
## Set up the UI
|
||||
|
||||
Though you can type the URL into the address bar, a navigation UI is easier for the user and more common.
|
||||
Replace the default placeholder markup in `app.component.html` with a custom nav
|
||||
so you can easily navigate to your modules in the browser:
|
||||
|
||||
|
||||
<code-example path="lazy-loading-ngmodules/src/app/app.component.html" header="app.component.html" region="app-component-template" header="src/app/app.component.html"></code-example>
|
||||
|
||||
|
||||
To see your app in the browser so far, enter the following command in the terminal window:
|
||||
|
||||
<code-example language="bash">
|
||||
@ -142,7 +102,7 @@ These buttons work, because the CLI automatically added the routes to the featur
|
||||
|
||||
{@a config-routes}
|
||||
|
||||
### Imports and route configuration
|
||||
## Imports and route configuration
|
||||
|
||||
The CLI automatically added each feature module to the routes map at the application level.
|
||||
Finish this off by adding the default route. In the `app-routing.module.ts` file, update the `routes` array with the following:
|
||||
@ -174,7 +134,7 @@ The other feature module's routing module is configured similarly.
|
||||
|
||||
<code-example path="lazy-loading-ngmodules/src/app/orders/orders-routing.module.ts" id="orders-routing.module.ts" region="orders-routing-module-detail" header="src/app/orders/orders-routing.module.ts (excerpt)"></code-example>
|
||||
|
||||
### Verify lazy loading
|
||||
## Confirm it’s working
|
||||
|
||||
You can check to see that a module is indeed being lazy loaded with the Chrome developer tools. In Chrome, open the dev tools by pressing `Cmd+Option+i` on a Mac or `Ctrl+Shift+j` on a PC and go to the Network Tab.
|
||||
|
||||
@ -215,105 +175,6 @@ The `forRoot()` method takes care of the *global* injector configuration for the
|
||||
The `forChild()` method has no injector configuration. It uses directives such as `RouterOutlet` and `RouterLink`.
|
||||
For more information, see the [`forRoot()` pattern](guide/singleton-services#forRoot) section of the [Singleton Services](guide/singleton-services) guide.
|
||||
|
||||
{@a preloading}
|
||||
|
||||
## Preloading
|
||||
|
||||
Preloading improves UX by loading parts of your app in the background.
|
||||
You can preload modules or component data.
|
||||
|
||||
### Preloading modules
|
||||
|
||||
Preloading modules improves UX by loading parts of your app in the background so users don't have to wait for the elements to download when they activate a route.
|
||||
|
||||
To enable preloading of all lazy loaded modules, import the `PreloadAllModules` token from the Angular `router`.
|
||||
|
||||
<code-example header="AppRoutingModule (excerpt)">
|
||||
|
||||
import { PreloadAllModules } from '@angular/router';
|
||||
|
||||
</code-example>
|
||||
|
||||
Still in the `AppRoutingModule`, specify your preloading strategy in `forRoot()`.
|
||||
|
||||
<code-example header="AppRoutingModule (excerpt)">
|
||||
|
||||
RouterModule.forRoot(
|
||||
appRoutes,
|
||||
{
|
||||
preloadingStrategy: PreloadAllModules
|
||||
}
|
||||
)
|
||||
|
||||
</code-example>
|
||||
|
||||
### Preloading component data
|
||||
|
||||
To preload component data, you can use a `resolver`.
|
||||
Resolvers improve UX by blocking the page load until all necessary data is available to fully display the page.
|
||||
|
||||
#### Resolvers
|
||||
|
||||
Create a resolver service.
|
||||
With the CLI, the command to generate a service is as follows:
|
||||
|
||||
|
||||
<code-example language="none" class="code-shell">
|
||||
ng generate service <service-name>
|
||||
</code-example>
|
||||
|
||||
In your service, import the following router members, implement `Resolve`, and inject the `Router` service:
|
||||
|
||||
<code-example header="Resolver service (excerpt)">
|
||||
|
||||
import { Resolve } from '@angular/router';
|
||||
|
||||
...
|
||||
|
||||
export class CrisisDetailResolverService implements Resolve<> {
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<> {
|
||||
// your logic goes here
|
||||
}
|
||||
}
|
||||
|
||||
</code-example>
|
||||
|
||||
Import this resolver into your module's routing module.
|
||||
|
||||
<code-example header="Feature module's routing module (excerpt)">
|
||||
|
||||
import { YourResolverService } from './your-resolver.service';
|
||||
|
||||
</code-example>
|
||||
|
||||
Add a `resolve` object to the component's `route` configuration.
|
||||
|
||||
<code-example header="Feature module's routing module (excerpt)">
|
||||
{
|
||||
path: '/your-path',
|
||||
component: YourComponent,
|
||||
resolve: {
|
||||
crisis: YourResolverService
|
||||
}
|
||||
}
|
||||
</code-example>
|
||||
|
||||
|
||||
In the component, use an `Observable` to get the data from the `ActivatedRoute`.
|
||||
|
||||
|
||||
<code-example header="Component (excerpt)">
|
||||
ngOnInit() {
|
||||
this.route.data
|
||||
.subscribe((your-parameters) => {
|
||||
// your data-specific code goes here
|
||||
});
|
||||
}
|
||||
</code-example>
|
||||
|
||||
For more information with a working example, see the [routing tutorial section on preloading](guide/router#preloading-background-loading-of-feature-areas).
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
## More on NgModules and routing
|
||||
|
@ -1,49 +1,45 @@
|
||||
# Hooking into the component lifecycle
|
||||
# Lifecycle hooks
|
||||
|
||||
A component instance has a lifecycle that starts when Angular instantiates the component class and renders the component view along with its child views.
|
||||
The lifecycle continues with change detection, as Angular checks to see when data-bound properties change, and updates both the view and the component instance as needed.
|
||||
The lifecycle ends when Angular destroys the component instance and removes its rendered template from the DOM.
|
||||
Directives have a similar lifecycle, as Angular creates, updates, and destroys instances in the course of execution.
|
||||
A component has a lifecycle managed by Angular.
|
||||
|
||||
Your application can use [lifecycle hook methods](guide/glossary#lifecycle-hook "Definition of lifecycle hook") to tap into key events in the lifecycle of a component or directive in order to initialize new instances, initiate change detection when needed, respond to updates during change detection, and clean up before deletion of instances.
|
||||
Angular creates and renders components along with their children, checks when their data-bound properties change, and destroys them before removing them from the DOM.
|
||||
|
||||
## Prerequisites
|
||||
Angular offers **lifecycle hooks**
|
||||
that provide visibility into these key life moments and the ability to act when they occur.
|
||||
|
||||
Before working with lifecycle hooks, you should have a basic understanding of the following:
|
||||
|
||||
* [TypeScript programming](https://www.typescriptlang.org/).
|
||||
* Angular app-design fundamentals, as described in [Angular Concepts](guide/architecture "Introduction to fundamental app-design concepts").
|
||||
A directive has the same set of lifecycle hooks.
|
||||
|
||||
{@a hooks-overview}
|
||||
|
||||
## Responding to lifecycle events
|
||||
## Component lifecycle hooks overview
|
||||
|
||||
You can respond to events in the lifecycle of a component or directive by implementing one or more of the *lifecycle hook* interfaces in the Angular `core` library.
|
||||
The hooks give you the opportunity to act on a component or directive instance at the appropriate moment, as Angular creates, updates, or destroys that instance.
|
||||
Directive and component instances have a lifecycle
|
||||
as Angular creates, updates, and destroys them.
|
||||
Developers can tap into key moments in that lifecycle by implementing
|
||||
one or more of the *lifecycle hook* interfaces in the Angular `core` library.
|
||||
|
||||
Each interface defines the prototype for a single hook method, whose name is the interface name prefixed with `ng`.
|
||||
For example, the `OnInit` interface has a hook method named `ngOnInit()`. If you implement this method in your component or directive class, Angular calls it shortly after checking the input properties for that component or directive for the first time.
|
||||
Each interface has a single hook method whose name is the interface name prefixed with `ng`.
|
||||
For example, the `OnInit` interface has a hook method named `ngOnInit()`
|
||||
that Angular calls shortly after creating the component:
|
||||
|
||||
<code-example path="lifecycle-hooks/src/app/peek-a-boo.component.ts" region="ngOnInit" header="peek-a-boo.component.ts (excerpt)"></code-example>
|
||||
|
||||
You don't have to implement all (or any) of the lifecycle hooks, just the ones you need.
|
||||
No directive or component will implement all of the lifecycle hooks.
|
||||
Angular only calls a directive/component hook method *if it is defined*.
|
||||
|
||||
{@a hooks-purpose-timing}
|
||||
|
||||
### Lifecycle event sequence
|
||||
## Lifecycle sequence
|
||||
|
||||
After your application instantiates a component or directive by calling its constructor, Angular calls the hook methods you have implemented at the appropriate point in the lifecycle of that instance.
|
||||
|
||||
Angular executes hook methods in the following sequence. You can use them to perform the following kinds of operations.
|
||||
*After* creating a component/directive by calling its constructor, Angular
|
||||
calls the lifecycle hook methods in the following sequence at specific moments:
|
||||
|
||||
<table width="100%">
|
||||
<col width="20%"></col>
|
||||
<col width="60%"></col>
|
||||
<col width="20%"></col>
|
||||
<col width="80%"></col>
|
||||
<tr>
|
||||
<th>Hook method</th>
|
||||
<th>Purpose</th>
|
||||
<th>Timing</th>
|
||||
<th>Hook</th>
|
||||
<th>Purpose and Timing</th>
|
||||
</tr>
|
||||
<tr style='vertical-align:top'>
|
||||
<td>
|
||||
@ -51,15 +47,9 @@ Angular executes hook methods in the following sequence. You can use them to per
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Respond when Angular sets or resets data-bound input properties.
|
||||
Respond when Angular (re)sets data-bound input properties.
|
||||
The method receives a `SimpleChanges` object of current and previous property values.
|
||||
|
||||
Note that this happens very frequently, so any operation you perform here impacts performance significantly.
|
||||
See details in [Using change detection hooks](#onchanges) in this document.
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Called before `ngOnInit()` and whenever one or more data-bound input properties change.
|
||||
|
||||
</td>
|
||||
@ -70,14 +60,10 @@ Angular executes hook methods in the following sequence. You can use them to per
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Initialize the directive or component after Angular first displays the data-bound properties
|
||||
and sets the directive or component's input properties.
|
||||
See details in [Initializing a component or directive](#oninit) in this document.
|
||||
Initialize the directive/component after Angular first displays the data-bound properties
|
||||
and sets the directive/component's input properties.
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Called once, after the first `ngOnChanges()`.
|
||||
Called _once_, after the _first_ `ngOnChanges()`.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@ -88,12 +74,8 @@ Angular executes hook methods in the following sequence. You can use them to per
|
||||
<td>
|
||||
|
||||
Detect and act upon changes that Angular can't or won't detect on its own.
|
||||
See details and example in [Defining custom change detection](#docheck) in this document.
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Called immediately after `ngOnChanges()` on every change detection run, and immediately after `ngOnInit()` on the first run.
|
||||
Called during every change detection run, immediately after `ngOnChanges()` and `ngOnInit()`.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@ -103,13 +85,7 @@ Angular executes hook methods in the following sequence. You can use them to per
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Respond after Angular projects external content into the component's view, or into the view that a directive is in.
|
||||
|
||||
See details and example in [Responding to changes in content](#aftercontent) in this document.
|
||||
|
||||
|
||||
</td>
|
||||
<td>
|
||||
Respond after Angular projects external content into the component's view / the view that a directive is in.
|
||||
|
||||
Called _once_ after the first `ngDoCheck()`.
|
||||
|
||||
@ -121,15 +97,9 @@ Angular executes hook methods in the following sequence. You can use them to per
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Respond after Angular checks the content projected into the directive or component.
|
||||
Respond after Angular checks the content projected into the directive/component.
|
||||
|
||||
See details and example in [Responding to projected content changes](#aftercontent) in this document.
|
||||
|
||||
</td>
|
||||
|
||||
<td>
|
||||
|
||||
Called after `ngAfterContentInit()` and every subsequent `ngDoCheck()`.
|
||||
Called after the `ngAfterContentInit()` and every subsequent `ngDoCheck()`.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@ -139,15 +109,10 @@ Angular executes hook methods in the following sequence. You can use them to per
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Respond after Angular initializes the component's views and child views, or the view that contains the directive.
|
||||
|
||||
See details and example in [Responding to view changes](#afterview) in this document.
|
||||
|
||||
</td>
|
||||
|
||||
<td>
|
||||
Respond after Angular initializes the component's views and child views / the view that a directive is in.
|
||||
|
||||
Called _once_ after the first `ngAfterContentChecked()`.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr style='vertical-align:top'>
|
||||
@ -156,11 +121,7 @@ Angular executes hook methods in the following sequence. You can use them to per
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Respond after Angular checks the component's views and child views, or the view that contains the directive.
|
||||
|
||||
</td>
|
||||
|
||||
<td>
|
||||
Respond after Angular checks the component's views and child views / the view that a directive is in.
|
||||
|
||||
Called after the `ngAfterViewInit()` and every subsequent `ngAfterContentChecked()`.
|
||||
|
||||
@ -172,32 +133,53 @@ Angular executes hook methods in the following sequence. You can use them to per
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Cleanup just before Angular destroys the directive or component.
|
||||
Cleanup just before Angular destroys the directive/component.
|
||||
Unsubscribe Observables and detach event handlers to avoid memory leaks.
|
||||
See details in [Cleaning up on instance destruction](#ondestroy) in this document.
|
||||
|
||||
</td>
|
||||
|
||||
<td>
|
||||
|
||||
Called immediately before Angular destroys the directive or component.
|
||||
Called _just before_ Angular destroys the directive/component.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{@a interface-optional}
|
||||
|
||||
## Interfaces are optional (technically)
|
||||
|
||||
The interfaces are optional for JavaScript and Typescript developers from a purely technical perspective.
|
||||
The JavaScript language doesn't have interfaces.
|
||||
Angular can't see TypeScript interfaces at runtime because they disappear from the transpiled JavaScript.
|
||||
|
||||
Fortunately, they aren't necessary.
|
||||
You don't have to add the lifecycle hook interfaces to directives and components to benefit from the hooks themselves.
|
||||
|
||||
Angular instead inspects directive and component classes and calls the hook methods *if they are defined*.
|
||||
Angular finds and calls methods like `ngOnInit()`, with or without the interfaces.
|
||||
|
||||
Nonetheless, it's good practice to add interfaces to TypeScript directive classes
|
||||
in order to benefit from strong typing and editor tooling.
|
||||
|
||||
{@a other-lifecycle-hooks}
|
||||
|
||||
## Other Angular lifecycle hooks
|
||||
|
||||
Other Angular sub-systems may have their own lifecycle hooks apart from these component hooks.
|
||||
|
||||
3rd party libraries might implement their hooks as well in order to give developers more
|
||||
control over how these libraries are used.
|
||||
|
||||
{@a the-sample}
|
||||
|
||||
### Lifecycle example set
|
||||
## Lifecycle examples
|
||||
|
||||
The <live-example></live-example>
|
||||
demonstrates the use of lifecycle hooks through a series of exercises
|
||||
demonstrates the lifecycle hooks in action through a series of exercises
|
||||
presented as components under the control of the root `AppComponent`.
|
||||
In each case a *parent* component serves as a test rig for
|
||||
|
||||
They follow a common pattern: a *parent* component serves as a test rig for
|
||||
a *child* component that illustrates one or more of the lifecycle hook methods.
|
||||
|
||||
The following table lists the exercises with brief descriptions.
|
||||
The sample code is also used to illustrate specific tasks in the following sections.
|
||||
Here's a brief description of each exercise:
|
||||
|
||||
<table width="100%">
|
||||
<col width="20%"></col>
|
||||
@ -223,9 +205,12 @@ The sample code is also used to illustrate specific tasks in the following secti
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Shows how you can use lifecycle hooks with a custom directive.
|
||||
The `SpyDirective` implements the `ngOnInit()` and `ngOnDestroy()` hooks,
|
||||
and uses them to watch and report when an element goes in or out of the current view.
|
||||
Directives have lifecycle hooks too.
|
||||
A `SpyDirective` can log when the element it spies upon is
|
||||
created or destroyed using the `ngOnInit` and `ngOnDestroy` hooks.
|
||||
|
||||
This example applies the `SpyDirective` to a `<div>` in an `ngFor` *hero* repeater
|
||||
managed by the parent `SpyComponent`.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@ -235,9 +220,9 @@ The sample code is also used to illustrate specific tasks in the following secti
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Demonstrates how Angular calls the `ngOnChanges()` hook
|
||||
every time one of the component input properties changes,
|
||||
and shows how to interpret the `changes` object passed to the hook method.
|
||||
See how Angular calls the `ngOnChanges()` hook with a `changes` object
|
||||
every time one of the component input properties changes.
|
||||
Shows how to interpret the `changes` object.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@ -247,8 +232,8 @@ The sample code is also used to illustrate specific tasks in the following secti
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Implements the `ngDoCheck()` method with custom change detection.
|
||||
Watch the hook post changes to a log to see how often Angular calls this hook.
|
||||
Implements an `ngDoCheck()` method with custom change detection.
|
||||
See how often Angular calls this hook and watch it post changes to a log.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@ -258,8 +243,8 @@ The sample code is also used to illustrate specific tasks in the following secti
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Shows what Angular means by a [view](guide/glossary#view "Definition of view.").
|
||||
Demonstrates the `ngAfterViewInit()` and `ngAfterViewChecked()` hooks.
|
||||
Shows what Angular means by a *view*.
|
||||
Demonstrates the `ngAfterViewInit` and `ngAfterViewChecked` hooks.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@ -271,87 +256,40 @@ The sample code is also used to illustrate specific tasks in the following secti
|
||||
|
||||
Shows how to project external content into a component and
|
||||
how to distinguish projected content from a component's view children.
|
||||
Demonstrates the `ngAfterContentInit()` and `ngAfterContentChecked()` hooks.
|
||||
Demonstrates the `ngAfterContentInit` and `ngAfterContentChecked` hooks.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr style='vertical-align:top'>
|
||||
<td>
|
||||
<a href="#counter">Counter</a>
|
||||
Counter
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Demonstrates a combination of a component and a directive, each with its own hooks.
|
||||
Demonstrates a combination of a component and a directive
|
||||
each with its own hooks.
|
||||
|
||||
In this example, a `CounterComponent` logs a change (via `ngOnChanges`)
|
||||
every time the parent component increments its input counter property.
|
||||
Meanwhile, the `SpyDirective` from the previous example is applied
|
||||
to the `CounterComponent` log where it watches log entries being created and destroyed.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
{@a oninit}
|
||||
|
||||
## Initializing a component or directive
|
||||
|
||||
Use the `ngOnInit()` method to perform the following initialization tasks.
|
||||
|
||||
* Perform complex initializations outside of the constructor.
|
||||
Components should be cheap and safe to construct.
|
||||
You should not, for example, fetch data in a component constructor.
|
||||
You shouldn't worry that a new component will try to contact a remote server when
|
||||
created under test or before you decide to display it.
|
||||
|
||||
An `ngOnInit()` is a good place for a component to fetch its initial data.
|
||||
For an example, see the [Tour of Heroes tutorial](tutorial/toh-pt4#oninit).
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
In [Flaw: Constructor does Real Work](http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/), Misko Hevery, Angular team lead, explains why you should avoid complex constructor logic.
|
||||
|
||||
</div>
|
||||
|
||||
* Set up the component after Angular sets the input properties.
|
||||
Constructors should do no more than set the initial local variables to simple values.
|
||||
|
||||
Keep in mind that a directive's data-bound input properties are not set until _after construction_.
|
||||
If you need to initialize the directive based on those properties, set them when `ngOnInit()` runs.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
The `ngOnChanges()` method is your first opportunity to access those properties.
|
||||
Angular calls `ngOnChanges()` before `ngOnInit()`, but also many times after that.
|
||||
It only calls `ngOnInit()` once.
|
||||
|
||||
</div>
|
||||
|
||||
{@a ondestroy}
|
||||
|
||||
## Cleaning up on instance destruction
|
||||
|
||||
Put cleanup logic in `ngOnDestroy()`, the logic that must run before Angular destroys the directive.
|
||||
|
||||
This is the place to free resources that won't be garbage-collected automatically.
|
||||
You risk memory leaks if you neglect to do so.
|
||||
|
||||
* Unsubscribe from Observables and DOM events.
|
||||
* Stop interval timers.
|
||||
* Unregister all callbacks that the directive registered with global or application services.
|
||||
|
||||
The `ngOnDestroy()` method is also the time to notify another part of the application that the component is going away.
|
||||
|
||||
|
||||
## General examples
|
||||
|
||||
The following examples demonstrate the call sequence and relative frequency of the various lifecycle events, and how the hooks can be used separately or together for components and directives.
|
||||
The remainder of this page discusses selected exercises in further detail.
|
||||
|
||||
{@a peek-a-boo}
|
||||
|
||||
### Sequence and frequency of all lifecycle events
|
||||
## Peek-a-boo: all hooks
|
||||
|
||||
To show how Angular calls the hooks in the expected order, the `PeekABooComponent` demonstrates all of the hooks in one component.
|
||||
The `PeekABooComponent` demonstrates all of the hooks in one component.
|
||||
|
||||
In practice you would rarely, if ever, implement all of the interfaces the way this demo does.
|
||||
You would rarely, if ever, implement all of the interfaces like this.
|
||||
The peek-a-boo exists to show how Angular calls the hooks in the expected order.
|
||||
|
||||
The following snapshot reflects the state of the log after the user clicked the *Create...* button and then the *Destroy...* button.
|
||||
This snapshot reflects the state of the log after the user clicked the *Create...* button and then the *Destroy...* button.
|
||||
|
||||
<div class="lightbox">
|
||||
<img src="generated/images/guide/lifecycle-hooks/peek-a-boo.png" alt="Peek-a-boo">
|
||||
@ -363,42 +301,52 @@ The sequence of log messages follows the prescribed hook calling order:
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Notice that the log confirms that input properties (the `name` property in this case) have no assigned values at construction.
|
||||
The input properties are available to the `onInit()` method for further initialization.
|
||||
The constructor isn't an Angular hook *per se*.
|
||||
The log confirms that input properties (the `name` property in this case) have no assigned values at construction.
|
||||
|
||||
</div>
|
||||
|
||||
Had the user clicked the *Update Hero* button, the log would show another `OnChanges` and two more triplets of `DoCheck`, `AfterContentChecked` and `AfterViewChecked`.
|
||||
Notice that these three hooks fire *often*, so it is important to keep their logic as lean as possible.
|
||||
Had the user clicked the *Update Hero* button, the log would show another `OnChanges` and two more triplets of
|
||||
`DoCheck`, `AfterContentChecked` and `AfterViewChecked`.
|
||||
Clearly these three hooks fire *often*. Keep the logic in these hooks as lean as possible!
|
||||
|
||||
The next examples focus on hook details.
|
||||
|
||||
|
||||
{@a spy}
|
||||
|
||||
### Use directives to watch the DOM
|
||||
## Spying *OnInit* and *OnDestroy*
|
||||
|
||||
The `Spy` example demonstrates how you can use hook method for directives as well as components.
|
||||
The `SpyDirective` implements two hooks, `ngOnInit()` and `ngOnDestroy()`, in order to discover when a watched element is in the current view.
|
||||
Go undercover with these two spy hooks to discover when an element is initialized or destroyed.
|
||||
|
||||
This template applies the `SpyDirective` to a `<div>` in the `ngFor` *hero* repeater managed by the parent `SpyComponent`.
|
||||
This is the perfect infiltration job for a directive.
|
||||
The heroes will never know they're being watched.
|
||||
|
||||
The example does not perform any initialization or clean-up.
|
||||
It just tracks the appearance and disappearance of an element in the view by recording when the directive itself is instantiated and destroyed.
|
||||
<div class="alert is-helpful">
|
||||
|
||||
A spy directive like this can provide insight into a DOM object that you cannot change directly.
|
||||
You can't touch the implementation of a native `<div>`, or modify a third party component.
|
||||
You can, however watch these elements with a directive.
|
||||
Kidding aside, pay attention to two key points:
|
||||
|
||||
The directive defines `ngOnInit()` and `ngOnDestroy()` hooks
|
||||
1. Angular calls hook methods for *directives* as well as components.<br><br>
|
||||
|
||||
2. A spy directive can provide insight into a DOM object that you cannot change directly.
|
||||
Obviously you can't touch the implementation of a native `<div>`.
|
||||
You can't modify a third party component either.
|
||||
But you can watch both with a directive.
|
||||
|
||||
</div>
|
||||
|
||||
The sneaky spy directive is simple, consisting almost entirely of `ngOnInit()` and `ngOnDestroy()` hooks
|
||||
that log messages to the parent via an injected `LoggerService`.
|
||||
|
||||
<code-example path="lifecycle-hooks/src/app/spy.directive.ts" region="spy-directive" header="src/app/spy.directive.ts"></code-example>
|
||||
|
||||
You can apply the spy to any native or component element, and see that it is initialized and destroyed
|
||||
You can apply the spy to any native or component element and it'll be initialized and destroyed
|
||||
at the same time as that element.
|
||||
Here it is attached to the repeated hero `<div>`:
|
||||
|
||||
<code-example path="lifecycle-hooks/src/app/spy.component.html" region="template" header="src/app/spy.component.html"></code-example>
|
||||
|
||||
Each spy's creation and destruction marks the appearance and disappearance of the attached hero `<div>`
|
||||
Each spy's birth and death marks the birth and death of the attached hero `<div>`
|
||||
with an entry in the *Hook Log* as seen here:
|
||||
|
||||
<div class="lightbox">
|
||||
@ -411,20 +359,70 @@ The *Reset* button clears the `heroes` list.
|
||||
Angular removes all hero `<div>` elements from the DOM and destroys their spy directives at the same time.
|
||||
The spy's `ngOnDestroy()` method reports its last moments.
|
||||
|
||||
{@a counter}
|
||||
The `ngOnInit()` and `ngOnDestroy()` methods have more vital roles to play in real applications.
|
||||
|
||||
### Use component and directive hooks together
|
||||
{@a oninit}
|
||||
|
||||
In this example, a `CounterComponent` uses the `ngOnChanges()` method to log a change every time the parent component increments its input `counter` property.
|
||||
### _OnInit()_
|
||||
|
||||
This example applies the `SpyDirective` from the previous example to the `CounterComponent` log, in order to watch the creation and destruction of log entries.
|
||||
Use `ngOnInit()` for two main reasons:
|
||||
|
||||
1. To perform complex initializations shortly after construction.
|
||||
1. To set up the component after Angular sets the input properties.
|
||||
|
||||
Experienced developers agree that components should be cheap and safe to construct.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Misko Hevery, Angular team lead,
|
||||
[explains why](http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/)
|
||||
you should avoid complex constructor logic.
|
||||
|
||||
</div>
|
||||
|
||||
Don't fetch data in a component constructor.
|
||||
You shouldn't worry that a new component will try to contact a remote server when
|
||||
created under test or before you decide to display it.
|
||||
Constructors should do no more than set the initial local variables to simple values.
|
||||
|
||||
An `ngOnInit()` is a good place for a component to fetch its initial data. The
|
||||
[Tour of Heroes Tutorial](tutorial/toh-pt4#oninit) guide shows how.
|
||||
|
||||
|
||||
Remember also that a directive's data-bound input properties are not set until _after construction_.
|
||||
That's a problem if you need to initialize the directive based on those properties.
|
||||
They'll have been set when `ngOnInit()` runs.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
The `ngOnChanges()` method is your first opportunity to access those properties.
|
||||
Angular calls `ngOnChanges()` before `ngOnInit()` and many times after that.
|
||||
It only calls `ngOnInit()` once.
|
||||
|
||||
</div>
|
||||
|
||||
You can count on Angular to call the `ngOnInit()` method _soon_ after creating the component.
|
||||
That's where the heavy initialization logic belongs.
|
||||
|
||||
{@a ondestroy}
|
||||
|
||||
### _OnDestroy()_
|
||||
|
||||
Put cleanup logic in `ngOnDestroy()`, the logic that *must* run before Angular destroys the directive.
|
||||
|
||||
This is the time to notify another part of the application that the component is going away.
|
||||
|
||||
This is the place to free resources that won't be garbage collected automatically.
|
||||
Unsubscribe from Observables and DOM events. Stop interval timers.
|
||||
Unregister all callbacks that this directive registered with global or application services.
|
||||
You risk memory leaks if you neglect to do so.
|
||||
|
||||
{@a onchanges}
|
||||
|
||||
## Using change detection hooks
|
||||
## _OnChanges()_
|
||||
|
||||
Angular calls the `ngOnChanges()` method of a component or directive whenever it detects changes to the ***input properties***.
|
||||
The *onChanges* example demonstrates this by monitoring the `OnChanges()` hook.
|
||||
Angular calls its `ngOnChanges()` method whenever it detects changes to ***input properties*** of the component (or directive).
|
||||
This example monitors the `OnChanges` hook.
|
||||
|
||||
<code-example path="lifecycle-hooks/src/app/on-changes.component.ts" region="ng-on-changes" header="on-changes.component.ts (excerpt)"></code-example>
|
||||
|
||||
@ -436,7 +434,7 @@ The example component, `OnChangesComponent`, has two input properties: `hero` an
|
||||
|
||||
<code-example path="lifecycle-hooks/src/app/on-changes.component.ts" region="inputs" header="src/app/on-changes.component.ts"></code-example>
|
||||
|
||||
The host `OnChangesParentComponent` binds to them as follows.
|
||||
The host `OnChangesParentComponent` binds to them like this:
|
||||
|
||||
<code-example path="lifecycle-hooks/src/app/on-changes-parent.component.html" region="on-changes" header="src/app/on-changes-parent.component.html"></code-example>
|
||||
|
||||
@ -447,20 +445,51 @@ Here's the sample in action as the user makes changes.
|
||||
</div>
|
||||
|
||||
The log entries appear as the string value of the *power* property changes.
|
||||
Notice, however, that the `ngOnChanges()` method does not catch changes to `hero.name`.
|
||||
This is because Angular calls the hook only when the value of the input property changes.
|
||||
In this case, `hero` is the input property, and the value of the `hero` property is the *reference to the hero object*.
|
||||
The object reference did not change when the value of its own `name` property changed.
|
||||
But the `ngOnChanges` does not catch changes to `hero.name`
|
||||
That's surprising at first.
|
||||
|
||||
Angular only calls the hook when the value of the input property changes.
|
||||
The value of the `hero` property is the *reference to the hero object*.
|
||||
Angular doesn't care that the hero's own `name` property changed.
|
||||
The hero object *reference* didn't change so, from Angular's perspective, there is no change to report!
|
||||
|
||||
{@a docheck}
|
||||
|
||||
## _DoCheck()_
|
||||
|
||||
Use the `DoCheck` hook to detect and act upon changes that Angular doesn't catch on its own.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Use this method to detect a change that Angular overlooked.
|
||||
|
||||
</div>
|
||||
|
||||
The *DoCheck* sample extends the *OnChanges* sample with the following `ngDoCheck()` hook:
|
||||
|
||||
<code-example path="lifecycle-hooks/src/app/do-check.component.ts" region="ng-do-check" header="DoCheckComponent (ngDoCheck)"></code-example>
|
||||
|
||||
This code inspects certain _values of interest_, capturing and comparing their current state against previous values.
|
||||
It writes a special message to the log when there are no substantive changes to the `hero` or the `power`
|
||||
so you can see how often `DoCheck` is called. The results are illuminating:
|
||||
|
||||
<div class="lightbox">
|
||||
<img src='generated/images/guide/lifecycle-hooks/do-check-anim.gif' alt="DoCheck">
|
||||
</div>
|
||||
|
||||
While the `ngDoCheck()` hook can detect when the hero's `name` has changed, it has a frightful cost.
|
||||
This hook is called with enormous frequency—after _every_
|
||||
change detection cycle no matter where the change occurred.
|
||||
It's called over twenty times in this example before the user can do anything.
|
||||
|
||||
Most of these initial checks are triggered by Angular's first rendering of *unrelated data elsewhere on the page*.
|
||||
Mere mousing into another `<input>` triggers a call.
|
||||
Relatively few calls reveal actual changes to pertinent data.
|
||||
Clearly our implementation must be very lightweight or the user experience suffers.
|
||||
|
||||
{@a afterview}
|
||||
|
||||
### Responding to view changes
|
||||
|
||||
As Angular traverses the [view hierarchy](guide/glossary#view-hierarchy "Definition of view hierarchy definition") during change detection, it needs to be sure that a change in a child does not attempt to cause a change in its own parent. Such a change would not be rendered properly, because of how [unidirectional data flow](guide/glossary#unidirectional-data-flow "Definition") works.
|
||||
|
||||
If you need to make a change that inverts the expected data flow, you must trigger a new change detection cycle to allow that change to be rendered.
|
||||
The examples illustrate how to make such changes safely.
|
||||
## AfterView
|
||||
|
||||
The *AfterView* sample explores the `AfterViewInit()` and `AfterViewChecked()` hooks that Angular calls
|
||||
*after* it creates a component's child views.
|
||||
@ -477,46 +506,47 @@ The following hooks take action based on changing values *within the child view*
|
||||
which can only be reached by querying for the child view via the property decorated with
|
||||
[@ViewChild](api/core/ViewChild).
|
||||
|
||||
|
||||
<code-example path="lifecycle-hooks/src/app/after-view.component.ts" region="hooks" header="AfterViewComponent (class excerpts)"></code-example>
|
||||
|
||||
{@a wait-a-tick}
|
||||
|
||||
#### Wait before updating the view
|
||||
|
||||
In this example, the `doSomething()` method updates the screen when the hero name exceeds 10 characters, but waits a tick before updating `comment`.
|
||||
### Abide by the unidirectional data flow rule
|
||||
The `doSomething()` method updates the screen when the hero name exceeds 10 characters.
|
||||
|
||||
<code-example path="lifecycle-hooks/src/app/after-view.component.ts" region="do-something" header="AfterViewComponent (doSomething)"></code-example>
|
||||
|
||||
Both the `AfterViewInit()` and `AfterViewChecked()` hooks fire after the component's view has been composed.
|
||||
If you modify the code so that the hook updates the component's data-bound `comment` property immediately, you can see that Angular throws an error.
|
||||
Why does the `doSomething()` method wait a tick before updating `comment`?
|
||||
|
||||
The `LoggerService.tick_then()` statement postpones the log update
|
||||
for one turn of the browser's JavaScript cycle, which triggers a new change-detection cycle.
|
||||
Angular's unidirectional data flow rule forbids updates to the view *after* it has been composed.
|
||||
Both of these hooks fire _after_ the component's view has been composed.
|
||||
|
||||
#### Write lean hook methods to avoid performance problems
|
||||
Angular throws an error if the hook updates the component's data-bound `comment` property immediately (try it!).
|
||||
The `LoggerService.tick_then()` postpones the log update
|
||||
for one turn of the browser's JavaScript cycle and that's just long enough.
|
||||
|
||||
When you run the *AfterView* sample, notice how frequently Angular calls `AfterViewChecked()`$emdash;often when there are no changes of interest.
|
||||
Be very careful about how much logic or computation you put into one of these methods.
|
||||
Here's *AfterView* in action:
|
||||
|
||||
<div class="lightbox">
|
||||
|
||||
<img src='generated/images/guide/lifecycle-hooks/after-view-anim.gif' alt="AfterView">
|
||||
|
||||
</div>
|
||||
|
||||
Notice that Angular frequently calls `AfterViewChecked()`, often when there are no changes of interest.
|
||||
Write lean hook methods to avoid performance problems.
|
||||
|
||||
{@a aftercontent}
|
||||
{@a aftercontent-hooks}
|
||||
|
||||
## AfterContent
|
||||
|
||||
The *AfterContent* sample explores the `AfterContentInit()` and `AfterContentChecked()` hooks that Angular calls
|
||||
*after* Angular projects external content into the component.
|
||||
|
||||
{@a content-projection}
|
||||
|
||||
### Responding to projected content changes
|
||||
### Content projection
|
||||
|
||||
*Content projection* is a way to import HTML content from outside the component and insert that content
|
||||
into the component's template in a designated spot.
|
||||
You can identify content projection in a template by looking for the following constructs.
|
||||
|
||||
* HTML between component element tags.
|
||||
* The presence of `<ng-content>` tags in the component's template.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
@ -524,12 +554,9 @@ You can identify content projection in a template by looking for the following c
|
||||
|
||||
</div>
|
||||
|
||||
The *AfterContent* sample explores the `AfterContentInit()` and `AfterContentChecked()` hooks that Angular calls *after* Angular projects external content into the component.
|
||||
|
||||
Consider this variation on the [previous _AfterView_](#afterview) example.
|
||||
Consider this variation on the [previous _AfterView_](guide/lifecycle-hooks#afterview) example.
|
||||
This time, instead of including the child view within the template, it imports the content from
|
||||
the `AfterContentComponent`'s parent.
|
||||
The following is the parent's template.
|
||||
the `AfterContentComponent`'s parent. Here's the parent's template:
|
||||
|
||||
<code-example path="lifecycle-hooks/src/app/after-content.component.ts" region="parent-template" header="AfterContentParentComponent (template excerpt)"></code-example>
|
||||
|
||||
@ -537,7 +564,7 @@ Notice that the `<app-child>` tag is tucked between the `<after-content>` tags.
|
||||
Never put content between a component's element tags *unless you intend to project that content
|
||||
into the component*.
|
||||
|
||||
Now look at the component's template.
|
||||
Now look at the component's template:
|
||||
|
||||
<code-example path="lifecycle-hooks/src/app/after-content.component.ts" region="template" header="AfterContentComponent (template)"></code-example>
|
||||
|
||||
@ -549,8 +576,18 @@ In this case, the projected content is the `<app-child>` from the parent.
|
||||
<img src='generated/images/guide/lifecycle-hooks/projected-child-view.png' alt="Projected Content">
|
||||
</div>
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
#### Using AfterContent hooks
|
||||
The telltale signs of *content projection* are twofold:
|
||||
|
||||
* HTML between component element tags.
|
||||
* The presence of `<ng-content>` tags in the component's template.
|
||||
|
||||
</div>
|
||||
|
||||
{@a aftercontent-hooks}
|
||||
|
||||
### AfterContent hooks
|
||||
|
||||
*AfterContent* hooks are similar to the *AfterView* hooks.
|
||||
The key difference is in the child component.
|
||||
@ -569,44 +606,11 @@ which can only be reached by querying for them via the property decorated with
|
||||
|
||||
{@a no-unidirectional-flow-worries}
|
||||
|
||||
<div class="alert is-helpful>
|
||||
### No unidirectional flow worries with _AfterContent_
|
||||
|
||||
<header>No need to wait for content updates</header>
|
||||
This component's `doSomething()` method update's the component's data-bound `comment` property immediately.
|
||||
There's no [need to wait](guide/lifecycle-hooks#wait-a-tick).
|
||||
|
||||
This component's `doSomething()` method updates the component's data-bound `comment` property immediately.
|
||||
There's no need to [delay the update to ensure proper rendering](#wait-a-tick "Delaying updates").
|
||||
|
||||
Angular calls both *AfterContent* hooks before calling either of the *AfterView* hooks.
|
||||
Recall that Angular calls both *AfterContent* hooks before calling either of the *AfterView* hooks.
|
||||
Angular completes composition of the projected content *before* finishing the composition of this component's view.
|
||||
There is a small window between the `AfterContent...` and `AfterView...` hooks that allows you to modify the host view.
|
||||
|
||||
</div>
|
||||
|
||||
{@a docheck}
|
||||
|
||||
## Defining custom change detection
|
||||
|
||||
To monitor changes that occur where `ngOnChanges()` won't catch them, you can implement your own change check, as shown in the *DoCheck* example.
|
||||
This example shows how you can use the `ngDoCheck()` hook to detect and act upon changes that Angular doesn't catch on its own.
|
||||
|
||||
The *DoCheck* sample extends the *OnChanges* sample with the following `ngDoCheck()` hook:
|
||||
|
||||
<code-example path="lifecycle-hooks/src/app/do-check.component.ts" region="ng-do-check" header="DoCheckComponent (ngDoCheck)"></code-example>
|
||||
|
||||
This code inspects certain _values of interest_, capturing and comparing their current state against previous values.
|
||||
It writes a special message to the log when there are no substantive changes to the `hero` or the `power` so you can see how often `DoCheck()` is called.
|
||||
The results are illuminating.
|
||||
|
||||
<div class="lightbox">
|
||||
<img src='generated/images/guide/lifecycle-hooks/do-check-anim.gif' alt="DoCheck">
|
||||
</div>
|
||||
|
||||
While the `ngDoCheck()` hook can detect when the hero's `name` has changed, it is very expensive.
|
||||
This hook is called with enormous frequency—after _every_
|
||||
change detection cycle no matter where the change occurred.
|
||||
It's called over twenty times in this example before the user can do anything.
|
||||
|
||||
Most of these initial checks are triggered by Angular's first rendering of *unrelated data elsewhere on the page*.
|
||||
Just moving the cursor into another `<input>` triggers a call.
|
||||
Relatively few calls reveal actual changes to pertinent data.
|
||||
If you use this hook, your implementation must be extremely lightweight or the user experience suffers.
|
||||
There is a small window between the `AfterContent...` and `AfterView...` hooks to modify the host view.
|
||||
|
@ -53,11 +53,11 @@ With dynamic queries (`static: false`), the query resolves after either `ngAfter
|
||||
The result will be updated for changes to your view, such as changes to `ngIf` and `ngFor` blocks.
|
||||
|
||||
For more information, see the following entries in the
|
||||
[Static Query Migration Guide](guide/static-query-migration):
|
||||
[Static Query Migration Guide](https://angular.io/guide/static-query-migration):
|
||||
|
||||
* [How do I choose which `static` flag value to use: `true` or `false`?](guide/static-query-migration#how-do-i-choose-which-static-flag-value-to-use-true-or-false)
|
||||
* [How do I choose which `static` flag value to use: `true` or `false`?](https://angular.io/guide/static-query-migration#how-do-i-choose-which-static-flag-value-to-use-true-or-false)
|
||||
|
||||
* [Is there a case where I should use `{static: true}`?](guide/static-query-migration#is-there-a-case-where-i-should-use-static-true)
|
||||
* [Is there a case where I should use `{static: true}`?](https://angular.io/guide/static-query-migration#is-there-a-case-where-i-should-use-static-true)
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -207,7 +207,7 @@ The following table summarizes the `@NgModule` metadata properties.
|
||||
|
||||
Angular automatically adds components in the module's `bootstrap` and route definitions into the `entryComponents` list.
|
||||
|
||||
That leaves only components bootstrapped using one of the imperative techniques, such as [`ViewComponentRef.createComponent()`](api/core/ViewContainerRef#createComponent) as undiscoverable.
|
||||
That leaves only components bootstrapped using one of the imperative techniques, such as [`ViewComponentRef.createComponent()`](https://angular.io/api/core/ViewContainerRef#createComponent) as undiscoverable.
|
||||
|
||||
Dynamic component loading is not common in most apps beyond the router. If you need to dynamically load components, you must add these components to the `entryComponents` list yourself.
|
||||
|
||||
|
@ -12,7 +12,7 @@ Angular provides an `EventEmitter` class that is used when publishing values fro
|
||||
`EventEmitter` extends [RxJS `Subject`](https://rxjs.dev/api/index/class/Subject), adding an `emit()` method so it can send arbitrary values.
|
||||
When you call `emit()`, it passes the emitted value to the `next()` method of any subscribed observer.
|
||||
|
||||
A good example of usage can be found in the [EventEmitter](api/core/EventEmitter) documentation. Here is the example component that listens for open and close events:
|
||||
A good example of usage can be found in the [EventEmitter](https://angular.io/api/core/EventEmitter) documentation. Here is the example component that listens for open and close events:
|
||||
|
||||
`<zippy (open)="onOpen($event)" (close)="onClose($event)"></zippy>`
|
||||
|
||||
@ -30,7 +30,7 @@ Angular’s `HttpClient` returns observables from HTTP method calls. For instanc
|
||||
|
||||
## Async pipe
|
||||
|
||||
The [AsyncPipe](api/common/AsyncPipe) subscribes to an observable or promise and returns the latest value it has emitted. When a new value is emitted, the pipe marks the component to be checked for changes.
|
||||
The [AsyncPipe](https://angular.io/api/common/AsyncPipe) subscribes to an observable or promise and returns the latest value it has emitted. When a new value is emitted, the pipe marks the component to be checked for changes.
|
||||
|
||||
The following example binds the `time` observable to the component's view. The observable continuously updates the view with the current time.
|
||||
|
||||
@ -38,16 +38,16 @@ The following example binds the `time` observable to the component's view. The o
|
||||
|
||||
## Router
|
||||
|
||||
[`Router.events`](api/router/Router#events) provides events as observables. You can use the `filter()` operator from RxJS to look for events of interest, and subscribe to them in order to make decisions based on the sequence of events in the navigation process. Here's an example:
|
||||
[`Router.events`](https://angular.io/api/router/Router#events) provides events as observables. You can use the `filter()` operator from RxJS to look for events of interest, and subscribe to them in order to make decisions based on the sequence of events in the navigation process. Here's an example:
|
||||
|
||||
<code-example path="observables-in-angular/src/main.ts" header="Router events" region="router"></code-example>
|
||||
|
||||
The [ActivatedRoute](api/router/ActivatedRoute) is an injected router service that makes use of observables to get information about a route path and parameters. For example, `ActivatedRoute.url` contains an observable that reports the route path or paths. Here's an example:
|
||||
The [ActivatedRoute](https://angular.io/api/router/ActivatedRoute) is an injected router service that makes use of observables to get information about a route path and parameters. For example, `ActivatedRoute.url` contains an observable that reports the route path or paths. Here's an example:
|
||||
|
||||
<code-example path="observables-in-angular/src/main.ts" header="ActivatedRoute" region="activated_route"></code-example>
|
||||
|
||||
## Reactive forms
|
||||
|
||||
Reactive forms have properties that use observables to monitor form control values. The [`FormControl`](api/forms/FormControl) properties `valueChanges` and `statusChanges` contain observables that raise change events. Subscribing to an observable form-control property is a way of triggering application logic within the component class. For example:
|
||||
Reactive forms have properties that use observables to monitor form control values. The [`FormControl`](https://angular.io/api/forms/FormControl) properties `valueChanges` and `statusChanges` contain observables that raise change events. Subscribing to an observable form-control property is a way of triggering application logic within the component class. For example:
|
||||
|
||||
<code-example path="observables-in-angular/src/main.ts" header="Reactive forms" region="forms"></code-example>
|
||||
|
@ -58,8 +58,8 @@ Though you can provide services by lazy loading modules, not all services can be
|
||||
Another way to limit provider scope is by adding the service you want to limit to the component’s
|
||||
`providers` array. Component providers and NgModule providers are independent of each other. This
|
||||
method is helpful when you want to eagerly load a module that needs a service all to itself.
|
||||
Providing a service in the component limits the service only to that component and its descendants.
|
||||
Other components in the same module can’t access it.
|
||||
Providing a service in the component limits the service only to that component (other components in
|
||||
the same module can’t access it).
|
||||
|
||||
<code-example path="providers/src/app/app.component.ts" region="component-providers" header="src/app/app.component.ts"></code-example>
|
||||
|
||||
|
@ -9,11 +9,11 @@ A basic understanding of the following concepts:
|
||||
|
||||
<hr>
|
||||
|
||||
The [AnimationOptions](api/animations/AnimationOptions) interface in Angular animations enables you to create animations that you can reuse across different components.
|
||||
The [AnimationOptions](https://angular.io/api/animations/AnimationOptions) interface in Angular animations enables you to create animations that you can reuse across different components.
|
||||
|
||||
## Creating reusable animations
|
||||
|
||||
To create a reusable animation, use the [`animation()`](api/animations/animation) method to define an animation in a separate `.ts` file and declare this animation definition as a `const` export variable. You can then import and reuse this animation in any of your app components using the [`useAnimation()`](api/animations/useAnimation) API.
|
||||
To create a reusable animation, use the [`animation()`](https://angular.io/api/animations/animation) method to define an animation in a separate `.ts` file and declare this animation definition as a `const` export variable. You can then import and reuse this animation in any of your app components using the [`useAnimation()`](https://angular.io/api/animations/useAnimation) API.
|
||||
|
||||
<code-example path="animations/src/app/animations.ts" header="src/app/animations.ts" region="reusable" language="typescript"></code-example>
|
||||
|
||||
|
@ -1,262 +0,0 @@
|
||||
# Using Angular routes in a single-page application
|
||||
|
||||
This tutorial describes how you can build a single-page application, SPA that uses multiple Angular routes.
|
||||
|
||||
|
||||
In an SPA, all of your application's functions exist in a single HTML page.
|
||||
As users access your application's features, the browser needs to render only the parts that matter to the user, instead of loading a new page. This pattern can significantly improve your application's user exprience.
|
||||
|
||||
To define how users navigate through your application, you use routes. You can add routes to define how users navigate from one part of your application to another.
|
||||
You can also configure routes to guard against unexpected or unauthorized behavior.
|
||||
|
||||
To explore a sample app featuring the contents of this tutorial, see the <live-example></live-example>.
|
||||
|
||||
## Objectives
|
||||
|
||||
* Organize a sample application's features into modules.
|
||||
* Define how to navigate to a component.
|
||||
* Pass information to a component using a parameter.
|
||||
* Structure routes by nesting several routes.
|
||||
* Check whether users can access a route.
|
||||
* Control whether the application can discard unsaved changes.
|
||||
* Improve performance by pre-fetching route data and lazy loading feature modules.
|
||||
* Require specific criteria to load components.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To complete this tutorial, you should have a basic understanding of the following concepts:
|
||||
|
||||
* JavaScript
|
||||
* HTML
|
||||
* CSS
|
||||
* [Angular CLI](/cli)
|
||||
|
||||
You might find the [Tour of Heroes tutorial](/tutorial) helpful, but it is not required.
|
||||
|
||||
## Create a sample application
|
||||
|
||||
Using the Angular CLI, create a new application, _angular-router-sample_. This application will have two components: _crisis-list_ and _heroes-list_.
|
||||
|
||||
1. Create a new Angular project, _angular-router-sample_.
|
||||
|
||||
<code-example language="sh">
|
||||
ng new angular-router-sample
|
||||
</code-example>
|
||||
|
||||
When prompted with `Would you like to add Angular routing?`, select `N`.
|
||||
|
||||
When prompted with `Which stylesheet format would you like to use?`, select `CSS`.
|
||||
|
||||
After a few moments, a new project, `angular-router-sample`, is ready.
|
||||
|
||||
1. From your terminal, navigate to the `angular-router-sample` directory.
|
||||
|
||||
1. Create a component, _crisis-list_.
|
||||
|
||||
<code-example language="sh">
|
||||
ng generate component crisis-list
|
||||
</code-example>
|
||||
|
||||
1. In your code editor, locate the file, `crisis-list.component.html` and replace
|
||||
the placeholder content with the following HTML.
|
||||
|
||||
<code-example header="src/app/crisis-list/crisis-list.component.html" path="router-tutorial/src/app/crisis-list/crisis-list.component.html"></code-example>
|
||||
|
||||
1. Create a second component, _heroes-list_.
|
||||
|
||||
<code-example language="sh">
|
||||
ng generate component heroes-list
|
||||
</code-example>
|
||||
|
||||
1. In your code editor, locate the file, `heroes-list.component.html` and replace the placeholder content with the following HTML.
|
||||
|
||||
<code-example header="src/app/heroes-list/heroes-list.component.html" path="router-tutorial/src/app/heroes-list/heroes-list.component.html"></code-example>
|
||||
|
||||
1. In your code editor, open the file, `app.component.html` and replace its contents with the following HTML.
|
||||
|
||||
<code-example header="src/app/app.component.html" path="router-tutorial/src/app/app.component.html" region="setup"></code-example>
|
||||
|
||||
1. Verify that your new application runs as expected by running the `ng serve` command.
|
||||
|
||||
<code-example language="sh">
|
||||
ng serve
|
||||
</code-example>
|
||||
|
||||
1. Open a browser to `http://localhost:4200`.
|
||||
|
||||
You should see a single web page, consisting of a title and the HTML of your two components.
|
||||
|
||||
## Import `RouterModule` from `@angular/router`
|
||||
|
||||
Routing allows you to display specific views of your application depending on the URL path.
|
||||
To add this functionality to your sample application, you need to update the `app.module.ts` file to use the module, `RouterModule`.
|
||||
You import this module from `@angular/router`.
|
||||
|
||||
1. From your code editor, open the `app.module.ts` file.
|
||||
|
||||
1. Add the following `import` statement.
|
||||
|
||||
<code-example header="src/app/app.module.ts" path="router-tutorial/src/app/app.module.ts" region="router-import"></code-example>
|
||||
|
||||
## Define your routes
|
||||
|
||||
In this section, you'll define two routes:
|
||||
|
||||
* The route `/crisis-center` opens the `crisis-center` component.
|
||||
* The route `/heroes-list` opens the `heroes-list` component.
|
||||
|
||||
A route definition is a JavaScript object. Each route typically has two propteries. The first property, `path`, is a string
|
||||
that specifies the URL path for the route. The second property, `component`, is a string that specifies
|
||||
what component your application should display for that path.
|
||||
|
||||
1. From your code editor, open the `app.module.ts` file.
|
||||
|
||||
1. Locate the `@NgModule()` section.
|
||||
|
||||
1. Replace the `imports` array in that section with the following.
|
||||
|
||||
<code-example header="src/app/app.module.ts" path="router-tutorial/src/app/app.module.ts" region="import-basic"></code-example>
|
||||
|
||||
This code adds the `RouterModule` to the `imports` array. Next, the code uses the `forRoot()` method of the `RouterModule` to
|
||||
define your two routes. This method takes an array of JavaScript objects, with each object defining the proprties of a route.
|
||||
The `forRoot()` method ensures that your application only instantiates one `RouterModule`. For more information, see
|
||||
[Singleton Services](/guide/singleton-services#forroot-and-the-router).
|
||||
|
||||
## Update your component with `router-outlet`
|
||||
|
||||
At this point, you have defined two routes for your application. However, your application
|
||||
still has both the `crisis-list` and `heroes-list` components hard-coded in your `app.component.html` template. For your routes to
|
||||
work, you need to update your template to dynamically load a component based on the URL path.
|
||||
|
||||
To implement this functionality, you add the `router-outlet` directive to your template file.
|
||||
|
||||
1. From your code editor, open the `app.component.html` file.
|
||||
|
||||
1. Delete the following lines.
|
||||
|
||||
<code-example header="src/app/app.component.html" path="router-tutorial/src/app/app.component.html" region="components"></code-example>
|
||||
|
||||
1. Add the `router-outlet` directive.
|
||||
|
||||
<code-example header="src/app/app.component.html" path="router-tutorial/src/app/app.component.html" region="router-outlet"></code-example>
|
||||
|
||||
View your updated application in your browser. You should see only the application title. To
|
||||
view the `crisis-list` component, add `crisis-list` to the end of the path in your browser's
|
||||
address bar. For example:
|
||||
|
||||
<code-example language="none">
|
||||
http://localhost:4200/crisis-list
|
||||
</code-example>
|
||||
|
||||
Notice that the `crisis-list` component displays. Angular is using the route you defined to dynamically load the
|
||||
component. You can load the `heroes-list` component the same way:
|
||||
|
||||
<code-example language="none">
|
||||
http://localhost:4200/heroes-list
|
||||
</code-example>
|
||||
|
||||
## Control navigation with UI elements
|
||||
|
||||
Currently, your application supports two routes. However, the only way to use those routes
|
||||
is for the user to manually type the path in the browser's address bar. In this section, you'll
|
||||
add two links that users can click to navigate between the `heroes-list` and `crisis-list`
|
||||
components. You'll also add some CSS styles. While these styles are not required, they make
|
||||
it easier to identify the link for the currently-displayed component. You'll add that functionality
|
||||
in the next section.
|
||||
|
||||
1. Open the `app.component.html` file and add the following HTML below the title.
|
||||
|
||||
<code-example header="src/app/app.component.html" path="router-tutorial/src/app/app.component.html" region="nav"></code-example>
|
||||
|
||||
This HTML uses an Angular directive, `routerLink`. This directive connects the routes
|
||||
you defined to your template files.
|
||||
|
||||
1. Open the `app.component.css` file and add the following styles.
|
||||
|
||||
<code-example header="src/app/app.component.css" path="router-tutorial/src/app/app.component.css"></code-example>
|
||||
|
||||
|
||||
If you view your application in the browser, you should see these two links. When you click
|
||||
on a link, the corresponding component appears.
|
||||
|
||||
## Identify the active route
|
||||
|
||||
While users can navigate your application using the links you added in the previous section,
|
||||
they don't have an easy way to identify what the active route is. You can add this functionality
|
||||
using Angular's `routerLinkActive` directive.
|
||||
|
||||
1. From your code editor, open the `app.component.html` file.
|
||||
|
||||
1. Update the anchor tags to include the `routerLinkActive` directive.
|
||||
|
||||
<code-example header="src/app/app.component.html" path="router-tutorial/src/app/app.component.html" region="routeractivelink"></code-example>
|
||||
|
||||
View your application again. As you click one of the buttons, the style for that button updates
|
||||
automatically, identifying the active component to the user. By adding the `routerLinkActive`
|
||||
directive, you inform your application to apply a specific CSS class to the active route. In this
|
||||
tutorial, that CSS class is `activebutton`, but you could use any class that you want.
|
||||
|
||||
## Adding a redirect
|
||||
|
||||
In this step of the tutorial, you add a route that redirects the user to display the `/heroes-list` component.
|
||||
|
||||
1. From your code editor, open the `app.module.ts` file.
|
||||
|
||||
1. In the `imports` array, update the `RouterModule` section as follows.
|
||||
|
||||
<code-example header="src/app/app.module.ts" path="router-tutorial/src/app/app.module.ts" region="import-redirect"></code-example>
|
||||
|
||||
Notice that this new route uses an empty string as its path. In addition, it replaces the `component` property with two new ones:
|
||||
|
||||
* `redirectTo`. This property instructs Angular to redirect from an empty path to the
|
||||
`heroes-list` path.
|
||||
* `pathMatch`. This property instructs Angular on how much of the URL to match. For this
|
||||
tutorial, you should set this property to `full`. This strategy is recommended when
|
||||
you have an empty string for a path. For more information about this property,
|
||||
see the [Route API documentation](/api/router/Route).
|
||||
|
||||
Now when you open your application, it displays the `heroes-list` component by default.
|
||||
|
||||
## Adding a 404 page
|
||||
|
||||
It is possible for a user to try to access a route that you have not defined. To account for
|
||||
this behavior, a best practice is to display a 404 page. In this section, you'll create a 404 page and
|
||||
update your route configuration to show that page for any unspecified routes.
|
||||
|
||||
1. From the terminal, create a new component, `PageNotFound`.
|
||||
|
||||
<code-example language="sh">
|
||||
ng generate component page-not-found
|
||||
</code-example>
|
||||
|
||||
1. From your code editor, open the `page-not-found.component.html` file and replace its contents
|
||||
with the following HTML.
|
||||
|
||||
<code-example header="src/app/page-not-found/page-not-found.component.html" path="router-tutorial/src/app/page-not-found/page-not-found.component.html"></code-example>
|
||||
|
||||
1. Open the `app.module.ts` file. In the `imports` array, update the `RouterModule` section as follows.
|
||||
|
||||
<code-example header="src/app/app.module.ts" path="router-tutorial/src/app/app.module.ts" region="import-wildcard"></code-example>
|
||||
|
||||
The new route uses a path, `**`. This path is how Angular identifies a wildcard route. Any route
|
||||
that does not match an existing route in your configuration will use this route.
|
||||
|
||||
<div class="alert is-important">
|
||||
Notice that the wildcard route is placed at the end of the array. The order of your
|
||||
routes is important, as Angular applies routes in order and uses the first match it finds.
|
||||
</div>
|
||||
|
||||
Try navigating to a non-existing route on your application, such as `http://localhost:4200/powers`.
|
||||
This route doesn't match anything defined in your `app.module.ts` file. However, because you
|
||||
defined a wildcard route, the application automatically displays your `PageNotFound` component.
|
||||
|
||||
## Next steps
|
||||
|
||||
At this point, you have a basic application that uses Angular's routing feature to change
|
||||
what components the user can see based on the URL address. You have extended these features
|
||||
to include a redirect, as well as a wildcard route to display a custom 404 page.
|
||||
|
||||
For more information about routing, see the following topics:
|
||||
|
||||
* [In-app Routing and Navigation](/guide/router)
|
||||
* [Router API](/api/router)
|
@ -1,8 +1,8 @@
|
||||
# In-app navigation: routing to views
|
||||
# In-app navigation with routing
|
||||
|
||||
In a single-page app, you change what the user sees by showing or hiding portions of the display that correspond to particular components, rather than going out to the server to get a new page.
|
||||
As users perform application tasks, they need to move between the different [views](guide/glossary#view "Definition of view") that you have defined.
|
||||
To implement this kind of navigation within the single page of your app, you use the Angular **`Router`**.
|
||||
You want the applications you build to be as fast and responsive as possible.
|
||||
In Angular, a common best practice to improve responsiveness is to build _singe-page apps_.
|
||||
With single-page apps, your users stay on a single page, but their view of that page changes depending on what the user wants to do.
|
||||
|
||||
To handle the navigation from one [view](guide/glossary#view) to the next, you use the Angular _router_.
|
||||
The router enables navigation by interpreting a browser URL as an instruction to change the view.
|
||||
@ -169,7 +169,7 @@ To get information from a route:
|
||||
|
||||
<code-example header="In the component (excerpt)">
|
||||
ngOnInit() {
|
||||
this.route.queryParams.subscribe(params => {
|
||||
this.activatedRoute.queryParams.subscribe(params => {
|
||||
this.name = params['name'];
|
||||
});
|
||||
}
|
||||
@ -357,12 +357,130 @@ Inject `ActivatedRoute` and `Router` in the constructor of the component class s
|
||||
|
||||
{@a lazy-loading}
|
||||
|
||||
## Lazy loading
|
||||
## Lazy loading modules
|
||||
|
||||
You can configure your routes to lazy load modules, which means that Angular only loads modules as needed, rather than loading all modules when the app launches.
|
||||
Additionally, you can preload parts of your app in the background to improve the user experience.
|
||||
To lazy load Angular modules, use `loadchildren` (instead of `component`) in your `AppRoutingModule` `routes` configuration as follows:
|
||||
|
||||
<code-example header="AppRoutingModule (excerpt)">
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'items',
|
||||
loadChildren: () => import('./items/items.module').then(m => m.ItemsModule)
|
||||
}
|
||||
];
|
||||
|
||||
</code-example>
|
||||
|
||||
In the lazy loaded module's routing module, add a route for the component.
|
||||
|
||||
<code-example header="Routing module for lazy loaded module (excerpt)">
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ItemsComponent
|
||||
}
|
||||
];
|
||||
|
||||
</code-example>
|
||||
|
||||
Also be sure to remove the `ItemsModule` from the `AppModule`. For more information on lazy loading modules see [Lazy-loading feature modules](guide/lazy-loading-ngmodules).
|
||||
|
||||
|
||||
{@a preloading}
|
||||
|
||||
## Preloading
|
||||
|
||||
Preloading improves UX by loading parts of your app in the background. You can preload modules or component data.
|
||||
|
||||
### Preloading modules
|
||||
|
||||
To enable preloading of all lazy loaded modules, import the `PreloadAllModules` token from the Angular `router`.
|
||||
|
||||
<code-example header="AppRoutingModule (excerpt)">
|
||||
|
||||
import { PreloadAllModules } from '@angular/router';
|
||||
|
||||
</code-example>
|
||||
|
||||
Still in the `AppRoutingModule`, specify your preloading strategy in `forRoot()`.
|
||||
|
||||
<code-example header="AppRoutingModule (excerpt)">
|
||||
|
||||
RouterModule.forRoot(
|
||||
appRoutes,
|
||||
{
|
||||
preloadingStrategy: PreloadAllModules
|
||||
}
|
||||
)
|
||||
|
||||
</code-example>
|
||||
|
||||
### Preloading component data
|
||||
|
||||
You can preload component data so that all elements and data on a page render at the same time when the user activates a route. To preload component data, you can use a `resolver`.
|
||||
|
||||
#### Resolvers
|
||||
|
||||
Create a resolver service. With the CLI, the command to generate a service is as follows:
|
||||
|
||||
|
||||
<code-example language="none" class="code-shell">
|
||||
ng generate service <service-name>
|
||||
</code-example>
|
||||
|
||||
In your service, import the following router members, implement `Resolve`, and inject the `Router` service:
|
||||
|
||||
<code-example header="Resolver service (excerpt)">
|
||||
|
||||
import { Resolve } from '@angular/router';
|
||||
|
||||
...
|
||||
|
||||
export class CrisisDetailResolverService implements Resolve<> {
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<> {
|
||||
// your logic goes here
|
||||
}
|
||||
}
|
||||
|
||||
</code-example>
|
||||
|
||||
Import this resolver into your module's routing module.
|
||||
|
||||
<code-example header="Feature module's routing module (excerpt)">
|
||||
|
||||
import { YourResolverService } from './your-resolver.service';
|
||||
|
||||
</code-example>
|
||||
|
||||
Add a `resolve` object to the component's `route` configuration.
|
||||
|
||||
<code-example header="Feature module's routing module (excerpt)">
|
||||
{
|
||||
path: '/your-path',
|
||||
component: YourComponent,
|
||||
resolve: {
|
||||
crisis: YourResolverService
|
||||
}
|
||||
}
|
||||
</code-example>
|
||||
|
||||
|
||||
In the component, use an observable to get the data from the `ActivatedRoute`.
|
||||
|
||||
|
||||
<code-example header="Component (excerpt)">
|
||||
ngOnInit() {
|
||||
this.route.data
|
||||
.subscribe((your-parameters) => {
|
||||
// your data-specific code goes here
|
||||
});
|
||||
}
|
||||
</code-example>
|
||||
|
||||
For more information with a working example, see the [routing tutorial section on preloading](guide/router#preloading-background-loading-of-feature-areas).
|
||||
|
||||
For more information on lazy loading and preloading see the dedicated guide [Lazy loading NgModules](guide/lazy-loading-ngmodules).
|
||||
|
||||
## Preventing unauthorized access
|
||||
|
||||
@ -2619,7 +2737,7 @@ If the user is logged in, it returns true and the navigation continues.
|
||||
The `ActivatedRouteSnapshot` contains the _future_ route that will be activated and the `RouterStateSnapshot` contains the _future_ `RouterState` of the application, should you pass through the guard check.
|
||||
|
||||
If the user is not logged in, you store the attempted URL the user came from using the `RouterStateSnapshot.url` and tell the router to redirect to a login page—a page you haven't created yet.
|
||||
Returning a `UrlTree` tells the `Router` to cancel the current navigation and schedule a new one to redirect the user.
|
||||
This secondary navigation automatically cancels the current navigation; `checkLogin()` returns `false`.
|
||||
|
||||
{@a add-login-component}
|
||||
|
||||
@ -2672,8 +2790,8 @@ Extend the `AuthGuard` to protect when navigating between the `admin` routes.
|
||||
Open `auth.guard.ts` and add the `CanActivateChild` interface to the imported tokens from the router package.
|
||||
|
||||
Next, implement the `canActivateChild()` method which takes the same arguments as the `canActivate()` method: an `ActivatedRouteSnapshot` and `RouterStateSnapshot`.
|
||||
The `canActivateChild()` method can return an `Observable<boolean|UrlTree>` or `Promise<boolean|UrlTree>` for async checks and a `boolean` or `UrlTree` for sync checks.
|
||||
This one returns either `true` to allow the user to access the admin feature module or `UrlTree` to redirect the user to the login page instead:
|
||||
The `canActivateChild()` method can return an `Observable<boolean>` or `Promise<boolean>` for async checks and a `boolean` for sync checks.
|
||||
This one returns a `boolean`:
|
||||
|
||||
<code-example path="router/src/app/auth/auth.guard.3.ts" header="src/app/auth/auth.guard.ts (excerpt)" region="can-activate-child"></code-example>
|
||||
|
||||
@ -2838,7 +2956,7 @@ Update the `CrisisDetailComponent` to get the crisis from the `ActivatedRoute.d
|
||||
|
||||
<code-example path="router/src/app/crisis-center/crisis-detail/crisis-detail.component.ts" header="src/app/crisis-center/crisis-detail/crisis-detail.component.ts (ngOnInit v2)" region="ngOnInit"></code-example>
|
||||
|
||||
Note the following three important points:
|
||||
Note the following two important points:
|
||||
|
||||
1. The router's `Resolve` interface is optional.
|
||||
The `CrisisDetailResolverService` doesn't inherit from a base class.
|
||||
@ -2846,8 +2964,6 @@ The router looks for that method and calls it if found.
|
||||
|
||||
1. The router calls the resolver in any case where the the user could navigate away so you don't have to code for each use case.
|
||||
|
||||
1. Returning an empty `Observable` in at least one resolver will cancel navigation.
|
||||
|
||||
The relevant Crisis Center code for this milestone follows.
|
||||
|
||||
<code-tabs>
|
||||
|
@ -61,7 +61,7 @@ If the current tab needs to be updated to the latest app version immediately, it
|
||||
|
||||
<code-example path="service-worker-getting-started/src/app/prompt-update.service.ts" header="prompt-update.service.ts" region="sw-activate"></code-example>
|
||||
|
||||
Doing this could break lazy-loading in currently running apps, especially if the lazy-loaded chunks use filenames with hashes, which change every version.
|
||||
Doing this could break lazy-loading into currently running apps, especially if the lazy-loaded chunks use filenames with hashes, which change every version.
|
||||
|
||||
## More on Angular service workers
|
||||
|
||||
|
@ -74,9 +74,6 @@ interface AssetGroup {
|
||||
files?: string[];
|
||||
urls?: string[];
|
||||
};
|
||||
cacheQueryOptions?: {
|
||||
ignoreSearch?: boolean;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@ -113,12 +110,6 @@ This section describes the resources to cache, broken up into the following grou
|
||||
* `urls` includes both URLs and URL patterns that will be matched at runtime. These resources are not fetched directly and do not have content hashes, but they will be cached according to their HTTP headers. This is most useful for CDNs such as the Google Fonts service.<br>
|
||||
_(Negative glob patterns are not supported and `?` will be matched literally; i.e. it will not match any character other than `?`.)_
|
||||
|
||||
### `cacheQueryOptions`
|
||||
|
||||
These options are used to modify the matching behavior of requests. They are passed to the browsers `Cache#match` function. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Cache/match) for details. Currently, only the following options are supported:
|
||||
|
||||
* `ignoreSearch`: Ignore query parameters. Defaults to `false`.
|
||||
|
||||
## `dataGroups`
|
||||
|
||||
Unlike asset resources, data requests are not versioned along with the app. They're cached according to manually-configured policies that are more useful for situations such as API requests and other data dependencies.
|
||||
@ -136,9 +127,6 @@ export interface DataGroup {
|
||||
timeout?: string;
|
||||
strategy?: 'freshness' | 'performance';
|
||||
};
|
||||
cacheQueryOptions?: {
|
||||
ignoreSearch?: boolean;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@ -193,10 +181,6 @@ The Angular service worker can use either of two caching strategies for data res
|
||||
|
||||
* `freshness` optimizes for currency of data, preferentially fetching requested data from the network. Only if the network times out, according to `timeout`, does the request fall back to the cache. This is useful for resources that change frequently; for example, account balances.
|
||||
|
||||
### `cacheQueryOptions`
|
||||
|
||||
See [assetGroups](#assetgroups) for details.
|
||||
|
||||
## `navigationUrls`
|
||||
|
||||
This optional section enables you to specify a custom list of URLs that will be redirected to the index file.
|
||||
|
@ -23,7 +23,7 @@ The above command completes the following actions:
|
||||
2. Enables service worker build support in the CLI.
|
||||
3. Imports and registers the service worker in the app module.
|
||||
4. Updates the `index.html` file:
|
||||
* Includes a link to add the `manifest.webmanifest` file.
|
||||
* Includes a link to add the `manifest.json` file.
|
||||
* Adds meta tags for `theme-color`.
|
||||
5. Installs icon files to support the installed Progressive Web App (PWA).
|
||||
6. Creates the service worker configuration file called [`ngsw-config.json`](/guide/service-worker-config), which specifies the caching behaviors and other settings.
|
||||
|
@ -6,9 +6,10 @@ It includes information about prerequisites, installing the CLI, creating an ini
|
||||
|
||||
|
||||
<div class="callout is-helpful">
|
||||
<header>Try Angular without local setup</header>
|
||||
<header>Learning Angular</header>
|
||||
|
||||
If you are new to Angular, see [Getting Started](start). Getting Started helps you quickly learn the essentials of Angular, in the context of building a basic online store app. It leverages the [StackBlitz](https://stackblitz.com/) online development environment, so you don't need to set up your local environment until you're ready.
|
||||
|
||||
If you are new to Angular, you might want to start with [Try it now!](start), which introduces the essentials of Angular in the context of a ready-made basic online store app that you can examine and modify. This standalone tutorial takes advantage of the interactive [StackBlitz](https://stackblitz.com/) environment for online development. You don't need to set up your local environment until you're ready.
|
||||
|
||||
</div>
|
||||
|
||||
@ -118,10 +119,18 @@ You will see:
|
||||
|
||||
## Next steps
|
||||
|
||||
* For a more thorough introduction to the fundamental concepts and terminology of Angular single-page app architecture and design principles, read the [Angular Concepts](guide/architecture) section.
|
||||
|
||||
* Work through the [Tour of Heroes Tutorial](tutorial), a complete hands-on exercise that introduces you to the app development process using the Angular CLI and walks through important subsystems.
|
||||
* If you are new to Angular, see the [Getting Started](start) tutorial. Getting Started helps you quickly learn the essentials of Angular, in the context of building a basic online store app.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Getting Started assumes the [StackBlitz](https://stackblitz.com/) online development environment.
|
||||
To learn how to export an app from StackBlitz to your local environment, skip ahead to the [Deployment](start/start-deployment "Getting Started: Deployment") section.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
* To learn more about using the Angular CLI, see the [CLI Overview](cli "CLI Overview"). In addition to creating the initial workspace and app scaffolding, you can use the CLI to generate Angular code such as components and services. The CLI supports the full development cycle, including building, testing, bundling, and deployment.
|
||||
|
||||
|
||||
* For more information about the Angular files generated by `ng new`, see [Workspace and Project File Structure](guide/file-structure).
|
||||
|
@ -44,7 +44,7 @@ For more information on how to choose, see the [next question](#how-do-i-choose)
|
||||
{@a how-do-i-choose}
|
||||
### How do I choose which `static` flag value to use: `true` or `false`?
|
||||
|
||||
In the official API docs, we have always recommended retrieving query results in [`ngAfterViewInit` for view queries](api/core/ViewChild#description) and [`ngAfterContentInit` for content queries](api/core/ContentChild#description).
|
||||
In the official API docs, we have always recommended retrieving query results in [`ngAfterViewInit` for view queries](https://angular.io/api/core/ViewChild#description) and [`ngAfterContentInit` for content queries](https://angular.io/api/core/ContentChild#description).
|
||||
This is because by the time those lifecycle hooks run, change detection has completed for the relevant nodes and we can guarantee that we have collected all the possible query results.
|
||||
|
||||
Most applications will want to use `{static: false}` for the same reason. This setting will ensure query matches that are dependent on binding resolution (e.g. results inside `*ngIf`s or `*ngFor`s) will be found by the query.
|
||||
|
@ -832,89 +832,6 @@ When the `condition` is truthy, the top (A) paragraph is removed and the bottom
|
||||
<img src='generated/images/guide/structural-directives/unless-anim.gif' alt="UnlessDirective in action">
|
||||
</div>
|
||||
|
||||
{@a directive-type-checks}
|
||||
|
||||
## Improving template type checking for custom directives
|
||||
|
||||
You can improve template type checking for custom directives by adding template guard properties to your directive definition.
|
||||
These properties help the Angular template type checker find mistakes in the template at compile time, which can avoid runtime errors those mistakes can cause.
|
||||
|
||||
Use the type-guard properties to inform the template type checker of an expected type, thus improving compile-time type-checking for that template.
|
||||
|
||||
* A property `ngTemplateGuard_(someInputProperty)` lets you specify a more accurate type for an input expression within the template.
|
||||
* The `ngTemplateContextGuard` static property declares the type of the template context.
|
||||
|
||||
This section provides example of both kinds of type-guard property.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
For more information, see [Template type checking guide](guide/template-typecheck "Template type-checking guide").
|
||||
|
||||
</div>
|
||||
|
||||
{@a narrowing-input-types}
|
||||
|
||||
### Make in-template type requirements more specific with template guards
|
||||
|
||||
A structural directive in a template controls whether that template is rendered at run time, based on its input expression.
|
||||
To help the compiler catch template type errors, you should specify as closely as possible the required type of a directive's input expression when it occurs inside the template.
|
||||
|
||||
A type guard function *narrows* the expected type of an input expression to a subset of types that might be passed to the directive within the template at run time.
|
||||
You can provide such a function to help the type-checker infer the proper type for the expression at compile time.
|
||||
|
||||
For example, the `NgIf` implementation uses type-narrowing to ensure that the
|
||||
template is only instantiated if the input expression to `*ngIf` is truthy.
|
||||
To provide the specific type requirement, the `NgIf` directive defines a [static property `ngTemplateGuard_ngIf: 'binding'`](api/common/NgIf#static-properties).
|
||||
The `binding` value is a special case for a common kind of type-narrowing where the input expression is evaluated in order to satisfy the type requirement.
|
||||
|
||||
To provide a more specific type for an input expression to a directive within the template, add a `ngTemplateGuard_xx` property to the directive, where the suffix to the static property name is the `@Input` field name.
|
||||
The value of the property can be either a general type-narrowing function based on its return type, or the string `"binding"` as in the case of `NgIf`.
|
||||
|
||||
For example, consider the following structural directive that takes the result of a template expression as an input.
|
||||
|
||||
<code-example language="ts" header="IfLoadedDirective">
|
||||
export type Loaded<T> = { type: 'loaded', data: T };
|
||||
export type Loading = { type: 'loading' };
|
||||
export type LoadingState<T> = Loaded<T> | Loading;
|
||||
export class IfLoadedDirective<T> {
|
||||
@Input('ifLoaded') set state(state: LoadingState<T>) {}
|
||||
static ngTemplateGuard_state<T>(dir: IfLoadedDirective<T>, expr: LoadingState<T>): expr is Loaded<T> { return true; };
|
||||
export interface Person {
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `<div *ifLoaded="state">{{ state.data }}</div>`,
|
||||
})
|
||||
export class AppComponent {
|
||||
state: LoadingState<Person>;
|
||||
}
|
||||
</code-example>
|
||||
|
||||
In this example, the `LoadingState<T>` type permits either of two states, `Loaded<T>` or `Loading`. The expression used as the directive’s `state` input is of the umbrella type `LoadingState`, as it’s unknown what the loading state is at that point.
|
||||
|
||||
The `IfLoadedDirective` definition declares the static field `ngTemplateGuard_state`, which expresses the narrowing behavior.
|
||||
Within the `AppComponent` template, the `*ifLoaded` structural directive should render this template only when `state` is actually `Loaded<Person>`.
|
||||
The type guard allows the type checker to infer that the acceptable type of `state` within the template is a `Loaded<T>`, and further infer that `T` must be an instance of `Person`.
|
||||
|
||||
{@a narrowing-context-type}
|
||||
|
||||
### Typing the directive's context
|
||||
|
||||
If your structural directive provides a context to the instantiated template, you can properly type it inside the template by providing a static `ngTemplateContextGuard` function.
|
||||
The following snippet shows an example of such a function.
|
||||
|
||||
<code-example language="ts" header="myDirective.ts">
|
||||
@Directive({…})
|
||||
export class ExampleDirective {
|
||||
// Make sure the template checker knows the type of the context with which the
|
||||
// template of this directive will be rendered
|
||||
static ngTemplateContextGuard(dir: ExampleDirective, ctx: unknown): ctx is ExampleContext { return true; };
|
||||
|
||||
// …
|
||||
}
|
||||
</code-example>
|
||||
|
||||
|
||||
|
||||
{@a summary}
|
||||
@ -962,7 +879,7 @@ Here is the source from the `src/app/` folder.
|
||||
|
||||
|
||||
|
||||
You learned:
|
||||
You learned
|
||||
|
||||
* that structural directives manipulate HTML layout.
|
||||
* to use [`<ng-container>`](guide/structural-directives#ngcontainer) as a grouping element when there is no suitable host element.
|
||||
|
@ -432,7 +432,7 @@ Attributes can be changed by `setAttribute()`, which re-initializes correspondin
|
||||
</div>
|
||||
|
||||
For more information, see the [MDN Interfaces documentation](https://developer.mozilla.org/en-US/docs/Web/API#Interfaces) which has API docs for all the standard DOM elements and their properties.
|
||||
Comparing the [`<td>` attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td) to the [`<td>` properties](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement) provides a helpful example for differentiation.
|
||||
Comparing the [`<td>` attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td) attributes to the [`<td>` properties](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement) provides a helpful example for differentiation.
|
||||
In particular, you can navigate from the attributes page to the properties via "DOM interface" link, and navigate the inheritance hierarchy up to `HTMLTableCellElement`.
|
||||
|
||||
|
||||
@ -473,7 +473,7 @@ To control the state of the button, set the `disabled` *property*,
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Though you could technically set the `[attr.disabled]` attribute binding, the values are different in that the property binding requires to be a boolean value, while its corresponding attribute binding relies on whether the value is `null` or not. Consider the following:
|
||||
Though you could technically set the `[attr.disabled]` attribute binding, the values are different in that the property binding requires to a boolean value, while its corresponding attribute binding relies on whether the value is `null` or not. Consider the following:
|
||||
|
||||
```html
|
||||
<input [disabled]="condition ? true : false">
|
||||
@ -1455,7 +1455,7 @@ Angular provides *value accessors* for all of the basic HTML form elements and t
|
||||
|
||||
You can't apply `[(ngModel)]` to a non-form native element or a
|
||||
third-party custom component until you write a suitable value accessor. For more information, see
|
||||
the API documentation on [DefaultValueAccessor](api/forms/DefaultValueAccessor).
|
||||
the API documentation on [DefaultValueAccessor](https://angular.io/api/forms/DefaultValueAccessor).
|
||||
|
||||
You don't need a value accessor for an Angular component that
|
||||
you write because you can name the value and event properties
|
||||
|
@ -167,8 +167,6 @@ Here, during type checking of the template for `AppComponent`, the `[user]="sele
|
||||
Therefore, Angular assigns the `selectedUser` property to `UserDetailComponent.user`, which would result in an error if their types were incompatible.
|
||||
TypeScript checks the assignment according to its type system, obeying flags such as `strictNullChecks` as they are configured in the application.
|
||||
|
||||
You can avoid run-time type errors by providing more specific in-template type requirements to the template type checker. Make the input type requirements for your own directives as specific as possible by providing template-guard functions in the directive definition. See [Improving template type checking for custom directives](guide/structural-directives#directive-type-checks), and [Input setter coercion](#input-setter-coercion) in this guide.
|
||||
|
||||
|
||||
### Strict null checks
|
||||
|
||||
@ -203,7 +201,7 @@ There are two potential workarounds to the above issues:
|
||||
|
||||
As a library author, you can take several measures to provide an optimal experience for your users.
|
||||
First, enabling `strictNullChecks` and including `null` in an input's type, as appropriate, communicates to your consumers whether they can provide a nullable value or not.
|
||||
Additionally, it is possible to provide type hints that are specific to the template type checker. See [Improving template type checking for custom directives](guide/structural-directives#directive-type-checks), and [Input setter coercion](#input-setter-coercion) below.
|
||||
Additionally, it is possible to provide type hints that are specific to the template type checker, see the [Input setter coercion](guide/template-typecheck#input-setter-coercion) section of this guide.
|
||||
|
||||
|
||||
{@a input-setter-coercion}
|
||||
|
@ -153,7 +153,7 @@ You can define more than one animation trigger for a component. You can attach a
|
||||
|
||||
### Parent-child animations
|
||||
|
||||
Each time an animation is triggered in Angular, the parent animation always get priority and child animations are blocked. In order for a child animation to run, the parent animation must query each of the elements containing child animations and then allow the animations to run using the [`animateChild()`](api/animations/animateChild) function.
|
||||
Each time an animation is triggered in Angular, the parent animation always get priority and child animations are blocked. In order for a child animation to run, the parent animation must query each of the elements containing child animations and then allow the animations to run using the [`animateChild()`](https://angular.io/api/animations/animateChild) function.
|
||||
|
||||
#### Disabling an animation on an HTML element
|
||||
|
||||
@ -178,7 +178,7 @@ You can't selectively disable multiple animations on a single element.
|
||||
|
||||
However, selective child animations can still be run on a disabled parent in one of the following ways:
|
||||
|
||||
* A parent animation can use the [`query()`](api/animations/query) function to collect inner elements located in disabled areas of the HTML template.
|
||||
* A parent animation can use the [`query()`](https://angular.io/api/animations/query) function to collect inner elements located in disabled areas of the HTML template.
|
||||
Those elements can still animate.
|
||||
|
||||
* A subanimation can be queried by a parent and then later animated with the `animateChild()` function.
|
||||
|
@ -53,7 +53,7 @@ The initial `tsconfig.json` for an Angular app typically looks like the followin
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"strictTemplates": true,
|
||||
"fullTemplateTypeCheck": true,
|
||||
"strictInjectionParameters": true
|
||||
}
|
||||
}
|
||||
@ -62,6 +62,7 @@ The initial `tsconfig.json` for an Angular app typically looks like the followin
|
||||
|
||||
{@a noImplicitAny}
|
||||
|
||||
|
||||
### *noImplicitAny* and *suppressImplicitAnyIndexErrors*
|
||||
|
||||
TypeScript developers disagree about whether the `noImplicitAny` flag should be `true` or `false`.
|
||||
@ -95,7 +96,6 @@ For more information about how the TypeScript configuration affects compilation,
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{@a typings}
|
||||
|
||||
## TypeScript typings
|
||||
@ -146,6 +146,7 @@ For instance, to install typings for `jasmine` you run `npm install @types/jasmi
|
||||
|
||||
{@a target}
|
||||
|
||||
|
||||
### *target*
|
||||
|
||||
By default, the target is `es2015`, which is supported only in modern browsers. You can configure the target to `es5` to specifically support legacy browsers. [Differential loading](guide/deployment#differential-loading) is also provided by the Angular CLI to support modern, and legacy browsers with separate bundles.
|
||||
|
@ -37,8 +37,8 @@ See our [template type-checking guide](guide/template-typecheck) for more inform
|
||||
| [`entryComponents`](api/core/NgModule#entryComponents) | none | See [`entryComponents`](guide/deprecations#entryComponents) |
|
||||
| [`CurrencyPipe` - `DEFAULT_CURRENCY_CODE`](api/common/CurrencyPipe#currency-code-deprecation)| `{provide: DEFAULT_CURRENCY_CODE, useValue: 'USD'}` | From v11 the default code will be extracted from the locale data given by `LOCAL_ID`, rather than `USD`. |
|
||||
| [`ANALYZE_FOR_ENTRY_COMPONENTS`](api/core/ANALYZE_FOR_ENTRY_COMPONENTS) | none | See [`ANALYZE_FOR_ENTRY_COMPONENTS`](guide/deprecations#entryComponents) |
|
||||
| `ModuleWithProviders` without a generic | `ModuleWithProviders` with a generic | |
|
||||
| Undecorated base classes that use Angular features | Base classes with `@Directive()` decorator that use Angular features | |
|
||||
| `ModuleWithProviders` without a generic | `ModuleWithProviders` with a generic | See [`ModuleWithProviders` section](guide/deprecations#moduleWithProviders) |
|
||||
| Undecorated base classes that use Angular features | Base classes with `@Directive()` decorator that use Angular features | See [undecorated base classes section](guide/deprecations#undecorated-base-classes) |
|
||||
| `esm5` and `fesm5` distribution in `@angular/*` npm packages | `esm2015` and `fesm2015` entrypoints | See [`esm5` and `fesm5`](guide/deprecations#esm5-fesm5) |
|
||||
| [`TestBed.get`](api/core/testing/TestBed#get) | [`TestBed.inject`](api/core/testing/TestBed#inject) | Same behavior, but type safe. |
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Using published libraries
|
||||
|
||||
When building Angular applications you can take advantage of sophisticated first-party libraries, such as [Angular Material](https://material.angular.io/), as well as rich ecosystem of third-party libraries.
|
||||
See the [Angular Resources](resources) page for links to the most popular ones.
|
||||
See the [Angular Resources](https://angular.io/resources) page for links to the most popular ones.
|
||||
|
||||
## Installing libraries
|
||||
|
||||
|
BIN
aio/content/images/bios/alex-eagle.jpg
Normal file
BIN
aio/content/images/bios/alex-eagle.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
Before Width: | Height: | Size: 7.0 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user