Compare commits
185 Commits
Author | SHA1 | Date | |
---|---|---|---|
9de9227de3 | |||
6c5ddb2649 | |||
7a822d2dca | |||
26dbefd11a | |||
6f6bfcb01c | |||
07ab0ee04c | |||
5d6ead4e23 | |||
b03aa7e279 | |||
b5b33e0361 | |||
42ffab630c | |||
d2d4225ce6 | |||
218e82ebb4 | |||
12072954de | |||
1980d69ec3 | |||
2b63b7f15f | |||
3fdd304b03 | |||
525dc6a036 | |||
12e52a7696 | |||
8abde5c49c | |||
e6d7f2022e | |||
8d51691c42 | |||
b85ec051fb | |||
9ab42c970e | |||
cebb674f6e | |||
fc59d841a9 | |||
a07917bd22 | |||
89901e4ccb | |||
bcff8739a7 | |||
54288401f2 | |||
371a973e5e | |||
710fca2b8e | |||
120ce42ac2 | |||
1503408a3d | |||
48025eb3e8 | |||
11c2e8c14d | |||
d1966fc29b | |||
2e251b7d2e | |||
d035747247 | |||
532bcc0f67 | |||
c272351282 | |||
f3bbfa4284 | |||
e13fcbaa6f | |||
5a14a15e32 | |||
d3ee052ee4 | |||
e201a84f59 | |||
fcad075ade | |||
30ae8805af | |||
657d98d9b2 | |||
24f48ed97f | |||
d0c26f4bfa | |||
6279117058 | |||
52fc08754d | |||
287d841486 | |||
df0859f7e1 | |||
4ec7cd12c0 | |||
d06b6de409 | |||
9b53054ea8 | |||
bfe7657006 | |||
7ff845b72f | |||
bf6fbf5a74 | |||
e4f05d1952 | |||
f11fc1e3bd | |||
9064f4ecdb | |||
0eda98a28b | |||
60921e8efd | |||
c78df781f7 | |||
dc800b2f9e | |||
79aaaa3254 | |||
c2dbcd36a6 | |||
788d5d7046 | |||
9ff778abe8 | |||
d58b5ce486 | |||
f25d00a45d | |||
4c2bd64642 | |||
e6c416fe74 | |||
36dc1c7872 | |||
c5ef307d95 | |||
74afc2df82 | |||
0a8e8cd1f3 | |||
4f9798007d | |||
0328e030b3 | |||
ae7b5f4dc3 | |||
a13bf202f6 | |||
19003a42f3 | |||
1a4456d432 | |||
406ce8c884 | |||
d6904882d0 | |||
c9cac92628 | |||
7403ba13d5 | |||
349539e551 | |||
315ad6370a | |||
64a415b91c | |||
47d6ab9d92 | |||
8cac5fec20 | |||
2b0c2a44d0 | |||
11f65c0c6c | |||
1d9e00ec38 | |||
2f140f5118 | |||
8f48dc0653 | |||
89f6b341a3 | |||
d83f62d30a | |||
1112875981 | |||
a4572c3b12 | |||
45bd6d6e40 | |||
4caacf2aff | |||
9f53df8168 | |||
3ef4b079e4 | |||
49db9eeb33 | |||
9efd7afd8a | |||
0b33828970 | |||
97556fd2ca | |||
d95b2f0100 | |||
2f572772b0 | |||
bb09cd0e41 | |||
628f957c4a | |||
02599e452a | |||
0700279fb6 | |||
a491f7e2af | |||
af4fe3aa4e | |||
6faaec60b1 | |||
12e3db8d6f | |||
824d9a8cbf | |||
8a531e2917 | |||
afc5b3eede | |||
7d2ea938ee | |||
d63ba9cfd3 | |||
a4b388d4ec | |||
aebd6620d7 | |||
39bd9a7c94 | |||
4b93df06e0 | |||
72664cac19 | |||
bc7a8a85f2 | |||
375aa7399d | |||
7dbbe24ccf | |||
677d277ccc | |||
224aaae352 | |||
5cdf806126 | |||
3a97972e58 | |||
f5e1faa75e | |||
5bec534bfc | |||
5edeee69dd | |||
ce85cbf2d3 | |||
c305b5ca31 | |||
b970028057 | |||
e67c69a782 | |||
03a8b16ec9 | |||
fd4ce84584 | |||
0671e540c2 | |||
45c7b23cc8 | |||
9b31f77c19 | |||
927d691d56 | |||
4fb5e21426 | |||
6518dae45e | |||
83d68b3521 | |||
cf28373629 | |||
598b3ff5d7 | |||
011937555f | |||
f33d8fe11d | |||
c9d80b2fc8 | |||
81c40cb5e8 | |||
822036362b | |||
eee8c7f718 | |||
1797390c8b | |||
4a4b6be731 | |||
4b1dcaf0f5 | |||
4f8d30361a | |||
098ba19560 | |||
5149d98acc | |||
db693f482f | |||
84c9c6ecc2 | |||
a52d103341 | |||
ec77bc4fc5 | |||
1129f10f26 | |||
3006a560fd | |||
8e9be6ce0d | |||
99e4daec94 | |||
90c249bc78 | |||
83941d68df | |||
a30fd2993b | |||
a84093a971 | |||
37e1c04e6a | |||
985762b5bc | |||
be0f994657 | |||
6aa259246e | |||
ad7850e4b8 |
93
.bazelignore
93
.bazelignore
@ -1,10 +1,97 @@
|
||||
# Bazel does not yet support wildcards or other .gitignore semantics for
|
||||
# .bazelignore. Two issues for this feature request are outstanding:
|
||||
# https://github.com/bazelbuild/bazel/issues/7093
|
||||
# https://github.com/bazelbuild/bazel/issues/8106
|
||||
.git
|
||||
node_modules
|
||||
dist
|
||||
aio/content
|
||||
aio/node_modules
|
||||
aio/tools/examples/shared/node_modules
|
||||
integration/bazel
|
||||
integration/bazel-schematics/demo
|
||||
integration/platform-server/node_modules
|
||||
packages/bazel/node_modules
|
||||
integration/bazel/bazel-bazel
|
||||
integration/bazel/bazel-bin
|
||||
integration/bazel/bazel-out
|
||||
integration/bazel/bazel-testlogs
|
||||
integration/bazel-schematics/demo
|
||||
# All integration test node_modules folders
|
||||
integration/bazel/node_modules
|
||||
integration/bazel-schematics/node_modules
|
||||
integration/cli-hello-world/node_modules
|
||||
integration/cli-hello-world-ivy-compat/node_modules
|
||||
integration/cli-hello-world-ivy-i18n/node_modules
|
||||
integration/cli-hello-world-ivy-minimal/node_modules
|
||||
integration/cli-hello-world-lazy/node_modules
|
||||
integration/cli-hello-world-lazy-rollup/node_modules
|
||||
integration/dynamic-compiler/node_modules
|
||||
integration/hello_world__closure/node_modules
|
||||
integration/hello_world__systemjs_umd/node_modules
|
||||
integration/i18n/node_modules
|
||||
integration/injectable-def/node_modules
|
||||
integration/ivy-i18n/node_modules
|
||||
integration/language_service_plugin/node_modules
|
||||
integration/ng_elements/node_modules
|
||||
integration/ng_elements_schematics/node_modules
|
||||
integration/ng_update/node_modules
|
||||
integration/ng_update_migrations/node_modules
|
||||
integration/ngcc/node_modules
|
||||
integration/platform-server/node_modules
|
||||
integration/service-worker-schema/node_modules
|
||||
integration/side-effects/node_modules
|
||||
integration/terser/node_modules
|
||||
integration/typings_test_ts36/node_modules
|
||||
integration/typings_test_ts37/node_modules
|
||||
# All integration test .yarn_local_cache folders
|
||||
integration/bazel/.yarn_local_cache
|
||||
integration/bazel-schematics/.yarn_local_cache
|
||||
integration/cli-hello-world/.yarn_local_cache
|
||||
integration/cli-hello-world-ivy-compat/.yarn_local_cache
|
||||
integration/cli-hello-world-ivy-i18n/.yarn_local_cache
|
||||
integration/cli-hello-world-ivy-minimal/.yarn_local_cache
|
||||
integration/cli-hello-world-lazy/.yarn_local_cache
|
||||
integration/cli-hello-world-lazy-rollup/.yarn_local_cache
|
||||
integration/dynamic-compiler/.yarn_local_cache
|
||||
integration/hello_world__closure/.yarn_local_cache
|
||||
integration/hello_world__systemjs_umd/.yarn_local_cache
|
||||
integration/i18n/.yarn_local_cache
|
||||
integration/injectable-def/.yarn_local_cache
|
||||
integration/ivy-i18n/.yarn_local_cache
|
||||
integration/language_service_plugin/.yarn_local_cache
|
||||
integration/ng_elements/.yarn_local_cache
|
||||
integration/ng_elements_schematics/.yarn_local_cache
|
||||
integration/ng_update/.yarn_local_cache
|
||||
integration/ng_update_migrations/.yarn_local_cache
|
||||
integration/ngcc/.yarn_local_cache
|
||||
integration/platform-server/.yarn_local_cache
|
||||
integration/service-worker-schema/.yarn_local_cache
|
||||
integration/side-effects/.yarn_local_cache
|
||||
integration/terser/.yarn_local_cache
|
||||
integration/typings_test_ts36/.yarn_local_cache
|
||||
integration/typings_test_ts37/.yarn_local_cache
|
||||
# All integration test NPM_PACKAGE_MANIFEST.json folders
|
||||
integration/bazel/NPM_PACKAGE_MANIFEST.json
|
||||
integration/bazel-schematics/NPM_PACKAGE_MANIFEST.json
|
||||
integration/cli-hello-world/NPM_PACKAGE_MANIFEST.json
|
||||
integration/cli-hello-world-ivy-compat/NPM_PACKAGE_MANIFEST.json
|
||||
integration/cli-hello-world-ivy-i18n/NPM_PACKAGE_MANIFEST.json
|
||||
integration/cli-hello-world-ivy-minimal/NPM_PACKAGE_MANIFEST.json
|
||||
integration/cli-hello-world-lazy/NPM_PACKAGE_MANIFEST.json
|
||||
integration/cli-hello-world-lazy-rollup/NPM_PACKAGE_MANIFEST.json
|
||||
integration/dynamic-compiler/NPM_PACKAGE_MANIFEST.json
|
||||
integration/hello_world__closure/NPM_PACKAGE_MANIFEST.json
|
||||
integration/hello_world__systemjs_umd/NPM_PACKAGE_MANIFEST.json
|
||||
integration/i18n/NPM_PACKAGE_MANIFEST.json
|
||||
integration/injectable-def/NPM_PACKAGE_MANIFEST.json
|
||||
integration/ivy-i18n/NPM_PACKAGE_MANIFEST.json
|
||||
integration/language_service_plugin/NPM_PACKAGE_MANIFEST.json
|
||||
integration/ng_elements/NPM_PACKAGE_MANIFEST.json
|
||||
integration/ng_elements_schematics/NPM_PACKAGE_MANIFEST.json
|
||||
integration/ng_update/NPM_PACKAGE_MANIFEST.json
|
||||
integration/ng_update_migrations/NPM_PACKAGE_MANIFEST.json
|
||||
integration/ngcc/NPM_PACKAGE_MANIFEST.json
|
||||
integration/platform-server/NPM_PACKAGE_MANIFEST.json
|
||||
integration/service-worker-schema/NPM_PACKAGE_MANIFEST.json
|
||||
integration/side-effects/NPM_PACKAGE_MANIFEST.json
|
||||
integration/terser/NPM_PACKAGE_MANIFEST.json
|
||||
integration/typings_test_ts36/NPM_PACKAGE_MANIFEST.json
|
||||
integration/typings_test_ts37/NPM_PACKAGE_MANIFEST.json
|
||||
|
11
.bazelrc
11
.bazelrc
@ -62,6 +62,16 @@ test --test_output=errors
|
||||
|
||||
# Bazel flags for CircleCI are in /.circleci/bazel.linux.rc and /.circleci/bazel.windows.rc
|
||||
|
||||
##################################
|
||||
# Settings for integration tests #
|
||||
##################################
|
||||
|
||||
# Trick bazel into treating BUILD files under integration/bazel as being regular files
|
||||
# This lets us glob() up all the files inside this integration test to make them inputs to tests
|
||||
# (Note, we cannot use common --deleted_packages because the bazel version command doesn't support it)
|
||||
build --deleted_packages=integration/bazel,integration/bazel/src,integration/bazel/src/hello-world,integration/bazel/test,integration/bazel/test/e2e
|
||||
query --deleted_packages=integration/bazel,integration/bazel/src,integration/bazel/src/hello-world,integration/bazel/test,integration/bazel/test/e2e
|
||||
|
||||
################################
|
||||
# Temporary Settings for Ivy #
|
||||
################################
|
||||
@ -100,7 +110,6 @@ build:remote --javabase=@rbe_ubuntu1604_angular//java:jdk
|
||||
build:remote --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_hostjdk8
|
||||
build:remote --java_toolchain=@bazel_tools//tools/jdk:toolchain_hostjdk8
|
||||
build:remote --crosstool_top=@rbe_ubuntu1604_angular//cc:toolchain
|
||||
build:remote --action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1
|
||||
build:remote --extra_toolchains=@rbe_ubuntu1604_angular//config:cc-toolchain
|
||||
build:remote --extra_execution_platforms=//tools:rbe_ubuntu1604-angular
|
||||
build:remote --host_platform=//tools:rbe_ubuntu1604-angular
|
||||
|
@ -22,8 +22,8 @@ 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 v3-angular-node-12-{{ checksum "yarn.lock" }}-{{ checksum "WORKSPACE" }}-{{ checksum "packages/bazel/package.bzl" }}-{{ checksum "aio/yarn.lock" }}
|
||||
var_4: &cache_key_fallback v3-angular-node-12-
|
||||
var_3: &cache_key v4-angular-node-12-{{ checksum "yarn.lock" }}-{{ checksum "WORKSPACE" }}-{{ checksum "packages/bazel/package.bzl" }}-{{ checksum "aio/yarn.lock" }}
|
||||
var_4: &cache_key_fallback v4-angular-node-12-
|
||||
var_3_win: &cache_key_win v5-angular-win-node-12-{{ checksum "yarn.lock" }}-{{ checksum "WORKSPACE" }}-{{ checksum "packages/bazel/package.bzl" }}-{{ checksum "aio/yarn.lock" }}
|
||||
var_4_win: &cache_key_win_fallback v5-angular-win-node-12-
|
||||
|
||||
@ -65,8 +65,8 @@ var_10: &only_on_master
|
||||
# (Using the tag in not necessary when pinning by ID, but include it anyway for documentation purposes.)
|
||||
# **NOTE 2**: If you change the version of the docker images, also change the `cache_key` suffix.
|
||||
# **NOTE 3**: If you change the version of the `*-browsers` docker image, make sure the
|
||||
# `CI_CHROMEDRIVER_VERSION_ARG` env var (in `.circleci/env.sh`) points to a ChromeDriver
|
||||
# version that is compatible with the Chrome version in the image.
|
||||
# `--versions.chrome` arg in `integration/bazel-schematics/test.sh` specifies a
|
||||
# ChromeDriver version that is compatible with the Chrome version in the image.
|
||||
executors:
|
||||
default-executor:
|
||||
parameters:
|
||||
@ -78,20 +78,6 @@ executors:
|
||||
resource_class: << parameters.resource_class >>
|
||||
working_directory: ~/ng
|
||||
|
||||
browsers-executor:
|
||||
parameters:
|
||||
resource_class:
|
||||
type: string
|
||||
default: medium
|
||||
docker:
|
||||
# The browser docker image comes with Chrome and Firefox preinstalled. This is just
|
||||
# needed for jobs that run tests without Bazel. Bazel runs tests with browsers that will be
|
||||
# fetched by the Webtesting rules. Therefore for jobs that run tests with Bazel, we don't need a
|
||||
# docker image with browsers pre-installed.
|
||||
- image: circleci/node:12.14.1-browsers@sha256:792797ab9be3179be7c9fc38a0931a3349288e699467c8d646d7c54e148ae46c
|
||||
resource_class: << parameters.resource_class >>
|
||||
working_directory: ~/ng
|
||||
|
||||
windows-executor:
|
||||
working_directory: ~/ng
|
||||
resource_class: windows.medium
|
||||
@ -136,11 +122,29 @@ commands:
|
||||
# circleci/node:x.x.x-browsers image.
|
||||
sudo apt-get -y install libgtk-3-0 libasound2 libnss3 libxss1
|
||||
|
||||
# Install java runtime which is required by some integration tests such as
|
||||
# //integration:hello_world__closure_test, //integration:i18n_test and
|
||||
# //integration:ng_elements_test to run the closure compiler
|
||||
install_java:
|
||||
description: Install java
|
||||
steps:
|
||||
- run:
|
||||
name: Install java
|
||||
command: |
|
||||
sudo apt-get update
|
||||
# Install java runtime
|
||||
sudo apt-get install default-jre
|
||||
|
||||
# Initializes the CI environment by setting up common environment variables.
|
||||
init_environment:
|
||||
description: Initializing environment (setting up variables)
|
||||
steps:
|
||||
- run: ./.circleci/env.sh
|
||||
- run:
|
||||
name: Set up environment
|
||||
environment:
|
||||
CIRCLE_GIT_BASE_REVISION: << pipeline.git.base_revision >>
|
||||
CIRCLE_GIT_REVISION: << pipeline.git.revision >>
|
||||
command: ./.circleci/env.sh
|
||||
- run:
|
||||
# Configure git as the CircleCI `checkout` command does.
|
||||
# This is needed because we only checkout on the setup job.
|
||||
@ -270,15 +274,22 @@ jobs:
|
||||
- run: 'yarn bazel:lint ||
|
||||
(echo -e "\n.bzl files have lint errors. Please run ''yarn bazel:lint-fix''"; exit 1)'
|
||||
|
||||
- run: yarn gulp lint
|
||||
- run: yarn lint
|
||||
- run: node tools/pullapprove/verify.js
|
||||
|
||||
test:
|
||||
executor:
|
||||
name: default-executor
|
||||
resource_class: xlarge
|
||||
# Now that large integration tests are running locally in parallel (they can't run on RBE yet
|
||||
# as they require network access for yarn install), this test is running out of memory
|
||||
# consistently with the xlarge machine.
|
||||
# TODO: switch back to xlarge once integration tests are running on remote-exec
|
||||
resource_class: 2xlarge+
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
- install_chrome_libs
|
||||
- install_java
|
||||
- run:
|
||||
command: yarn bazel test //... --build_tag_filters=-ivy-only --test_tag_filters=-ivy-only
|
||||
no_output_timeout: 20m
|
||||
@ -291,6 +302,7 @@ jobs:
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
- install_chrome_libs
|
||||
# We need to explicitly specify the --symlink_prefix option because otherwise we would
|
||||
# not be able to easily find the output bin directory when uploading artifacts for size
|
||||
# measurements.
|
||||
@ -317,11 +329,7 @@ jobs:
|
||||
path: dist/bin/packages/core/test/bundling/todo/bundle.min.js.br
|
||||
destination: core/todo/bundle.br
|
||||
|
||||
# This job is currently a PoC for running tests on SauceLabs via bazel. It runs a subset of the
|
||||
# tests in `legacy-unit-tests-saucelabs` (see
|
||||
# [BUILD.bazel](https://github.com/angular/angular/blob/ef44f51d5/BUILD.bazel#L66-L92)).
|
||||
#
|
||||
# NOTE: This is currently limited to master builds only. See the `default_workflow` configuration.
|
||||
# NOTE: This is currently limited to master builds only. See the `monitoring` configuration.
|
||||
saucelabs_view_engine:
|
||||
executor:
|
||||
name: default-executor
|
||||
@ -334,16 +342,18 @@ jobs:
|
||||
- init_environment
|
||||
- init_saucelabs_environment
|
||||
- run:
|
||||
name: Run Bazel tests on Saucelabs
|
||||
name: Run Bazel tests on Saucelabs with ViewEngine
|
||||
# See /tools/saucelabs/README.md for more info
|
||||
command: |
|
||||
yarn bazel run //tools/saucelabs:sauce_service_setup
|
||||
yarn bazel test //:saucelabs_unit_tests_poc_suite --config=saucelabs
|
||||
TESTS=$(./node_modules/.bin/bazel query --output label '(kind(karma_web_test, ...) intersect attr("tags", "saucelabs", ...)) except attr("tags", "ivy-only", ...) except attr("tags", "fixme-saucelabs-ve", ...)')
|
||||
yarn bazel test --config=saucelabs ${TESTS}
|
||||
yarn bazel run //tools/saucelabs:sauce_service_stop
|
||||
no_output_timeout: 20m
|
||||
no_output_timeout: 40m
|
||||
- notify_webhook_on_fail:
|
||||
webhook_url_env_var: SLACK_DEV_INFRA_CI_FAILURES_WEBHOOK_URL
|
||||
|
||||
# NOTE: This is currently limited to master builds only. See the `monitoring` configuration.
|
||||
saucelabs_ivy:
|
||||
executor:
|
||||
name: default-executor
|
||||
@ -356,13 +366,16 @@ jobs:
|
||||
- init_environment
|
||||
- init_saucelabs_environment
|
||||
- run:
|
||||
name: Run Bazel tests on Saucelabs
|
||||
name: Run Bazel tests on Saucelabs with Ivy
|
||||
# See /tools/saucelabs/README.md for more info
|
||||
command: |
|
||||
yarn bazel run //tools/saucelabs:sauce_service_setup
|
||||
yarn bazel test //:saucelabs_unit_tests --config=saucelabs --config=ivy
|
||||
TESTS=$(./node_modules/.bin/bazel query --output label '(kind(karma_web_test, ...) intersect attr("tags", "saucelabs", ...)) except attr("tags", "no-ivy-aot", ...) except attr("tags", "fixme-saucelabs-ivy", ...)')
|
||||
yarn bazel test --config=saucelabs --config=ivy ${TESTS}
|
||||
yarn bazel run //tools/saucelabs:sauce_service_stop
|
||||
no_output_timeout: 20m
|
||||
no_output_timeout: 40m
|
||||
- notify_webhook_on_fail:
|
||||
webhook_url_env_var: SLACK_DEV_INFRA_CI_FAILURES_WEBHOOK_URL
|
||||
|
||||
test_aio:
|
||||
executor: default-executor
|
||||
@ -440,13 +453,13 @@ jobs:
|
||||
type: boolean
|
||||
default: false
|
||||
executor:
|
||||
# Needed because the example e2e tests depend on Chrome.
|
||||
name: browsers-executor
|
||||
name: default-executor
|
||||
resource_class: xlarge
|
||||
parallelism: 5
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
- install_chrome_libs
|
||||
# Install aio
|
||||
- run: yarn --cwd aio install --frozen-lockfile --non-interactive
|
||||
- when:
|
||||
@ -507,7 +520,7 @@ jobs:
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
- run: node scripts/build-packages-dist.js
|
||||
- run: node scripts/build/build-packages-dist.js
|
||||
|
||||
# Save the npm packages from //packages/... for other workflow jobs to read
|
||||
- persist_to_workspace:
|
||||
@ -532,7 +545,7 @@ jobs:
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
- run: node scripts/build-ivy-npm-packages.js
|
||||
- run: node scripts/build/build-ivy-npm-packages.js
|
||||
|
||||
# Save the npm packages from //packages/... for other workflow jobs to read
|
||||
- persist_to_workspace:
|
||||
@ -540,25 +553,18 @@ jobs:
|
||||
paths:
|
||||
- ng/dist/packages-dist-ivy-aot
|
||||
|
||||
# We run the integration tests outside of Bazel for now.
|
||||
# They are a separate workflow job so that they can be easily re-run.
|
||||
# When the tests are ported to bazel test targets, they should move to the "test"
|
||||
# job above, as part of the bazel test command. That has flaky_test_attempts so the
|
||||
# need to re-run manually should be alleviated.
|
||||
# We run a subset of the integration tests outside of Bazel that track
|
||||
# payload size.
|
||||
# See comments inside the integration/run_tests.sh script.
|
||||
# TODO(gregmagolan): move payload size tracking to Bazel and remove this job.
|
||||
integration_test:
|
||||
executor:
|
||||
# Needed because the integration/bazel-schematics test expects Chrome to be installed
|
||||
# TODO(gregmagolan): remove the dependency on local chrome from that test
|
||||
name: browsers-executor
|
||||
# Note: we run Bazel in one of the integration tests, and it can consume >2G
|
||||
# of memory. Together with the system under test, this can exhaust the RAM
|
||||
# on a 4G worker so we use a larger machine here too.
|
||||
resource_class: xlarge
|
||||
parallelism: 4
|
||||
executor: default-executor
|
||||
parallelism: 3
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
- install_chrome_libs
|
||||
- install_java
|
||||
# Runs the integration tests in parallel across multiple CircleCI container instances. The
|
||||
# amount of container nodes for this job is controlled by the "parallelism" option.
|
||||
- run: ./integration/run_tests.sh ${CIRCLE_NODE_INDEX} ${CIRCLE_NODE_TOTAL}
|
||||
@ -625,12 +631,11 @@ jobs:
|
||||
- run: ./scripts/ci/publish-build-artifacts.sh
|
||||
|
||||
aio_monitoring_stable:
|
||||
# This job needs Chrome to be globally installed because the tests run with Protractor
|
||||
# which does not load the browser through the Bazel webtesting rules.
|
||||
executor: browsers-executor
|
||||
executor: default-executor
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
- install_chrome_libs
|
||||
- run: setPublicVar_CI_STABLE_BRANCH
|
||||
- run:
|
||||
name: Check out `aio/` and yarn from the stable branch
|
||||
@ -813,17 +818,6 @@ workflows:
|
||||
- legacy-unit-tests-saucelabs:
|
||||
requires:
|
||||
- setup
|
||||
- saucelabs_ivy:
|
||||
requires:
|
||||
- test_ivy_aot
|
||||
- saucelabs_view_engine:
|
||||
# This job is currently a PoC and a subset of `legacy-unit-tests-saucelabs`. Running on
|
||||
# master only to avoid wasting resources.
|
||||
# TODO: Run this job on all branches (including PRs) as soon as it is not a PoC and
|
||||
# we can remove the legacy saucelabs job.
|
||||
<<: *only_on_master
|
||||
requires:
|
||||
- setup
|
||||
- test_aio:
|
||||
requires:
|
||||
- setup
|
||||
@ -873,7 +867,6 @@ workflows:
|
||||
- test
|
||||
- test_ivy_aot
|
||||
- integration_test
|
||||
- saucelabs_ivy
|
||||
# Only publish if `aio`/`docs` tests using the locally built Angular packages pass
|
||||
- test_aio_local
|
||||
- test_aio_local_viewengine
|
||||
@ -905,7 +898,7 @@ workflows:
|
||||
requires:
|
||||
- test_ivy_aot
|
||||
|
||||
aio_monitoring:
|
||||
monitoring:
|
||||
jobs:
|
||||
- setup
|
||||
- aio_monitoring_stable:
|
||||
@ -914,8 +907,26 @@ workflows:
|
||||
- aio_monitoring_next:
|
||||
requires:
|
||||
- setup
|
||||
- saucelabs_ivy:
|
||||
# Testing saucelabs via Bazel currently taking longer than the legacy saucelabs job as it
|
||||
# each karma_web_test target is provisioning and tearing down browsers which is adding
|
||||
# a lot of overhead. Running once daily on master only to avoid wasting resources and
|
||||
# slowing down CI for PRs.
|
||||
# TODO: Run this job on all branches (including PRs) once karma_web_test targets can
|
||||
# share provisioned browsers and we can remove the legacy saucelabs job.
|
||||
requires:
|
||||
- setup
|
||||
- saucelabs_view_engine:
|
||||
# Testing saucelabs via Bazel currently taking longer than the legacy saucelabs job as it
|
||||
# each karma_web_test target is provisioning and tearing down browsers which is adding
|
||||
# a lot of overhead. Running once daily on master only to avoid wasting resources and
|
||||
# slowing down CI for PRs.
|
||||
# TODO: Run this job on all branches (including PRs) once karma_web_test targets can
|
||||
# share provisioned browsers and we can remove the legacy saucelabs job.
|
||||
requires:
|
||||
- setup
|
||||
triggers:
|
||||
- schedule:
|
||||
<<: *only_on_master
|
||||
# Runs AIO monitoring jobs at 10:00AM every day.
|
||||
# Runs monitoring jobs at 10:00AM every day.
|
||||
cron: "0 10 * * *"
|
||||
|
@ -19,19 +19,10 @@ 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";
|
||||
# ChromeDriver version compatible with the Chrome version included in the docker image used in
|
||||
# `.circleci/config.yml`. See http://chromedriver.chromium.org/downloads for a list of versions.
|
||||
# This variable is intended to be passed as an arg to the `webdriver-manager update` command (e.g.
|
||||
# `"postinstall": "webdriver-manager update $CI_CHROMEDRIVER_VERSION_ARG"`).
|
||||
setPublicVar CI_CHROMEDRIVER_VERSION_ARG "--versions.chrome 79.0.3945.130";
|
||||
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).
|
||||
# NOTE: With [CircleCI Pipelines](https://circleci.com/docs/2.0/build-processing) enabled,
|
||||
# `CIRCLE_COMPARE_URL` is no longer available and the commit range cannot be reliably
|
||||
# detected. Fall back to only considering the last commit (which is accurate in the majority
|
||||
# of cases for push builds).
|
||||
setPublicVar CI_COMMIT_RANGE "`[[ ${CIRCLE_PR_NUMBER:-false} != false ]] && echo "" || echo "$CIRCLE_SHA1~1...$CIRCLE_SHA1"`";
|
||||
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";
|
||||
|
@ -1,166 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* **Usage:**
|
||||
* ```
|
||||
* node get-commit-range <build-number> [<compare-url> [<circle-token>]]
|
||||
* ```
|
||||
*
|
||||
* Returns the commit range, either extracting it from `compare-url` (if defined), which is of the
|
||||
* format of the `CIRCLE_COMPARE_URL` environment variable, or by retrieving the equivalent of
|
||||
* `CIRCLE_COMPARE_URL` for jobs that are part of a rerun workflow and extracting it from there.
|
||||
*
|
||||
* > !!! WARNING !!!
|
||||
* > !!
|
||||
* > !! When [CircleCI Pipelines](https://circleci.com/docs/2.0/build-processing) is enabled, the
|
||||
* > !! `CIRCLE_COMPARE_URL` environment variable is not available at all and this script does not
|
||||
* > !! work.
|
||||
* > !!!!!!!!!!!!!!!
|
||||
*
|
||||
* **Context:**
|
||||
* CircleCI sets the `CIRCLE_COMPARE_URL` environment variable (from which we can extract the commit
|
||||
* range) on push builds (a.k.a. non-PR, non-scheduled builds). Yet, when a workflow is rerun
|
||||
* (either from the beginning or from failed jobs) - e.g. when a job flakes - CircleCI does not set
|
||||
* the `CIRCLE_COMPARE_URL`.
|
||||
*
|
||||
* **Implementation details:**
|
||||
* This script relies on the fact that all rerun workflows share the same CircleCI workspace and the
|
||||
* (undocumented) fact that the workspace ID happens to be the same as the workflow ID that first
|
||||
* created it.
|
||||
*
|
||||
* For example, for a job on push build workflows, the CircleCI API will return data that look like:
|
||||
* ```js
|
||||
* {
|
||||
* compare: 'THE_COMPARE_URL_WE_ARE_LOOKING_FOR',
|
||||
* //...
|
||||
* previous: {
|
||||
* // ...
|
||||
* build_num: 12345,
|
||||
* },
|
||||
* //...
|
||||
* workflows: {
|
||||
* //...
|
||||
* workflow_id: 'SOME_ID_A',
|
||||
* workspace_id: 'SOME_ID_A', // Same as `workflow_id`.
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* If the workflow is rerun, the data for jobs on the new workflow will look like:
|
||||
* ```js
|
||||
* {
|
||||
* compare: null, // ¯\_(ツ)_/¯
|
||||
* //...
|
||||
* previous: {
|
||||
* // ...
|
||||
* build_num: 23456,
|
||||
* },
|
||||
* //...
|
||||
* workflows: {
|
||||
* //...
|
||||
* workflow_id: 'SOME_ID_B',
|
||||
* workspace_id: 'SOME_ID_A', // Different from current `workflow_id`.
|
||||
* // Same as original `workflow_id`. \o/
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* This script uses the `previous.build_num` (which points to the previous build number on the same
|
||||
* branch) to traverse the jobs backwards, until it finds a job from the original workflow. Such a
|
||||
* job (if found) should also contain the compare URL.
|
||||
*
|
||||
* **NOTE 1:**
|
||||
* This is only useful on workflows which are created by rerunning a workflow for which
|
||||
* `CIRCLE_COMPARE_URL` was defined.
|
||||
*
|
||||
* **NOTE 2:**
|
||||
* The `circleToken` will be used for CircleCI API requests if provided, but it is not needed for
|
||||
* accessing the read-only endpoints that we need (as long as the current project is FOSS and the
|
||||
* corresponding setting is turned on in "Advanced Settings" in the project dashboard).
|
||||
*
|
||||
* ---
|
||||
* Inspired by https://circleci.com/orbs/registry/orb/iynere/compare-url
|
||||
* (source code: https://github.com/iynere/compare-url-orb).
|
||||
*
|
||||
* We are not using the `compare-url` orb for the following reasons:
|
||||
* 1. (By looking at the code) it would only work if the rerun workflow is the latest workflow on
|
||||
* the branch (which is not guaranteed to be true).
|
||||
* 2. It is less efficient (e.g. makes unnecessary CircleCI API requests for builds on different
|
||||
* branches, installs extra dependencies, persists files to the workspace (as a means of passing
|
||||
* the result to the calling job), etc.).
|
||||
* 3. It is slightly more complicated to setup and consume than our own script.
|
||||
* 4. Its implementation is more complicated than needed for our usecase (e.g. handles different git
|
||||
* providers, handles newly created branches, etc.).
|
||||
*/
|
||||
|
||||
// Imports
|
||||
const {get: httpsGet} = require('https');
|
||||
|
||||
// Constants
|
||||
const API_URL_BASE = 'https://circleci.com/api/v1.1/project/github/angular/angular';
|
||||
const COMPARE_URL_RE = /^.*\/([0-9a-f]+\.\.\.[0-9a-f]+)$/i;
|
||||
|
||||
// Run
|
||||
_main(process.argv.slice(2));
|
||||
|
||||
// Helpers
|
||||
async function _main([buildNumber, compareUrl = '', circleToken = '']) {
|
||||
try {
|
||||
if (!buildNumber || isNaN(buildNumber)) {
|
||||
throw new Error(
|
||||
'Missing or invalid arguments.\n' +
|
||||
'Expected: buildNumber (number), compareUrl? (string), circleToken? (string)');
|
||||
}
|
||||
|
||||
if (!compareUrl) {
|
||||
compareUrl = await getCompareUrl(buildNumber, circleToken);
|
||||
}
|
||||
|
||||
const commitRangeMatch = COMPARE_URL_RE.exec(compareUrl)
|
||||
const commitRange = commitRangeMatch ? commitRangeMatch[1] : '';
|
||||
|
||||
console.log(commitRange);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function getBuildInfo(buildNumber, circleToken) {
|
||||
console.error(`BUILD ${buildNumber}`);
|
||||
const url = `${API_URL_BASE}/${buildNumber}?circle-token=${circleToken}`;
|
||||
return getJson(url);
|
||||
}
|
||||
|
||||
async function getCompareUrl(buildNumber, circleToken) {
|
||||
let info = await getBuildInfo(buildNumber, circleToken);
|
||||
const targetWorkflowId = info.workflows.workspace_id;
|
||||
|
||||
while (info.workflows.workflow_id !== targetWorkflowId) {
|
||||
info = await getBuildInfo(info.previous.build_num, circleToken);
|
||||
}
|
||||
|
||||
return info.compare || '';
|
||||
}
|
||||
|
||||
function getJson(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const opts = {headers: {Accept: 'application/json'}};
|
||||
const onResponse = res => {
|
||||
const statusCode = res.statusCode || -1;
|
||||
const isSuccess = (200 <= statusCode) && (statusCode < 400);
|
||||
let responseText = '';
|
||||
|
||||
res.
|
||||
on('error', reject).
|
||||
on('data', d => responseText += d).
|
||||
on('end', () => isSuccess ?
|
||||
resolve(JSON.parse(responseText)) :
|
||||
reject(`Error getting '${url}' (status ${statusCode}):\n${responseText}`));
|
||||
};
|
||||
|
||||
httpsGet(url, opts, onResponse).
|
||||
on('error', reject).
|
||||
end();
|
||||
});
|
||||
}
|
@ -40,6 +40,7 @@
|
||||
# AndrewKushnir - Andrew Kushnir
|
||||
# andrewseguin - Andrew Seguin
|
||||
# atscott - Andrew Scott
|
||||
# ayazhafiz - Ayaz Hafiz
|
||||
# clydin - Charles Lyding
|
||||
# crisbeto - Kristiyan Kostadinov
|
||||
# dennispbrown - Denny Brown
|
||||
@ -108,6 +109,9 @@ pullapprove_conditions:
|
||||
- condition: "'PR state: WIP' not in labels"
|
||||
unmet_status: pending
|
||||
explanation: "Waiting to send reviews as PR is WIP"
|
||||
- condition: "not draft"
|
||||
unmet_status: pending
|
||||
explanation: "Waiting to send reviews as PR is in draft"
|
||||
|
||||
|
||||
groups:
|
||||
@ -627,6 +631,7 @@ groups:
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- ayazhafiz
|
||||
- kyliau
|
||||
teams:
|
||||
- ~framework-global-approvers
|
||||
@ -640,7 +645,8 @@ groups:
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
'packages/zone.js/**'
|
||||
'packages/zone.js/**',
|
||||
'aio/content/guide/zone.md'
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
@ -950,6 +956,7 @@ groups:
|
||||
'tools/ng_rollup_bundle/**',
|
||||
'tools/ngcontainer/**',
|
||||
'tools/npm/**',
|
||||
'tools/npm_integration_test/**',
|
||||
'tools/public_api_guard/BUILD.bazel',
|
||||
'tools/public_api_guard/public_api_guard.bzl',
|
||||
'tools/pullapprove/**',
|
||||
@ -961,6 +968,7 @@ groups:
|
||||
'tools/testing/**',
|
||||
'tools/ts-api-guardian/**',
|
||||
'tools/tslint/**',
|
||||
'tools/utils/**',
|
||||
'tools/validate-commit-message/**',
|
||||
'tools/yarn/**',
|
||||
'tools/*',
|
||||
|
75
BUILD.bazel
75
BUILD.bazel
@ -1,7 +1,5 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load("//tools:defaults.bzl", "karma_web_test")
|
||||
|
||||
exports_files([
|
||||
"LICENSE",
|
||||
"protractor-perf.conf.js",
|
||||
@ -46,76 +44,3 @@ filegroup(
|
||||
"@npm//:node_modules/angular-mocks-1.6/angular-mocks.js",
|
||||
],
|
||||
)
|
||||
|
||||
# To run manually:
|
||||
# Setup your SAUCE_USERNAME, SAUCE_ACCESS_KEY & SAUCE_TUNNEL_IDENTIFIER.
|
||||
# If on OSX, also set SAUCE_CONNECT to the path of your `sc` binary.
|
||||
# environment variables and run:
|
||||
# ```
|
||||
# yarn bazel run //tools/saucelabs:sauce_service_setup
|
||||
# yarn bazel test //:saucelabs_unit_tests --config=saucelabs --config=ivy
|
||||
# ```
|
||||
# See /tools/saucelabs/README.md for more info on karma Saucelabs tests under Bazel.
|
||||
karma_web_test(
|
||||
name = "saucelabs_unit_tests",
|
||||
# Default timeout is moderate (5min). This causes the test to be terminated while
|
||||
# Saucelabs browsers keep running. Ultimately resulting in failing tests and browsers
|
||||
# unnecessarily being acquired. Our specified Saucelabs idle timeout is 10min, so we use
|
||||
# Bazel's long timeout (15min). This ensures that Karma can shut down properly.
|
||||
timeout = "long",
|
||||
karma = "//tools/saucelabs:karma-saucelabs",
|
||||
tags = [
|
||||
"manual",
|
||||
"no-remote-exec",
|
||||
"saucelabs",
|
||||
],
|
||||
deps = [
|
||||
"//packages/core/test/acceptance:acceptance_lib",
|
||||
],
|
||||
)
|
||||
|
||||
SAUCE_TEST_SUITE_TARGETS = [
|
||||
"packages/common/http/test:test_lib",
|
||||
"packages/common/http/testing/test:test_lib",
|
||||
"packages/common/test:test_lib",
|
||||
"packages/core/test:test_lib",
|
||||
"packages/forms/test:test_lib",
|
||||
"packages/http/test:test_lib",
|
||||
]
|
||||
|
||||
[
|
||||
# These target runs in CI with View Engine as a Saucelabs and Bazel proof-of-concept. It's a
|
||||
# subset of the legacy saucelabs tests.
|
||||
karma_web_test(
|
||||
name = "saucelabs_unit_tests_poc_%s" % test.replace("/", "_").replace(":", "_").replace(".", "_"),
|
||||
# Default timeout is moderate (5min). This causes the test to be terminated while
|
||||
# Saucelabs browsers keep running. Ultimately resulting in failing tests and browsers
|
||||
# unnecessarily being acquired. Our specified Saucelabs idle timeout is 10min, so we use
|
||||
# Bazel's long timeout (15min). This ensures that Karma can shut down properly.
|
||||
timeout = "long",
|
||||
karma = "//tools/saucelabs:karma-saucelabs",
|
||||
tags = [
|
||||
"exclusive",
|
||||
"manual",
|
||||
"no-remote-exec",
|
||||
"saucelabs",
|
||||
],
|
||||
deps = ["//%s" % test],
|
||||
)
|
||||
for test in SAUCE_TEST_SUITE_TARGETS
|
||||
]
|
||||
|
||||
# To run manually:
|
||||
# Setup your SAUCE_USERNAME, SAUCE_ACCESS_KEY & SAUCE_TUNNEL_IDENTIFIER.
|
||||
# If on OSX, also set SAUCE_CONNECT to the path of your `sc` binary.
|
||||
# environment variables and run:
|
||||
# ```
|
||||
# yarn bazel run //tools/saucelabs:sauce_service_setup
|
||||
# yarn bazel test //:saucelabs_unit_tests_poc_suite --config=saucelabs
|
||||
# ```
|
||||
# See /tools/saucelabs/README.md for more info on karma Saucelabs tests under Bazel.
|
||||
test_suite(
|
||||
name = "saucelabs_unit_tests_poc_suite",
|
||||
tags = ["manual"],
|
||||
tests = ["//:saucelabs_unit_tests_poc_%s" % test.replace("/", "_").replace(":", "_").replace(".", "_") for test in SAUCE_TEST_SUITE_TARGETS],
|
||||
)
|
||||
|
114
CHANGELOG.md
114
CHANGELOG.md
@ -1,3 +1,117 @@
|
||||
<a name="9.0.5"></a>
|
||||
## [9.0.5](https://github.com/angular/angular/compare/9.0.4...9.0.5) (2020-03-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** allow computeStyle to work on elements created in Node ([#35810](https://github.com/angular/angular/issues/35810)) ([2b63b7f](https://github.com/angular/angular/commit/2b63b7f))
|
||||
* **animations:** process shorthand `margin` and `padding` styles correctly ([#35701](https://github.com/angular/angular/issues/35701)) ([2e251b7](https://github.com/angular/angular/commit/2e251b7)), closes [#35463](https://github.com/angular/angular/issues/35463)
|
||||
* **bazel:** ng_package rule creates incorrect UMD module exports ([#35792](https://github.com/angular/angular/issues/35792)) ([c272351](https://github.com/angular/angular/commit/c272351)), closes [angular/components#18652](https://github.com/angular/components/issues/18652)
|
||||
* **compiler:** support i18n attributes on `<ng-template>` tags ([#35681](https://github.com/angular/angular/issues/35681)) ([d1966fc](https://github.com/angular/angular/commit/d1966fc))
|
||||
* **compiler:** type-checking error for duplicate variables in templates ([#35674](https://github.com/angular/angular/issues/35674)) ([1207295](https://github.com/angular/angular/commit/1207295)), closes [#35186](https://github.com/angular/angular/issues/35186)
|
||||
* **core:** allow null / undefined values in query results ([#35796](https://github.com/angular/angular/issues/35796)) ([120ce42](https://github.com/angular/angular/commit/120ce42)), closes [#35673](https://github.com/angular/angular/issues/35673)
|
||||
* **core:** handle `<ng-template>` with local refs in i18n blocks ([#35758](https://github.com/angular/angular/issues/35758)) ([5a14a15](https://github.com/angular/angular/commit/5a14a15))
|
||||
* **core:** log error instead of warning for unknown properties and elements ([#35798](https://github.com/angular/angular/issues/35798)) ([218e82e](https://github.com/angular/angular/commit/218e82e)), closes [#35699](https://github.com/angular/angular/issues/35699)
|
||||
* **core:** Remove `debugger` statement ([#35763](https://github.com/angular/angular/issues/35763)) ([e201a84](https://github.com/angular/angular/commit/e201a84)), closes [#35470](https://github.com/angular/angular/issues/35470)
|
||||
* **core:** Remove `debugger` statement when assert is thrown ([#35763](https://github.com/angular/angular/issues/35763)) ([d3ee052](https://github.com/angular/angular/commit/d3ee052)), closes [#35470](https://github.com/angular/angular/issues/35470)
|
||||
* **core:** treat `[class]` and `[className]` as unrelated bindings ([#35668](https://github.com/angular/angular/issues/35668)) ([48025eb](https://github.com/angular/angular/commit/48025eb)), closes [#35577](https://github.com/angular/angular/issues/35577)
|
||||
* **core:** unable to NgModuleRef.injector in module constructor ([#35731](https://github.com/angular/angular/issues/35731)) ([1980d69](https://github.com/angular/angular/commit/1980d69)), closes [#35677](https://github.com/angular/angular/issues/35677) [#35639](https://github.com/angular/angular/issues/35639)
|
||||
* **core:** use proper configuration to compile Injectable in JIT ([#35706](https://github.com/angular/angular/issues/35706)) ([df0859f](https://github.com/angular/angular/commit/df0859f))
|
||||
* **ivy:** narrow `NgIf` context variables in template type checker ([#35125](https://github.com/angular/angular/issues/35125)) ([fcad075](https://github.com/angular/angular/commit/fcad075)), closes [#34572](https://github.com/angular/angular/issues/34572)
|
||||
* **ivy:** support dynamic query tokens in AOT mode ([#35307](https://github.com/angular/angular/issues/35307)) ([52fc087](https://github.com/angular/angular/commit/52fc087)), closes [#34267](https://github.com/angular/angular/issues/34267)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **language-service:** modularize error messages ([#35678](https://github.com/angular/angular/issues/35678)) ([bcff873](https://github.com/angular/angular/commit/bcff873)), closes [#32663](https://github.com/angular/angular/issues/32663)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **core:** add micro benchmark for destroy hook invocation ([#35784](https://github.com/angular/angular/issues/35784)) ([a07917b](https://github.com/angular/angular/commit/a07917b))
|
||||
* **core:** adding micro benchmark for host bindings ([#35705](https://github.com/angular/angular/issues/35705)) ([4ec7cd1](https://github.com/angular/angular/commit/4ec7cd1)), closes [#35568](https://github.com/angular/angular/issues/35568)
|
||||
* **core:** use multiple directives in host bindings micro benchmark ([#35736](https://github.com/angular/angular/issues/35736)) ([e13fcba](https://github.com/angular/angular/commit/e13fcba))
|
||||
* **ngcc:** only create tasks for non-processed formats ([#35719](https://github.com/angular/angular/issues/35719)) ([#35832](https://github.com/angular/angular/issues/35832)) ([3fdd304](https://github.com/angular/angular/commit/3fdd304))
|
||||
* **ngcc:** spawn workers lazily ([#35719](https://github.com/angular/angular/issues/35719)) ([#35832](https://github.com/angular/angular/issues/35832)) ([525dc6a](https://github.com/angular/angular/commit/525dc6a)), closes [#35717](https://github.com/angular/angular/issues/35717)
|
||||
|
||||
|
||||
|
||||
<a name="9.0.4"></a>
|
||||
## [9.0.4](https://github.com/angular/angular/compare/9.0.3...9.0.4) (2020-02-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ngcc:** allow deep-import warnings to be ignored ([#35683](https://github.com/angular/angular/issues/35683)) ([9064f4e](https://github.com/angular/angular/commit/9064f4e)), closes [#35615](https://github.com/angular/angular/issues/35615)
|
||||
* **ngcc:** handle mappings outside the content when flattening source-maps ([#35718](https://github.com/angular/angular/issues/35718)) ([bfe7657](https://github.com/angular/angular/commit/bfe7657)), closes [#35709](https://github.com/angular/angular/issues/35709)
|
||||
* **ngcc:** handle missing sources when flattening source-maps ([#35718](https://github.com/angular/angular/issues/35718)) ([7ff845b](https://github.com/angular/angular/commit/7ff845b)), closes [#35709](https://github.com/angular/angular/issues/35709)
|
||||
|
||||
|
||||
|
||||
<a name="9.0.3"></a>
|
||||
## [9.0.3](https://github.com/angular/angular/compare/9.0.2...9.0.3) (2020-02-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** false positive when detecting Node in Webpack builds ([#35134](https://github.com/angular/angular/issues/35134)) ([224aaae](https://github.com/angular/angular/commit/224aaae)), closes [#35117](https://github.com/angular/angular/issues/35117)
|
||||
* **animations:** Remove ɵAnimationDriver from private exports ([#35690](https://github.com/angular/angular/issues/35690)) ([c2dbcd3](https://github.com/angular/angular/commit/c2dbcd3))
|
||||
* **compiler:** use FatalDiagnosticError to generate better error messages ([#35244](https://github.com/angular/angular/issues/35244)) ([72664ca](https://github.com/angular/angular/commit/72664ca))
|
||||
* **core:** make subclass inherit developer-defined data ([#35105](https://github.com/angular/angular/issues/35105)) ([f5e1faa](https://github.com/angular/angular/commit/f5e1faa))
|
||||
* **core:** support sanitizer value in the [style] bindings ([#35564](https://github.com/angular/angular/issues/35564)) ([36dc1c7](https://github.com/angular/angular/commit/36dc1c7)), closes [#35476](https://github.com/angular/angular/issues/35476)
|
||||
* **core:** Add `style="{{exp}}"` based interpolation ([#34202](https://github.com/angular/angular/issues/34202)) ([d63ba9c](https://github.com/angular/angular/commit/d63ba9c)), closes [#33575](https://github.com/angular/angular/issues/33575)
|
||||
* **core:** add strictLiteralTypes to align core + VE checking of literals ([#35462](https://github.com/angular/angular/issues/35462)) ([628f957](https://github.com/angular/angular/commit/628f957))
|
||||
* **core:** better inference for circularly referenced directive types ([#35622](https://github.com/angular/angular/issues/35622)) ([4c2bd64](https://github.com/angular/angular/commit/4c2bd64)), closes [#35372](https://github.com/angular/angular/issues/35372) [#35603](https://github.com/angular/angular/issues/35603) [#35522](https://github.com/angular/angular/issues/35522)
|
||||
* **core:** emulate a View Engine type-checking bug with safe navigation ([#35462](https://github.com/angular/angular/issues/35462)) ([02599e4](https://github.com/angular/angular/commit/02599e4))
|
||||
* **core:** error in AOT when pipe inherits constructor from injectable that uses DI ([#35468](https://github.com/angular/angular/issues/35468)) ([bb09cd0](https://github.com/angular/angular/commit/bb09cd0)), closes [#35277](https://github.com/angular/angular/issues/35277)
|
||||
* **core:** error when accessing NgModuleRef.componentFactoryResolver in constructor ([#35637](https://github.com/angular/angular/issues/35637)) ([d690488](https://github.com/angular/angular/commit/d690488)), closes [#35580](https://github.com/angular/angular/issues/35580)
|
||||
* **core:** incorrectly generating shared pure function between null and object literal ([#35481](https://github.com/angular/angular/issues/35481)) ([8a531e2](https://github.com/angular/angular/commit/8a531e2)), closes [#33705](https://github.com/angular/angular/issues/33705) [#35298](https://github.com/angular/angular/issues/35298)
|
||||
* **core:** injecting incorrect provider when re-providing injectable with useClass ([#34574](https://github.com/angular/angular/issues/34574)) ([79aaaa3](https://github.com/angular/angular/commit/79aaaa3)), closes [#34110](https://github.com/angular/angular/issues/34110)
|
||||
* **core:** provide a more detailed error message for NG6002/NG6003 ([#35620](https://github.com/angular/angular/issues/35620)) ([e6c416f](https://github.com/angular/angular/commit/e6c416f))
|
||||
* **language-service:** get the right 'ElementAst' in the nested HTML tag ([#35317](https://github.com/angular/angular/issues/35317)) ([7403ba1](https://github.com/angular/angular/commit/7403ba1))
|
||||
* **language-service:** infer context type of structural directives ([#35537](https://github.com/angular/angular/issues/35537)) ([#35561](https://github.com/angular/angular/issues/35561)) ([a491f7e](https://github.com/angular/angular/commit/a491f7e))
|
||||
* **language-service:** provide hover for interpolation in attribute value ([#35494](https://github.com/angular/angular/issues/35494)) ([0700279](https://github.com/angular/angular/commit/0700279)), closes [PR#34847](https://github.com/PR/issues/34847)
|
||||
* **localize:** improve placeholder mismatch error message ([#35593](https://github.com/angular/angular/issues/35593)) ([1112875](https://github.com/angular/angular/commit/1112875))
|
||||
* **localize:** support minified ES5 `$localize` calls ([#35562](https://github.com/angular/angular/issues/35562)) ([bc7a8a8](https://github.com/angular/angular/commit/bc7a8a8)), closes [#35376](https://github.com/angular/angular/issues/35376)
|
||||
* **ngcc:** add default config for `angular2-highcharts` ([#35527](https://github.com/angular/angular/issues/35527)) ([aebd662](https://github.com/angular/angular/commit/aebd662)), closes [#35399](https://github.com/angular/angular/issues/35399)
|
||||
* **ngcc:** capture path-mapped entry-points that start with same string ([#35592](https://github.com/angular/angular/issues/35592)) ([d83f62d](https://github.com/angular/angular/commit/d83f62d)), closes [#35536](https://github.com/angular/angular/issues/35536)
|
||||
* **ngcc:** correctly detect emitted TS helpers in ES5 ([#35191](https://github.com/angular/angular/issues/35191)) ([af4fe3a](https://github.com/angular/angular/commit/af4fe3a))
|
||||
* **ngcc:** correctly detect outer aliased class identifiers in ES5 ([#35527](https://github.com/angular/angular/issues/35527)) ([39bd9a7](https://github.com/angular/angular/commit/39bd9a7)), closes [#35399](https://github.com/angular/angular/issues/35399)
|
||||
* **ngcc:** handle imports in dts files when processing CommonJS ([#35191](https://github.com/angular/angular/issues/35191)) ([12e3db8](https://github.com/angular/angular/commit/12e3db8)), closes [#34356](https://github.com/angular/angular/issues/34356)
|
||||
* **router:** removed unused ApplicationRef dependency ([#35642](https://github.com/angular/angular/issues/35642)) ([2f140f5](https://github.com/angular/angular/commit/2f140f5)), closes [/github.com/angular/angular/commit/5a849829c42330d7e88e83e916e6e36380c97a97#diff-c0baae5e1df628e1a217e8dc38557](https://github.com//github.com/angular/angular/commit/5a849829c42330d7e88e83e916e6e36380c97a97/issues/diff-c0baae5e1df628e1a217e8dc38557)
|
||||
* **service-worker:** treat 503 as offline ([#35595](https://github.com/angular/angular/issues/35595)) ([64a415b](https://github.com/angular/angular/commit/64a415b)), closes [#35571](https://github.com/angular/angular/issues/35571)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ngcc:** implement source-map flattening ([#35132](https://github.com/angular/angular/issues/35132)) ([0a8e8cd](https://github.com/angular/angular/commit/0a8e8cd))
|
||||
* **zone.js** add an tickOptions parameter with property processNewMacroTasksSynchronously. ([#33838](https://github.com/angular/angular/issues/33838)) ([7d2ea93](https://github.com/angular/angular/commit/7d2ea93)), closes [#33799](https://github.com/angular/angular/issues/33799)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **core:** avoid recursive scope recalculation when TestBed.overrideModule is used ([#35454](https://github.com/angular/angular/issues/35454)) ([349539e](https://github.com/angular/angular/commit/349539e))
|
||||
* **core:** remove unused event argument in listener instructions ([#35097](https://github.com/angular/angular/issues/35097)) ([afc5b3e](https://github.com/angular/angular/commit/afc5b3e))
|
||||
|
||||
|
||||
|
||||
<a name="9.0.2"></a>
|
||||
## [9.0.2](https://github.com/angular/angular/compare/9.0.1...9.0.2) (2020-02-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** better handing of ICUs outside of i18n blocks ([#35347](https://github.com/angular/angular/issues/35347)) ([4fb5e21](https://github.com/angular/angular/commit/4fb5e21))
|
||||
* **core:** correctly concatenate static and dynamic binding to `class` when shadowed ([#35350](https://github.com/angular/angular/issues/35350)) ([8220363](https://github.com/angular/angular/commit/8220363)), closes [#35335](https://github.com/angular/angular/issues/35335)
|
||||
* **core:** remove support for `Map`/`Set` in `[class]`/`[style]` bindings ([#35392](https://github.com/angular/angular/issues/35392)) ([1797390](https://github.com/angular/angular/commit/1797390))
|
||||
* **ivy:** `LFrame` needs to release memory on `leaveView()` ([#35156](https://github.com/angular/angular/issues/35156)) ([4b1dcaf](https://github.com/angular/angular/commit/4b1dcaf)), closes [#35148](https://github.com/angular/angular/issues/35148)
|
||||
* **ivy:** add attributes and classes to host elements based on selector ([#34481](https://github.com/angular/angular/issues/34481)) ([03a8b16](https://github.com/angular/angular/commit/03a8b16))
|
||||
* **ivy:** error if directive with synthetic property binding is on same node as directive that injects ViewContainerRef ([#35343](https://github.com/angular/angular/issues/35343)) ([a30fd29](https://github.com/angular/angular/commit/a30fd29)), closes [#35342](https://github.com/angular/angular/issues/35342)
|
||||
* **ivy:** queries should match elements inside ng-container with the descendants: false option ([#35384](https://github.com/angular/angular/issues/35384)) ([fd4ce84](https://github.com/angular/angular/commit/fd4ce84)), closes [#34768](https://github.com/angular/angular/issues/34768)
|
||||
* **ivy:** wrong context passed to ngOnDestroy when resolved multiple times ([#35249](https://github.com/angular/angular/issues/35249)) ([0671e54](https://github.com/angular/angular/commit/0671e54)), closes [#35167](https://github.com/angular/angular/issues/35167)
|
||||
|
||||
|
||||
|
||||
<a name="9.0.1"></a>
|
||||
## [9.0.1](https://github.com/angular/angular/compare/9.0.0...9.0.1) (2020-02-12)
|
||||
|
||||
|
@ -232,8 +232,8 @@ There are currently a few exceptions to the "use package name" rule:
|
||||
* **changelog**: used for updating the release notes in CHANGELOG.md
|
||||
* **docs-infra**: used for docs-app (angular.io) related changes within the /aio directory of the
|
||||
repo
|
||||
* **ivy**: used for changes to the [Ivy renderer](https://github.com/angular/angular/issues/21706).
|
||||
* **ngcc**: used for changes to the [Angular Compatibility Compiler](./packages/compiler-cli/ngcc/README.md)
|
||||
* **ve**: used for changes specific to ViewEngine (legacy compiler/renderer).
|
||||
* none/empty string: useful for `style`, `test` and `refactor` changes that are done across all
|
||||
packages (e.g. `style: add missing semicolons`) and for docs changes that are not related to a
|
||||
specific package (e.g. `docs: fix typo in tutorial`).
|
||||
|
12
WORKSPACE
12
WORKSPACE
@ -8,8 +8,8 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
# Fetch rules_nodejs so we can install our npm dependencies
|
||||
http_archive(
|
||||
name = "build_bazel_rules_nodejs",
|
||||
sha256 = "6bcef105e75cac3c5f8212e0d0431b6ec1aaa1963e093b0091474ab98ecf29d2",
|
||||
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/1.2.2/rules_nodejs-1.2.2.tar.gz"],
|
||||
sha256 = "b6670f9f43faa66e3009488bbd909bc7bc46a5a9661a33f6bc578068d1837f37",
|
||||
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/1.3.0/rules_nodejs-1.3.0.tar.gz"],
|
||||
)
|
||||
|
||||
# Check the bazel version and download npm dependencies
|
||||
@ -18,6 +18,7 @@ load("@build_bazel_rules_nodejs//:index.bzl", "check_bazel_version", "check_rule
|
||||
# Bazel version must be at least the following version because:
|
||||
# - 0.26.0 managed_directories feature added which is required for nodejs rules 0.30.0
|
||||
# - 0.27.0 has a fix for managed_directories after `rm -rf node_modules`
|
||||
# - 2.1.0 feature added to honor .bazelignore in external repositories
|
||||
check_bazel_version(
|
||||
message = """
|
||||
You no longer need to install Bazel on your machine.
|
||||
@ -26,10 +27,10 @@ Try running `yarn bazel` instead.
|
||||
(If you did run that, check that you've got a fresh `yarn install`)
|
||||
|
||||
""",
|
||||
minimum_bazel_version = "2.0.0",
|
||||
minimum_bazel_version = "2.1.0",
|
||||
)
|
||||
|
||||
check_rules_nodejs_version(minimum_version_string = "1.2.2")
|
||||
check_rules_nodejs_version(minimum_version_string = "1.3.0")
|
||||
|
||||
# Setup the Node.js toolchain
|
||||
node_repositories(
|
||||
@ -42,8 +43,11 @@ node_repositories(
|
||||
package_json = ["//:package.json"],
|
||||
)
|
||||
|
||||
load("//integration:angular_integration_test.bzl", "npm_package_archives")
|
||||
|
||||
yarn_install(
|
||||
name = "npm",
|
||||
manual_build_file_contents = npm_package_archives(),
|
||||
package_json = "//:package.json",
|
||||
yarn_lock = "//:yarn.lock",
|
||||
)
|
||||
|
@ -4,9 +4,10 @@
|
||||
"cmd": "yarn",
|
||||
"args": [
|
||||
"e2e",
|
||||
"--protractor-config=e2e/protractor-puppeteer.conf.js",
|
||||
"--no-webdriver-update",
|
||||
"--port={PORT}"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,10 @@
|
||||
"cmd": "yarn",
|
||||
"args": [
|
||||
"e2e",
|
||||
"--protractor-config=e2e/protractor-puppeteer.conf.js",
|
||||
"--no-webdriver-update",
|
||||
"--port={PORT}"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -39,10 +39,10 @@ export class CartComponent implements OnInit {
|
||||
// #enddocregion props-services
|
||||
onSubmit(customerData) {
|
||||
// Process checkout data here
|
||||
console.warn('Your order has been submitted', customerData);
|
||||
|
||||
this.items = this.cartService.clearCart();
|
||||
this.checkoutForm.reset();
|
||||
|
||||
console.warn('Your order has been submitted', customerData);
|
||||
}
|
||||
// #docregion props-services, inject-form-builder, checkout-form, checkout-form-group
|
||||
}
|
||||
|
@ -39,8 +39,8 @@ export class ProductDetailsComponent implements OnInit {
|
||||
// #enddocregion props-methods, get-product
|
||||
// #docregion add-to-cart
|
||||
addToCart(product) {
|
||||
window.alert('Your product has been added to the cart!');
|
||||
this.cartService.addToCart(product);
|
||||
window.alert('Your product has been added to the cart!');
|
||||
}
|
||||
// #docregion props-methods, get-product, inject-cart-service
|
||||
}
|
||||
|
@ -28,9 +28,9 @@ describe('HeroesService', () => {
|
||||
|
||||
// Inject the http, test controller, and service-under-test
|
||||
// as they will be referenced by each test.
|
||||
httpClient = TestBed.get(HttpClient);
|
||||
httpTestingController = TestBed.get(HttpTestingController);
|
||||
heroService = TestBed.get(HeroesService);
|
||||
httpClient = TestBed.inject(HttpClient);
|
||||
httpTestingController = TestBed.inject(HttpTestingController);
|
||||
heroService = TestBed.inject(HeroesService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -44,7 +44,7 @@ describe('HeroesService', () => {
|
||||
let expectedHeroes: Hero[];
|
||||
|
||||
beforeEach(() => {
|
||||
heroService = TestBed.get(HeroesService);
|
||||
heroService = TestBed.inject(HeroesService);
|
||||
expectedHeroes = [
|
||||
{ id: 1, name: 'A' },
|
||||
{ id: 2, name: 'B' },
|
||||
|
@ -27,8 +27,8 @@ describe('HttpClient testing', () => {
|
||||
});
|
||||
|
||||
// Inject the http service and test controller for each test
|
||||
httpClient = TestBed.get(HttpClient);
|
||||
httpTestingController = TestBed.get(HttpTestingController);
|
||||
httpClient = TestBed.inject(HttpClient);
|
||||
httpTestingController = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
// #enddocregion setup
|
||||
// #docregion afterEach
|
||||
|
@ -5,9 +5,10 @@
|
||||
"cmd": "yarn",
|
||||
"args": [
|
||||
"e2e",
|
||||
"--protractor-config=e2e/protractor-puppeteer.conf.js",
|
||||
"--no-webdriver-update",
|
||||
"--port={PORT}"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthService } from '../auth.service';
|
||||
|
||||
@Component({
|
||||
@ -25,12 +25,12 @@ export class LoginComponent {
|
||||
this.authService.login().subscribe(() => {
|
||||
this.setMessage();
|
||||
if (this.authService.isLoggedIn) {
|
||||
// Get the redirect URL from our auth service
|
||||
// If no redirect has been set, use the default
|
||||
let redirect = this.authService.redirectUrl ? this.router.parseUrl(this.authService.redirectUrl) : '/admin';
|
||||
// Usually you would use the redirect URL from the auth service.
|
||||
// However to keep the example simple, we will always redirect to `/admin`.
|
||||
const redirectUrl = '/admin';
|
||||
|
||||
// Redirect the user
|
||||
this.router.navigateByUrl(redirect);
|
||||
this.router.navigate([redirectUrl]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
import { Router,
|
||||
NavigationExtras } from '@angular/router';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { Component } from '@angular/core';
|
||||
import { NavigationExtras, Router } from '@angular/router';
|
||||
import { AuthService } from '../auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
@ -26,9 +25,9 @@ export class LoginComponent {
|
||||
this.authService.login().subscribe(() => {
|
||||
this.setMessage();
|
||||
if (this.authService.isLoggedIn) {
|
||||
// Get the redirect URL from our auth service
|
||||
// If no redirect has been set, use the default
|
||||
let redirect = this.authService.redirectUrl ? this.router.parseUrl(this.authService.redirectUrl) : '/admin';
|
||||
// Usually you would use the redirect URL from the auth service.
|
||||
// However to keep the example simple, we will always redirect to `/admin`.
|
||||
const redirectUrl = '/admin';
|
||||
|
||||
// #docregion preserve
|
||||
// Set our navigation extras object
|
||||
@ -39,7 +38,7 @@ export class LoginComponent {
|
||||
};
|
||||
|
||||
// Redirect the user
|
||||
this.router.navigateByUrl(redirect, navigationExtras);
|
||||
this.router.navigate([redirectUrl], navigationExtras);
|
||||
// #enddocregion preserve
|
||||
}
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"projectType": "service-worker",
|
||||
"e2e": [
|
||||
{"cmd": "yarn", "args": ["e2e", "--no-webdriver-update", "--port={PORT}"]},
|
||||
{"cmd": "yarn", "args": ["e2e", "--protractor-config=e2e/protractor-puppeteer.conf.js", "--no-webdriver-update", "--port={PORT}"]},
|
||||
{"cmd": "yarn", "args": ["build", "--prod"]},
|
||||
{"cmd": "node", "args": ["--eval", "assert(fs.existsSync('./dist/ngsw.json'), 'ngsw.json is missing')"]},
|
||||
{"cmd": "node", "args": ["--eval", "assert(fs.existsSync('./dist/ngsw-worker.js'), 'ngsw-worker.js is missing')"]},
|
||||
|
@ -25,21 +25,21 @@ describe('Angular async helper', () => {
|
||||
async(() => { setTimeout(() => { actuallyDone = true; }, 0); }));
|
||||
|
||||
it('should run async test with task', async(() => {
|
||||
const id = setInterval(() => {
|
||||
actuallyDone = true;
|
||||
clearInterval(id);
|
||||
}, 100);
|
||||
}));
|
||||
const id = setInterval(() => {
|
||||
actuallyDone = true;
|
||||
clearInterval(id);
|
||||
}, 100);
|
||||
}));
|
||||
|
||||
it('should run async test with successful promise', async(() => {
|
||||
const p = new Promise(resolve => { setTimeout(resolve, 10); });
|
||||
p.then(() => { actuallyDone = true; });
|
||||
}));
|
||||
const p = new Promise(resolve => { setTimeout(resolve, 10); });
|
||||
p.then(() => { actuallyDone = true; });
|
||||
}));
|
||||
|
||||
it('should run async test with failed promise', async(() => {
|
||||
const p = new Promise((resolve, reject) => { setTimeout(reject, 10); });
|
||||
p.catch(() => { actuallyDone = true; });
|
||||
}));
|
||||
const p = new Promise((resolve, reject) => { setTimeout(reject, 10); });
|
||||
p.catch(() => { actuallyDone = true; });
|
||||
}));
|
||||
|
||||
// Use done. Can also use async or fakeAsync.
|
||||
it('should run async test with successful delayed Observable', (done: DoneFn) => {
|
||||
@ -48,56 +48,84 @@ describe('Angular async helper', () => {
|
||||
});
|
||||
|
||||
it('should run async test with successful delayed Observable', async(() => {
|
||||
const source = of (true).pipe(delay(10));
|
||||
source.subscribe(val => actuallyDone = true, err => fail(err));
|
||||
}));
|
||||
const source = of (true).pipe(delay(10));
|
||||
source.subscribe(val => actuallyDone = true, err => fail(err));
|
||||
}));
|
||||
|
||||
it('should run async test with successful delayed Observable', fakeAsync(() => {
|
||||
const source = of (true).pipe(delay(10));
|
||||
source.subscribe(val => actuallyDone = true, err => fail(err));
|
||||
const source = of (true).pipe(delay(10));
|
||||
source.subscribe(val => actuallyDone = true, err => fail(err));
|
||||
|
||||
tick(10);
|
||||
}));
|
||||
tick(10);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('fakeAsync', () => {
|
||||
// #docregion fake-async-test-tick
|
||||
it('should run timeout callback with delay after call tick with millis', fakeAsync(() => {
|
||||
let called = false;
|
||||
setTimeout(() => { called = true; }, 100);
|
||||
tick(100);
|
||||
expect(called).toBe(true);
|
||||
}));
|
||||
let called = false;
|
||||
setTimeout(() => { called = true; }, 100);
|
||||
tick(100);
|
||||
expect(called).toBe(true);
|
||||
}));
|
||||
// #enddocregion fake-async-test-tick
|
||||
|
||||
// #docregion fake-async-test-tick-new-macro-task-sync
|
||||
it('should run new macro task callback with delay after call tick with millis',
|
||||
fakeAsync(() => {
|
||||
function nestedTimer(cb: () => any): void { setTimeout(() => setTimeout(() => cb())); }
|
||||
const callback = jasmine.createSpy('callback');
|
||||
nestedTimer(callback);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
tick(0);
|
||||
// the nested timeout will also be triggered
|
||||
expect(callback).toHaveBeenCalled();
|
||||
}));
|
||||
// #enddocregion fake-async-test-tick-new-macro-task-sync
|
||||
|
||||
// #docregion fake-async-test-tick-new-macro-task-async
|
||||
it('should not run new macro task callback with delay after call tick with millis',
|
||||
fakeAsync(() => {
|
||||
function nestedTimer(cb: () => any): void { setTimeout(() => setTimeout(() => cb())); }
|
||||
const callback = jasmine.createSpy('callback');
|
||||
nestedTimer(callback);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
tick(0, {processNewMacroTasksSynchronously: false});
|
||||
// the nested timeout will not be triggered
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
tick(0);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
}));
|
||||
// #enddocregion fake-async-test-tick-new-macro-task-async
|
||||
|
||||
// #docregion fake-async-test-date
|
||||
it('should get Date diff correctly in fakeAsync', fakeAsync(() => {
|
||||
const start = Date.now();
|
||||
tick(100);
|
||||
const end = Date.now();
|
||||
expect(end - start).toBe(100);
|
||||
}));
|
||||
const start = Date.now();
|
||||
tick(100);
|
||||
const end = Date.now();
|
||||
expect(end - start).toBe(100);
|
||||
}));
|
||||
// #enddocregion fake-async-test-date
|
||||
|
||||
// #docregion fake-async-test-rxjs
|
||||
it('should get Date diff correctly in fakeAsync with rxjs scheduler', fakeAsync(() => {
|
||||
// need to add `import 'zone.js/dist/zone-patch-rxjs-fake-async'
|
||||
// to patch rxjs scheduler
|
||||
let result = null;
|
||||
of ('hello').pipe(delay(1000)).subscribe(v => { result = v; });
|
||||
expect(result).toBeNull();
|
||||
tick(1000);
|
||||
expect(result).toBe('hello');
|
||||
// need to add `import 'zone.js/dist/zone-patch-rxjs-fake-async'
|
||||
// to patch rxjs scheduler
|
||||
let result = null;
|
||||
of ('hello').pipe(delay(1000)).subscribe(v => { result = v; });
|
||||
expect(result).toBeNull();
|
||||
tick(1000);
|
||||
expect(result).toBe('hello');
|
||||
|
||||
const start = new Date().getTime();
|
||||
let dateDiff = 0;
|
||||
interval(1000).pipe(take(2)).subscribe(() => dateDiff = (new Date().getTime() - start));
|
||||
const start = new Date().getTime();
|
||||
let dateDiff = 0;
|
||||
interval(1000).pipe(take(2)).subscribe(() => dateDiff = (new Date().getTime() - start));
|
||||
|
||||
tick(1000);
|
||||
expect(dateDiff).toBe(1000);
|
||||
tick(1000);
|
||||
expect(dateDiff).toBe(2000);
|
||||
}));
|
||||
tick(1000);
|
||||
expect(dateDiff).toBe(1000);
|
||||
tick(1000);
|
||||
expect(dateDiff).toBe(2000);
|
||||
}));
|
||||
// #enddocregion fake-async-test-rxjs
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,21 @@
|
||||
// #docplaster
|
||||
// #docregion without-toBlob-macrotask
|
||||
import { TestBed, async, tick, fakeAsync } from '@angular/core/testing';
|
||||
import { CanvasComponent } from './canvas.component';
|
||||
|
||||
describe('CanvasComponent', () => {
|
||||
// #enddocregion without-toBlob-macrotask
|
||||
// #docregion enable-toBlob-macrotask
|
||||
beforeEach(() => {
|
||||
window['__zone_symbol__FakeAsyncTestMacroTask'] = [
|
||||
{
|
||||
source: 'HTMLCanvasElement.toBlob',
|
||||
callbackArgs: [{ size: 200 }],
|
||||
},
|
||||
];
|
||||
});
|
||||
// #enddocregion enable-toBlob-macrotask
|
||||
// #docregion without-toBlob-macrotask
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
@ -8,20 +23,16 @@ describe('CanvasComponent', () => {
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
window['__zone_symbol__FakeAsyncTestMacroTask'] = [
|
||||
{
|
||||
source: 'HTMLCanvasElement.toBlob',
|
||||
callbackArgs: [{ size: 200 }]
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
it('should be able to generate blob data from canvas', fakeAsync(() => {
|
||||
const fixture = TestBed.createComponent(CanvasComponent);
|
||||
const canvasComp = fixture.debugElement.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(canvasComp.blobSize).toBe(0);
|
||||
|
||||
tick();
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app.blobSize).toBeGreaterThan(0);
|
||||
expect(canvasComp.blobSize).toBeGreaterThan(0);
|
||||
}));
|
||||
});
|
||||
|
||||
// #enddocregion without-toBlob-macrotask
|
||||
|
@ -1,25 +1,32 @@
|
||||
// #docplaster
|
||||
// #docregion import-canvas-patch
|
||||
// Import patch to make async `HTMLCanvasElement` methods (such as `.toBlob()`) Zone.js-aware.
|
||||
// Either import in `polyfills.ts` (if used in more than one places in the app) or in the component
|
||||
// file using `HTMLCanvasElement` (if it is only used in a single file).
|
||||
import 'zone.js/dist/zone-patch-canvas';
|
||||
// #enddocregion import-canvas-patch
|
||||
// #docregion main
|
||||
import { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'sample-canvas',
|
||||
template: '<canvas #sampleCanvas width="200" height="200"></canvas>'
|
||||
template: '<canvas #sampleCanvas width="200" height="200"></canvas>',
|
||||
})
|
||||
export class CanvasComponent implements AfterViewInit {
|
||||
blobSize: number;
|
||||
blobSize = 0;
|
||||
@ViewChild('sampleCanvas') sampleCanvas: ElementRef;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngAfterViewInit() {
|
||||
const canvas = this.sampleCanvas.nativeElement;
|
||||
const canvas: HTMLCanvasElement = this.sampleCanvas.nativeElement;
|
||||
const context = canvas.getContext('2d');
|
||||
if (context) {
|
||||
context.clearRect(0, 0, 200, 200);
|
||||
context.fillStyle = '#FF1122';
|
||||
context.fillRect(0, 0, 200, 200);
|
||||
canvas.toBlob((blob: any) => {
|
||||
this.blobSize = blob.size;
|
||||
});
|
||||
}
|
||||
|
||||
context.clearRect(0, 0, 200, 200);
|
||||
context.fillStyle = '#FF1122';
|
||||
context.fillRect(0, 0, 200, 200);
|
||||
|
||||
canvas.toBlob(blob => {
|
||||
this.blobSize = blob.size;
|
||||
});
|
||||
}
|
||||
}
|
||||
// #enddocregion main
|
||||
|
@ -5,9 +5,9 @@ import { Observable } from 'rxjs';
|
||||
class DummyHeroesComponent {
|
||||
|
||||
heroes: Observable<Hero[]>;
|
||||
|
||||
// #docregion ctor
|
||||
constructor(private heroService: HeroService) {}
|
||||
|
||||
// #enddocregion ctor
|
||||
// #docregion getHeroes
|
||||
getHeroes(): void {
|
||||
// #docregion get-heroes
|
||||
|
@ -5,8 +5,8 @@ import { Component, OnInit } from '@angular/core';
|
||||
import { Hero } from '../hero';
|
||||
// #docregion hero-service-import
|
||||
import { HeroService } from '../hero.service';
|
||||
import { MessageService } from '../message.service';
|
||||
// #enddocregion hero-service-import
|
||||
import { MessageService } from '../message.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-heroes',
|
||||
|
@ -570,7 +570,7 @@ In the template type-checking phase, the Angular template compiler uses the Type
|
||||
Enable this phase explicitly by adding the compiler option `"fullTemplateTypeCheck"` in the `"angularCompilerOptions"` of the project's `tsconfig.json`
|
||||
(see [Angular Compiler Options](guide/angular-compiler-options)).
|
||||
|
||||
<div class="alert is-helpful>
|
||||
<div class="alert is-helpful">
|
||||
|
||||
In [Angular Ivy](guide/ivy), the template type checker has been completely rewritten to be more capable as well as stricter, meaning it can catch a variety of new errors that the previous type checker would not detect.
|
||||
|
||||
|
@ -11,6 +11,8 @@ about the features and tools that can help you develop and deliver Angular appli
|
||||
|
||||
## Application architecture
|
||||
|
||||
* The [Components and templates](guide/displaying-data) guide explains how to connect the application data in your [components](guide/glossary#component) to your page-display [templates](guide/glossary#template), to create a complete interactive application.
|
||||
|
||||
* The [NgModules](guide/ngmodules) guide provides in-depth information on the modular structure of an Angular application.
|
||||
|
||||
* The [Routing and navigation](guide/router) guide provides in-depth information on how to construct applications that allow a user to navigate to different [views](guide/glossary#view) within your single-page app.
|
||||
|
@ -80,6 +80,7 @@ In the table below, you can find a list of packages which implement deployment f
|
||||
| [Netlify](https://www.netlify.com/) | [`@netlify-builder/deploy`](https://npmjs.org/package/@netlify-builder/deploy) |
|
||||
| [GitHub pages](https://pages.github.com/) | [`angular-cli-ghpages`](https://npmjs.org/package/angular-cli-ghpages) |
|
||||
| [NPM](https://npmjs.com/) | [`ngx-deploy-npm`](https://npmjs.org/package/ngx-deploy-npm) |
|
||||
| [Amazon Cloud S3](https://aws.amazon.com/s3/?nc2=h_ql_prod_st_s3) | [`@jefiozie/ngx-aws-deploy`](https://www.npmjs.com/package/@jefiozie/ngx-aws-deploy) |
|
||||
|
||||
If you're deploying to a self-managed server or there's no builder for your favorite cloud platform, you can either create a builder that allows you to use the `ng deploy` command, or read through this guide to learn how to manually deploy your app.
|
||||
|
||||
|
@ -1,11 +1,20 @@
|
||||
# Displaying data
|
||||
# Displaying data in views
|
||||
|
||||
You can display data by binding controls in an HTML template to properties of an Angular component.
|
||||
Angular [components](guide/glossary#component) form the data structure of your application.
|
||||
The HTML [template](guide/glossary#template) associated with a component provides the means to display that data in the context of a web page.
|
||||
Together, a component's class and template form a [view](guide/glossary#view) of your application data.
|
||||
|
||||
In this page, you'll create a component with a list of heroes.
|
||||
You'll display the list of hero names and
|
||||
conditionally show a message below the list.
|
||||
The process of combining data values with their representation on the page is called [data binding](guide/glossary#data-binding).
|
||||
You display your data to a user (and collect data from the user) by *binding* controls in the HTML template to the data properties of the component class.
|
||||
|
||||
In addition, you can add logic to the template by including [directives](guide/glossary#directive), which tell Angular how to modify the page as it is rendered.
|
||||
|
||||
Angular defines a *template language* that expands HTML notation with syntax that allows you to define various kinds of data binding and logical directives.
|
||||
When the page is rendered, Angular interprets the template syntax to update the HTML according to your logic and current data state.
|
||||
Before you read the complete [template syntax guide](guide/template-syntax), the exercises on this page give you a quick demonstration of how template syntax works.
|
||||
|
||||
In this demo, you'll create a component with a list of heroes.
|
||||
You'll display the list of hero names and conditionally show a message below the list.
|
||||
The final UI looks like this:
|
||||
|
||||
<div class="lightbox">
|
||||
@ -14,20 +23,14 @@ The final UI looks like this:
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
|
||||
|
||||
The <live-example></live-example> demonstrates all of the syntax and code
|
||||
snippets described in this page.
|
||||
|
||||
The <live-example></live-example> demonstrates all of the syntax and code snippets described in this page.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{@a interpolation}
|
||||
|
||||
## Showing component properties with interpolation
|
||||
The easiest way to display a component property
|
||||
is to bind the property name through interpolation.
|
||||
The easiest way to display a component property is to bind the property name through interpolation.
|
||||
With interpolation, you put the property name in the view template, enclosed in double curly braces: `{{myHero}}`.
|
||||
|
||||
Use the CLI command [`ng new displaying-data`](cli/new) to create a workspace and app named `displaying-data`.
|
||||
@ -39,63 +42,43 @@ changing the template and the body of the component.
|
||||
|
||||
When you're done, it should look like this:
|
||||
|
||||
|
||||
<code-example path="displaying-data/src/app/app.component.1.ts" header="src/app/app.component.ts"></code-example>
|
||||
|
||||
|
||||
|
||||
You added two properties to the formerly empty component: `title` and `myHero`.
|
||||
|
||||
The template displays the two component properties using double curly brace
|
||||
interpolation:
|
||||
|
||||
|
||||
<code-example path="displaying-data/src/app/app.component.1.ts" header="src/app/app.component.ts (template)" region="template"></code-example>
|
||||
|
||||
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
|
||||
|
||||
The template is a multi-line string within ECMAScript 2015 backticks (<code>\`</code>).
|
||||
The backtick (<code>\`</code>)—which is *not* the same character as a single
|
||||
quote (`'`)—allows you to compose a string over several lines, which makes the
|
||||
HTML more readable.
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
Angular automatically pulls the value of the `title` and `myHero` properties from the component and
|
||||
inserts those values into the browser. Angular updates the display
|
||||
when these properties change.
|
||||
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
|
||||
|
||||
More precisely, the redisplay occurs after some kind of asynchronous event related to
|
||||
the view, such as a keystroke, a timer completion, or a response to an HTTP request.
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
Notice that you don't call **new** to create an instance of the `AppComponent` class.
|
||||
Angular is creating an instance for you. How?
|
||||
|
||||
The CSS `selector` in the `@Component` decorator specifies an element named `<app-root>`.
|
||||
That element is a placeholder in the body of your `index.html` file:
|
||||
|
||||
|
||||
<code-example path="displaying-data/src/index.html" header="src/index.html (body)" region="body"></code-example>
|
||||
|
||||
|
||||
|
||||
When you bootstrap with the `AppComponent` class (in <code>main.ts</code>), Angular looks for a `<app-root>`
|
||||
in the `index.html`, finds it, instantiates an instance of `AppComponent`, and renders it
|
||||
inside the `<app-root>` tag.
|
||||
@ -109,45 +92,44 @@ Now run the app. It should display the title and hero name:
|
||||
The next few sections review some of the coding choices in the app.
|
||||
|
||||
|
||||
## Template inline or template file?
|
||||
## Choosing the template source
|
||||
|
||||
The `@Component` metadata tells Angular where to find the component's template.
|
||||
You can store your component's template in one of two places.
|
||||
You can define it *inline* using the `template` property, or you can define
|
||||
the template in a separate HTML file and link to it in
|
||||
the component metadata using the `@Component` decorator's `templateUrl` property.
|
||||
|
||||
The choice between inline and separate HTML is a matter of taste,
|
||||
circumstances, and organization policy.
|
||||
Here the app uses inline HTML because the template is small and the demo
|
||||
is simpler without the additional HTML file.
|
||||
* You can define the template *inline* using the `template` property of the `@Component` decorator. An inline template is useful for a small demo or test.
|
||||
* Alternatively, you can define the template in a separate HTML file and link to that file in the `templateUrl` property of the `@Component` decorator. This configuration is typical for anything more complex than a small test or demo, and is the default when you generate a new component.
|
||||
|
||||
In either style, the template data bindings have the same access to the component's properties.
|
||||
Here the app uses inline HTML because the template is small and the demo is simpler without the additional HTML file.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
By default, the Angular CLI command [`ng generate component`](cli/generate) generates components with a template file. You can override that with:
|
||||
By default, the Angular CLI command [`ng generate component`](cli/generate) generates components with a template file.
|
||||
You can override that by adding the "-t" (short for `inlineTemplate=true`) option:
|
||||
|
||||
<code-example hideCopy language="sh" class="code-shell">
|
||||
ng generate component hero -it
|
||||
ng generate component hero -t
|
||||
</code-example>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## Constructor or variable initialization?
|
||||
|
||||
Although this example uses variable assignment to initialize the components, you could instead declare and initialize the properties using a constructor:
|
||||
## Initialization
|
||||
|
||||
The following example uses variable assignment to initialize the components.
|
||||
|
||||
<code-example path="displaying-data/src/app/app-ctor.component.1.ts" region="class"></code-example>
|
||||
|
||||
You could instead declare and initialize the properties using a constructor.
|
||||
This app uses more terse "variable assignment" style simply for brevity.
|
||||
|
||||
|
||||
This app uses more terse "variable assignment" style simply for brevity.
|
||||
|
||||
{@a ngFor}
|
||||
|
||||
## Showing an array property with ***ngFor**
|
||||
## Add logic to loop through data
|
||||
|
||||
The `*ngFor` directive (predefined by Angular) lets you loop through data. The following example uses the directive to show all of the values in an array property.
|
||||
|
||||
To display a list of heroes, begin by adding an array of hero names to the component and redefine `myHero` to be the first name in the array.
|
||||
|
||||
@ -155,15 +137,12 @@ To display a list of heroes, begin by adding an array of hero names to the compo
|
||||
<code-example path="displaying-data/src/app/app.component.2.ts" header="src/app/app.component.ts (class)" region="class"></code-example>
|
||||
|
||||
|
||||
|
||||
Now use the Angular `ngFor` directive in the template to display
|
||||
each item in the `heroes` list.
|
||||
Now use the Angular `ngFor` directive in the template to display each item in the `heroes` list.
|
||||
|
||||
|
||||
<code-example path="displaying-data/src/app/app.component.2.ts" header="src/app/app.component.ts (template)" region="template"></code-example>
|
||||
|
||||
|
||||
|
||||
This UI uses the HTML unordered list with `<ul>` and `<li>` tags. The `*ngFor`
|
||||
in the `<li>` element is the Angular "repeater" directive.
|
||||
It marks that `<li>` element (and its children) as the "repeater template":
|
||||
@ -171,20 +150,13 @@ It marks that `<li>` element (and its children) as the "repeater template":
|
||||
|
||||
<code-example path="displaying-data/src/app/app.component.2.ts" header="src/app/app.component.ts (li)" region="li"></code-example>
|
||||
|
||||
|
||||
|
||||
<div class="alert is-important">
|
||||
|
||||
|
||||
|
||||
Don't forget the leading asterisk (\*) in `*ngFor`. It is an essential part of the syntax.
|
||||
For more information, see the [Template Syntax](guide/template-syntax#ngFor) page.
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
Notice the `hero` in the `ngFor` double-quoted instruction;
|
||||
it is an example of a template input variable. Read
|
||||
more about template input variables in the [microsyntax](guide/template-syntax#microsyntax) section of
|
||||
@ -194,18 +166,13 @@ Angular duplicates the `<li>` for each item in the list, setting the `hero` vari
|
||||
to the item (the hero) in the current iteration. Angular uses that variable as the
|
||||
context for the interpolation in the double curly braces.
|
||||
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
|
||||
|
||||
In this case, `ngFor` is displaying an array, but `ngFor` can
|
||||
repeat items for any [iterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) object.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
Now the heroes appear in an unordered list.
|
||||
|
||||
<div class="lightbox">
|
||||
@ -228,13 +195,11 @@ of hero names into an array of `Hero` objects. For that you'll need a `Hero` cla
|
||||
ng generate class hero
|
||||
</code-example>
|
||||
|
||||
With the following code:
|
||||
This command creates the following code.
|
||||
|
||||
|
||||
<code-example path="displaying-data/src/app/hero.ts" header="src/app/hero.ts"></code-example>
|
||||
|
||||
|
||||
|
||||
You've defined a class with a constructor and two properties: `id` and `name`.
|
||||
|
||||
It might not look like the class has properties, but it does.
|
||||
@ -245,8 +210,6 @@ Consider the first parameter:
|
||||
|
||||
<code-example path="displaying-data/src/app/hero.ts" header="src/app/hero.ts (id)" region="id"></code-example>
|
||||
|
||||
|
||||
|
||||
That brief syntax does a lot:
|
||||
|
||||
* Declares a constructor parameter and its type.
|
||||
@ -254,7 +217,6 @@ That brief syntax does a lot:
|
||||
* Initializes that property with the corresponding argument when creating an instance of the class.
|
||||
|
||||
|
||||
|
||||
### Using the Hero class
|
||||
|
||||
After importing the `Hero` class, the `AppComponent.heroes` property can return a _typed_ array
|
||||
@ -273,7 +235,6 @@ Fix that to display only the hero's `name` property.
|
||||
<code-example path="displaying-data/src/app/app.component.3.ts" header="src/app/app.component.ts (template)" region="template"></code-example>
|
||||
|
||||
|
||||
|
||||
The display looks the same, but the code is clearer.
|
||||
|
||||
{@a ngIf}
|
||||
@ -291,46 +252,35 @@ To see it in action, add the following paragraph at the bottom of the template:
|
||||
<code-example path="displaying-data/src/app/app.component.ts" header="src/app/app.component.ts (message)" region="message"></code-example>
|
||||
|
||||
|
||||
|
||||
<div class="alert is-important">
|
||||
|
||||
|
||||
|
||||
Don't forget the leading asterisk (\*) in `*ngIf`. It is an essential part of the syntax.
|
||||
Read more about `ngIf` and `*` in the [ngIf section](guide/template-syntax#ngIf) of the [Template Syntax](guide/template-syntax) page.
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
The template expression inside the double quotes,
|
||||
`*ngIf="heroes.length > 3"`, looks and behaves much like TypeScript.
|
||||
When the component's list of heroes has more than three items, Angular adds the paragraph
|
||||
to the DOM and the message appears. If there are three or fewer items, Angular omits the
|
||||
paragraph, so no message appears. For more information,
|
||||
see the [template expressions](guide/template-syntax#template-expressions) section of the
|
||||
[Template Syntax](guide/template-syntax) page.
|
||||
to the DOM and the message appears.
|
||||
If there are three or fewer items, Angular omits the paragraph, so no message appears.
|
||||
|
||||
For more information, see [template expressions](guide/template-syntax#template-expressions).
|
||||
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
|
||||
|
||||
Angular isn't showing and hiding the message. It is adding and removing the paragraph element from the DOM. That improves performance, especially in larger projects when conditionally including or excluding
|
||||
big chunks of HTML with many data bindings.
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
Try it out. Because the array has four items, the message should appear.
|
||||
Go back into <code>app.component.ts</code> and delete or comment out one of the elements from the heroes array.
|
||||
The browser should refresh automatically and the message should disappear.
|
||||
|
||||
|
||||
|
||||
## Summary
|
||||
Now you know how to use:
|
||||
|
||||
@ -341,7 +291,6 @@ Now you know how to use:
|
||||
|
||||
Here's the final code:
|
||||
|
||||
|
||||
<code-tabs>
|
||||
|
||||
<code-pane header="src/app/app.component.ts" path="displaying-data/src/app/app.component.ts" region="final">
|
||||
|
@ -54,17 +54,13 @@ See [Keeping Up-to-Date](guide/updating "Updating your projects") for more infor
|
||||
{@a previews}
|
||||
### Preview releases
|
||||
|
||||
We let you preview what's coming by providing Beta releases and Release Candidates (`rc`) for each major and minor release:
|
||||
We let you preview what's coming by providing "Next" and Release Candidates (`rc`) pre-releases for each major and minor release:
|
||||
|
||||
<!--
|
||||
* **Next:** The release that is under active development. The next release is indicated by a release tag appended with the `next` identifier, such as `8.1.0-next.0`. For the next version of the documentation, see [next.angular.io](https://next.angular.io).
|
||||
-->
|
||||
* **Next:** The release that is under active development and testing. The next release is indicated by a release tag appended with the `-next` identifier, such as `8.1.0-next.0`.
|
||||
|
||||
* **Beta:** A release that is under active development and testing. A Beta release is indicated by a release tag appended with the `beta` identifier, such as `8.0.0-beta.0`.
|
||||
* **Release candidate:** A release that is feature complete and in final testing. A release candidate is indicated by a release tag appended with the `-rc` identifier, such as version `8.1.0-rc.0`.
|
||||
|
||||
* **Release candidate:** A release that is feature complete and in final testing. A release candidate is indicated by a release tag appended with the `rc` identifier, such as version `8.1.0-rc`.
|
||||
|
||||
The next version of the documentation is available at [next.angular.io](https://next.angular.io). This includes any documentation for Beta or Release Candidate features and APIs.
|
||||
The latest `next` or `rc` pre-release version of the documentation is available at [next.angular.io](https://next.angular.io).
|
||||
|
||||
|
||||
{@a frequency}
|
||||
@ -74,7 +70,7 @@ We work toward a regular schedule of releases, so that you can plan and coordina
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Disclaimer: Dates are offered as general guidance and will be adjusted by us when necessary to ensure delivery of a high-quality platform.
|
||||
Disclaimer: Dates are offered as general guidance and will be adjusted by us when necessary to ensure delivery of a high-quality platform.
|
||||
|
||||
</div>
|
||||
|
||||
@ -84,9 +80,9 @@ In general, you can expect the following release cycle:
|
||||
|
||||
* 1-3 minor releases for each major release
|
||||
|
||||
* A patch release almost every week
|
||||
* A patch release and pre-release (`next` or `rc`) build almost every week
|
||||
|
||||
This cadence of releases gives you access to new features as soon as they are ready, while maintaining the stability and reliability of the platform for production users.
|
||||
This cadence of releases gives eager developers access to new features as soon as they are fully developed and pass through our code review and integration testing processes, while maintaining the stability and reliability of the platform for production users that prefer to receive features after they have been validated by Google and other developers that use the pre-release builds.
|
||||
|
||||
|
||||
|
||||
|
@ -3302,7 +3302,13 @@ Although it doesn't actually log in, it has what you need for this discussion.
|
||||
It has an `isLoggedIn` flag to tell you whether the user is authenticated.
|
||||
Its `login` method simulates an API call to an external service by returning an
|
||||
observable that resolves successfully after a short pause.
|
||||
The `redirectUrl` property will store the attempted URL so you can navigate to it after authenticating.
|
||||
The `redirectUrl` property stores the URL that the user wanted to access so you can navigate to it after authentication.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
To keep things simple, this example redirects unauthenticated users to `/admin`.
|
||||
|
||||
</div>
|
||||
|
||||
Revise the `AuthGuard` to call it.
|
||||
|
||||
|
@ -117,7 +117,7 @@ You will see:
|
||||
<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/deployment "Getting Started: Deployment") section.
|
||||
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>
|
||||
|
||||
|
@ -9,7 +9,7 @@ For a sample app using the app-wide singleton service that this page describes,
|
||||
|
||||
There are two ways to make a service a singleton in Angular:
|
||||
|
||||
* Declare `root` for the value of the `@Injectable()` `providedIn` property
|
||||
* Set the `providedIn` property of the `@Injectable()` to `"root"`.
|
||||
* Include the service in the `AppModule` or in a module that is only imported by the `AppModule`
|
||||
|
||||
|
||||
|
@ -237,16 +237,15 @@ You're free to change anything, anywhere, during this turn of the event loop.
|
||||
|
||||
Like template expressions, template *statements* use a language that looks like JavaScript.
|
||||
The template statement parser differs from the template expression parser and
|
||||
specifically supports both basic assignment (`=`) and chaining expressions
|
||||
(with <code>;</code> or <code>,</code>).
|
||||
specifically supports both basic assignment (`=`) and chaining expressions with <code>;</code>.
|
||||
|
||||
However, certain JavaScript syntax is not allowed:
|
||||
However, certain JavaScript and template expression syntax is not allowed:
|
||||
|
||||
* <code>new</code>
|
||||
* increment and decrement operators, `++` and `--`
|
||||
* operator assignment, such as `+=` and `-=`
|
||||
* the bitwise operators `|` and `&`
|
||||
* the [template expression operators](guide/template-syntax#expression-operators)
|
||||
* the bitwise operators, such as `|` and `&`
|
||||
* the [pipe operator](guide/template-syntax#pipe)
|
||||
|
||||
### Statement context
|
||||
|
||||
@ -731,11 +730,11 @@ As you can see here, the `parentItem` in `AppComponent` is a string, which the `
|
||||
The previous simple example showed passing in a string. To pass in an object,
|
||||
the syntax and thinking are the same.
|
||||
|
||||
In this scenario, `ListItemComponent` is nested within `AppComponent` and the `item` property expects an object.
|
||||
In this scenario, `ListItemComponent` is nested within `AppComponent` and the `items` property expects an array of objects.
|
||||
|
||||
<code-example path="property-binding/src/app/app.component.html" region="pass-object" header="src/app/app.component.html"></code-example>
|
||||
|
||||
The `item` property is declared in the `ListItemComponent` with a type of `Item` and decorated with `@Input()`:
|
||||
The `items` property is declared in the `ListItemComponent` with a type of `Item` and decorated with `@Input()`:
|
||||
|
||||
<code-example path="property-binding/src/app/list-item/list-item.component.ts" region="item-input" header="src/app/list-item.component.ts"></code-example>
|
||||
|
||||
@ -748,7 +747,7 @@ specify a different item in `app.component.ts` so that the new item will render:
|
||||
|
||||
<code-example path="property-binding/src/app/app.component.ts" region="pass-object" header="src/app.component.ts"></code-example>
|
||||
|
||||
You just have to make sure, in this case, that you're supplying an object because that's the type of `item` and is what the nested component, `ListItemComponent`, expects.
|
||||
You just have to make sure, in this case, that you're supplying an array of objects because that's the type of `items` and is what the nested component, `ListItemComponent`, expects.
|
||||
|
||||
In this example, `AppComponent` specifies a different `item` object
|
||||
(`currentItem`) and passes it to the nested `ListItemComponent`. `ListItemComponent` was able to use `currentItem` because it matches what an `Item` object is according to `item.ts`. The `item.ts` file is where
|
||||
@ -1066,7 +1065,7 @@ However, using the above style binding syntax without `NgStyle` is preferred bec
|
||||
{@a styling-precedence}
|
||||
### Styling Precedence
|
||||
|
||||
A single HTML element can have its CSS class list and style values bound to a multiple sources (for example, host bindings from multiple directives).
|
||||
A single HTML element can have its CSS class list and style values bound to multiple sources (for example, host bindings from multiple directives).
|
||||
|
||||
When there are multiple bindings to the same class name or style property, Angular uses a set of precedence rules to resolve conflicts and determine which classes or styles are ultimately applied to the element.
|
||||
|
||||
|
@ -121,6 +121,7 @@ In case of a false positive like these, there are a few options:
|
||||
|`strictOutputEventTypes`|Whether `$event` will have the correct type for event bindings to component/directive an `@Output()`, or to animation events. If disabled, it will be `any`.|
|
||||
|`strictDomEventTypes`|Whether `$event` will have the correct type for event bindings to DOM events. If disabled, it will be `any`.|
|
||||
|`strictContextGenerics`|Whether the type parameters of generic components will be inferred correctly (including any generic bounds). If disabled, any type parameters will be `any`.|
|
||||
|`strictLiteralTypes`|Whether object and array literals declared in the template will have their type inferred. If disabled, the type of such literals will be `any`.|
|
||||
|
||||
|
||||
If you still have issues after troubleshooting with these flags, you can fall back to full mode by disabling `strictTemplates`.
|
||||
|
@ -899,8 +899,7 @@ In production, change detection kicks in automatically
|
||||
when Angular creates a component or the user enters a keystroke or
|
||||
an asynchronous activity (e.g., AJAX) completes.
|
||||
|
||||
The `TestBed.createComponent` does _not_ trigger change detection.
|
||||
a fact confirmed in the revised test:
|
||||
The `TestBed.createComponent` does _not_ trigger change detection; a fact confirmed in the revised test:
|
||||
|
||||
<code-example
|
||||
path="testing/src/app/banner/banner.component.spec.ts" region="test-w-o-detect-changes"></code-example>
|
||||
@ -1051,8 +1050,7 @@ attempt to reach an authentication server.
|
||||
These behaviors can be hard to intercept.
|
||||
It is far easier and safer to create and register a test double in place of the real `UserService`.
|
||||
|
||||
This particular test suite supplies a minimal mock of the `UserService` that satisfies the needs of the `WelcomeComponent`
|
||||
and its tests:
|
||||
This particular test suite supplies a minimal mock of the `UserService` that satisfies the needs of the `WelcomeComponent` and its tests:
|
||||
|
||||
<code-example
|
||||
path="testing/src/app/welcome/welcome.component.spec.ts"
|
||||
@ -1266,7 +1264,7 @@ You do have to call [tick()](api/core/testing/tick) to advance the (virtual) clo
|
||||
Calling [tick()](api/core/testing/tick) simulates the passage of time until all pending asynchronous activities finish.
|
||||
In this case, it waits for the error handler's `setTimeout()`.
|
||||
|
||||
The [tick()](api/core/testing/tick) function accepts milliseconds as a parameter (defaults to 0 if not provided). The parameter represents how much the virtual clock advances. For example, if you have a `setTimeout(fn, 100)` in a `fakeAsync()` test, you need to use tick(100) to trigger the fn callback.
|
||||
The [tick()](api/core/testing/tick) function accepts milliseconds and tickOptions as parameters, the millisecond (defaults to 0 if not provided) parameter represents how much the virtual clock advances. For example, if you have a `setTimeout(fn, 100)` in a `fakeAsync()` test, you need to use tick(100) to trigger the fn callback. The tickOptions is an optional parameter with a property called `processNewMacroTasksSynchronously` (defaults to true) represents whether to invoke new generated macro tasks when ticking.
|
||||
|
||||
<code-example
|
||||
path="testing/src/app/demo/async-helper.spec.ts"
|
||||
@ -1276,6 +1274,22 @@ The [tick()](api/core/testing/tick) function accepts milliseconds as a parameter
|
||||
The [tick()](api/core/testing/tick) function is one of the Angular testing utilities that you import with `TestBed`.
|
||||
It's a companion to `fakeAsync()` and you can only call it within a `fakeAsync()` body.
|
||||
|
||||
#### tickOptions
|
||||
|
||||
<code-example
|
||||
path="testing/src/app/demo/async-helper.spec.ts"
|
||||
region="fake-async-test-tick-new-macro-task-sync">
|
||||
</code-example>
|
||||
|
||||
In this example, we have a new macro task (nested setTimeout), by default, when we `tick`, the setTimeout `outside` and `nested` will both be triggered.
|
||||
|
||||
<code-example
|
||||
path="testing/src/app/demo/async-helper.spec.ts"
|
||||
region="fake-async-test-tick-new-macro-task-async">
|
||||
</code-example>
|
||||
|
||||
And in some case, we don't want to trigger the new maco task when ticking, we can use `tick(milliseconds, {processNewMacroTasksSynchronously: false})` to not invoke new maco task.
|
||||
|
||||
#### Comparing dates inside fakeAsync()
|
||||
|
||||
`fakeAsync()` simulates passage of time, which allows you to calculate the difference between dates inside `fakeAsync()`.
|
||||
@ -1314,51 +1328,46 @@ You can also use RxJS scheduler in `fakeAsync()` just like using `setTimeout()`
|
||||
|
||||
#### Support more macroTasks
|
||||
|
||||
By default `fakeAsync()` supports the following `macroTasks`.
|
||||
By default, `fakeAsync()` supports the following macro tasks.
|
||||
|
||||
- setTimeout
|
||||
- setInterval
|
||||
- requestAnimationFrame
|
||||
- webkitRequestAnimationFrame
|
||||
- mozRequestAnimationFrame
|
||||
- `setTimeout`
|
||||
- `setInterval`
|
||||
- `requestAnimationFrame`
|
||||
- `webkitRequestAnimationFrame`
|
||||
- `mozRequestAnimationFrame`
|
||||
|
||||
If you run other `macroTask` such as `HTMLCanvasElement.toBlob()`, `Unknown macroTask scheduled in fake async test` error will be thrown.
|
||||
If you run other macro tasks such as `HTMLCanvasElement.toBlob()`, an _"Unknown macroTask scheduled in fake async test"_ error will be thrown.
|
||||
|
||||
<code-tabs>
|
||||
<code-pane
|
||||
header="src/app/shared/canvas.component.spec.ts (failing)"
|
||||
path="testing/src/app/shared/canvas.component.spec.ts"
|
||||
header="src/app/shared/canvas.component.spec.ts">
|
||||
region="without-toBlob-macrotask">
|
||||
</code-pane>
|
||||
<code-pane
|
||||
header="src/app/shared/canvas.component.ts"
|
||||
path="testing/src/app/shared/canvas.component.ts"
|
||||
header="src/app/shared/canvas.component.ts">
|
||||
region="main">
|
||||
</code-pane>
|
||||
</code-tabs>
|
||||
|
||||
If you want to support such a case, you need to define the `macroTask` you want to support in `beforeEach()`.
|
||||
If you want to support such a case, you need to define the macro task you want to support in `beforeEach()`.
|
||||
For example:
|
||||
|
||||
```javascript
|
||||
beforeEach(() => {
|
||||
window['__zone_symbol__FakeAsyncTestMacroTask'] = [
|
||||
{
|
||||
source: 'HTMLCanvasElement.toBlob',
|
||||
callbackArgs: [{ size: 200 }]
|
||||
}
|
||||
];
|
||||
});
|
||||
<code-example
|
||||
header="src/app/shared/canvas.component.spec.ts (excerpt)"
|
||||
path="testing/src/app/shared/canvas.component.spec.ts"
|
||||
region="enable-toBlob-macrotask">
|
||||
</code-example>
|
||||
|
||||
Note that in order to make the `<canvas>` element Zone.js-aware in your app, you need to import the `zone-patch-canvas` patch (either in `polyfills.ts` or in the specific file that uses `<canvas>`):
|
||||
|
||||
<code-example
|
||||
header="src/polyfills.ts or src/app/shared/canvas.component.ts"
|
||||
path="testing/src/app/shared/canvas.component.ts"
|
||||
region="import-canvas-patch">
|
||||
</code-example>
|
||||
|
||||
it('toBlob should be able to run in fakeAsync', fakeAsync(() => {
|
||||
const canvas: HTMLCanvasElement = document.getElementById('canvas') as HTMLCanvasElement;
|
||||
let blob = null;
|
||||
canvas.toBlob(function(b) {
|
||||
blob = b;
|
||||
});
|
||||
tick();
|
||||
expect(blob.size).toBe(200);
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
#### Async observables
|
||||
|
||||
@ -3582,13 +3591,13 @@ The Angular `By` class has three static methods for common predicates:
|
||||
|
||||
<hr>
|
||||
|
||||
{@a faq}
|
||||
{@a useful-tips}
|
||||
|
||||
## Frequently Asked Questions
|
||||
## Useful tips
|
||||
|
||||
{@a q-spec-file-location}
|
||||
|
||||
#### Why put spec file next to the file it tests?
|
||||
#### Place your spec file next to the file it tests
|
||||
|
||||
It's a good idea to put unit test spec files in the same folder
|
||||
as the application source code files that they test:
|
||||
@ -3599,11 +3608,9 @@ as the application source code files that they test:
|
||||
- When you move the source (inevitable), you remember to move the test.
|
||||
- When you rename the source file (inevitable), you remember to rename the test file.
|
||||
|
||||
<hr>
|
||||
|
||||
{@a q-specs-in-test-folder}
|
||||
|
||||
#### When would I put specs in a test folder?
|
||||
#### Place your spec files in a test folder
|
||||
|
||||
Application integration specs can test the interactions of multiple parts
|
||||
spread across folders and modules.
|
||||
@ -3615,15 +3622,17 @@ It's often better to create an appropriate folder for them in the `tests` direct
|
||||
Of course specs that test the test helpers belong in the `test` folder,
|
||||
next to their corresponding helper files.
|
||||
|
||||
{@a q-e2e}
|
||||
{@a q-kiss}
|
||||
|
||||
#### Why not rely on E2E tests of DOM integration?
|
||||
#### Keep it simple
|
||||
|
||||
The component DOM tests described in this guide often require extensive setup and
|
||||
advanced techniques whereas the [unit tests](#component-class-testing)
|
||||
are comparatively simple.
|
||||
[Component class testing](#component-class-testing) should be kept very clean and simple.
|
||||
It should test only a single unit. On a first glance, you should be able to understand
|
||||
what the test is testing. If it's doing more, then it doesn't belong here.
|
||||
|
||||
#### Why not defer DOM integration tests to end-to-end (E2E) testing?
|
||||
{@a q-end-to-end}
|
||||
|
||||
#### Use E2E (end-to-end) to test more than a single unit
|
||||
|
||||
E2E tests are great for high-level validation of the entire system.
|
||||
But they can't give you the comprehensive test coverage that you'd expect from unit tests.
|
||||
|
@ -127,8 +127,7 @@ people who otherwise couldn't use the app at all.
|
||||
### Show the first page quickly
|
||||
|
||||
Displaying the first page quickly can be critical for user engagement.
|
||||
[53 percent of mobile site visits are abandoned](https://www.thinkwithgoogle.com/marketing-resources/data-measurement/mobile-page-speed-new-industry-benchmarks/)
|
||||
if pages take longer than 3 seconds to load.
|
||||
Pages that load faster perform better, [even with changes as small as 100ms](https://web.dev/shopping-for-speed-on-ebay/).
|
||||
Your app may have to launch faster to engage these users before they decide to do something else.
|
||||
|
||||
With Angular Universal, you can generate landing pages for the app that look like the complete app.
|
||||
|
@ -539,12 +539,14 @@ of multiple words. In Angular, you would bind these attributes using camelCase:
|
||||
|
||||
<code-example format="">
|
||||
[myHero]="hero"
|
||||
(heroDeleted)="handleHeroDeleted($event)"
|
||||
</code-example>
|
||||
|
||||
But when using them from AngularJS templates, you must use kebab-case:
|
||||
|
||||
<code-example format="">
|
||||
[my-hero]="hero"
|
||||
(hero-deleted)="handleHeroDeleted($event)"
|
||||
</code-example>
|
||||
|
||||
</div>
|
||||
@ -1162,11 +1164,19 @@ Begin by installing TypeScript to the project.
|
||||
</code-example>
|
||||
|
||||
Install type definitions for the existing libraries that
|
||||
you're using but that don't come with prepackaged types: AngularJS and the
|
||||
you're using but that don't come with prepackaged types: AngularJS, AngularJS Material, and the
|
||||
Jasmine unit test framework.
|
||||
|
||||
For the PhoneCat app, we can install the necessary type definitions by running the following command:
|
||||
|
||||
<code-example format="">
|
||||
npm install @types/jasmine @types/angular @types/angular-animate @types/angular-cookies @types/angular-mocks @types/angular-resource @types/angular-route @types/angular-sanitize --save-dev
|
||||
npm install @types/jasmine @types/angular @types/angular-animate @types/angular-aria @types/angular-cookies @types/angular-mocks @types/angular-resource @types/angular-route @types/angular-sanitize --save-dev
|
||||
</code-example>
|
||||
|
||||
If you are using AngularJS Material, you can install the type definitions via:
|
||||
|
||||
<code-example format="">
|
||||
npm install @types/angular-material --save-dev
|
||||
</code-example>
|
||||
|
||||
You should also configure the TypeScript compiler with a `tsconfig.json` in the project directory
|
||||
|
418
aio/content/guide/zone.md
Normal file
418
aio/content/guide/zone.md
Normal file
@ -0,0 +1,418 @@
|
||||
# NgZone
|
||||
|
||||
A zone is an execution context that persists across async tasks. You can think of it as [thread-local storage](http://en.wikipedia.org/wiki/Thread-local_storage) for JavaScript VMs.
|
||||
This guide describes how to use Angular's NgZone to automatically detect changes in the component to update HTML.
|
||||
|
||||
## Fundamentals of change detection
|
||||
|
||||
To understand the benefits of `NgZone`, it is important to have a clear grasp of what change detection is and how it works.
|
||||
|
||||
### Displaying and updating data in Angular
|
||||
|
||||
In Angular, you can [display data](guide/displaying-data) by binding controls in an HTML template to the properties of an Angular component.
|
||||
|
||||
<code-example path="displaying-data/src/app/app.component.1.ts" header="src/app/app.component.ts"></code-example>
|
||||
|
||||
In addition, you can bind DOM events to a method of an Angular component. In such methods, you can also update a property of the Angular component, which updates the corresponding data displayed in the template.
|
||||
|
||||
<code-example path="user-input/src/app/click-me.component.ts" region="click-me-component" header="src/app/click-me.component.ts"></code-example>
|
||||
|
||||
In both of the above examples, the component's code updates only the property of the component.
|
||||
However, the HTML is also updated automatically.
|
||||
This guide describes how and when Angular renders the HTML based on the data from the Angular component.
|
||||
|
||||
|
||||
### Detecting changes with plain JavaScript
|
||||
|
||||
To clarify how changes are detected and values updated, consider the following code written in plain JavaScript.
|
||||
|
||||
```javascript
|
||||
<html>
|
||||
<div id="dataDiv"></div>
|
||||
<button id="btn">updateData<btn>
|
||||
<canvas id="canvas"><canvas>
|
||||
<script>
|
||||
let value = 'initialValue';
|
||||
// initial rendering
|
||||
detectChange();
|
||||
|
||||
function renderHTML() {
|
||||
document.getElementById('dataDiv').innerText = value;
|
||||
}
|
||||
|
||||
function detectChange() {
|
||||
const currentValue = document.getElementById('dataDiv').innerText;
|
||||
if (currentValue !== value) {
|
||||
renderHTML();
|
||||
}
|
||||
}
|
||||
|
||||
// example 1: update data inside button click event handler
|
||||
document.getElementById('btn').addEventListener('click', () => {
|
||||
// update value
|
||||
value = 'button update value';
|
||||
// call detectChange manually
|
||||
detectChange();
|
||||
});
|
||||
|
||||
// example 2: Http Request
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.addEventListener('load', function() {
|
||||
// get response from server
|
||||
value = this.responseText;
|
||||
// call detectChange manually
|
||||
detectChange();
|
||||
});
|
||||
xhr.open('GET', serverUrl);
|
||||
xhr.send();
|
||||
|
||||
// example 3: setTimeout
|
||||
setTimeout(() => {
|
||||
// update value inside setTimeout callback
|
||||
value = 'timeout update value';
|
||||
// call detectChange manually
|
||||
detectChange();
|
||||
}, 100);
|
||||
|
||||
// example 4: Promise.then
|
||||
Promise.resolve('promise resolved a value').then((v) => {
|
||||
// update value inside Promise thenCallback
|
||||
value = v;
|
||||
// call detectChange manually
|
||||
detectChange();
|
||||
}, 100);
|
||||
|
||||
// example 5: some other asynchronous APIs
|
||||
document.getElementById('canvas').toBlob(blob => {
|
||||
// update value when blob data is created from the canvas
|
||||
value = `value updated by canvas, size is ${blog.size}`;
|
||||
// call detectChange manually
|
||||
detectChange();
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
```
|
||||
|
||||
After you update the data, you need to call `detectChange()` manually to check whether the data changed.
|
||||
If the data changed, you render the HTML to reflect the updated data.
|
||||
|
||||
In Angular, this step is unnecessary. Whenever you update the data, your HTML is updated automatically.
|
||||
|
||||
### When apps update HTML
|
||||
|
||||
To understand how change detection works, first consider when the application needs to update the HTML. Typically, updates occur for one of the following reasons:
|
||||
|
||||
1. Component initialization. For example, when bootstrapping an Angular application, Angular loads the bootstrap component and triggers the [ApplicationRef.tick()](api/core/ApplicationRef#tick) to call change detection and View Rendering. Just as in the [displaying data](guide/displaying-data) sample, the `AppComponent` is the bootstrap component. This component has the properties `title` and `myHero`, which the application renders in the HTML.
|
||||
|
||||
2. Event listener. The DOM event listener can update the data in an Angular component and also trigger change detection, as in the following example.
|
||||
|
||||
<code-example path="user-input/src/app/click-me.component.ts" region="click-me-component" header="src/app/click-me.component.ts"></code-example>
|
||||
|
||||
3. Http Data Request. You can also get data from a server through an Http request. For example:
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: '<div>{{data}}</div>';
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
data = 'initial value';
|
||||
constructor(private httpClient: HttpClient) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.httpClient.get(serverUrl).subscribe(response => {
|
||||
// user does not need to trigger change detection manually
|
||||
data = response.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. MacroTasks, such as `setTimeout()`/`setInterval()`. You can also update the data in the callback function of `macroTask` such as `setTimeout()`. For example:
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: '<div>{{data}}</div>';
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
data = 'initial value';
|
||||
|
||||
ngOnInit() {
|
||||
setTimeout(() => {
|
||||
// user does not need to trigger change detection manually
|
||||
data = 'value updated';
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. MicroTask, such as `Promise.then()`. Other asynchronous APIs return a Promise object (such as `fetch`), so the `then()` callback function can also update the data. For example:
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: '<div>{{data}}</div>';
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
data = 'initial value';
|
||||
|
||||
ngOnInit() {
|
||||
Promise.resolve(1).then(v => {
|
||||
// user does not need to trigger change detection manually
|
||||
data = v;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. Other async operations. In addition to `addEventListener()`/`setTimeout()`/`Promise.then()`, there are other operations that can update the data asynchronously. Some examples include `WebSocket.onmessage()` and `Canvas.toBlob()`.
|
||||
|
||||
The preceding list contains most common scenarios in which the application might change the data. Angular runs change detection whenever it detects that data could have changed.
|
||||
The result of change detection is that DOM is updated with new data. Angular detects the changes in different ways. For component initialization, Angular calls change detection explicitly. For [asynchronous operations](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous), Angular uses a Zone to detect changes in places where the data could have possibly mutated and it runs change detection automatically.
|
||||
|
||||
|
||||
## Zones and execution contexts
|
||||
|
||||
A zone provides an execution context that persists across async tasks. [Execution Context](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this) is an abstract concept that holds information about the environment within the current code being executed. Consider the following example.
|
||||
|
||||
```javascript
|
||||
const callback = function() {
|
||||
console.log('setTimeout callback context is', this);
|
||||
}
|
||||
|
||||
const ctx1 = {
|
||||
name: 'ctx1'
|
||||
};
|
||||
const ctx2 = {
|
||||
name: 'ctx2'
|
||||
};
|
||||
|
||||
const func = function() {
|
||||
console.log('caller context is', this);
|
||||
setTimeout(callback);
|
||||
}
|
||||
|
||||
func.apply(ctx1);
|
||||
func.apply(ctx2);
|
||||
```
|
||||
|
||||
The value of `this` in the callback of `setTimeout` might differ depending on when `setTimeout` is called.
|
||||
Thus you can lose the context in asynchronous operations.
|
||||
|
||||
A zone provides a new zone context other than `this`, the zone context persists across asynchronous operations.
|
||||
In the following example, the new zone context is called `zoneThis`.
|
||||
|
||||
```javascript
|
||||
zone.run(() => {
|
||||
// now you are in a zone
|
||||
expect(zoneThis).toBe(zone);
|
||||
setTimeout(function() {
|
||||
// the zoneThis context will be the same zone
|
||||
// when the setTimeout is scheduled
|
||||
expect(zoneThis).toBe(zone);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
This new context, `zoneThis`, can be retrieved from the `setTimeout()` callback function, and this context is the same when the `setTimeout()` is scheduled.
|
||||
To get the context, you can call [`Zone.current`](https://github.com/angular/angular/blob/master/packages/zone.js/lib/zone.ts).
|
||||
|
||||
### Zones and async lifecycle hooks
|
||||
|
||||
Zone.js can create contexts that persist across asynchronous operations as well as provide lifecycle hooks for asynchronous operations.
|
||||
|
||||
```javascript
|
||||
const zone = Zone.current.fork({
|
||||
name: 'zone',
|
||||
onScheduleTask: function(delegate, curr, target, task) {
|
||||
console.log('new task is scheduled: ', task.type, task.source);
|
||||
return delegate.scheduleTask(target, task);
|
||||
},
|
||||
onInvokeTask: function(delegate, curr, target, task, applyThis, applyArgs) {
|
||||
console.log('task will be invoked', task.type, task.source);
|
||||
return delegate.invokeTask(target, task, applyThis, applyArgs);
|
||||
},
|
||||
onHasTask: function(delegate, curr, target, hasTaskState) {
|
||||
console.log('task state changed in the zone', hasTaskState);
|
||||
return delegate.hasTask(target, hasTaskState);
|
||||
},
|
||||
onInvoke: function(delegate, curr, target, callback, applyThis, applyArgs) {
|
||||
console.log('the callback will be invoked', callback);
|
||||
return delegate.invoke(target, callback, applyThis, applyArgs);
|
||||
}
|
||||
});
|
||||
zone.run(() => {
|
||||
setTimeout(() => {
|
||||
console.log('timeout callback is invoked.');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
The above example creates a zone with several hooks.
|
||||
|
||||
`onXXXTask` hooks trigger when the status of Task changes.
|
||||
The Zone Task concept is very similar to the Javascript VM Task concept.
|
||||
- `macroTask`: such as `setTimeout()`.
|
||||
- `microTask`: such as `Promise.then()`.
|
||||
- `eventTask`: such as `element.addEventListener()`.
|
||||
|
||||
The `onInvoke` hook triggers when a synchronize function is executed in a Zone.
|
||||
|
||||
These hooks trigger under the following circumstances:
|
||||
|
||||
- `onScheduleTask`: triggers when a new asynchronous task is scheduled, such as when you call `setTimeout()`.
|
||||
- `onInvokeTask`: triggers when an asynchronous task is about to execute, such as when the callback of `setTimeout()` is about to execute.
|
||||
- `onHasTask`: triggers when the status of one kind of task inside a zone changes from stable to unstable or from unstable to stable. A status of stable means there are no tasks inside the Zone, while unstable means a new task is scheduled in the zone.
|
||||
- `onInvoke`: triggers when a synchronize function is going to execute in the zone.
|
||||
|
||||
With these hooks, `Zone` can monitor the status of all synchronize and asynchronous operations inside a zone.
|
||||
|
||||
The above example returns the following output.
|
||||
|
||||
```
|
||||
the callback will be invoked () => {
|
||||
setTimeout(() => {
|
||||
console.log('timeout callback is invoked.');
|
||||
});
|
||||
}
|
||||
new task is scheduled: macroTask setTimeout
|
||||
task state changed in the zone { microTask: false,
|
||||
macroTask: true,
|
||||
eventTask: false,
|
||||
change: 'macroTask' }
|
||||
task will be invoked macroTask setTimeout
|
||||
timeout callback is invoked.
|
||||
task state changed in the zone { microTask: false,
|
||||
macroTask: false,
|
||||
eventTask: false,
|
||||
change: 'macroTask' }
|
||||
```
|
||||
|
||||
All of the functions of Zone are provided by a library called [zone.js](https://github.com/angular/angular/tree/master/packages/zone.js/README.md).
|
||||
This library implements those features by intercepting asynchronous APIs through monkey patching.
|
||||
Monkey patching is a technique to add or modify the default behavior of a function at runtime without changing the source code.
|
||||
|
||||
## NgZone
|
||||
|
||||
While Zone.js can monitor all the states of synchronous and asynchronous operations, Angular additionally provides a service called NgZone.
|
||||
This service creates a zone named `angular` to automatically trigger change detection when the following conditions are satisfied:
|
||||
|
||||
1. When a sync or async function is executed.
|
||||
1. When there is no `microTask` scheduled.
|
||||
|
||||
### NgZone `run()`/`runOutsideOfAngular()`
|
||||
|
||||
`Zone` handles most asynchronous APIs such as `setTimeout()`, `Promise.then(),and `addEventListener()`.
|
||||
For the full list, see the [Zone Module document](https://github.com/angular/angular/blob/master/packages/zone.js/MODULE.md).
|
||||
Therefore in those asynchronous APIs, you don't need to trigger change detection manually.
|
||||
|
||||
There are still some third party APIs that Zone does not handle.
|
||||
In those cases, the NgZone service provides a [`run()`](api/core/NgZone#run) method that allows you to execute a function inside the angular zone.
|
||||
This function, and all asynchronous operations in that function, trigger change detection automatically at the correct time.
|
||||
|
||||
```typescript
|
||||
export class AppComponent implements OnInit {
|
||||
constructor(private ngZone: NgZone) {}
|
||||
ngOnInit() {
|
||||
// new async API is not handled by Zone, so you need to
|
||||
// use ngZone.run to make the asynchronous operation in angular zone
|
||||
// and trigger change detection automatically
|
||||
this.ngZone.run(() => {
|
||||
someNewAsyncAPI(() => {
|
||||
// update data of component
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By default, all asynchronous operations are inside the angular zone, which triggers change detection automatically.
|
||||
Another common case is when you don't want to trigger change detection.
|
||||
In that situation, you can use another NgZone method: [runOutsideAngular()](api/core/NgZone#runoutsideangular).
|
||||
|
||||
```typescript
|
||||
export class AppComponent implements OnInit {
|
||||
constructor(private ngZone: NgZone) {}
|
||||
ngOnInit() {
|
||||
// you know no data will be updated
|
||||
// you don't want to do change detection in this
|
||||
// specified operation, you can call runOutsideAngular
|
||||
this.ngZone.runOutsideAngular(() => {
|
||||
setTimeout(() => {
|
||||
// do something will not update component data
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Seting up Zone.js
|
||||
|
||||
To make Zone.js available in Angular, you need to import the zone.js package.
|
||||
If you are using the Angular CLI, this step is done automatically, and you will see the following line in the `src/polyfills.ts`:
|
||||
|
||||
```typescript
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
```
|
||||
|
||||
Before importing the `zone.js` package, you can set the following configurations:
|
||||
|
||||
- You can disable some asynchronous API monkey patching for better performance.
|
||||
For example, you can disable the `requestAnimationFrame()` monkey patch, so the callback of `requestAnimationFrame()` will not trigger change detection.
|
||||
This is useful if, in your application, the callback of the `requestAnimationFrame()` will not update any data.
|
||||
- You can specify that certain DOM events not run inside the angular zone; for example, to prevent a `mousemove` or `scroll` event to trigger change detection.
|
||||
|
||||
There are several other settings you can change.
|
||||
To make these changes, you need to create a `zone-flags.ts` file, such as the following.
|
||||
|
||||
```typescript
|
||||
(window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
(window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
```
|
||||
|
||||
Next, import `zone-flags` before you import `zone` in the `polyfills.ts`.
|
||||
|
||||
```typescript
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular.
|
||||
*/
|
||||
import `./zone-flags`;
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
```
|
||||
|
||||
For more information of what you can configure, see the [zone.js](https://github.com/angular/angular/tree/master/packages/zone.js) documentation.
|
||||
|
||||
### NoopZone
|
||||
|
||||
`Zone` helps Angular know when to trigger change detection and let the developers focus on the application development.
|
||||
By default, `Zone` is loaded and works without additional configuration. However, you don't have to use `Zone` to make Angular work, instead opting to trigger change detection on your own.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
<h4>Disabling <code>Zone</code></h4>
|
||||
|
||||
**If you disable `Zone`, you will need to trigger all change detection at the correct timing yourself, which requires comprehensive knowledge of change detection**.
|
||||
|
||||
</div>
|
||||
|
||||
To remove `zone.js`, make the following changes.
|
||||
|
||||
1. Remove the `zone.js` import from `polyfills.ts`.
|
||||
|
||||
```typescript
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
// import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
```
|
||||
|
||||
2. Bootstrap Angular with `noop zone` in `src/main.ts`.
|
||||
|
||||
```typescript
|
||||
platformBrowserDynamic().bootstrapModule(AppModule, {ngZone: 'noop'})
|
||||
.catch(err => console.error(err));
|
||||
```
|
@ -67,22 +67,22 @@
|
||||
"tooltip": "Introduction to Angular's component model, template syntax, and component communication."
|
||||
},
|
||||
{
|
||||
"url": "start/routing",
|
||||
"url": "start/start-routing",
|
||||
"title": "Routing",
|
||||
"tooltip": "Introduction to routing between components using the browser's URL."
|
||||
},
|
||||
{
|
||||
"url": "start/data",
|
||||
"url": "start/start-data",
|
||||
"title": "Managing Data",
|
||||
"tooltip": "Introduction to services and accessing external data via HTTP."
|
||||
},
|
||||
{
|
||||
"url": "start/forms",
|
||||
"url": "start/start-forms",
|
||||
"title": "Forms",
|
||||
"tooltip": "Learn about fetching and managing data from users with forms."
|
||||
},
|
||||
{
|
||||
"url": "start/deployment",
|
||||
"url": "start/start-deployment",
|
||||
"title": "Deployment",
|
||||
"tooltip": "Move to local development, or deploy your application to Firebase or your own server."
|
||||
}
|
||||
@ -439,6 +439,11 @@
|
||||
"tooltip": "Animate route transitions."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "guide/zone",
|
||||
"title": "NgZone",
|
||||
"tooltip": "How NgZone works"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -92,7 +92,7 @@ To help you get going, the following steps use predefined product data from the
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
`*ngFor` is a "structural directive". Structural directives shape or reshape the DOM's structure, typically by adding, removing, and manipulating the elements to which they are attached. Any directive with an asterisk, `*`, is a structural directive.
|
||||
`*ngFor` is a "structural directive". Structural directives shape or reshape the DOM's structure, typically by adding, removing, and manipulating the elements to which they are attached. Directives with an asterisk, `*`, are structural directives.
|
||||
|
||||
</div>
|
||||
|
||||
@ -358,5 +358,5 @@ You've learned about the foundation of Angular: components and template syntax.
|
||||
You've also learned how the component class and template interact, and how components communicate with each other.
|
||||
|
||||
To continue exploring Angular, choose either of the following options:
|
||||
* [Continue to the "Routing" section](start/routing "Getting Started: Routing") to create a product details page that can be accessed by clicking a product name and that has its own URL pattern.
|
||||
* [Skip ahead to the "Deployment" section](start/deployment "Getting Started: Deployment") to move to local development, or deploy your app to Firebase or your own server.
|
||||
* [Continue to the "Routing" section](start/start-routing "Getting Started: Routing") to create a product details page that can be accessed by clicking a product name and that has its own URL pattern.
|
||||
* [Skip ahead to the "Deployment" section](start/start-deployment "Getting Started: Deployment") to move to local development, or deploy your app to Firebase or your own server.
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Managing Data
|
||||
# Getting Started with Angular: Managing Data
|
||||
|
||||
At the end of [Routing](start/routing "Getting Started: Routing"), the online store application has a product catalog with two views: a product list and product details.
|
||||
At the end of [Routing](start/start-routing "Getting Started: Routing"), the online store application has a product catalog with two views: a product list and product details.
|
||||
Users can click on a product name from the list to see details in a new view, with a distinct URL, or route.
|
||||
|
||||
This page guides you through creating the shopping cart in three phases:
|
||||
@ -29,7 +29,7 @@ about products in the cart.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Later, the [Forms](start/forms "Getting Started: Forms") part of
|
||||
Later, the [Forms](start/start-forms "Getting Started: Forms") part of
|
||||
this tutorial guides you through accessing this cart service
|
||||
from the page where the user checks out.
|
||||
|
||||
@ -234,7 +234,7 @@ This section shows you how to use the HTTP client to retrieve shipping prices fr
|
||||
|
||||
### Predefined shipping data
|
||||
|
||||
The app StackBlitz generates for this guide comes with predefined shipping data in `assets/shipping.json`.
|
||||
The application that StackBlitz generates for this guide comes with predefined shipping data in `assets/shipping.json`.
|
||||
Use this data to add shipping prices for items in the cart.
|
||||
|
||||
<code-example header="src/assets/shipping.json" path="getting-started/src/assets/shipping.json">
|
||||
@ -314,7 +314,7 @@ Now that your app can retrieve shipping data, create a shipping component and t
|
||||
|
||||
There's no link to the new shipping component yet, but you can see its template in the preview pane by entering the URL its route specifies. The URL has the pattern: `https://getting-started.stackblitz.io/shipping` where the `getting-started.stackblitz.io` part may be different for your StackBlitz project.
|
||||
|
||||
1. Modify the shipping component so it uses the cart service to retrieve shipping data via HTTP from the `shipping.json` file.
|
||||
1. Modify the shipping component so that it uses the cart service to retrieve shipping data via HTTP from the `shipping.json` file.
|
||||
|
||||
1. Import the cart service.
|
||||
|
||||
@ -362,5 +362,5 @@ Now that your app can retrieve shipping data, create a shipping component and t
|
||||
Congratulations! You have an online store application with a product catalog and shopping cart. You can also look up and display shipping prices.
|
||||
|
||||
To continue exploring Angular, choose either of the following options:
|
||||
* [Continue to the "Forms" section](start/forms "Getting Started: Forms") to finish the app by adding the shopping cart page and a checkout form.
|
||||
* [Skip ahead to the "Deployment" section](start/deployment "Getting Started: Deployment") to move to local development, or deploy your app to Firebase or your own server.
|
||||
* [Continue to the "Forms" section](start/start-forms "Getting Started: Forms") to finish the app by adding the shopping cart page and a checkout form.
|
||||
* [Skip ahead to the "Deployment" section](start/start-deployment "Getting Started: Deployment") to move to local development, or deploy your app to Firebase or your own server.
|
@ -1,4 +1,4 @@
|
||||
# Deployment
|
||||
# Getting Started with Angular: Deployment
|
||||
|
||||
|
||||
To deploy your application, you have to compile it, and then host the JavaScript, CSS, and HTML on a web server. Built Angular applications are very portable and can live in any environment or served by any technology, such as Node, Java, .NET, PHP, and many others.
|
||||
@ -6,13 +6,11 @@ To deploy your application, you have to compile it, and then host the JavaScript
|
||||
<div class="alert is-helpful">
|
||||
|
||||
|
||||
Whether you came here directly from [Your First App](start "Getting Started: Your First App"), or completed the entire online store application through the [Routing](start/routing "Getting Started: Routing"), [Managing Data](start/data "Getting Started: Managing Data"), and [Forms](start/forms "Getting Started: Forms") sections, you have an application that you can deploy by following the instructions in this section.
|
||||
Whether you came here directly from [Your First App](start "Getting Started: Your First App"), or completed the entire online store application through the [Routing](start/start-routing "Getting Started: Routing"), [Managing Data](start/start-data "Getting Started: Managing Data"), and [Forms](start/start-forms "Getting Started: Forms") sections, you have an application that you can deploy by following the instructions in this section.
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
## Share your application
|
||||
|
||||
StackBlitz projects are public by default, allowing you to share your Angular app via the project URL. Keep in mind that this is a great way to share ideas and prototypes, but it is not intended for production hosting.
|
||||
@ -24,9 +22,9 @@ StackBlitz projects are public by default, allowing you to share your Angular ap
|
||||
|
||||
## Building locally
|
||||
|
||||
To build your application locally or for production, you will need to download the source code from your StackBlitz project. Click the `Download Project` icon in the left menu across from `Project` to download your files.
|
||||
To build your application locally or for production, download the source code from your StackBlitz project by clicking the `Download Project` icon in the left menu across from `Project` to download your files.
|
||||
|
||||
Once you have the source code downloaded and unzipped, use the [Angular Console](https://angularconsole.com "Angular Console web site") to serve the application, or you install `Node.js` and have the Angular CLI installed.
|
||||
Once you have the source code downloaded and unzipped, use the [Angular Console](https://angularconsole.com "Angular Console web site") to serve the application, or install `Node.js` and serve your app with the Angular CLI.
|
||||
|
||||
From the terminal, install the Angular CLI globally with:
|
||||
|
||||
@ -34,7 +32,7 @@ From the terminal, install the Angular CLI globally with:
|
||||
npm install -g @angular/cli
|
||||
```
|
||||
|
||||
This will install the command `ng` into your system, which is the command you use to create new workspaces, new projects, serve your application during development, or produce builds that can be shared or distributed.
|
||||
This installs the command `ng` on your system, which is the command you use to create new workspaces, new projects, serve your application during development, or produce builds to share or distribute.
|
||||
|
||||
Create a new Angular CLI workspace using the [`ng new`](cli/new "CLI ng new command reference") command:
|
||||
|
||||
@ -42,7 +40,7 @@ Create a new Angular CLI workspace using the [`ng new`](cli/new "CLI ng new comm
|
||||
ng new my-project-name
|
||||
```
|
||||
|
||||
From there you replace the `/src` folder with the one from your `StackBlitz` download, and then perform a build.
|
||||
In your new CLI generated app, replace the `/src` folder with the one from your `StackBlitz` download, and then perform a build.
|
||||
|
||||
```sh
|
||||
ng build --prod
|
||||
@ -58,7 +56,7 @@ If the above `ng build` command throws an error about missing packages, append t
|
||||
|
||||
#### Hosting the built project
|
||||
|
||||
The files in the `dist/my-project-name` folder are static and can be hosted on any web server capable of serving files (`Node.js`, Java, .NET) or any backend (Firebase, Google Cloud, App Engine, others).
|
||||
The files in the `dist/my-project-name` folder are static. This means you can host them on any web server capable of serving files (such as `Node.js`, Java, .NET), or any backend (such as Firebase, Google Cloud, or App Engine).
|
||||
|
||||
### Hosting an Angular app on Firebase
|
||||
|
||||
@ -66,33 +64,33 @@ One of the easiest ways to get your site live is to host it using Firebase.
|
||||
|
||||
1. Sign up for a firebase account on [Firebase](https://firebase.google.com/ "Firebase web site").
|
||||
1. Create a new project, giving it any name you like.
|
||||
1. Install the `firebase-tools` CLI that will handle your deployment using `npm install -g firebase-tools`.
|
||||
1. Add the `@angular/fire` schematics that will handle your deployment using `ng add @angular/fire`.
|
||||
1. Connect your CLI to your Firebase account and initialize the connection to your project using `firebase login` and `firebase init`.
|
||||
1. Follow the prompts to select the `Firebase` project you are creating for hosting.
|
||||
- Select the `Hosting` option on the first prompt.
|
||||
- Select the project you previously created on Firebase.
|
||||
- Select `dist/my-project-name` as the public directory.
|
||||
1. Deploy your application with `firebase deploy`, because the command `firebase init` has created a `firebase.json` file that tells Firebase how to serve your app.
|
||||
- Select the `Hosting` option on the first prompt.
|
||||
- Select the project you previously created on Firebase.
|
||||
- Select `dist/my-project-name` as the public directory.
|
||||
1. Deploy your application with `ng deploy`.
|
||||
1. Once deployed, visit https://your-firebase-project-name.firebaseapp.com to see it live!
|
||||
|
||||
### Hosting an Angular app anywhere else
|
||||
|
||||
To host an Angular app on another web host, you'll need to upload or send the files to the host.
|
||||
Because you are building a Single Page Application, you'll also need to make sure you redirect any invalid URLs to your `index.html` file.
|
||||
Learn more about development and distribution of your application in the [Building & Serving](guide/build "Building and Serving Angular Apps") and [Deployment](guide/deployment "Deployment guide") guides.
|
||||
To host an Angular app on another web host, upload or send the files to the host.
|
||||
Because you are building a single page application, you'll also need to make sure you redirect any invalid URLs to your `index.html` file.
|
||||
Read more about development and distribution of your application in the [Building & Serving](guide/build "Building and Serving Angular Apps") and [Deployment](guide/deployment "Deployment guide") guides.
|
||||
|
||||
## Join our community
|
||||
## Join the Angular community
|
||||
|
||||
You are now an Angular developer! [Share this moment](https://twitter.com/intent/tweet?url=https://angular.io/start&text=I%20just%20finished%20the%20Angular%20Getting%20Started%20Tutorial "Angular on Twitter"), tell us what you thought of this Getting Started, or submit [suggestions for future editions](https://github.com/angular/angular/issues/new/choose "Angular GitHub repository new issue form").
|
||||
You are now an Angular developer! [Share this moment](https://twitter.com/intent/tweet?url=https://angular.io/start&text=I%20just%20finished%20the%20Angular%20Getting%20Started%20Tutorial "Angular on Twitter"), tell us what you thought of this Getting Started, or submit [suggestions for future editions](https://github.com/angular/angular/issues/new/choose "Angular GitHub repository new issue form").
|
||||
|
||||
Angular offers many more capabilities, and you now have a foundation that empowers you to build an application and explore those other capabilities:
|
||||
|
||||
* Angular provides advanced capabilities for mobile apps, animation, internationalization, server-side rendering, and more.
|
||||
* [Angular Material](https://material.angular.io/ "Angular Material web site") offers an extensive library of Material Design components.
|
||||
* [Angular Protractor](https://protractor.angular.io/ "Angular Protractor web site") offers an end-to-end testing framework for Angular apps.
|
||||
* Angular also has an extensive [network of 3rd-party tools and libraries](https://angular.io/resources "Angular resources list").
|
||||
* Angular provides advanced capabilities for mobile apps, animation, internationalization, server-side rendering, and more.
|
||||
* [Angular Material](https://material.angular.io/ "Angular Material web site") offers an extensive library of Material Design components.
|
||||
* [Angular Protractor](https://protractor.angular.io/ "Angular Protractor web site") offers an end-to-end testing framework for Angular apps.
|
||||
* Angular also has an extensive [network of 3rd-party tools and libraries](https://angular.io/resources "Angular resources list").
|
||||
|
||||
Keep current by following the [Angular blog](https://blog.angular.io/ "Angular blog").
|
||||
Keep current by following the [Angular blog](https://blog.angular.io/ "Angular blog").
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Forms
|
||||
# Getting Started with Angular: Forms
|
||||
|
||||
At the end of [Managing Data](start/data "Getting Started: Managing Data"), the online store application has a product catalog and a shopping cart.
|
||||
At the end of [Managing Data](start/start-data "Getting Started: Managing Data"), the online store application has a product catalog and a shopping cart.
|
||||
|
||||
This section walks you through adding a form-based checkout feature to collect user information as part of checkout.
|
||||
|
||||
@ -40,7 +40,7 @@ of the constructor.
|
||||
|
||||
1. For the checkout process, users need to submit their name and address. When they submit their order, the form should reset and the cart should clear.
|
||||
|
||||
1. In `cart.component.ts`, define an `onSubmit()` method to process the form. Use the `CartService` `clearCart()` method to empty the cart items and reset the form after it is submission. In a real-world app, this method would also submit the data to an external server. The entire cart component class is as follows:
|
||||
1. In `cart.component.ts`, define an `onSubmit()` method to process the form. Use the `CartService` `clearCart()` method to empty the cart items and reset the form after its submission. In a real-world app, this method would also submit the data to an external server. The entire cart component class is as follows:
|
||||
|
||||
<code-example header="src/app/cart/cart.component.ts" path="getting-started/src/app/cart/cart.component.ts">
|
||||
</code-example>
|
||||
@ -82,4 +82,4 @@ To confirm submission, open the console where you should see an object containin
|
||||
|
||||
Congratulations! You have a complete online store application with a product catalog, a shopping cart, and a checkout function.
|
||||
|
||||
[Continue to the "Deployment" section](start/deployment "Getting Started: Deployment") to move to local development, or deploy your app to Firebase or your own server.
|
||||
[Continue to the "Deployment" section](start/start-deployment "Getting Started: Deployment") to move to local development, or deploy your app to Firebase or your own server.
|
@ -1,4 +1,4 @@
|
||||
# Routing
|
||||
# Getting Started with Angular: Routing
|
||||
|
||||
At the end of [Your First App](start "Getting Started: Your First App"), the online store application has a basic product catalog.
|
||||
The app doesn't have any variable states or navigation.
|
||||
@ -73,7 +73,7 @@ The product details component handles the display of each product. The Angular R
|
||||
The `ActivatedRoute` is specific to each routed component that the Angular Router loads. It contains information about the
|
||||
route, its parameters, and additional data associated with the route.
|
||||
|
||||
By injecting the `ActivatedRoute`, you are configuring the component to use a service. While this part of the Getting Started tutorial uses this syntax briefly, the [Managing Data](start/data "Getting Started: Managing Data") page covers services in more detail.
|
||||
By injecting the `ActivatedRoute`, you are configuring the component to use a service. While this part of the Getting Started tutorial uses this syntax briefly, the [Managing Data](start/start-data "Getting Started: Managing Data") page covers services in more detail.
|
||||
|
||||
|
||||
1. In the `ngOnInit()` method, subscribe to route parameters and fetch the product based on the `productId`.
|
||||
@ -111,5 +111,5 @@ Congratulations! You have integrated routing into your online store.
|
||||
* Users can click on a product name from the list to see details in a new view, with a distinct URL/route.
|
||||
|
||||
To continue exploring Angular, choose either of the following options:
|
||||
* [Continue to the "Managing Data" section](start/data "Getting Started: Managing Data") to add a shopping cart feature, use a service to manage the cart data and use HTTP to retrieve external data for shipping prices.
|
||||
* [Skip ahead to the Deployment section](start/deployment "Getting Started: Deployment") to deploy your app to Firebase or move to local development.
|
||||
* [Continue to the "Managing Data" section](start/start-data "Getting Started: Managing Data") to add a shopping cart feature, use a service to manage the cart data and use HTTP to retrieve external data for shipping prices.
|
||||
* [Skip ahead to the Deployment section](start/start-deployment "Getting Started: Deployment") to deploy your app to Firebase or move to local development.
|
@ -122,7 +122,7 @@ Replace the definition of the `heroes` property with a simple declaration.
|
||||
|
||||
Add a private `heroService` parameter of type `HeroService` to the constructor.
|
||||
|
||||
<code-example path="toh-pt4/src/app/heroes/heroes.component.ts" header="src/app/heroes/heroes.component.ts" region="ctor">
|
||||
<code-example path="toh-pt4/src/app/heroes/heroes.component.1.ts" header="src/app/heroes/heroes.component.ts" region="ctor">
|
||||
</code-example>
|
||||
|
||||
The parameter simultaneously defines a private `heroService` property and identifies it as a `HeroService` injection site.
|
||||
|
@ -30,6 +30,12 @@
|
||||
{"type": 301, "source": "/getting-started", "destination": "/start"},
|
||||
{"type": 301, "source": "/getting-started/:rest*", "destination": "/start/:rest*"},
|
||||
|
||||
// Renaming of Getting Started topics
|
||||
{"type": 301, "source": "/start/data", "destination": "/start/start-data"},
|
||||
{"type": 301, "source": "/start/deployment", "destination": "/start/start-deployment"},
|
||||
{"type": 301, "source": "/start/forms", "destination": "/start/start-forms"},
|
||||
{"type": 301, "source": "/start/routing", "destination": "/start/start-routing"},
|
||||
|
||||
// some top level guide pages on old site were moved below the guide folder
|
||||
{"type": 301, "source": "/styleguide", "destination": "/guide/styleguide"},
|
||||
{"type": 301, "source": "/docs/styleguide", "destination": "/guide/styleguide"},
|
||||
|
@ -14,6 +14,9 @@ module.exports = function (config) {
|
||||
require('@angular-devkit/build-angular/plugins/karma'),
|
||||
{'reporter:jasmine-seed': ['type', JasmineSeedReporter]},
|
||||
],
|
||||
proxies: {
|
||||
'/dummy/image': 'src/assets/images/logos/angular/angular.png',
|
||||
},
|
||||
client: {
|
||||
clearContext: false, // leave Jasmine Spec Runner output visible in browser
|
||||
jasmine: {
|
||||
@ -35,10 +38,10 @@ module.exports = function (config) {
|
||||
ChromeHeadlessNoSandbox: {
|
||||
base: 'ChromeHeadless',
|
||||
// See /integration/README.md#browser-tests for more info on these args
|
||||
flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio']
|
||||
}
|
||||
flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio'],
|
||||
},
|
||||
},
|
||||
browsers: [process.env['CI'] ? 'ChromeHeadlessNoSandbox' : 'Chrome'],
|
||||
browsers: ['ChromeHeadlessNoSandbox'],
|
||||
browserNoActivityTimeout: 60000,
|
||||
singleRun: false,
|
||||
restartOnFileChange: true,
|
||||
|
@ -131,6 +131,18 @@
|
||||
"!/news",
|
||||
"!/news.html",
|
||||
"!/news/",
|
||||
"!/start/data",
|
||||
"!/start/data/",
|
||||
"!/start/data.html",
|
||||
"!/start/deployment",
|
||||
"!/start/deployment/",
|
||||
"!/start/deployment.html",
|
||||
"!/start/forms",
|
||||
"!/start/forms/",
|
||||
"!/start/forms.html",
|
||||
"!/start/routing",
|
||||
"!/start/routing/",
|
||||
"!/start/routing.html",
|
||||
"!/styleguide",
|
||||
"!/styleguide/**",
|
||||
"!/testing",
|
||||
|
@ -87,28 +87,28 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "9.0.0-rc.11",
|
||||
"@angular/cdk": "9.0.0-rc.8",
|
||||
"@angular/common": "9.0.0-rc.11",
|
||||
"@angular/compiler": "9.0.0-rc.11",
|
||||
"@angular/core": "9.0.0-rc.11",
|
||||
"@angular/elements": "9.0.0-rc.11",
|
||||
"@angular/forms": "9.0.0-rc.11",
|
||||
"@angular/material": "9.0.0-rc.8",
|
||||
"@angular/platform-browser": "9.0.0-rc.11",
|
||||
"@angular/platform-browser-dynamic": "9.0.0-rc.11",
|
||||
"@angular/router": "9.0.0-rc.11",
|
||||
"@angular/service-worker": "9.0.0-rc.11",
|
||||
"@webcomponents/custom-elements": "^1.2.0",
|
||||
"@angular/animations": "9.0.0",
|
||||
"@angular/cdk": "^9.0.0",
|
||||
"@angular/common": "9.0.0",
|
||||
"@angular/compiler": "9.0.0",
|
||||
"@angular/core": "9.0.0",
|
||||
"@angular/elements": "9.0.0",
|
||||
"@angular/forms": "9.0.0",
|
||||
"@angular/material": "^9.0.0",
|
||||
"@angular/platform-browser": "9.0.0",
|
||||
"@angular/platform-browser-dynamic": "9.0.0",
|
||||
"@angular/router": "9.0.0",
|
||||
"@angular/service-worker": "9.0.0",
|
||||
"@webcomponents/custom-elements": "1.2.1",
|
||||
"rxjs": "^6.5.3",
|
||||
"tslib": "^1.10.0",
|
||||
"zone.js": "~0.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "0.900.0-rc.14",
|
||||
"@angular/cli": "9.0.0-rc.14",
|
||||
"@angular/compiler-cli": "9.0.0-rc.11",
|
||||
"@angular/language-service": "9.0.0-rc.11",
|
||||
"@angular-devkit/build-angular": "0.900.1",
|
||||
"@angular/cli": "9.0.1",
|
||||
"@angular/compiler-cli": "9.0.0",
|
||||
"@angular/language-service": "9.0.0",
|
||||
"@types/jasmine": "^3.4.2",
|
||||
"@types/jasminewd2": "^2.0.8",
|
||||
"@types/lunr": "^2.3.2",
|
||||
@ -123,7 +123,7 @@
|
||||
"cross-spawn": "^5.1.0",
|
||||
"css-selector-parser": "^1.3.0",
|
||||
"dgeni": "^0.4.11",
|
||||
"dgeni-packages": "^0.27.5",
|
||||
"dgeni-packages": "^0.28.3",
|
||||
"entities": "^1.1.1",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-plugin-jasmine": "^2.2.0",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 2987,
|
||||
"main-es2015": 455897,
|
||||
"main-es2015": 450612,
|
||||
"polyfills-es2015": 52195
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@
|
||||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 2987,
|
||||
"main-es2015": 448928,
|
||||
"main-es2015": 451469,
|
||||
"polyfills-es2015": 52195
|
||||
}
|
||||
}
|
||||
@ -21,7 +21,7 @@
|
||||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 3097,
|
||||
"main-es2015": 426513,
|
||||
"main-es2015": 429230,
|
||||
"polyfills-es2015": 52195
|
||||
}
|
||||
}
|
||||
|
@ -27,10 +27,10 @@
|
||||
*/
|
||||
|
||||
// Imports
|
||||
const puppeteer = require('puppeteer');
|
||||
const lighthouse = require('lighthouse');
|
||||
const printer = require('lighthouse/lighthouse-cli/printer');
|
||||
const logger = require('lighthouse-logger');
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
// Constants
|
||||
const AUDIT_CATEGORIES = ['accessibility', 'best-practices', 'performance', 'pwa', 'seo'];
|
||||
@ -83,10 +83,8 @@ function formatScore(score) {
|
||||
}
|
||||
|
||||
async function launchChromeAndRunLighthouse(url, flags, config) {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--remote-debugging-port=9222']});
|
||||
flags.port = browser.port;
|
||||
const browser = await puppeteer.launch();
|
||||
flags.port = (new URL(browser.wsEndpoint())).port;
|
||||
|
||||
try {
|
||||
return await lighthouse(url, flags, config);
|
||||
|
@ -22,7 +22,7 @@
|
||||
<img *ngSwitchCase="true" src="assets/images/logos/angular/logo-nav@2x.png" width="150" height="40" title="Home" alt="Home">
|
||||
<img *ngSwitchDefault src="assets/images/logos/angular/shield-large.svg" width="37" height="40" title="Home" alt="Home">
|
||||
</a>
|
||||
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes"></aio-top-menu>
|
||||
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes" [currentNode]="currentNodes?.TopBar"></aio-top-menu>
|
||||
<aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event)"></aio-search-box>
|
||||
<div class="toolbar-external-icons-container">
|
||||
<a href="https://twitter.com/angular" title="Twitter" aria-label="Angular on twitter">
|
||||
|
@ -1,35 +1,32 @@
|
||||
import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core';
|
||||
import { inject, ComponentFixture, TestBed, fakeAsync, flushMicrotasks, tick } from '@angular/core/testing';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, fakeAsync, flushMicrotasks, inject, TestBed, tick } from '@angular/core/testing';
|
||||
import { MatProgressBar } from '@angular/material/progress-bar';
|
||||
import { MatSidenav } from '@angular/material/sidenav';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { Subject, of, timer } from 'rxjs';
|
||||
import { first, mapTo } from 'rxjs/operators';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { AppModule } from './app.module';
|
||||
import { CurrentNodes } from 'app/navigation/navigation.model';
|
||||
import { By, Title } from '@angular/platform-browser';
|
||||
import { ElementsLoader } from 'app/custom-elements/elements-loader';
|
||||
import { DocumentService } from 'app/documents/document.service';
|
||||
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
||||
import { CurrentNodes } from 'app/navigation/navigation.model';
|
||||
import { NavigationNode, NavigationService } from 'app/navigation/navigation.service';
|
||||
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
|
||||
import { SearchService } from 'app/search/search.service';
|
||||
import { Deployment } from 'app/shared/deployment.service';
|
||||
import { ElementsLoader } from 'app/custom-elements/elements-loader';
|
||||
import { GaService } from 'app/shared/ga.service';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { ScrollService } from 'app/shared/scroll.service';
|
||||
import { SearchResultsComponent } from 'app/shared/search-results/search-results.component';
|
||||
import { SelectComponent } from 'app/shared/select/select.component';
|
||||
import { TocItem, TocService } from 'app/shared/toc.service';
|
||||
import { of, Subject, timer } from 'rxjs';
|
||||
import { first, mapTo } from 'rxjs/operators';
|
||||
import { MockLocationService } from 'testing/location.service';
|
||||
import { MockLogger } from 'testing/logger.service';
|
||||
import { MockSearchService } from 'testing/search.service';
|
||||
import { NavigationNode, NavigationService } from 'app/navigation/navigation.service';
|
||||
import { ScrollService } from 'app/shared/scroll.service';
|
||||
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
|
||||
import { SearchResultsComponent } from 'app/shared/search-results/search-results.component';
|
||||
import { SearchService } from 'app/search/search.service';
|
||||
import { SelectComponent } from 'app/shared/select/select.component';
|
||||
import { TocItem, TocService } from 'app/shared/toc.service';
|
||||
import { AppComponent } from './app.component';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
const sideBySideBreakPoint = 992;
|
||||
const hideToCBreakPoint = 800;
|
||||
@ -405,10 +402,12 @@ describe('AppComponent', () => {
|
||||
// Older docs versions have an href
|
||||
it('should navigate when change to a version with a url', async () => {
|
||||
await setupSelectorForTesting();
|
||||
locationService.urlSubject.next('new-page?id=1#section-1');
|
||||
const versionWithUrlIndex = component.docVersions.findIndex(v => !!v.url);
|
||||
const versionWithUrl = component.docVersions[versionWithUrlIndex];
|
||||
const versionWithUrlAndPage = `${versionWithUrl.url}new-page?id=1#section-1`;
|
||||
selectElement.triggerEventHandler('change', { option: versionWithUrl, index: versionWithUrlIndex});
|
||||
expect(locationService.go).toHaveBeenCalledWith(versionWithUrl.url);
|
||||
expect(locationService.go).toHaveBeenCalledWith(versionWithUrlAndPage);
|
||||
});
|
||||
|
||||
it('should not navigate when change to a version without a url', async () => {
|
||||
@ -418,6 +417,15 @@ describe('AppComponent', () => {
|
||||
selectElement.triggerEventHandler('change', { option: versionWithoutUrl, index: versionWithoutUrlIndex });
|
||||
expect(locationService.go).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate when change to a version with a url that does not end with `/`', async () => {
|
||||
await setupSelectorForTesting();
|
||||
locationService.urlSubject.next('docs#section-1');
|
||||
const versionWithoutSlashIndex = component.docVersions.length;
|
||||
const versionWithoutSlashUrl = component.docVersions[versionWithoutSlashIndex] = { url: 'https://next.angular.io', title: 'foo' };
|
||||
selectElement.triggerEventHandler('change', { option: versionWithoutSlashUrl, index: versionWithoutSlashIndex });
|
||||
expect(locationService.go).toHaveBeenCalledWith('https://next.angular.io/docs#section-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('currentDocument', () => {
|
||||
@ -793,7 +801,7 @@ describe('AppComponent', () => {
|
||||
const searchService = TestBed.inject(SearchService) as Partial<SearchService> as MockSearchService;
|
||||
|
||||
const results = [
|
||||
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '', deprecated: false }
|
||||
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '', deprecated: false, topics: '' }
|
||||
];
|
||||
|
||||
searchService.searchResults.next({ query: 'something', results });
|
||||
|
@ -1,18 +1,15 @@
|
||||
import { Component, ElementRef, HostBinding, HostListener, OnInit,
|
||||
QueryList, ViewChild, ViewChildren } from '@angular/core';
|
||||
import { Component, ElementRef, HostBinding, HostListener, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
|
||||
import { MatSidenav } from '@angular/material/sidenav';
|
||||
|
||||
import { CurrentNodes, NavigationService, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
|
||||
import { DocumentService, DocumentContents } from 'app/documents/document.service';
|
||||
import { DocumentContents, DocumentService } from 'app/documents/document.service';
|
||||
import { NotificationComponent } from 'app/layout/notification/notification.component';
|
||||
import { CurrentNodes, NavigationNode, NavigationService, VersionInfo } from 'app/navigation/navigation.service';
|
||||
import { SearchResults } from 'app/search/interfaces';
|
||||
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
|
||||
import { SearchService } from 'app/search/search.service';
|
||||
import { Deployment } from 'app/shared/deployment.service';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { NotificationComponent } from 'app/layout/notification/notification.component';
|
||||
import { ScrollService } from 'app/shared/scroll.service';
|
||||
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
|
||||
import { SearchResults } from 'app/search/interfaces';
|
||||
import { SearchService } from 'app/search/search.service';
|
||||
import { TocService } from 'app/shared/toc.service';
|
||||
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
|
||||
@ -77,6 +74,8 @@ export class AppComponent implements OnInit {
|
||||
|
||||
versionInfo: VersionInfo;
|
||||
|
||||
private currentUrl: string;
|
||||
|
||||
get isOpened() { return this.isSideBySide && this.isSideNavDoc; }
|
||||
get mode() { return this.isSideBySide ? 'side' : 'over'; }
|
||||
|
||||
@ -148,27 +147,27 @@ export class AppComponent implements OnInit {
|
||||
this.navigationService.versionInfo,
|
||||
this.navigationService.navigationViews.pipe(map(views => views['docVersions'])),
|
||||
]).subscribe(([versionInfo, versions]) => {
|
||||
// TODO(pbd): consider whether we can lookup the stable and next versions from the internet
|
||||
const computedVersions: NavigationNode[] = [
|
||||
{ title: 'next', url: 'https://next.angular.io' },
|
||||
{ title: 'stable', url: 'https://angular.io' },
|
||||
];
|
||||
if (this.deployment.mode === 'archive') {
|
||||
computedVersions.push({ title: `v${versionInfo.major}` });
|
||||
}
|
||||
this.docVersions = [...computedVersions, ...versions];
|
||||
// TODO(pbd): consider whether we can lookup the stable and next versions from the internet
|
||||
const computedVersions: NavigationNode[] = [
|
||||
{ title: 'next', url: 'https://next.angular.io/' },
|
||||
{ title: 'stable', url: 'https://angular.io/' },
|
||||
];
|
||||
if (this.deployment.mode === 'archive') {
|
||||
computedVersions.push({ title: `v${versionInfo.major}` });
|
||||
}
|
||||
this.docVersions = [...computedVersions, ...versions];
|
||||
|
||||
// Find the current version - eithers title matches the current deployment mode
|
||||
// or its title matches the major version of the current version info
|
||||
this.currentDocVersion = this.docVersions.find(version =>
|
||||
version.title === this.deployment.mode || version.title === `v${versionInfo.major}`)!;
|
||||
this.currentDocVersion.title += ` (v${versionInfo.raw})`;
|
||||
});
|
||||
// Find the current version - eithers title matches the current deployment mode
|
||||
// or its title matches the major version of the current version info
|
||||
this.currentDocVersion = this.docVersions.find(version =>
|
||||
version.title === this.deployment.mode || version.title === `v${versionInfo.major}`)!;
|
||||
this.currentDocVersion.title += ` (v${versionInfo.raw})`;
|
||||
});
|
||||
|
||||
this.navigationService.navigationViews.subscribe(views => {
|
||||
this.footerNodes = views['Footer'] || [];
|
||||
this.footerNodes = views['Footer'] || [];
|
||||
this.sideNavNodes = views['SideNav'] || [];
|
||||
this.topMenuNodes = views['TopBar'] || [];
|
||||
this.topMenuNodes = views['TopBar'] || [];
|
||||
this.topMenuNarrowNodes = views['TopBarNarrow'] || this.topMenuNodes;
|
||||
});
|
||||
|
||||
@ -188,6 +187,8 @@ export class AppComponent implements OnInit {
|
||||
this.navigationService.currentNodes, // ...needed to determine `sidenav` state
|
||||
]).pipe(first())
|
||||
.subscribe(() => this.updateShell());
|
||||
|
||||
this.locationService.currentUrl.subscribe(url => this.currentUrl = url);
|
||||
}
|
||||
|
||||
onDocReady() {
|
||||
@ -231,7 +232,8 @@ export class AppComponent implements OnInit {
|
||||
onDocVersionChange(versionIndex: number) {
|
||||
const version = this.docVersions[versionIndex];
|
||||
if (version.url) {
|
||||
this.locationService.go(version.url);
|
||||
const versionUrl = version.url + (!version.url.endsWith('/') ? '/' : '');
|
||||
this.locationService.go(`${versionUrl}${this.currentUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -263,7 +265,7 @@ export class AppComponent implements OnInit {
|
||||
}
|
||||
|
||||
// Deal with anchor clicks; climb DOM tree until anchor found (or null)
|
||||
let target: HTMLElement|null = eventTarget;
|
||||
let target: HTMLElement | null = eventTarget;
|
||||
while (target && !(target instanceof HTMLAnchorElement)) {
|
||||
target = target.parentElement;
|
||||
}
|
||||
@ -406,7 +408,7 @@ export class AppComponent implements OnInit {
|
||||
if (key === '/' || keyCode === 191) {
|
||||
this.focusSearchBox();
|
||||
}
|
||||
if (key === 'Escape' || keyCode === 27 ) {
|
||||
if (key === 'Escape' || keyCode === 27) {
|
||||
// escape key
|
||||
if (this.showSearchResults) {
|
||||
this.hideSearchResults();
|
||||
|
@ -88,7 +88,7 @@ describe('AnnouncementBarComponent', () => {
|
||||
describe('rendering', () => {
|
||||
beforeEach(() => {
|
||||
component.announcement = {
|
||||
imageUrl: 'link/to/image',
|
||||
imageUrl: 'dummy/image',
|
||||
linkUrl: 'link/to/website',
|
||||
message: 'this is an <b>important</b> message',
|
||||
endDate: '2018-03-01',
|
||||
@ -102,7 +102,7 @@ describe('AnnouncementBarComponent', () => {
|
||||
});
|
||||
|
||||
it('should display an image', () => {
|
||||
expect(element.querySelector('img')!.src).toContain('link/to/image');
|
||||
expect(element.querySelector('img')!.src).toContain('dummy/image');
|
||||
});
|
||||
|
||||
it('should display a link', () => {
|
||||
|
@ -111,6 +111,22 @@ describe('NotificationComponent', () => {
|
||||
expect(getItemSpy).toHaveBeenCalledWith('aio-notification/survey-january-2018');
|
||||
expect(component.showNotification).toBe('hide');
|
||||
});
|
||||
|
||||
it('should not break when cookies are disabled in the browser', () => {
|
||||
configTestingModule();
|
||||
|
||||
// Simulate `window.localStorage` being inaccessible, when cookies are disabled.
|
||||
const mockWindow: MockWindow = TestBed.inject(WindowToken);
|
||||
Object.defineProperty(mockWindow, 'localStorage', {
|
||||
get() { throw new Error('The operation is insecure'); },
|
||||
});
|
||||
|
||||
expect(() => createComponent()).not.toThrow();
|
||||
expect(component.showNotification).toBe('show');
|
||||
|
||||
component.dismiss();
|
||||
expect(component.showNotification).toBe('hide');
|
||||
});
|
||||
});
|
||||
|
||||
@Component({
|
||||
|
@ -20,7 +20,7 @@ const LOCAL_STORAGE_NAMESPACE = 'aio-notification/';
|
||||
]
|
||||
})
|
||||
export class NotificationComponent implements OnInit {
|
||||
private get localStorage() { return this.window.localStorage; }
|
||||
private storage: Storage;
|
||||
|
||||
@Input() dismissOnContentClick: boolean;
|
||||
@Input() notificationId: string;
|
||||
@ -31,12 +31,27 @@ export class NotificationComponent implements OnInit {
|
||||
showNotification: 'show'|'hide';
|
||||
|
||||
constructor(
|
||||
@Inject(WindowToken) private window: Window,
|
||||
@Inject(WindowToken) window: Window,
|
||||
@Inject(CurrentDateToken) private currentDate: Date
|
||||
) {}
|
||||
) {
|
||||
try {
|
||||
this.storage = window.localStorage;
|
||||
} catch {
|
||||
// When cookies are disabled in the browser, even trying to access
|
||||
// `window.localStorage` throws an error. Use a no-op storage.
|
||||
this.storage = {
|
||||
length: 0,
|
||||
clear: () => undefined,
|
||||
getItem: () => null,
|
||||
key: () => null,
|
||||
removeItem: () => undefined,
|
||||
setItem: () => undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const previouslyHidden = this.localStorage.getItem(LOCAL_STORAGE_NAMESPACE + this.notificationId) === 'hide';
|
||||
const previouslyHidden = this.storage.getItem(LOCAL_STORAGE_NAMESPACE + this.notificationId) === 'hide';
|
||||
const expired = this.currentDate > new Date(this.expirationDate);
|
||||
this.showNotification = previouslyHidden || expired ? 'hide' : 'show';
|
||||
}
|
||||
@ -48,7 +63,7 @@ export class NotificationComponent implements OnInit {
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.localStorage.setItem(LOCAL_STORAGE_NAMESPACE + this.notificationId, 'hide');
|
||||
this.storage.setItem(LOCAL_STORAGE_NAMESPACE + this.notificationId, 'hide');
|
||||
this.showNotification = 'hide';
|
||||
this.dismissed.next();
|
||||
}
|
||||
|
@ -1,42 +1,86 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { TopMenuComponent } from './top-menu.component';
|
||||
import { NavigationService, NavigationViews } from 'app/navigation/navigation.service';
|
||||
|
||||
describe('TopMenuComponent', () => {
|
||||
let component: TopMenuComponent;
|
||||
let fixture: ComponentFixture<TopMenuComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ TopMenuComponent ],
|
||||
providers: [
|
||||
{ provide: NavigationService, useClass: TestNavigationService }
|
||||
]
|
||||
});
|
||||
});
|
||||
// Helpers
|
||||
const getListItems = () => {
|
||||
const list: HTMLUListElement = fixture.debugElement.nativeElement.querySelector('ul');
|
||||
return Array.from(list.querySelectorAll('li'));
|
||||
};
|
||||
const getSelected = (items: HTMLLIElement[]) =>
|
||||
items.filter(item => item.classList.contains('selected'));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
TopMenuComponent,
|
||||
],
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(TopMenuComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
component.nodes = [
|
||||
{url: 'api', title: 'API', tooltip: 'API docs'},
|
||||
{url: 'features', title: 'Features', tooltip: 'Angular features overview'},
|
||||
];
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
it('should create an item for each navigation node', () => {
|
||||
const items = getListItems();
|
||||
const links = items.map(item => item.querySelector('a'))
|
||||
.filter((link): link is NonNullable<typeof link> => link !== null);
|
||||
|
||||
expect(links.length).toBe(2);
|
||||
expect(links.map(link => link.pathname)).toEqual(['/api', '/features']);
|
||||
expect(links.map(link => link.textContent)).toEqual(['API', 'Features']);
|
||||
expect(links.map(link => link.title)).toEqual(['API docs', 'Angular features overview']);
|
||||
});
|
||||
|
||||
it('should mark the currently selected node with `.selected`', () => {
|
||||
const items = getListItems();
|
||||
expect(getSelected(items)).toEqual([]);
|
||||
|
||||
component.currentNode = {url: 'api', view: 'foo', nodes: []};
|
||||
fixture.detectChanges();
|
||||
expect(getSelected(items)).toEqual([items[0]]);
|
||||
|
||||
component.currentNode = {url: 'features', view: 'foo', nodes: []};
|
||||
fixture.detectChanges();
|
||||
expect(getSelected(items)).toEqual([items[1]]);
|
||||
|
||||
component.currentNode = {url: 'something/else', view: 'foo', nodes: []};
|
||||
fixture.detectChanges();
|
||||
expect(getSelected(items)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not mark any node with `.selected` if the current URL is undefined', () => {
|
||||
component.nodes = [
|
||||
{url: '', title: 'API', tooltip: 'API docs'},
|
||||
{url: undefined, title: 'Features', tooltip: 'Angular features overview'},
|
||||
];
|
||||
fixture.detectChanges();
|
||||
const items = getListItems();
|
||||
|
||||
component.currentNode = undefined;
|
||||
fixture.detectChanges();
|
||||
expect(getSelected(items)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should correctly mark a node with `.selected` even if its URL is empty', () => {
|
||||
component.nodes = [
|
||||
{url: '', title: 'API', tooltip: 'API docs'},
|
||||
{url: undefined, title: 'Features', tooltip: 'Angular features overview'},
|
||||
];
|
||||
fixture.detectChanges();
|
||||
const items = getListItems();
|
||||
|
||||
component.currentNode = {url: '', view: 'Empty url', nodes: []};
|
||||
fixture.detectChanges();
|
||||
expect(getSelected(items)).toEqual([items[0]]);
|
||||
});
|
||||
});
|
||||
|
||||
//// Test Helpers ////
|
||||
class TestNavigationService {
|
||||
navJson = {
|
||||
TopBar: [
|
||||
{url: 'api', title: 'API' },
|
||||
{url: 'features', title: 'Features' }
|
||||
],
|
||||
};
|
||||
|
||||
navigationViews = new BehaviorSubject<NavigationViews>(this.navJson);
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { NavigationNode } from 'app/navigation/navigation.service';
|
||||
import { CurrentNode, NavigationNode } from 'app/navigation/navigation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'aio-top-menu',
|
||||
template: `
|
||||
<ul role="navigation">
|
||||
<li *ngFor="let node of nodes">
|
||||
<a class="nav-link" [href]="node.url" [title]="node.title">
|
||||
<li *ngFor="let node of nodes" [ngClass]="{selected: node.url === currentUrl}">
|
||||
<a class="nav-link" [href]="node.url" [title]="node.tooltip">
|
||||
<span class="nav-link-inner">{{ node.title }}</span>
|
||||
</a>
|
||||
</li>
|
||||
@ -14,5 +14,7 @@ import { NavigationNode } from 'app/navigation/navigation.service';
|
||||
})
|
||||
export class TopMenuComponent {
|
||||
@Input() nodes: NavigationNode[];
|
||||
@Input() currentNode: CurrentNode | undefined;
|
||||
|
||||
get currentUrl(): string | null { return this.currentNode ? this.currentNode.url : null; }
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ export interface SearchResult {
|
||||
type: string;
|
||||
titleWords: string;
|
||||
keywords: string;
|
||||
topics: string;
|
||||
deprecated: boolean;
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ interface PageInfo {
|
||||
type: string;
|
||||
titleWords: string;
|
||||
keyWords: string;
|
||||
topics: string;
|
||||
}
|
||||
|
||||
addEventListener('message', handleMessage);
|
||||
@ -27,6 +28,7 @@ function createIndex(loadIndexFn: IndexLoader): lunr.Index {
|
||||
queryLexer.termSeparator = lunr.tokenizer.separator = /\s+/;
|
||||
return lunr(/** @this */function() {
|
||||
this.ref('path');
|
||||
this.field('topics', { boost: 15 });
|
||||
this.field('titleWords', { boost: 10 });
|
||||
this.field('headingWords', { boost: 5 });
|
||||
this.field('members', { boost: 4 });
|
||||
|
@ -1,30 +1,57 @@
|
||||
<div class="search-results">
|
||||
<div *ngIf="searchAreas.length; then searchResults; else notFound"></div>
|
||||
<div class="search-results" [ngSwitch]="searchState">
|
||||
|
||||
<ng-container *ngSwitchCase="'in-progress'">
|
||||
<p class="no-results">Searching ...</p>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'results-found'">
|
||||
<h2 class="visually-hidden">Search Results</h2>
|
||||
<div class="search-area" *ngFor="let area of searchAreas">
|
||||
<h3 class="search-section-header">{{area.name}} ({{area.pages.length + area.priorityPages.length}})</h3>
|
||||
<ul class="priority-pages" >
|
||||
<li class="search-page" *ngFor="let page of area.priorityPages">
|
||||
<a class="search-result-item" href="{{ page.path }}" (click)="onResultSelected(page, $event)">
|
||||
<span class="symbol {{page.type}}" *ngIf="area.name === 'api'"></span>
|
||||
<span [class.deprecated-api-item]="page.deprecated">{{ page.title }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li class="search-page" *ngFor="let page of area.pages">
|
||||
<a class="search-result-item" href="{{ page.path }}" (click)="onResultSelected(page, $event)">
|
||||
<span class="symbol {{page.type}}" *ngIf="area.name === 'api'"></span>
|
||||
<span [class.deprecated-api-item]="page.deprecated">{{ page.title }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'no-results-found'">
|
||||
<div class="search-area">
|
||||
<p class="no-results">
|
||||
No results found.<br>
|
||||
Here are a few links that might be helpful in finding what you are looking for:
|
||||
</p>
|
||||
<ul class="priority-pages">
|
||||
<li class="search-page">
|
||||
<a class="search-result-item" href="api">API reference</a>
|
||||
</li>
|
||||
<li class="search-page">
|
||||
<a class="search-result-item" href="resources">Resources</a>
|
||||
</li>
|
||||
<li class="search-page">
|
||||
<a class="search-result-item" href="guide/glossary">Glossary</a>
|
||||
</li>
|
||||
<li class="search-page">
|
||||
<a class="search-result-item" href="guide/cheatsheet">Cheat-sheet</a>
|
||||
</li>
|
||||
<li class="search-page">
|
||||
<a class="search-result-item" href="https://blog.angular.io/">Angular blog</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #searchResults>
|
||||
<h2 class="visually-hidden">Search Results</h2>
|
||||
<div class="search-area" *ngFor="let area of searchAreas">
|
||||
<h3 class="search-section-header">{{area.name}} ({{area.pages.length + area.priorityPages.length}})</h3>
|
||||
<ul class="priority-pages" >
|
||||
<li class="search-page" *ngFor="let page of area.priorityPages">
|
||||
<a class="search-result-item" href="{{ page.path }}" (click)="onResultSelected(page, $event)">
|
||||
<span class="symbol {{page.type}}" *ngIf="area.name === 'api'"></span>
|
||||
<span [class.deprecated-api-item]="page.deprecated">{{ page.title }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li class="search-page" *ngFor="let page of area.pages">
|
||||
<a class="search-result-item" href="{{ page.path }}" (click)="onResultSelected(page, $event)">
|
||||
<span class="symbol {{page.type}}" *ngIf="area.name === 'api'"></span>
|
||||
<span [class.deprecated-api-item]="page.deprecated">{{ page.title }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #notFound>
|
||||
<p class="not-found">{{notFoundMessage}}</p>
|
||||
</ng-template>
|
||||
|
@ -37,21 +37,21 @@ describe('SearchResultsComponent', () => {
|
||||
|
||||
/** Get a full set of test results. "Take" what you need */
|
||||
beforeEach(() => {
|
||||
apiD = { path: 'api/d', title: 'API D', deprecated: false, keywords: '', titleWords: '', type: '' };
|
||||
apiC = { path: 'api/c', title: 'API C', deprecated: false, keywords: '', titleWords: '', type: '' };
|
||||
guideA = { path: 'guide/a', title: 'Guide A', deprecated: false, keywords: '', titleWords: '', type: '' };
|
||||
guideB = { path: 'guide/b', title: 'Guide B', deprecated: false, keywords: '', titleWords: '', type: '' };
|
||||
guideAC = { path: 'guide/a/c', title: 'Guide A - C', deprecated: false, keywords: '', titleWords: '', type: '' };
|
||||
guideE = { path: 'guide/e', title: 'Guide e', deprecated: false, keywords: '', titleWords: '', type: '' };
|
||||
guideF = { path: 'guide/f', title: 'Guide f', deprecated: false, keywords: '', titleWords: '', type: '' };
|
||||
guideG = { path: 'guide/g', title: 'Guide g', deprecated: false, keywords: '', titleWords: '', type: '' };
|
||||
guideH = { path: 'guide/h', title: 'Guide h', deprecated: false, keywords: '', titleWords: '', type: '' };
|
||||
guideI = { path: 'guide/i', title: 'Guide i', deprecated: false, keywords: '', titleWords: '', type: '' };
|
||||
guideJ = { path: 'guide/j', title: 'Guide j', deprecated: false, keywords: '', titleWords: '', type: '' };
|
||||
guideK = { path: 'guide/k', title: 'Guide k', deprecated: false, keywords: '', titleWords: '', type: '' };
|
||||
guideL = { path: 'guide/l', title: 'Guide l', deprecated: false, keywords: '', titleWords: '', type: '' };
|
||||
guideM = { path: 'guide/m', title: 'Guide m', deprecated: false, keywords: '', titleWords: '', type: '' };
|
||||
guideN = { path: 'guide/n', title: 'Guide n', deprecated: false, keywords: '', titleWords: '', type: '' };
|
||||
apiD = { path: 'api/d', title: 'API D', deprecated: false, keywords: '', titleWords: '', type: '', topics: '' };
|
||||
apiC = { path: 'api/c', title: 'API C', deprecated: false, keywords: '', titleWords: '', type: '', topics: '' };
|
||||
guideA = { path: 'guide/a', title: 'Guide A', deprecated: false, keywords: '', titleWords: '', type: '', topics: '' };
|
||||
guideB = { path: 'guide/b', title: 'Guide B', deprecated: false, keywords: '', titleWords: '', type: '', topics: '' };
|
||||
guideAC = { path: 'guide/a/c', title: 'Guide A - C', deprecated: false, keywords: '', titleWords: '', type: '', topics: '' };
|
||||
guideE = { path: 'guide/e', title: 'Guide e', deprecated: false, keywords: '', titleWords: '', type: '', topics: '' };
|
||||
guideF = { path: 'guide/f', title: 'Guide f', deprecated: false, keywords: '', titleWords: '', type: '', topics: '' };
|
||||
guideG = { path: 'guide/g', title: 'Guide g', deprecated: false, keywords: '', titleWords: '', type: '', topics: '' };
|
||||
guideH = { path: 'guide/h', title: 'Guide h', deprecated: false, keywords: '', titleWords: '', type: '', topics: '' };
|
||||
guideI = { path: 'guide/i', title: 'Guide i', deprecated: false, keywords: '', titleWords: '', type: '', topics: '' };
|
||||
guideJ = { path: 'guide/j', title: 'Guide j', deprecated: false, keywords: '', titleWords: '', type: '', topics: '' };
|
||||
guideK = { path: 'guide/k', title: 'Guide k', deprecated: false, keywords: '', titleWords: '', type: '', topics: '' };
|
||||
guideL = { path: 'guide/l', title: 'Guide l', deprecated: false, keywords: '', titleWords: '', type: '', topics: '' };
|
||||
guideM = { path: 'guide/m', title: 'Guide m', deprecated: false, keywords: '', titleWords: '', type: '', topics: '' };
|
||||
guideN = { path: 'guide/n', title: 'Guide n', deprecated: false, keywords: '', titleWords: '', type: '', topics: '' };
|
||||
|
||||
standardResults = [
|
||||
guideA, apiD, guideB, guideAC, apiC, guideN, guideM, guideL, guideK, guideJ, guideI, guideH, guideG, guideF, guideE,
|
||||
@ -80,13 +80,13 @@ describe('SearchResultsComponent', () => {
|
||||
|
||||
it('should special case results that are top level folders', () => {
|
||||
setSearchResults('', [
|
||||
{ path: 'tutorial', title: 'Tutorial index', type: '', keywords: '', titleWords: '', deprecated: false },
|
||||
{ path: 'tutorial/toh-pt1', title: 'Tutorial - part 1', type: '', keywords: '', titleWords: '', deprecated: false },
|
||||
{ path: 'tutorial', title: 'Tutorial index', type: '', keywords: '', titleWords: '', deprecated: false, topics: '' },
|
||||
{ path: 'tutorial/toh-pt1', title: 'Tutorial - part 1', type: '', keywords: '', titleWords: '', deprecated: false, topics: '' },
|
||||
]);
|
||||
expect(component.searchAreas).toEqual([
|
||||
{ name: 'tutorial', priorityPages: [
|
||||
{ path: 'tutorial', title: 'Tutorial index', type: '', keywords: '', titleWords: '', deprecated: false },
|
||||
{ path: 'tutorial/toh-pt1', title: 'Tutorial - part 1', type: '', keywords: '', titleWords: '', deprecated: false },
|
||||
{ path: 'tutorial', title: 'Tutorial index', type: '', keywords: '', titleWords: '', deprecated: false, topics: '' },
|
||||
{ path: 'tutorial/toh-pt1', title: 'Tutorial - part 1', type: '', keywords: '', titleWords: '', deprecated: false, topics: '' },
|
||||
], pages: [] }
|
||||
]);
|
||||
});
|
||||
@ -116,20 +116,20 @@ describe('SearchResultsComponent', () => {
|
||||
|
||||
it('should put search results with no containing folder into the default area (other)', () => {
|
||||
const results = [
|
||||
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '', deprecated: false }
|
||||
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '', deprecated: false, topics: '' }
|
||||
];
|
||||
|
||||
setSearchResults('', results);
|
||||
expect(component.searchAreas).toEqual([
|
||||
{ name: 'other', priorityPages: [
|
||||
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '', deprecated: false }
|
||||
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '', deprecated: false, topics: '' }
|
||||
], pages: [] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('should omit search results with no title', () => {
|
||||
const results = [
|
||||
{ path: 'news', title: '', type: 'marketing', keywords: '', titleWords: '', deprecated: false }
|
||||
{ path: 'news', title: '', type: 'marketing', keywords: '', titleWords: '', deprecated: false, topics: '' }
|
||||
];
|
||||
|
||||
setSearchResults('something', results);
|
||||
@ -170,6 +170,12 @@ describe('SearchResultsComponent', () => {
|
||||
expect(getText()).toContain('Searching ...');
|
||||
});
|
||||
|
||||
it('should not display default links while searching', () => {
|
||||
fixture.detectChanges();
|
||||
const resultLinks = fixture.debugElement.queryAll(By.css('.search-page a'));
|
||||
expect(resultLinks.length).toEqual(0);
|
||||
});
|
||||
|
||||
describe('when a search result anchor is clicked', () => {
|
||||
let searchResult: SearchResult;
|
||||
let selected: SearchResult|null;
|
||||
@ -179,7 +185,7 @@ describe('SearchResultsComponent', () => {
|
||||
component.resultSelected.subscribe((result: SearchResult) => selected = result);
|
||||
|
||||
selected = null;
|
||||
searchResult = { path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '', deprecated: false };
|
||||
searchResult = { path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '', deprecated: false, topics: '' };
|
||||
setSearchResults('something', [searchResult]);
|
||||
|
||||
fixture.detectChanges();
|
||||
@ -214,9 +220,25 @@ describe('SearchResultsComponent', () => {
|
||||
});
|
||||
|
||||
describe('when no query results', () => {
|
||||
it('should display "not found" message', () => {
|
||||
beforeEach(() => {
|
||||
setSearchResults('something', []);
|
||||
});
|
||||
|
||||
it('should display "not found" message', () => {
|
||||
expect(getText()).toContain('No results');
|
||||
});
|
||||
|
||||
it('should contain reference links', () => {
|
||||
const resultLinks = fixture.debugElement.queryAll(By.css('.search-page a'));
|
||||
const resultHrefs = resultLinks.map(a => a.nativeNode.getAttribute('href'));
|
||||
expect(resultHrefs.length).toEqual(5);
|
||||
expect(resultHrefs).toEqual([
|
||||
'api',
|
||||
'resources',
|
||||
'guide/glossary',
|
||||
'guide/cheatsheet',
|
||||
'https://blog.angular.io/',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
|
||||
import { SearchResult, SearchResults, SearchArea } from 'app/search/interfaces';
|
||||
|
||||
enum SearchState {
|
||||
InProgress = 'in-progress',
|
||||
ResultsFound = 'results-found',
|
||||
NoResultsFound = 'no-results-found'
|
||||
}
|
||||
|
||||
/**
|
||||
* A component to display search results in groups
|
||||
*/
|
||||
@ -23,11 +29,18 @@ export class SearchResultsComponent implements OnChanges {
|
||||
resultSelected = new EventEmitter<SearchResult>();
|
||||
|
||||
readonly defaultArea = 'other';
|
||||
notFoundMessage = 'Searching ...';
|
||||
readonly topLevelFolders = ['guide', 'tutorial'];
|
||||
searchState: SearchState = SearchState.InProgress;
|
||||
readonly topLevelFolders = ['api', 'cli', 'guide', 'start', 'tutorial'];
|
||||
searchAreas: SearchArea[] = [];
|
||||
|
||||
ngOnChanges() {
|
||||
if (this.searchResults === null) {
|
||||
this.searchState = SearchState.InProgress;
|
||||
} else if (this.searchResults.results.length) {
|
||||
this.searchState = SearchState.ResultsFound;
|
||||
} else {
|
||||
this.searchState = SearchState.NoResultsFound;
|
||||
}
|
||||
this.searchAreas = this.processSearchResults(this.searchResults);
|
||||
}
|
||||
|
||||
@ -43,7 +56,6 @@ export class SearchResultsComponent implements OnChanges {
|
||||
if (!search) {
|
||||
return [];
|
||||
}
|
||||
this.notFoundMessage = 'No results found.';
|
||||
const searchAreaMap: { [key: string]: SearchResult[] } = {};
|
||||
search.results.forEach(result => {
|
||||
if (!result.title) { return; } // bad data; should fix
|
||||
|
@ -20,9 +20,9 @@
|
||||
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="assets/images/favicons/favicon-144x144.png">
|
||||
|
||||
<!-- NOTE: These need to be kept in sync with `ngsw-config.json`. -->
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono&display=swap">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons&display=block">
|
||||
<!-- -->
|
||||
|
||||
<link rel="manifest" href="pwa-manifest.json">
|
||||
|
@ -138,7 +138,6 @@ a {
|
||||
color: $white;
|
||||
font-family: $main-font;
|
||||
text-transform: uppercase;
|
||||
padding: 21px 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
|
@ -22,20 +22,30 @@ footer {
|
||||
}
|
||||
|
||||
a {
|
||||
color: $white;
|
||||
color: $white;
|
||||
text-decoration: none;
|
||||
z-index: 20;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:visited {
|
||||
text-decoration: none;
|
||||
z-index: 20;
|
||||
position: relative;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
&:visited {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
// `outline-offset` is not applied on Chrome on Windows, if `outline-style` is `auto.
|
||||
outline: 1px solid rgba($white, 0.8);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
a.action {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@include font-size(16);
|
||||
text-transform: uppercase;
|
||||
@ -43,6 +53,7 @@ footer {
|
||||
margin: 8px 0 12px;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
margin: 10px 0px 5px;
|
||||
@ -56,9 +67,7 @@ footer {
|
||||
display: -ms-flexbox;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
text-align: left;
|
||||
margin: 0 0 40px;
|
||||
|
||||
@ -76,6 +85,7 @@ footer {
|
||||
|
||||
@media (max-width: 480px) {
|
||||
flex-direction: column;
|
||||
|
||||
.footer-block {
|
||||
margin: 8px 24px;
|
||||
}
|
||||
@ -90,16 +100,17 @@ footer {
|
||||
}
|
||||
|
||||
footer::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background:
|
||||
url('/assets/images/logos/angular/angular_whiteTransparent_withMargin.png') top 0 left 0 repeat,
|
||||
url('/assets/images/logos/angular/angular_whiteTransparent_withMargin.png') top 80px left 160px repeat;
|
||||
opacity: 0.05;
|
||||
background-size: 320px auto;
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: url("/assets/images/logos/angular/angular_whiteTransparent_withMargin.png")
|
||||
top 0 left 0 repeat,
|
||||
url("/assets/images/logos/angular/angular_whiteTransparent_withMargin.png")
|
||||
top 80px left 160px repeat;
|
||||
opacity: 0.05;
|
||||
background-size: 320px auto;
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ section#intro {
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s ease-in;
|
||||
|
||||
@media (max-width: 992px) {
|
||||
@media (max-width: 991px) {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 32px 16px;
|
||||
@ -441,4 +441,4 @@ div[layout=row]{
|
||||
|
||||
.events-container{
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ mat-sidenav-container div.mat-sidenav-content {
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: $accentblue auto 2px;
|
||||
outline: $focus-outline-onlight auto 2px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
|
@ -87,7 +87,7 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
|
||||
}
|
||||
|
||||
& .mat-icon {
|
||||
color: white;
|
||||
color: $white;
|
||||
position: inherit;
|
||||
}
|
||||
}
|
||||
@ -96,7 +96,17 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
|
||||
.nav-link.home {
|
||||
cursor: pointer;
|
||||
margin: 0 16px 0 0;
|
||||
padding: 21px 0;
|
||||
padding: 8px 0;
|
||||
|
||||
&:focus {
|
||||
// `outline-offset` is not applied on Chrome on Windows, if `outline-style` is `auto.
|
||||
outline: 1px solid $focus-outline-ondark;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 991px) {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
margin-right: 8px;
|
||||
@ -108,7 +118,7 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
|
||||
top: 12px;
|
||||
height: 40px;
|
||||
|
||||
@media (max-width: 992px) {
|
||||
@media (max-width: 991px) {
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
@ -136,32 +146,47 @@ aio-top-menu {
|
||||
list-style-type: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: $accentblue;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.nav-link {
|
||||
margin: 0;
|
||||
padding: 24px 0px;
|
||||
cursor: pointer;
|
||||
a.nav-link {
|
||||
margin: 0 4px;
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
|
||||
.nav-link-inner {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.nav-link-inner {
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
&:hover {
|
||||
background: rgba($white, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-link-inner {
|
||||
background: rgba($white, 0.15);
|
||||
border-radius: 4px;
|
||||
&:focus {
|
||||
outline: none;
|
||||
|
||||
.nav-link-inner {
|
||||
background: rgba($white, 0.15);
|
||||
border-radius: 1px;
|
||||
box-shadow: 0 0 1px 2px $focus-outline-ondark;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
.nav-link-inner {
|
||||
background: rgba($white, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
a.nav-link {
|
||||
.nav-link-inner {
|
||||
background: rgba($white, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -175,6 +200,7 @@ aio-search-box.search-container {
|
||||
width: 100%;
|
||||
min-width: 150px;
|
||||
height: 100%;
|
||||
margin-right: 16px;
|
||||
|
||||
input {
|
||||
color: $darkgray;
|
||||
@ -220,10 +246,18 @@ aio-search-box.search-container {
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 16px;
|
||||
padding: 24px;
|
||||
margin: 0 -16px;
|
||||
|
||||
&:focus {
|
||||
// `outline-offset` is not applied on Chrome on Windows, if `outline-style` is `auto.
|
||||
outline: 1px solid $focus-outline-ondark;
|
||||
outline-offset: -16px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
margin-left: 8px;
|
||||
margin: 0 0 0 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -1,12 +1,12 @@
|
||||
aio-contributor-list {
|
||||
@media handheld and (max-width: 480px), screen and (max-width: 480px), screen and (max-width: 900px) {
|
||||
.grid-fluid{
|
||||
.grid-fluid {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media handheld and (max-width: 480px), screen and (max-width: 480px), screen and (max-width: 900px) {
|
||||
.grid-fluid{
|
||||
.grid-fluid {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
float: none;
|
||||
|
@ -67,12 +67,17 @@ aio-search-results {
|
||||
}
|
||||
}
|
||||
|
||||
.not-found {
|
||||
.no-results {
|
||||
color: $white;
|
||||
text-align: center;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $white;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
display: block;
|
||||
}
|
||||
@ -104,6 +109,10 @@ aio-search-results {
|
||||
.not-found {
|
||||
color: $darkgray;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -113,3 +113,9 @@ table {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.events-container {
|
||||
tr > td, tr > th {
|
||||
width: 33%;
|
||||
}
|
||||
}
|
||||
|
@ -54,8 +54,12 @@ $purple-600: #8E24AA;
|
||||
$teal-500: #009688;
|
||||
$lightgrey: #F5F6F7;
|
||||
|
||||
// STATE COLORS
|
||||
$focus-outline-ondark: rgba($white, 0.8);
|
||||
$focus-outline-onlight: $accentblue;
|
||||
|
||||
// GRADIENTS
|
||||
$bluegradient: linear-gradient(145deg,#0D47A1,#42A5F5);
|
||||
$bluegradient: linear-gradient(145deg,$blue-900,$blue-400);
|
||||
$redgradient: linear-gradient(145deg,$darkred,$brightred);
|
||||
|
||||
// API LABEL COLOR AND SYMBOLS MAP
|
||||
|
@ -14,11 +14,11 @@ exports.config = {
|
||||
suite: 'full',
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
chromeOptions: process.env['CI'] ? {
|
||||
chromeOptions: {
|
||||
binary: require('puppeteer').executablePath(),
|
||||
// See /integration/README.md#browser-tests for more info on these args
|
||||
args: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio']
|
||||
} : {},
|
||||
args: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio'],
|
||||
},
|
||||
},
|
||||
directConnect: true,
|
||||
framework: 'jasmine',
|
||||
|
@ -172,10 +172,10 @@
|
||||
/docs/ts/latest/api/testing/fakeAsync-function.html /api/core/testing/fakeAsync
|
||||
/docs/ts/latest/cookbook/ts-to-js.html https://v2.angular.io/docs/ts/latest/cookbook/ts-to-js.html
|
||||
/getting-started /start
|
||||
/getting-started/routing /start/routing
|
||||
/getting-started/data /start/data
|
||||
/getting-started/forms /start/forms
|
||||
/getting-started/deployment /start/deployment
|
||||
/getting-started/routing /start/start-routing
|
||||
/getting-started/data /start/start-data
|
||||
/getting-started/forms /start/start-forms
|
||||
/getting-started/deployment /start/start-deployment
|
||||
/guide/cli-quickstart /start
|
||||
/guide/learning-angular /start
|
||||
/guide/learning-angular.html /start
|
||||
@ -187,5 +187,9 @@
|
||||
/guide/webpack https://v5.angular.io/guide/webpack
|
||||
/news https://blog.angular.io/
|
||||
/news.html https://blog.angular.io/
|
||||
/start/data /start/start-data
|
||||
/start/deployment /start/start-deployment
|
||||
/start/forms /start/start-forms
|
||||
/start/routing /start/start-routing
|
||||
/testing /guide/testing
|
||||
/testing/first-app-tests.html /guide/testing
|
@ -14,11 +14,11 @@ exports.config = {
|
||||
],
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
chromeOptions: process.env['CI'] ? {
|
||||
chromeOptions: {
|
||||
binary: require('puppeteer').executablePath(),
|
||||
// See /integration/README.md#browser-tests for more info on these args
|
||||
args: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio']
|
||||
} : {},
|
||||
args: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio'],
|
||||
},
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
|
@ -40,7 +40,8 @@ describe('site auto-scrolling', () => {
|
||||
expect(await page.getScrollTop()).not.toBe(0);
|
||||
|
||||
await page.docsMenuLink.click();
|
||||
expect(await page.getScrollTop()).toBe(0);
|
||||
// On some environments (e.g. CI) it takes some time for the page to load (and scroll to top).
|
||||
await browser.wait(async () => await page.getScrollTop() === 0, 1000);
|
||||
});
|
||||
|
||||
it('should scroll to top when navigating to the same page via a link', async () => {
|
||||
|
@ -112,6 +112,7 @@ class ExampleZipper {
|
||||
'!**/npm-debug.log',
|
||||
'!**/example-config.json',
|
||||
'!**/wallaby.js',
|
||||
'!e2e/protractor-puppeteer.conf.js',
|
||||
// AOT related files
|
||||
'!**/aot/**/*.*',
|
||||
'!**/*-aot.*'
|
||||
|
@ -15,8 +15,9 @@ const BOILERPLATE_PATHS = {
|
||||
'src/environments/environment.prod.ts', 'src/environments/environment.ts',
|
||||
'src/assets/.gitkeep', 'browserslist', 'src/favicon.ico', 'karma.conf.js',
|
||||
'src/polyfills.ts', 'src/test.ts', 'tsconfig.app.json', 'tsconfig.spec.json',
|
||||
'tslint.json', 'e2e/src/app.po.ts', 'e2e/protractor.conf.js', 'e2e/tsconfig.json',
|
||||
'.editorconfig', 'angular.json', 'package.json', 'tsconfig.json', 'tslint.json'
|
||||
'tslint.json', 'e2e/src/app.po.ts', 'e2e/protractor-puppeteer.conf.js',
|
||||
'e2e/protractor.conf.js', 'e2e/tsconfig.json', '.editorconfig', 'angular.json', 'package.json',
|
||||
'tsconfig.json', 'tslint.json'
|
||||
],
|
||||
systemjs: [
|
||||
'src/systemjs-angular-loader.js', 'src/systemjs.config.js', 'src/tsconfig.json',
|
||||
|
@ -10,7 +10,7 @@ describe('example-boilerplate tool', () => {
|
||||
const sharedDir = path.resolve(__dirname, 'shared');
|
||||
const sharedNodeModulesDir = path.resolve(sharedDir, 'node_modules');
|
||||
const BPFiles = {
|
||||
cli: 19,
|
||||
cli: 20,
|
||||
i18n: 2,
|
||||
universal: 2,
|
||||
systemjs: 7,
|
||||
|
@ -262,7 +262,13 @@ function runE2eTestsCLI(appDir, outputFile, bufferOutput, port) {
|
||||
const config = loadExampleConfig(appDir);
|
||||
const commands = config.e2e || [{
|
||||
cmd: 'yarn',
|
||||
args: ['e2e', '--prod', '--no-webdriver-update', `--port=${port || DEFAULT_CLI_EXAMPLE_PORT}`]
|
||||
args: [
|
||||
'e2e',
|
||||
'--prod',
|
||||
'--protractor-config=e2e/protractor-puppeteer.conf.js',
|
||||
'--no-webdriver-update',
|
||||
'--port={PORT}',
|
||||
],
|
||||
}];
|
||||
let bufferedOutput = `\n\n============== AIO example output for: ${appDir}\n\n`;
|
||||
|
||||
|
@ -0,0 +1,19 @@
|
||||
// @ts-check
|
||||
// A protractor config to use to run the tests using the Chrome version provided by `puppeteer`.
|
||||
// This is useful to ensure deterministic runs on CI and locally. This file is ignored when creating
|
||||
// StackBlitz examples and ZIP archives for each example.
|
||||
|
||||
const {config} = require('./protractor.conf.js');
|
||||
|
||||
exports.config = {
|
||||
...config,
|
||||
capabilities: {
|
||||
...config.capabilities,
|
||||
chromeOptions: {
|
||||
...config.capabilities.chromeOptions,
|
||||
binary: require('puppeteer').executablePath(),
|
||||
// See /integration/README.md#browser-tests for more info on these args
|
||||
args: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio'],
|
||||
},
|
||||
},
|
||||
};
|
@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"http-server": "http-server",
|
||||
"protractor": "protractor",
|
||||
"webdriver:update": "webdriver-manager update --standalone false --gecko false $CI_CHROMEDRIVER_VERSION_ARG",
|
||||
"webdriver:update": "node ../../../../scripts/webdriver-manager-update.js",
|
||||
"preinstall": "node ../../../../tools/yarn/check-yarn.js",
|
||||
"postinstall": "yarn webdriver:update"
|
||||
},
|
||||
@ -74,6 +74,7 @@
|
||||
"lite-server": "^2.2.2",
|
||||
"lodash": "^4.16.2",
|
||||
"protractor": "^5.4.2",
|
||||
"puppeteer": "2.1.1",
|
||||
"rimraf": "^2.5.4",
|
||||
"rollup": "^1.1.0",
|
||||
"rollup-plugin-commonjs": "^9.2.1",
|
||||
|
@ -20,7 +20,12 @@ exports.config = {
|
||||
|
||||
// Capabilities to be passed to the webdriver instance.
|
||||
capabilities: {
|
||||
'browserName': 'chrome',
|
||||
browserName: 'chrome',
|
||||
chromeOptions: {
|
||||
binary: require('puppeteer').executablePath(),
|
||||
// See /integration/README.md#browser-tests for more info on these args
|
||||
args: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio'],
|
||||
},
|
||||
},
|
||||
|
||||
// Framework to use. Jasmine is recommended.
|
||||
|
@ -450,6 +450,11 @@
|
||||
dependencies:
|
||||
"@types/sizzle" "*"
|
||||
|
||||
"@types/mime-types@^2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.0.tgz#9ca52cda363f699c69466c2a6ccdaad913ea7a73"
|
||||
integrity sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM=
|
||||
|
||||
"@types/mime@*":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b"
|
||||
@ -729,6 +734,11 @@ agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0:
|
||||
dependencies:
|
||||
es6-promisify "^5.0.0"
|
||||
|
||||
agent-base@5:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c"
|
||||
integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==
|
||||
|
||||
agentkeepalive@^3.4.1:
|
||||
version "3.5.2"
|
||||
resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67"
|
||||
@ -1942,6 +1952,16 @@ concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
|
||||
concat-stream@1.6.2:
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
|
||||
integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
|
||||
dependencies:
|
||||
buffer-from "^1.0.0"
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^2.2.2"
|
||||
typedarray "^0.0.6"
|
||||
|
||||
concat-stream@^1.5.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
|
||||
@ -2239,6 +2259,13 @@ debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8:
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@4, debug@^4.1.0, debug@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
|
||||
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^3.0.0, debug@^3.2.5, debug@^3.2.6:
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
|
||||
@ -2246,13 +2273,6 @@ debug@^3.0.0, debug@^3.2.5, debug@^3.2.6:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.1.0, debug@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
|
||||
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debuglog@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
|
||||
@ -2943,6 +2963,16 @@ extglob@^2.0.2, extglob@^2.0.4:
|
||||
snapdragon "^0.8.1"
|
||||
to-regex "^3.0.1"
|
||||
|
||||
extract-zip@^1.6.6:
|
||||
version "1.6.7"
|
||||
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9"
|
||||
integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=
|
||||
dependencies:
|
||||
concat-stream "1.6.2"
|
||||
debug "2.6.9"
|
||||
mkdirp "0.5.1"
|
||||
yauzl "2.4.1"
|
||||
|
||||
extsprintf@1.3.0, extsprintf@^1.2.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
|
||||
@ -2972,6 +3002,13 @@ faye-websocket@~0.11.1:
|
||||
dependencies:
|
||||
websocket-driver ">=0.5.1"
|
||||
|
||||
fd-slicer@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65"
|
||||
integrity sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=
|
||||
dependencies:
|
||||
pend "~1.2.0"
|
||||
|
||||
figgy-pudding@^3.1.0, figgy-pudding@^3.4.1, figgy-pudding@^3.5.1:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
|
||||
@ -3735,6 +3772,14 @@ https-proxy-agent@^2.2.1:
|
||||
agent-base "^4.1.0"
|
||||
debug "^3.1.0"
|
||||
|
||||
https-proxy-agent@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b"
|
||||
integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==
|
||||
dependencies:
|
||||
agent-base "5"
|
||||
debug "4"
|
||||
|
||||
humanize-ms@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
|
||||
@ -4887,6 +4932,11 @@ miller-rabin@^4.0.0:
|
||||
bn.js "^4.0.0"
|
||||
brorand "^1.0.1"
|
||||
|
||||
mime-db@1.43.0:
|
||||
version "1.43.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58"
|
||||
integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==
|
||||
|
||||
"mime-db@>= 1.40.0 < 2":
|
||||
version "1.40.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32"
|
||||
@ -4906,6 +4956,13 @@ mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.16,
|
||||
dependencies:
|
||||
mime-db "~1.30.0"
|
||||
|
||||
mime-types@^2.1.25:
|
||||
version "2.1.26"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06"
|
||||
integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==
|
||||
dependencies:
|
||||
mime-db "1.43.0"
|
||||
|
||||
mime-types@~2.1.18, mime-types@~2.1.19:
|
||||
version "2.1.20"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19"
|
||||
@ -4933,6 +4990,11 @@ mime@^1.6.0:
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||
|
||||
mime@^2.0.3:
|
||||
version "2.4.4"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5"
|
||||
integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==
|
||||
|
||||
mime@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"
|
||||
@ -5039,7 +5101,7 @@ mkdirp@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e"
|
||||
|
||||
mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0:
|
||||
mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
|
||||
dependencies:
|
||||
@ -5804,6 +5866,11 @@ pbkdf2@^3.0.3:
|
||||
safe-buffer "^5.0.1"
|
||||
sha.js "^2.4.8"
|
||||
|
||||
pend@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
|
||||
integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
|
||||
|
||||
performance-now@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
|
||||
@ -5939,6 +6006,11 @@ process@^0.11.0:
|
||||
version "0.11.10"
|
||||
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
|
||||
|
||||
progress@^2.0.1:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
||||
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
|
||||
|
||||
promise-inflight@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
||||
@ -6000,6 +6072,11 @@ proxy-addr@~2.0.4:
|
||||
forwarded "~0.1.2"
|
||||
ipaddr.js "1.9.0"
|
||||
|
||||
proxy-from-env@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
|
||||
integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=
|
||||
|
||||
prr@~0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
|
||||
@ -6056,6 +6133,22 @@ punycode@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d"
|
||||
|
||||
puppeteer@2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-2.1.1.tgz#ccde47c2a688f131883b50f2d697bd25189da27e"
|
||||
integrity sha512-LWzaDVQkk1EPiuYeTOj+CZRIjda4k2s5w4MK4xoH2+kgWV/SDlkYHmxatDdtYrciHUKSXTsGgPgPP8ILVdBsxg==
|
||||
dependencies:
|
||||
"@types/mime-types" "^2.1.0"
|
||||
debug "^4.1.0"
|
||||
extract-zip "^1.6.6"
|
||||
https-proxy-agent "^4.0.0"
|
||||
mime "^2.0.3"
|
||||
mime-types "^2.1.25"
|
||||
progress "^2.0.1"
|
||||
proxy-from-env "^1.0.0"
|
||||
rimraf "^2.6.1"
|
||||
ws "^6.1.0"
|
||||
|
||||
q@1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e"
|
||||
@ -8303,6 +8396,13 @@ ws@1.1.1:
|
||||
options ">=0.0.5"
|
||||
ultron "1.0.x"
|
||||
|
||||
ws@^6.1.0:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
|
||||
integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
|
||||
dependencies:
|
||||
async-limiter "~1.0.0"
|
||||
|
||||
ws@~3.3.1:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"
|
||||
@ -8481,6 +8581,13 @@ yargs@~3.10.0:
|
||||
decamelize "^1.0.0"
|
||||
window-size "0.1.0"
|
||||
|
||||
yauzl@2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005"
|
||||
integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=
|
||||
dependencies:
|
||||
fd-slicer "~1.0.1"
|
||||
|
||||
yeast@0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
|
||||
|
@ -11,12 +11,11 @@ const yargs = require('yargs');
|
||||
const PACKAGE_JSON = 'package.json';
|
||||
const YARN_LOCK = 'yarn.lock';
|
||||
const LOCAL_MARKER_PATH = 'node_modules/_local_.json';
|
||||
const PACKAGE_JSON_REGEX = /^[^/]+\/package\.json$/;
|
||||
|
||||
const ANGULAR_ROOT_DIR = path.resolve(__dirname, '../../..');
|
||||
const ANGULAR_DIST_PACKAGES = path.join(ANGULAR_ROOT_DIR, 'dist/packages-dist');
|
||||
const ANGULAR_DIST_PACKAGES_BUILD_SCRIPT = path.join(ANGULAR_ROOT_DIR, 'scripts/build-packages-dist.js');
|
||||
const ANGULAR_DIST_PACKAGES_BUILD_CMD = `"${process.execPath}" "${ANGULAR_DIST_PACKAGES_BUILD_SCRIPT}"`;
|
||||
const ANGULAR_DIST_PACKAGES_DIR = path.join(ANGULAR_ROOT_DIR, 'dist/packages-dist');
|
||||
const DIST_PACKAGES_BUILD_SCRIPT = path.join(ANGULAR_ROOT_DIR, 'scripts/build/build-packages-dist.js');
|
||||
const DIST_PACKAGES_BUILD_CMD = `"${process.execPath}" "${DIST_PACKAGES_BUILD_SCRIPT}"`;
|
||||
|
||||
/**
|
||||
* A tool that can install Angular dependencies for a project from NPM or from the
|
||||
@ -94,7 +93,7 @@ class NgPackagesInstaller {
|
||||
const pkg2 = packages[key2];
|
||||
if (pkg2) {
|
||||
// point the core Angular packages at the distributable folder
|
||||
deps[key2] = `file:${pkg2.parentDir}/${key2.replace('@angular/', '')}`;
|
||||
deps[key2] = `file:${pkg2.packageDir}`;
|
||||
this._log(`Overriding dependency of local ${key} with local package: ${key2}: ${deps[key2]}`);
|
||||
}
|
||||
});
|
||||
@ -178,14 +177,14 @@ class NgPackagesInstaller {
|
||||
const canBuild = process.platform !== 'win32';
|
||||
|
||||
if (canBuild) {
|
||||
this._log(`Building the Angular packages with: ${ANGULAR_DIST_PACKAGES_BUILD_SCRIPT}`);
|
||||
shelljs.exec(ANGULAR_DIST_PACKAGES_BUILD_CMD);
|
||||
this._log(`Building the Angular packages with: ${DIST_PACKAGES_BUILD_SCRIPT}`);
|
||||
shelljs.exec(DIST_PACKAGES_BUILD_CMD);
|
||||
} else {
|
||||
this._warn([
|
||||
'Automatically building the local Angular packages is currently not supported on Windows.',
|
||||
`Please, ensure '${ANGULAR_DIST_PACKAGES}' exists and is up-to-date (e.g. by running ` +
|
||||
`'${ANGULAR_DIST_PACKAGES_BUILD_SCRIPT}' in Git Bash for Windows, Windows Subsystem for Linux or a Linux ` +
|
||||
'docker container or VM).',
|
||||
`Please, ensure '${ANGULAR_DIST_PACKAGES_DIR}' exists and is up-to-date (e.g. by running ` +
|
||||
`'${DIST_PACKAGES_BUILD_SCRIPT}' in Git Bash for Windows, Windows Subsystem for Linux or a Linux docker ` +
|
||||
'container or VM).',
|
||||
'',
|
||||
'Proceeding anyway...',
|
||||
].join('\n'));
|
||||
@ -200,7 +199,7 @@ class NgPackagesInstaller {
|
||||
const sourcePackage = packages[key];
|
||||
if (sourcePackage) {
|
||||
// point the core Angular packages at the distributable folder
|
||||
mergedDependencies[key] = `file:${sourcePackage.parentDir}/${key.replace('@angular/', '')}`;
|
||||
mergedDependencies[key] = `file:${sourcePackage.packageDir}`;
|
||||
this._log(`Overriding dependency with local package: ${key}: ${mergedDependencies[key]}`);
|
||||
// grab peer dependencies
|
||||
const sourcePackagePeerDeps = sourcePackage.config.peerDependencies || {};
|
||||
@ -219,32 +218,43 @@ class NgPackagesInstaller {
|
||||
* (Detected as directories in '/dist/packages-dist/' that contain a top-level 'package.json' file.)
|
||||
*/
|
||||
_getDistPackages() {
|
||||
const packageConfigs = Object.create(null);
|
||||
const distDir = ANGULAR_DIST_PACKAGES;
|
||||
|
||||
this._log(`Angular distributable directory: ${distDir}.`);
|
||||
this._log(`Angular distributable directory: ${ANGULAR_DIST_PACKAGES_DIR}.`);
|
||||
|
||||
if (this.buildPackages) {
|
||||
this._buildDistPackages();
|
||||
}
|
||||
|
||||
shelljs
|
||||
.find(distDir)
|
||||
.map(filePath => filePath.slice(distDir.length + 1))
|
||||
.filter(filePath => PACKAGE_JSON_REGEX.test(filePath))
|
||||
.forEach(packagePath => {
|
||||
const packageName = `@angular/${packagePath.slice(0, -PACKAGE_JSON.length -1)}`;
|
||||
if (this.ignorePackages.indexOf(packageName) === -1) {
|
||||
const packageConfig = require(path.resolve(distDir, packagePath));
|
||||
packageConfigs[packageName] = {
|
||||
parentDir: distDir,
|
||||
packageJsonPath: path.resolve(distDir, packagePath),
|
||||
config: packageConfig
|
||||
};
|
||||
} else {
|
||||
this._log('Ignoring package', packageName);
|
||||
const collectPackages = containingDir => {
|
||||
const packages = {};
|
||||
|
||||
for (const dirName of shelljs.ls(containingDir)) {
|
||||
const packageDir = path.resolve(containingDir, dirName);
|
||||
const packageJsonPath = path.join(packageDir, PACKAGE_JSON);
|
||||
const packageConfig = fs.existsSync(packageJsonPath) ? require(packageJsonPath) : null;
|
||||
const packageName = packageConfig && packageConfig.name;
|
||||
|
||||
if (!packageConfig) {
|
||||
// No `package.json` found - this directory is not a package.
|
||||
continue;
|
||||
} else if (!packageName) {
|
||||
// No `name` property in `package.json`. (This should never happen.)
|
||||
throw new Error(`Package '${packageDir}' specifies no name in its '${PACKAGE_JSON}'.`);
|
||||
} else if (this.ignorePackages.includes(packageName)) {
|
||||
this._log(`Ignoring package '${packageName}'.`);
|
||||
continue;
|
||||
}
|
||||
});
|
||||
|
||||
packages[packageName] = {
|
||||
packageDir,
|
||||
packageJsonPath,
|
||||
config: packageConfig,
|
||||
};
|
||||
}
|
||||
|
||||
return packages;
|
||||
};
|
||||
|
||||
const packageConfigs = collectPackages(ANGULAR_DIST_PACKAGES_DIR);
|
||||
|
||||
this._log('Found the following Angular distributables:', Object.keys(packageConfigs).map(key => `\n - ${key}`));
|
||||
return packageConfigs;
|
||||
|
@ -66,7 +66,7 @@ describe('NgPackagesInstaller', () => {
|
||||
// These are the packages that are "found" in the dist directory
|
||||
dummyNgPackages = {
|
||||
'@angular/core': {
|
||||
parentDir: packagesDir,
|
||||
packageDir: `${packagesDir}/core`,
|
||||
packageJsonPath: `${packagesDir}/core/package.json`,
|
||||
config: {
|
||||
peerDependencies: {
|
||||
@ -77,17 +77,17 @@ describe('NgPackagesInstaller', () => {
|
||||
}
|
||||
},
|
||||
'@angular/common': {
|
||||
parentDir: packagesDir,
|
||||
packageDir: `${packagesDir}/common`,
|
||||
packageJsonPath: `${packagesDir}/common/package.json`,
|
||||
config: { peerDependencies: { '@angular/core': '4.4.4-1ab23cd4' } }
|
||||
},
|
||||
'@angular/compiler': {
|
||||
parentDir: packagesDir,
|
||||
packageDir: `${packagesDir}/compiler`,
|
||||
packageJsonPath: `${packagesDir}/compiler/package.json`,
|
||||
config: { peerDependencies: { '@angular/common': '4.4.4-1ab23cd4' } }
|
||||
},
|
||||
'@angular/compiler-cli': {
|
||||
parentDir: toolsDir,
|
||||
packageDir: `${toolsDir}/compiler-cli`,
|
||||
packageJsonPath: `${toolsDir}/compiler-cli/package.json`,
|
||||
config: {
|
||||
dependencies: { '@angular/tsc-wrapped': '4.4.4-1ab23cd4' },
|
||||
@ -95,7 +95,7 @@ describe('NgPackagesInstaller', () => {
|
||||
}
|
||||
},
|
||||
'@angular/tsc-wrapped': {
|
||||
parentDir: toolsDir,
|
||||
packageDir: `${toolsDir}/tsc-wrapped`,
|
||||
packageJsonPath: `${toolsDir}/tsc-wrapped/package.json`,
|
||||
config: {
|
||||
devDependencies: { '@angular/common': '4.4.4-1ab23cd4' },
|
||||
@ -253,7 +253,7 @@ describe('NgPackagesInstaller', () => {
|
||||
};
|
||||
|
||||
it('should build the local packages, when not on Windows', () => {
|
||||
const buildScript = path.join(ngRootDir, 'scripts/build-packages-dist.js');
|
||||
const buildScript = path.join(ngRootDir, 'scripts/build/build-packages-dist.js');
|
||||
const buildCmd = `"${process.execPath}" "${buildScript}"`;
|
||||
|
||||
buildDistPackagesOnPlatform('linux');
|
||||
@ -288,7 +288,10 @@ describe('NgPackagesInstaller', () => {
|
||||
});
|
||||
|
||||
describe('_getDistPackages()', () => {
|
||||
beforeEach(() => spyOn(NgPackagesInstaller.prototype, '_buildDistPackages'));
|
||||
beforeEach(() => {
|
||||
fs.existsSync.and.callThrough();
|
||||
spyOn(NgPackagesInstaller.prototype, '_buildDistPackages');
|
||||
});
|
||||
|
||||
it('should not build the local packages by default', () => {
|
||||
installer._getDistPackages();
|
||||
@ -309,7 +312,7 @@ describe('NgPackagesInstaller', () => {
|
||||
it('should include top level Angular packages', () => {
|
||||
const ngPackages = installer._getDistPackages();
|
||||
const expectedValue = jasmine.objectContaining({
|
||||
parentDir: jasmine.any(String),
|
||||
packageDir: jasmine.any(String),
|
||||
packageJsonPath: jasmine.any(String),
|
||||
config: jasmine.any(Object),
|
||||
});
|
||||
@ -323,12 +326,12 @@ describe('NgPackagesInstaller', () => {
|
||||
expect(ngPackages['@angular/upgrade/static']).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('should store each package\'s parent directory', () => {
|
||||
it('should store each package\'s directory', () => {
|
||||
const ngPackages = installer._getDistPackages();
|
||||
|
||||
// For example...
|
||||
expect(ngPackages['@angular/core'].parentDir).toBe(packagesDir);
|
||||
expect(ngPackages['@angular/router'].parentDir).toBeDefined(toolsDir);
|
||||
expect(ngPackages['@angular/core'].packageDir).toBe(path.join(packagesDir, 'core'));
|
||||
expect(ngPackages['@angular/router'].packageDir).toBe(path.join(packagesDir, 'router'));
|
||||
});
|
||||
|
||||
it('should not include packages that have been ignored', () => {
|
||||
|
@ -45,6 +45,11 @@ module.exports = new Package('angular-base', [
|
||||
.factory(require('./post-processors/add-image-dimensions'))
|
||||
.factory(require('./post-processors/auto-link-code'))
|
||||
|
||||
// Configure jsdoc-style tag parsing
|
||||
.config(function(inlineTagProcessor) {
|
||||
inlineTagProcessor.inlineTagDefinitions.push(require('./inline-tag-defs/custom-search-defs/'));
|
||||
})
|
||||
|
||||
.config(function(checkAnchorLinksProcessor) {
|
||||
// This is disabled here to prevent false negatives for the `docs-watch` task.
|
||||
// It is re-enabled in the main `angular.io-package`
|
||||
|
9
aio/tools/transforms/angular-base-package/inline-tag-defs/custom-search-defs/index.js
vendored
Normal file
9
aio/tools/transforms/angular-base-package/inline-tag-defs/custom-search-defs/index.js
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
name: 'searchKeywords',
|
||||
description: 'A shorthand for creating elements with search terms. Usage: `{@searchKeywords term1 term2 termN }`',
|
||||
handler: function(doc, tagName, tagDescription) {
|
||||
doc.searchKeywords = tagDescription;
|
||||
return doc;
|
||||
}
|
||||
};
|
||||
|
@ -91,7 +91,8 @@ module.exports = function generateKeywordsProcessor(log, readFilesProcessor) {
|
||||
titleWords: tokenize(doc.searchTitle).join(' '),
|
||||
headingWords: headingWords.sort().join(' '),
|
||||
keywords: words.sort().join(' '),
|
||||
members: members.sort().join(' ')
|
||||
members: members.sort().join(' '),
|
||||
topics: doc.searchKeywords
|
||||
};
|
||||
|
||||
});
|
||||
|
@ -4,10 +4,14 @@ module.exports = function processCliCommands(createDocMessage) {
|
||||
$runBefore: ['rendering-docs'],
|
||||
$process(docs) {
|
||||
const navigationDoc = docs.find(doc => doc.docType === 'navigation-json');
|
||||
const navigationNode = navigationDoc && navigationDoc.data['SideNav'].find(node => node.children && node.children.length && node.children[0].url === 'cli');
|
||||
const navigationNode = navigationDoc &&
|
||||
navigationDoc.data['SideNav'].find(
|
||||
node => node.children && node.children.length && node.children[0].url === 'cli');
|
||||
|
||||
if (!navigationNode) {
|
||||
throw new Error(createDocMessage('Missing `cli` url - CLI Commands must include a first child node with url set at `cli`', navigationDoc));
|
||||
throw new Error(createDocMessage(
|
||||
'Missing `cli` url - CLI Commands must include a first child node with url set at `cli`',
|
||||
navigationDoc));
|
||||
}
|
||||
|
||||
docs.forEach(doc => {
|
||||
@ -15,26 +19,31 @@ module.exports = function processCliCommands(createDocMessage) {
|
||||
doc.names = collectNames(doc.name, doc.commandAliases);
|
||||
|
||||
// Recursively process the options
|
||||
processOptions(doc, doc.options);
|
||||
const optionKeywords = new Set();
|
||||
processOptions(doc, doc.options, optionKeywords);
|
||||
doc.optionKeywords = Array.from(optionKeywords).join(' ');
|
||||
|
||||
// Add to navigation doc
|
||||
navigationNode.children.push({ url: doc.path, title: `ng ${doc.name}` });
|
||||
navigationNode.children.push({url: doc.path, title: `ng ${doc.name}`});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function processOptions(container, options) {
|
||||
function processOptions(container, options, optionKeywords) {
|
||||
container.positionalOptions = [];
|
||||
container.namedOptions = [];
|
||||
|
||||
options.forEach(option => {
|
||||
// Ignore any hidden options
|
||||
if (option.hidden) { return; }
|
||||
if (option.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
option.types = option.types || [option.type];
|
||||
option.names = collectNames(option.name, option.aliases);
|
||||
option.names.forEach(name => optionKeywords.add(name));
|
||||
|
||||
// Now work out what kind of option it is: positional/named
|
||||
if (option.positional !== undefined) {
|
||||
@ -48,7 +57,8 @@ function processOptions(container, options) {
|
||||
option.subcommands = getValues(option.subcommands);
|
||||
option.subcommands.forEach(subcommand => {
|
||||
subcommand.names = collectNames(subcommand.name, subcommand.aliases);
|
||||
processOptions(subcommand, subcommand.options);
|
||||
subcommand.names.forEach(name => optionKeywords.add(name));
|
||||
processOptions(subcommand, subcommand.options, optionKeywords);
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -57,7 +67,7 @@ function processOptions(container, options) {
|
||||
}
|
||||
|
||||
function collectNames(name, aliases) {
|
||||
return [name].concat(aliases);
|
||||
return [name].concat(aliases || []);
|
||||
}
|
||||
|
||||
function getValues(obj) {
|
||||
|
@ -7,11 +7,7 @@ describe('processCliCommands processor', () => {
|
||||
|
||||
const navigationStub = {
|
||||
docType: 'navigation-json',
|
||||
data: {
|
||||
SideNav: [{
|
||||
children: [{'url': 'cli'}]
|
||||
}]
|
||||
}
|
||||
data: {SideNav: [{children: [{'url': 'cli'}]}]}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@ -21,17 +17,13 @@ describe('processCliCommands processor', () => {
|
||||
createDocMessage = injector.get('createDocMessage');
|
||||
});
|
||||
|
||||
it('should be available on the injector', () => {
|
||||
expect(processor.$process).toBeDefined();
|
||||
});
|
||||
it('should be available on the injector', () => { expect(processor.$process).toBeDefined(); });
|
||||
|
||||
it('should run after the correct processor', () => {
|
||||
expect(processor.$runAfter).toEqual(['extra-docs-added']);
|
||||
});
|
||||
it('should run after the correct processor',
|
||||
() => { expect(processor.$runAfter).toEqual(['extra-docs-added']); });
|
||||
|
||||
it('should run before the correct processor', () => {
|
||||
expect(processor.$runBefore).toEqual(['rendering-docs']);
|
||||
});
|
||||
it('should run before the correct processor',
|
||||
() => { expect(processor.$runBefore).toEqual(['rendering-docs']); });
|
||||
|
||||
it('should collect the names (name + aliases)', () => {
|
||||
const doc = {
|
||||
@ -51,16 +43,16 @@ describe('processCliCommands processor', () => {
|
||||
name: 'name',
|
||||
commandAliases: [],
|
||||
options: [
|
||||
{ name: 'option1' },
|
||||
{ name: 'option2', hidden: true },
|
||||
{ name: 'option3' },
|
||||
{ name: 'option4', hidden: true },
|
||||
{name: 'option1'},
|
||||
{name: 'option2', hidden: true},
|
||||
{name: 'option3'},
|
||||
{name: 'option4', hidden: true},
|
||||
],
|
||||
};
|
||||
processor.$process([doc, navigationStub]);
|
||||
expect(doc.namedOptions).toEqual([
|
||||
jasmine.objectContaining({ name: 'option1' }),
|
||||
jasmine.objectContaining({ name: 'option3' }),
|
||||
jasmine.objectContaining({name: 'option1'}),
|
||||
jasmine.objectContaining({name: 'option3'}),
|
||||
]);
|
||||
});
|
||||
|
||||
@ -70,18 +62,18 @@ describe('processCliCommands processor', () => {
|
||||
name: 'name',
|
||||
commandAliases: [],
|
||||
options: [
|
||||
{ name: 'named1' },
|
||||
{ name: 'positional1', positional: 0},
|
||||
{ name: 'named2', hidden: true },
|
||||
{ name: 'positional2', hidden: true, positional: 1},
|
||||
{name: 'named1'},
|
||||
{name: 'positional1', positional: 0},
|
||||
{name: 'named2', hidden: true},
|
||||
{name: 'positional2', hidden: true, positional: 1},
|
||||
],
|
||||
};
|
||||
processor.$process([doc, navigationStub]);
|
||||
expect(doc.positionalOptions).toEqual([
|
||||
jasmine.objectContaining({ name: 'positional1', positional: 0}),
|
||||
jasmine.objectContaining({name: 'positional1', positional: 0}),
|
||||
]);
|
||||
expect(doc.namedOptions).toEqual([
|
||||
jasmine.objectContaining({ name: 'named1' }),
|
||||
jasmine.objectContaining({name: 'named1'}),
|
||||
]);
|
||||
});
|
||||
|
||||
@ -91,18 +83,34 @@ describe('processCliCommands processor', () => {
|
||||
name: 'name',
|
||||
commandAliases: [],
|
||||
options: [
|
||||
{ name: 'c' },
|
||||
{ name: 'a' },
|
||||
{ name: 'b' },
|
||||
{name: 'c'},
|
||||
{name: 'a'},
|
||||
{name: 'b'},
|
||||
],
|
||||
};
|
||||
processor.$process([doc, navigationStub]);
|
||||
expect(doc.namedOptions).toEqual([
|
||||
jasmine.objectContaining({ name: 'a' }),
|
||||
jasmine.objectContaining({ name: 'b' }),
|
||||
jasmine.objectContaining({ name: 'c' }),
|
||||
jasmine.objectContaining({name: 'a'}),
|
||||
jasmine.objectContaining({name: 'b'}),
|
||||
jasmine.objectContaining({name: 'c'}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should collect potential search terms from options for indexing', () => {
|
||||
const doc = {
|
||||
docType: 'cli-command',
|
||||
name: 'name',
|
||||
commandAliases: [],
|
||||
options: [
|
||||
{name: 'named1'},
|
||||
{name: 'positional1', positional: 0},
|
||||
{name: 'named2', hidden: true},
|
||||
{name: 'positional2', hidden: true, positional: 1},
|
||||
],
|
||||
};
|
||||
processor.$process([doc, navigationStub]);
|
||||
expect(doc.optionKeywords).toEqual('named1 positional1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('subcommands', () => {
|
||||
@ -117,15 +125,15 @@ describe('processCliCommands processor', () => {
|
||||
subcommand1: {
|
||||
name: 'subcommand1',
|
||||
options: [
|
||||
{ name: 'subcommand1-option1' },
|
||||
{ name: 'subcommand1-option2' },
|
||||
{name: 'subcommand1-option1'},
|
||||
{name: 'subcommand1-option2'},
|
||||
],
|
||||
},
|
||||
subcommand2: {
|
||||
name: 'subcommand2',
|
||||
options: [
|
||||
{ name: 'subcommand2-option1' },
|
||||
{ name: 'subcommand2-option2' },
|
||||
{name: 'subcommand2-option1'},
|
||||
{name: 'subcommand2-option2'},
|
||||
],
|
||||
}
|
||||
},
|
||||
@ -133,8 +141,8 @@ describe('processCliCommands processor', () => {
|
||||
};
|
||||
processor.$process([doc, navigationStub]);
|
||||
expect(doc.options[0].subcommands).toEqual([
|
||||
jasmine.objectContaining({ name: 'subcommand1' }),
|
||||
jasmine.objectContaining({ name: 'subcommand2' }),
|
||||
jasmine.objectContaining({name: 'subcommand1'}),
|
||||
jasmine.objectContaining({name: 'subcommand2'}),
|
||||
]);
|
||||
});
|
||||
|
||||
@ -149,15 +157,15 @@ describe('processCliCommands processor', () => {
|
||||
subcommand1: {
|
||||
name: 'subcommand1',
|
||||
options: [
|
||||
{ name: 'subcommand1-option1' },
|
||||
{ name: 'subcommand1-option2', hidden: true },
|
||||
{name: 'subcommand1-option1'},
|
||||
{name: 'subcommand1-option2', hidden: true},
|
||||
],
|
||||
},
|
||||
subcommand2: {
|
||||
name: 'subcommand2',
|
||||
options: [
|
||||
{ name: 'subcommand2-option1', hidden: true },
|
||||
{ name: 'subcommand2-option2' },
|
||||
{name: 'subcommand2-option1', hidden: true},
|
||||
{name: 'subcommand2-option2'},
|
||||
],
|
||||
}
|
||||
},
|
||||
@ -165,10 +173,10 @@ describe('processCliCommands processor', () => {
|
||||
};
|
||||
processor.$process([doc, navigationStub]);
|
||||
expect(doc.options[0].subcommands[0].namedOptions).toEqual([
|
||||
jasmine.objectContaining({ name: 'subcommand1-option1' }),
|
||||
jasmine.objectContaining({name: 'subcommand1-option1'}),
|
||||
]);
|
||||
expect(doc.options[0].subcommands[1].namedOptions).toEqual([
|
||||
jasmine.objectContaining({ name: 'subcommand2-option2' }),
|
||||
jasmine.objectContaining({name: 'subcommand2-option2'}),
|
||||
]);
|
||||
});
|
||||
|
||||
@ -183,15 +191,15 @@ describe('processCliCommands processor', () => {
|
||||
subcommand1: {
|
||||
name: 'subcommand1',
|
||||
options: [
|
||||
{ name: 'subcommand1-option1' },
|
||||
{ name: 'subcommand1-option2', positional: 0 },
|
||||
{name: 'subcommand1-option1'},
|
||||
{name: 'subcommand1-option2', positional: 0},
|
||||
],
|
||||
},
|
||||
subcommand2: {
|
||||
name: 'subcommand2',
|
||||
options: [
|
||||
{ name: 'subcommand2-option1', hidden: true },
|
||||
{ name: 'subcommand2-option2', hidden: true, positional: 1 },
|
||||
{name: 'subcommand2-option1', hidden: true},
|
||||
{name: 'subcommand2-option2', hidden: true, positional: 1},
|
||||
],
|
||||
}
|
||||
},
|
||||
@ -199,10 +207,10 @@ describe('processCliCommands processor', () => {
|
||||
};
|
||||
processor.$process([doc, navigationStub]);
|
||||
expect(doc.options[0].subcommands[0].positionalOptions).toEqual([
|
||||
jasmine.objectContaining({ name: 'subcommand1-option2', positional: 0}),
|
||||
jasmine.objectContaining({name: 'subcommand1-option2', positional: 0}),
|
||||
]);
|
||||
expect(doc.options[0].subcommands[0].namedOptions).toEqual([
|
||||
jasmine.objectContaining({ name: 'subcommand1-option1' }),
|
||||
jasmine.objectContaining({name: 'subcommand1-option1'}),
|
||||
]);
|
||||
|
||||
expect(doc.options[0].subcommands[1].positionalOptions).toEqual([]);
|
||||
@ -220,9 +228,9 @@ describe('processCliCommands processor', () => {
|
||||
subcommand1: {
|
||||
name: 'subcommand1',
|
||||
options: [
|
||||
{ name: 'c' },
|
||||
{ name: 'a' },
|
||||
{ name: 'b' },
|
||||
{name: 'c'},
|
||||
{name: 'a'},
|
||||
{name: 'b'},
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -230,47 +238,42 @@ describe('processCliCommands processor', () => {
|
||||
};
|
||||
processor.$process([doc, navigationStub]);
|
||||
expect(doc.options[0].subcommands[0].namedOptions).toEqual([
|
||||
jasmine.objectContaining({ name: 'a' }),
|
||||
jasmine.objectContaining({ name: 'b' }),
|
||||
jasmine.objectContaining({ name: 'c' }),
|
||||
jasmine.objectContaining({name: 'a'}),
|
||||
jasmine.objectContaining({name: 'b'}),
|
||||
jasmine.objectContaining({name: 'c'}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should add the command to the CLI node in the navigation doc if there is a first child node with a `cli` url', () => {
|
||||
const command = {
|
||||
docType: 'cli-command',
|
||||
name: 'command1',
|
||||
commandAliases: ['alias1', 'alias2'],
|
||||
options: [],
|
||||
path: 'cli/command1',
|
||||
};
|
||||
const navigation = {
|
||||
docType: 'navigation-json',
|
||||
data: {
|
||||
SideNav: [
|
||||
{ url: 'some/page', title: 'Some Page' },
|
||||
{
|
||||
title: 'CLI Commands',
|
||||
tooltip: 'Angular CLI command reference',
|
||||
children: [
|
||||
{
|
||||
'title': 'Overview',
|
||||
'url': 'cli'
|
||||
}
|
||||
]
|
||||
},
|
||||
{ url: 'other/page', title: 'Other Page' }
|
||||
]
|
||||
}
|
||||
};
|
||||
processor.$process([command, navigation]);
|
||||
expect(navigation.data.SideNav[1].title).toEqual('CLI Commands');
|
||||
expect(navigation.data.SideNav[1].children).toEqual([
|
||||
{ url: 'cli', title: 'Overview' },
|
||||
{ url: 'cli/command1', title: 'ng command1' },
|
||||
]);
|
||||
});
|
||||
it('should add the command to the CLI node in the navigation doc if there is a first child node with a `cli` url',
|
||||
() => {
|
||||
const command = {
|
||||
docType: 'cli-command',
|
||||
name: 'command1',
|
||||
commandAliases: ['alias1', 'alias2'],
|
||||
options: [],
|
||||
path: 'cli/command1',
|
||||
};
|
||||
const navigation = {
|
||||
docType: 'navigation-json',
|
||||
data: {
|
||||
SideNav: [
|
||||
{url: 'some/page', title: 'Some Page'}, {
|
||||
title: 'CLI Commands',
|
||||
tooltip: 'Angular CLI command reference',
|
||||
children: [{'title': 'Overview', 'url': 'cli'}]
|
||||
},
|
||||
{url: 'other/page', title: 'Other Page'}
|
||||
]
|
||||
}
|
||||
};
|
||||
processor.$process([command, navigation]);
|
||||
expect(navigation.data.SideNav[1].title).toEqual('CLI Commands');
|
||||
expect(navigation.data.SideNav[1].children).toEqual([
|
||||
{url: 'cli', title: 'Overview'},
|
||||
{url: 'cli/command1', title: 'ng command1'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should complain if there is no child with `cli` url', () => {
|
||||
const command = {
|
||||
@ -284,21 +287,50 @@ describe('processCliCommands processor', () => {
|
||||
docType: 'navigation-json',
|
||||
data: {
|
||||
SideNav: [
|
||||
{ url: 'some/page', title: 'Some Page' },
|
||||
{
|
||||
{url: 'some/page', title: 'Some Page'}, {
|
||||
title: 'CLI Commands',
|
||||
tooltip: 'Angular CLI command reference',
|
||||
children: [
|
||||
{
|
||||
'title': 'Overview',
|
||||
'url': 'client'
|
||||
}
|
||||
]
|
||||
children: [{'title': 'Overview', 'url': 'client'}]
|
||||
},
|
||||
{ url: 'other/page', title: 'Other Page' }
|
||||
{url: 'other/page', title: 'Other Page'}
|
||||
]
|
||||
}
|
||||
};
|
||||
expect(() => processor.$process([command, navigation])).toThrowError(createDocMessage('Missing `cli` url - CLI Commands must include a first child node with url set at `cli`', navigation));
|
||||
expect(() => processor.$process([command, navigation]))
|
||||
.toThrowError(createDocMessage(
|
||||
'Missing `cli` url - CLI Commands must include a first child node with url set at `cli`',
|
||||
navigation));
|
||||
});
|
||||
|
||||
it('should collect potential search terms from options for indexing', () => {
|
||||
const doc = {
|
||||
docType: 'cli-command',
|
||||
name: 'name',
|
||||
commandAliases: [],
|
||||
options: [{
|
||||
name: 'supercommand',
|
||||
subcommands: {
|
||||
subcommand1: {
|
||||
name: 'subcommand1',
|
||||
options: [
|
||||
{name: 'subcommand1-option1'},
|
||||
{name: 'subcommand1-option2'},
|
||||
],
|
||||
},
|
||||
subcommand2: {
|
||||
name: 'subcommand2',
|
||||
options: [
|
||||
{name: 'subcommand2-option1'},
|
||||
{name: 'subcommand2-option2'},
|
||||
],
|
||||
}
|
||||
},
|
||||
}],
|
||||
};
|
||||
processor.$process([doc, navigationStub]);
|
||||
expect(doc.optionKeywords)
|
||||
.toEqual(
|
||||
'supercommand subcommand1 subcommand1-option1 subcommand1-option2 subcommand2 subcommand2-option1 subcommand2-option2');
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -23,7 +23,7 @@
|
||||
{% block header %}
|
||||
<header class="api-header">
|
||||
<h1>{$ doc.name $}</h1>
|
||||
<label class="api-type-label {$ doc.docType $}">{$ doc.docType $}</label>
|
||||
{% if doc.isPrimaryPackage %}<label class="api-type-label package">package</label>{% else %}<label class="api-type-label {$ entry-point $}">entry-point</label>{% endif %}
|
||||
{% if doc.packageDeprecated or (not doc.isPrimaryPackage and doc.deprecated !== undefined) %}<label class="api-status-label deprecated">deprecated</label>{% endif %}
|
||||
{% if doc.security !== undefined %}<label class="api-status-label security">security</label>{% endif %}
|
||||
{% if doc.pipeOptions.pure === 'false' %}<label class="api-status-label impure-pipe">impure</label>{% endif %}
|
||||
|
4121
aio/yarn.lock
4121
aio/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -27,18 +27,14 @@ var CIconfiguration = {
|
||||
'IE10': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
'IE11': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
'Edge': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
|
||||
'Android4.4': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
|
||||
'Android5': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
|
||||
'Android6': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
'Android7': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
'Safari7': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
|
||||
'Safari8': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
|
||||
'Safari9': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
|
||||
'Safari10': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
|
||||
'iOS7': {unitTest: {target: 'BS', required: true}, e2e: {target: null, required: true}},
|
||||
'iOS8': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
|
||||
'iOS9': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
|
||||
'Android8': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
'Android9': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
'Android10': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
'Safari12': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
|
||||
'Safari13': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
|
||||
'iOS10': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
|
||||
'iOS11': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
|
||||
'WindowsPhone': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}}
|
||||
};
|
||||
|
||||
@ -98,6 +94,27 @@ var customLaunchers = {
|
||||
version: '7.1',
|
||||
device: 'Android GoogleAPI Emulator'
|
||||
},
|
||||
'SL_ANDROID8': {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'Chrome',
|
||||
platform: 'Android',
|
||||
version: '8.0',
|
||||
device: 'Android GoogleAPI Emulator'
|
||||
},
|
||||
'SL_ANDROID9': {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'Chrome',
|
||||
platform: 'Android',
|
||||
version: '9.0',
|
||||
device: 'Android GoogleAPI Emulator'
|
||||
},
|
||||
'SL_ANDROID10': {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'Chrome',
|
||||
platform: 'Android',
|
||||
version: '10.0',
|
||||
device: 'Android GoogleAPI Emulator'
|
||||
},
|
||||
|
||||
'BS_CHROME': {base: 'BrowserStack', browser: 'chrome', os: 'OS X', os_version: 'Yosemite'},
|
||||
'BS_FIREFOX': {base: 'BrowserStack', browser: 'firefox', os: 'Windows', os_version: '10'},
|
||||
|
@ -4,7 +4,7 @@ Currently all changes to Ivy are validated against the test suite of the
|
||||
`angular/components` repository. In order to debug the `components-repo-unit-tests` CI
|
||||
job, the following steps can be used:
|
||||
|
||||
1\) Build the Ivy package output by running `node ./scripts/build-ivy-npm-packages.js` in
|
||||
1\) Build the Ivy package output by running `node ./scripts/build/build-ivy-npm-packages.js` in
|
||||
the `angular/angular` repo.
|
||||
|
||||
2\) Clone the `angular/components` repository if not done yet ([quick link to repo](https://github.com/angular/components)).
|
||||
|
@ -69,7 +69,7 @@ yarn install
|
||||
To build Angular run:
|
||||
|
||||
```shell
|
||||
node ./scripts/build-packages-dist.js
|
||||
node ./scripts/build/build-packages-dist.js
|
||||
```
|
||||
|
||||
* Results are put in the `dist/packages-dist` folder.
|
||||
@ -129,7 +129,7 @@ where `$ANGULAR_PATH` is an environment variable of the absolute path of your An
|
||||
You can check that your code is properly formatted and adheres to coding style by running:
|
||||
|
||||
``` shell
|
||||
$ yarn gulp lint
|
||||
$ yarn lint
|
||||
```
|
||||
|
||||
## Publishing Snapshot Builds
|
||||
@ -190,7 +190,7 @@ c. Some package managers (such as `pnpm` or `yarn pnp`) might not work correctly
|
||||
### Publishing to GitHub repos
|
||||
You can also manually publish `*-builds` snapshots just like our CircleCI build does for upstream
|
||||
builds. Before being able to publish the packages, you need to build them locally by running the
|
||||
`./scripts/build-packages-dist.js` script.
|
||||
`./scripts/build/build-packages-dist.js` script.
|
||||
|
||||
First time, you need to create the GitHub repositories:
|
||||
|
||||
|
@ -41,7 +41,7 @@ cef93a51b (pr/24623_top) ci: scripts to review PRs locally
|
||||
637805a0c (pr/24623_base) docs: update `lowercase` pipe example in "AngularJS to Angular" guide (#24588)
|
||||
```
|
||||
|
||||
Knowing `pr/24623_top` and `pr/24623_base` makes it convenient to refer to different SHAs in PR when rebasing or reseting.
|
||||
Knowing `pr/24623_top` and `pr/24623_base` makes it convenient to refer to different SHAs in PR when rebasing or resetting.
|
||||
|
||||
### 2. Review PR
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user