mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
220 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ea541f4f8 | |||
| dd7eeb6929 | |||
| 1d204fcf2c | |||
| 7619c16697 | |||
| 52a564df94 | |||
| bb7b51bbc6 | |||
| 254d6cea9a | |||
| 1e8fe84c05 | |||
| 22ef5e0e60 | |||
| c4e1d45418 | |||
| d5a522a40a | |||
| ed832b4dd7 | |||
| ebf98c4695 | |||
| ac8c896fff | |||
| 37b3ac7f69 | |||
| b5820b6fa4 | |||
| 26fe7642fe | |||
| 1564771b3a | |||
| 989ba8fb65 | |||
| 9317f513d5 | |||
| d68333e508 | |||
| 4766817f02 | |||
| 575cafc163 | |||
| acbd2e9339 | |||
| a1c741846d | |||
| 8d09451d89 | |||
| ec115a31b8 | |||
| cbfe6284a2 | |||
| e50fd9ee87 | |||
| 5fe59706fd | |||
| cfb54c6a7b | |||
| 14268012ad | |||
| db7201fd84 | |||
| fc145d024b | |||
| 44b9d89917 | |||
| ae1db7a88d | |||
| 5c9fe8de29 | |||
| c49d23269c | |||
| 24212cd7ef | |||
| d5b7c9610d | |||
| ddd822fdc5 | |||
| d31f4de575 | |||
| 5900f0e133 | |||
| 61894570f1 | |||
| 09c5c18c7d | |||
| f09d16600b | |||
| 1b2c67d9fd | |||
| 2144b997fa | |||
| 208d81921d | |||
| 18f98e671b | |||
| dbd6e75b37 | |||
| 6a5ee688df | |||
| fb5e16e018 | |||
| 05121c0381 | |||
| a92bceda54 | |||
| b46b25c562 | |||
| fc86fe5dad | |||
| 9fcc1b7758 | |||
| ca7a143376 | |||
| 5db99cae22 | |||
| a3d9c82ca4 | |||
| c21b68dcb8 | |||
| 9c9a52540c | |||
| 7f53c0f4ac | |||
| 3f55567111 | |||
| fa90c74a73 | |||
| 09f8883826 | |||
| 7ab7734582 | |||
| 5061c4d41d | |||
| 3b22121c38 | |||
| 9c03b6371a | |||
| 6e45777f01 | |||
| 70006daac4 | |||
| 63539489da | |||
| f7c9ac16ea | |||
| 7bbf009216 | |||
| 0b96c37986 | |||
| 9a82067c03 | |||
| 14cabb00d2 | |||
| a1d9ce80f6 | |||
| 52e036a193 | |||
| c08705899f | |||
| 0c28997739 | |||
| 0dd532bcd6 | |||
| 8d7fd91a67 | |||
| 88dbeda931 | |||
| a15c7e8f98 | |||
| 5ab0ca53b6 | |||
| c24ea4b4f5 | |||
| c3b3b73e56 | |||
| b73d5aebe0 | |||
| 3f5e685ca9 | |||
| 69e4c9813e | |||
| 1eaaa5d1cc | |||
| 97e3b00e5b | |||
| 1a149bb50d | |||
| 29d4b78ada | |||
| 2e79d09d85 | |||
| dbd6be829f | |||
| f869b9146a | |||
| 1a05e2dc48 | |||
| aa29fc7b5c | |||
| b481eab39d | |||
| 2b0da62937 | |||
| 960e42b2ac | |||
| cc90fd55b2 | |||
| 1734d1e07e | |||
| ee20410a98 | |||
| 4321e4cdec | |||
| 3714305f84 | |||
| e5300d6d75 | |||
| 3700323e1c | |||
| 1398acd90c | |||
| f00f8c4863 | |||
| 3148ab93a9 | |||
| 7ff4c0bf39 | |||
| 2d51587f36 | |||
| bb9b33053c | |||
| 7eddf582a1 | |||
| 2a437616bb | |||
| 956b022254 | |||
| b839aeb2c1 | |||
| 0f8e748b07 | |||
| 895c5417e3 | |||
| 6336b8e5b3 | |||
| 10de52887b | |||
| 6f1c941dfe | |||
| 9bce9ce89e | |||
| 8227eb914e | |||
| 23003f817f | |||
| d98ce5ed4f | |||
| cff1c5622f | |||
| 6efa366e2c | |||
| ad9d98146e | |||
| 8390e12bdd | |||
| 43bbaf810d | |||
| 6842bf0515 | |||
| f261fa7c08 | |||
| d29883adff | |||
| de67c9fd01 | |||
| 9a8ef7cc28 | |||
| 4e1d825d83 | |||
| 356c521195 | |||
| 9a7eba2352 | |||
| e16dab82a1 | |||
| 913bbba42b | |||
| c7e5cb0371 | |||
| aa616db24f | |||
| de9290096c | |||
| 2c7d96afcb | |||
| 8bf3b53a1d | |||
| 56ca7d385b | |||
| e8fd452bd2 | |||
| 71ff12c1cc | |||
| 82d772857c | |||
| 2bebaa99cf | |||
| 8907fbbfed | |||
| 84b4ad812b | |||
| 76c6dca93f | |||
| 85546594a5 | |||
| 55b8c4f701 | |||
| 1e6f700b7d | |||
| 01fde59dd9 | |||
| a4debf14e3 | |||
| c6e64aa8ad | |||
| 7379d75782 | |||
| 21a51fb6a4 | |||
| b39db80f91 | |||
| 3b86dda068 | |||
| 64da1daa78 | |||
| d388522745 | |||
| 4f1870362e | |||
| 6c906a5bb9 | |||
| 6c61d20476 | |||
| e914da145b | |||
| 6550c59df5 | |||
| 032276253f | |||
| d607d0b93c | |||
| c02e4d72bc | |||
| 4d2ef0f5df | |||
| 1a4b489692 | |||
| cbd23ee0de | |||
| 5d08390584 | |||
| 9b1d3d2fce | |||
| f71eb58036 | |||
| f7a2dd88aa | |||
| 031e23b06a | |||
| 5b0980f050 | |||
| 54d09a6bc0 | |||
| 782f678236 | |||
| 62cec95981 | |||
| 2ec696a50f | |||
| 119fbddb83 | |||
| e8e09d3455 | |||
| d7c20c8886 | |||
| 3e1fad406a | |||
| aeeb1b55d3 | |||
| 331383b39b | |||
| 0050b01b62 | |||
| e632fd1f0f | |||
| 128038e486 | |||
| 42c8533f42 | |||
| 2cc8fb0a08 | |||
| 4013db6fd2 | |||
| 95ee74a66c | |||
| a782be62e9 | |||
| dc4115d7ed | |||
| d8a18717da | |||
| dd177e2c18 | |||
| 0b8208690c | |||
| 7af1f0a265 | |||
| d0b88a6810 | |||
| fd860e3ef7 | |||
| a01ae487d7 | |||
| 3769de2caa | |||
| 835b326759 | |||
| 92cf9fbebe | |||
| 09f0254bdd | |||
| 7b9490ad64 | |||
| f0e82c8af4 |
+1
-1
@@ -1 +1 @@
|
||||
4.2.2
|
||||
5.0.0
|
||||
|
||||
+67
-52
@@ -31,12 +31,12 @@ var_3: &cache_key v6-angular-node-14-{{ checksum "month.txt" }}-{{ checksum ".ba
|
||||
var_4: &cache_key_fallback v6-angular-node-14-{{ checksum "month.txt" }}-{{ checksum ".bazelversion" }}
|
||||
|
||||
# Windows needs its own cache key because binaries in node_modules are different.
|
||||
var_3_win: &cache_key_win v6-angular-win-node-14-{{ checksum "month.txt" }}-{{ checksum ".bazelversion" }}-{{ checksum "yarn.lock" }}-{{ checksum "WORKSPACE" }}-{{ checksum "aio/yarn.lock" }}
|
||||
var_4_win: &cache_key_win_fallback v6-angular-win-node-14-{{ checksum "month.txt" }}-{{ checksum ".bazelversion" }}
|
||||
var_3_win: &cache_key_win v7-angular-win-node-14-{{ checksum "month.txt" }}-{{ checksum ".bazelversion" }}-{{ checksum "yarn.lock" }}-{{ checksum "WORKSPACE" }}
|
||||
var_4_win: &cache_key_win_fallback v7-angular-win-node-14-{{ checksum "month.txt" }}-{{ checksum ".bazelversion" }}
|
||||
|
||||
# Cache key for the `components-repo-unit-tests` job. **Note** when updating the SHA in the
|
||||
# cache keys also update the SHA for the "COMPONENTS_REPO_COMMIT" environment variable.
|
||||
var_5: &components_repo_unit_tests_cache_key v1-angular-components-{{ checksum "month.txt" }}-d1cd928714c2d6e3de75f9469ce58b06aad6353c
|
||||
var_5: &components_repo_unit_tests_cache_key v1-angular-components-{{ checksum "month.txt" }}-e65f5f5bafcd00dbb64387878fb866283909a2dd
|
||||
var_6: &components_repo_unit_tests_cache_key_fallback v1-angular-components-{{ checksum "month.txt" }}
|
||||
|
||||
# Workspace initially persisted by the `setup` job, and then enhanced by `build-npm-packages`.
|
||||
@@ -58,11 +58,13 @@ var_9: &skip_on_pull_requests
|
||||
ignore:
|
||||
- /pull\/\d+/
|
||||
|
||||
# Filter to run a job on builds for the master branch only.
|
||||
var_10: &only_on_master
|
||||
# Filter to run a job on builds for the main branch only.
|
||||
var_10: &only_on_main_branch
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
# TODO(BRANCH_RENAME_CLEANUP): remove
|
||||
- master
|
||||
|
||||
# Filter to run a job on all releasable branches.
|
||||
@@ -70,9 +72,16 @@ var_11: &only_release_branches
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
# TODO(BRANCH_RENAME_CLEANUP): remove
|
||||
- master
|
||||
- /\d+\.\d+\.x/
|
||||
|
||||
# CircleCI orbs
|
||||
# https://circleci.com/developer/orbs.
|
||||
orbs:
|
||||
node: circleci/node@5.0.1
|
||||
|
||||
# Executor Definitions
|
||||
# https://circleci.com/docs/2.0/reusing-config/#authoring-reusable-executors
|
||||
# **NOTE 1**: Pin to exact images using an ID (SHA). See https://circleci.com/docs/2.0/circleci-images/#using-a-docker-image-id-to-pin-an-image-to-a-fixed-version.
|
||||
@@ -103,20 +112,8 @@ executors:
|
||||
windows-executor:
|
||||
working_directory: ~/ng
|
||||
resource_class: windows.2xlarge
|
||||
# CircleCI windows VMs do have the GitBash shell available:
|
||||
# https://github.com/CircleCI-Public/windows-preview-docs#shells
|
||||
# But in this specific case we really should not use it because Bazel must not be ran from
|
||||
# GitBash. These issues discuss why:
|
||||
# https://github.com/bazelbuild/bazel/issues/5751
|
||||
# https://github.com/bazelbuild/bazel/issues/5724#issuecomment-410194038
|
||||
# https://github.com/bazelbuild/bazel/issues/6339#issuecomment-441600879
|
||||
shell: powershell.exe -ExecutionPolicy Bypass
|
||||
shell: bash
|
||||
machine:
|
||||
# Windows preview image that includes the following:
|
||||
# - Visual Studio 2019 build tools
|
||||
# - Node 14
|
||||
# - yarn 1.17
|
||||
# - Python 3 3.7.4
|
||||
image: windows-server-2019-vs2019:201908-02
|
||||
|
||||
# Command Definitions
|
||||
@@ -188,30 +185,18 @@ commands:
|
||||
steps:
|
||||
- run:
|
||||
name: Save month to file
|
||||
# Note: Make sure this file is excluded in the `.gitignore` as otherwise the
|
||||
# snapshot stamping would have the `-with-local-changes` suffix.
|
||||
command: date +%Y-%m > month.txt
|
||||
|
||||
# Normally this would be an individual job instead of a command.
|
||||
# But startup and setup time for each individual windows job are high enough to discourage
|
||||
# many small jobs, so instead we use a command for setup unless the gain becomes significant.
|
||||
setup_win:
|
||||
description: Setup windows node environment
|
||||
yarn_install:
|
||||
description: Install dependencies
|
||||
steps:
|
||||
- checkout
|
||||
- save_month_to_file
|
||||
# Install Bazel pre-requisites that aren't in the preconfigured CircleCI Windows VM.
|
||||
- run: ./.circleci/windows-env.ps1
|
||||
- run: node --version
|
||||
- run: yarn --version
|
||||
- restore_cache:
|
||||
keys:
|
||||
- *cache_key_win
|
||||
- *cache_key_win_fallback
|
||||
# Install @bazel/bazelisk globally and use that for the first run.
|
||||
# Workaround for https://github.com/bazelbuild/rules_nodejs/issues/894
|
||||
# NB: the issue was for @bazel/bazel but the same problem applies to @bazel/bazelisk
|
||||
- run: yarn global add @bazel/bazelisk@$env:BAZELISK_VERSION
|
||||
# On Windows `~/` is not resolved when using as a CLI value. `../` results in the same path.
|
||||
- run: yarn install --frozen-lockfile --non-interactive --cache-folder ../.cache/yarn
|
||||
- run:
|
||||
name: Running Yarn install
|
||||
command: yarn install --frozen-lockfile --non-interactive --cache-folder ~/.cache/yarn
|
||||
# Yarn's requests sometimes take more than 10mins to complete (especially on Windows).
|
||||
no_output_timeout: 45m
|
||||
|
||||
notify_webhook_on_fail:
|
||||
description: Notify a webhook about failure
|
||||
@@ -255,11 +240,7 @@ jobs:
|
||||
keys:
|
||||
- *cache_key
|
||||
- *cache_key_fallback
|
||||
- run:
|
||||
name: Running Yarn install
|
||||
command: yarn install --frozen-lockfile --non-interactive --cache-folder ~/.cache/yarn
|
||||
# Yarn's requests sometimes take more than 10mins to complete.
|
||||
no_output_timeout: 45m
|
||||
- yarn_install
|
||||
- run: yarn --cwd aio install --frozen-lockfile --non-interactive --cache-folder ~/.cache/yarn
|
||||
# Make the bazel directories and add a file to them if they don't exist already so that
|
||||
# persist_to_workspace does not fail.
|
||||
@@ -675,6 +656,10 @@ jobs:
|
||||
# variable. It needs to be hardcoded here, because env variables interpolation is
|
||||
# not supported.
|
||||
- '/tmp/angular-components-repo'
|
||||
- run:
|
||||
# Copy zone.js package to dist/packages-dist, so `angular/components` can also use the local zone.js build.
|
||||
name: Setting up zone.js release packages.
|
||||
command: cp -r dist/zone.js-dist/zone.js dist/packages-dist/
|
||||
- run:
|
||||
# Updates the `angular/components` `package.json` file to refer to the release output
|
||||
# inside the `packages-dist` directory.
|
||||
@@ -718,16 +703,45 @@ jobs:
|
||||
test_win:
|
||||
executor: windows-executor
|
||||
steps:
|
||||
- setup_win
|
||||
- checkout
|
||||
- save_month_to_file
|
||||
- init_environment
|
||||
|
||||
# Setup the proper NodeJS version. The image comes pre-installed with `nvm-windows`.
|
||||
- run:
|
||||
name: Install NodeJS version from `.nvmrc`.
|
||||
command: |
|
||||
nvm install $(cat .nvmrc)
|
||||
nvm use $(cat .nvmrc)
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- *cache_key_win
|
||||
- *cache_key_win_fallback
|
||||
|
||||
# Install project dependencies, and install Bazelisk globally. This is necessary as
|
||||
# Windows might error when `bazel` is invoked from the project node modules. The Bazel
|
||||
# invocation might modify the symlinked project `node_modules` again, causing failures.
|
||||
- yarn_install
|
||||
- run: yarn global add @bazel/bazelisk@${BAZELISK_VERSION}
|
||||
|
||||
- run:
|
||||
name: Build all windows CI targets
|
||||
command: bazel build //packages/compiler-cli/...
|
||||
command: |
|
||||
$(yarn global bin)/bazelisk build //packages/compiler-cli/...
|
||||
no_output_timeout: 15m
|
||||
- run:
|
||||
name: Test all windows CI targets
|
||||
command: bazel test --test_tag_filters="-browser:chromium-local" //packages/compiler-cli/...
|
||||
command: |
|
||||
$(yarn global bin)/bazelisk test --test_tag_filters="-browser:chromium-local" //packages/compiler-cli/...
|
||||
no_output_timeout: 15m
|
||||
|
||||
- save_cache:
|
||||
key: *cache_key_win
|
||||
paths:
|
||||
- node_modules
|
||||
- ~/AppData/Local/bazelisk
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
default_workflow:
|
||||
@@ -786,9 +800,10 @@ workflows:
|
||||
# since the publishing script expects the legacy outputs layout.
|
||||
- build-npm-packages
|
||||
- legacy-unit-tests-saucelabs
|
||||
- components-repo-unit-tests:
|
||||
requires:
|
||||
- build-npm-packages
|
||||
# TODO(devversion): re-enable once `angular/components` is using `rules_nodejs` v5.
|
||||
#- components-repo-unit-tests:
|
||||
# requires:
|
||||
# - build-npm-packages
|
||||
- test_zonejs:
|
||||
requires:
|
||||
- setup
|
||||
@@ -811,7 +826,7 @@ workflows:
|
||||
- saucelabs:
|
||||
# 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
|
||||
# a lot of overhead. Running once daily in the main branch 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.
|
||||
@@ -819,6 +834,6 @@ workflows:
|
||||
- setup
|
||||
triggers:
|
||||
- schedule:
|
||||
<<: *only_on_master
|
||||
<<: *only_on_main_branch
|
||||
# Runs monitoring jobs at 10:00AM every day.
|
||||
cron: '0 10 * * *'
|
||||
|
||||
@@ -31,7 +31,10 @@ function setSecretVar() {
|
||||
local -r originalShellOptions=$(set +o);
|
||||
set +x -eu -o pipefail;
|
||||
|
||||
echo "export $1=\"${2:-}\";" >> $BASH_ENV;
|
||||
local assignmentStatement="export $1=\"${2:-}\";"
|
||||
|
||||
echo "${assignmentStatement}" >> $BASH_ENV;
|
||||
eval "${assignmentStatement}"
|
||||
|
||||
# Restore original shell options.
|
||||
eval "$originalShellOptions";
|
||||
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
####################################################################################################
|
||||
# Decrypt GCP Credentials and store them as the Google default credentials.
|
||||
####################################################################################################
|
||||
mkdir -p "$HOME/.config/gcloud";
|
||||
openssl aes-256-cbc -d -in "${PROJECT_ROOT}/.circleci/gcp_token" \
|
||||
-md md5 -k "$CIRCLE_PROJECT_REPONAME" -out "$HOME/.config/gcloud/application_default_credentials.json"
|
||||
|
||||
####################################################################################################
|
||||
# Set bazel configuration for CircleCI runs.
|
||||
####################################################################################################
|
||||
cp "${PROJECT_ROOT}/.circleci/bazel.linux.rc" "$HOME/.bazelrc";
|
||||
+12
-21
@@ -3,7 +3,6 @@
|
||||
# Variables
|
||||
readonly projectDir=$(realpath "$(dirname ${BASH_SOURCE[0]})/..")
|
||||
readonly envHelpersPath="$projectDir/.circleci/env-helpers.inc.sh";
|
||||
readonly bashEnvCachePath="$projectDir/.circleci/bash_env_cache";
|
||||
|
||||
# Load helpers and make them available everywhere (through `$BASH_ENV`).
|
||||
source $envHelpersPath;
|
||||
@@ -72,21 +71,10 @@ setPublicVar SAUCE_READY_FILE_TIMEOUT 120
|
||||
# the `save_cache` path configuration in `config.yml`
|
||||
setPublicVar COMPONENTS_REPO_TMP_DIR "/tmp/angular-components-repo"
|
||||
setPublicVar COMPONENTS_REPO_URL "https://github.com/angular/components.git"
|
||||
setPublicVar COMPONENTS_REPO_BRANCH "master"
|
||||
# TODO(BRANCH_RENAME_CLEANUP): fixup branch name to point to main branch
|
||||
setPublicVar COMPONENTS_REPO_BRANCH "main-branch-rename-do-not-delete"
|
||||
# **NOTE**: When updating the commit SHA, also update the cache key in the CircleCI `config.yml`.
|
||||
setPublicVar COMPONENTS_REPO_COMMIT "d1cd928714c2d6e3de75f9469ce58b06aad6353c"
|
||||
|
||||
|
||||
####################################################################################################
|
||||
# Decrypt GCP Credentials and store them as the Google default credentials.
|
||||
####################################################################################################
|
||||
mkdir -p "$HOME/.config/gcloud";
|
||||
openssl aes-256-cbc -d -in "${projectDir}/.circleci/gcp_token" \
|
||||
-md md5 -k "$CIRCLE_PROJECT_REPONAME" -out "$HOME/.config/gcloud/application_default_credentials.json"
|
||||
####################################################################################################
|
||||
# Set bazel configuration for CircleCI runs.
|
||||
####################################################################################################
|
||||
cp "${projectDir}/.circleci/bazel.linux.rc" "$HOME/.bazelrc";
|
||||
setPublicVar COMPONENTS_REPO_COMMIT "e65f5f5bafcd00dbb64387878fb866283909a2dd"
|
||||
|
||||
####################################################################################################
|
||||
# Create shell script in /tmp for Bazel actions to access CI envs without
|
||||
@@ -98,16 +86,19 @@ echo "export PROJECT_ROOT=\"${PROJECT_ROOT}\";" >> $bazelVarEnv
|
||||
echo "export CI_BRANCH=\"${CI_BRANCH}\";" >> $bazelVarEnv
|
||||
echo "export CI_BUILD_URL=\"${CI_BUILD_URL}\";" >> $bazelVarEnv
|
||||
echo "export CI_COMMIT=\"${CI_COMMIT}\";" >> $bazelVarEnv
|
||||
echo "export CI_COMMIT_RANGE=\"${CI_COMMIT_RANGE}\";" >> $bazelVarEnv
|
||||
echo "export CI_PULL_REQUEST=\"${CI_PULL_REQUEST}\";" >> $bazelVarEnv
|
||||
echo "export CI_REPO_NAME=\"${CI_REPO_NAME}\";" >> $bazelVarEnv
|
||||
echo "export CI_REPO_OWNER=\"${CI_REPO_OWNER}\";" >> $bazelVarEnv
|
||||
echo "export CI_SECRET_PAYLOAD_FIREBASE_TOKEN=\"${CI_SECRET_PAYLOAD_FIREBASE_TOKEN}\";" >> $bazelVarEnv
|
||||
|
||||
####################################################################################################
|
||||
# Platform-specific environment setup (which can leverage the base variables from here)
|
||||
####################################################################################################
|
||||
## Source `$BASH_ENV` to make the variables available immediately. ##
|
||||
## ***NOTE: This must remain the last action in this script*** ##
|
||||
####################################################################################################
|
||||
####################################################################################################
|
||||
source $BASH_ENV;
|
||||
|
||||
# Conditionally, load additional environment settings based on the current VM
|
||||
# operating system running. We detect Windows by checking for `%AppData%`.
|
||||
if [[ -n "${APPDATA}" ]]; then
|
||||
source ${projectDir}/.circleci/env.windows.sh
|
||||
else
|
||||
source ${projectDir}/.circleci/env.linux.sh
|
||||
fi
|
||||
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
####################################################################################################
|
||||
# Decrypt GCP Credentials and store them as the Google default credentials.
|
||||
####################################################################################################
|
||||
mkdir -p "${APPDATA}/gcloud/";
|
||||
openssl aes-256-cbc -d -in "${PROJECT_ROOT}/.circleci/gcp_token" \
|
||||
-md md5 -k "$CIRCLE_PROJECT_REPONAME" -out "${APPDATA}/gcloud/application_default_credentials.json"
|
||||
|
||||
####################################################################################################
|
||||
# Set bazel configuration for CircleCI runs.
|
||||
####################################################################################################
|
||||
cp "${PROJECT_ROOT}/.circleci/bazel.windows.rc" "${USERPROFILE}/.bazelrc";
|
||||
|
||||
|
||||
# Expose the Bazelisk version. We need to run Bazelisk globally since Windows has problems launching
|
||||
# Bazel from a node modules directoy that might be modified by the Bazel Yarn install then.
|
||||
setPublicVar BAZELISK_VERSION \
|
||||
"$(cd ${PROJECT_ROOT}; node -p 'require("./package.json").devDependencies["@bazel/bazelisk"]')"
|
||||
+12
-3
@@ -84,22 +84,31 @@ async function _main() {
|
||||
/**
|
||||
* Sort a list of fullpath refs into a list and then provide the first entry.
|
||||
*
|
||||
* The sort order will first find master ref, and then any semver ref, followed
|
||||
* The sort order will first find the the main branch, and then any semver ref, followed
|
||||
* by the rest of the refs in the order provided.
|
||||
*
|
||||
* Branches are sorted in this order as work is primarily done on master, and
|
||||
* Branches are sorted in this order as work is primarily done on main branches, and
|
||||
* otherwise on a semver branch. If neither of those were to match, the most
|
||||
* likely correct branch will be the first one encountered in the list.
|
||||
*/
|
||||
function getRefFromBranchList(gitOutput) {
|
||||
const branches = gitOutput.split('\n').map(b => b.split('/').slice(1).join('/').trim());
|
||||
return branches.sort((a, b) => {
|
||||
if (a === 'main') {
|
||||
return -1;
|
||||
}
|
||||
if (b === 'main') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// TODO(BRANCH_RENAME_CLEANUP): remove
|
||||
if (a === 'master') {
|
||||
return -1;
|
||||
}
|
||||
if (b === 'master') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const aIsSemver = semverRegex.test(a);
|
||||
const bIsSemver = semverRegex.test(b);
|
||||
if (aIsSemver && bIsSemver) {
|
||||
@@ -132,7 +141,7 @@ function getShaFromRef(ref) {
|
||||
* by committerdate.
|
||||
*
|
||||
* example:
|
||||
* upstream/master
|
||||
* upstream/main
|
||||
* upstream/9.0.x
|
||||
* upstream/test
|
||||
* upstream/1.1.x
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# Install Bazel pre-reqs on Windows
|
||||
# https://docs.bazel.build/versions/master/install-windows.html
|
||||
# https://docs.bazel.build/versions/master/windows.html
|
||||
# Install MSYS2 and packages
|
||||
choco install msys2 --version 20200903.0.0 --no-progress --package-parameters "/NoUpdate"
|
||||
C:\tools\msys64\usr\bin\bash.exe -l -c "pacman --needed --noconfirm -S zip unzip patch diffutils"
|
||||
|
||||
# Add PATH modifications to the Powershell profile. This is the win equivalent of .bash_profile.
|
||||
# https://docs.microsoft.com/en-us/previous-versions//bb613488(v=vs.85)
|
||||
new-item -path $profile -itemtype file -force
|
||||
# Paths for nodejs, npm, yarn, and msys2. Use single quotes to prevent interpolation.
|
||||
# Add before the original path to use msys2 instead of the installed gitbash.
|
||||
Add-Content $profile '$Env:path = "${Env:ProgramFiles}\nodejs\;C:\Users\circleci\AppData\Roaming\npm\;${Env:ProgramFiles(x86)}\Yarn\bin\;C:\Users\circleci\AppData\Local\Yarn\bin\;C:\tools\msys64\usr\bin\;" + $Env:path'
|
||||
# Environment variables for Bazel
|
||||
Add-Content $profile '$Env:BAZEL_SH = "C:\tools\msys64\usr\bin\bash.exe"'
|
||||
|
||||
# Get the bazelisk version devdep and store it in a global var for use in the circleci job.
|
||||
$bazeliskVersion = & ${Env:ProgramFiles}\nodejs\node.exe -e "console.log(require('./package.json').devDependencies['@bazel/bazelisk'])"
|
||||
# This is a tricky situation: we want $bazeliskVersion to be evaluated but not $Env:BAZELISK_VERSION.
|
||||
# Formatting works https://stackoverflow.com/questions/32127583/expand-variable-inside-single-quotes
|
||||
$bazeliskVersionGlobalVar = '$Env:BAZELISK_VERSION = "{0}"' -f $bazeliskVersion
|
||||
Add-Content $profile $bazeliskVersionGlobalVar
|
||||
|
||||
# Remove the CircleCI checkout SSH override, because it breaks cloning repositories through Bazel.
|
||||
# See https://circleci.com/gh/angular/angular/401454 for an example.
|
||||
# TODO: is this really needed? Maybe there's a better way. It doesn't happen on Linux or on Codefresh.
|
||||
git config --global --unset url.ssh://git@github.com.insteadOf
|
||||
|
||||
|
||||
####################################################################################################
|
||||
# Decrypt GCP Credentials and store them as the Google default credentials.
|
||||
####################################################################################################
|
||||
mkdir ${env:APPDATA}\gcloud
|
||||
openssl aes-256-cbc -d -in .circleci\gcp_token -md md5 -out "$env:APPDATA\gcloud\application_default_credentials.json" -k "$env:CIRCLE_PROJECT_REPONAME"
|
||||
|
||||
####################################################################################################
|
||||
# Set bazel configuration for CircleCI runs.
|
||||
####################################################################################################
|
||||
copy .circleci\bazel.windows.rc ${Env:USERPROFILE}\.bazelrc
|
||||
|
||||
####################################################################################################
|
||||
# Install specific version of node.
|
||||
####################################################################################################
|
||||
nvm install 16.10.0
|
||||
nvm use 16.10.0
|
||||
|
||||
# These Bazel prereqs aren't needed because the CircleCI image already includes them.
|
||||
# choco install yarn --version 1.16.0 --no-progress
|
||||
# choco install vcredist2015 --version 14.0.24215.20170201
|
||||
|
||||
# We don't need VS Build Tools for the tested bazel targets.
|
||||
# If it's needed again, uncomment these lines.
|
||||
# VS Build Tools are needed for Bazel C++ targets (like com_google_protobuf)
|
||||
# choco install visualstudio2019buildtools --version 16.1.2.0 --no-progress --package-parameters "--add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 --add Microsoft.Component.VC.Runtime.UCRTSDK --add Microsoft.VisualStudio.Component.Windows10SDK.17763"
|
||||
# Add-Content $profile '$Env:BAZEL_VC = "${Env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\BuildTools\VC\"'
|
||||
# Python is needed for Bazel Python targets
|
||||
# choco install python --version 3.5.1 --no-progress
|
||||
@@ -5,7 +5,7 @@ body:
|
||||
- type: dropdown
|
||||
id: affected-packages
|
||||
attributes:
|
||||
label: Which @angular/* package(s) are relevant/releated to the feature request?
|
||||
label: Which @angular/* package(s) are relevant/related to the feature request?
|
||||
options:
|
||||
- animations
|
||||
- bazel
|
||||
|
||||
@@ -4,11 +4,15 @@ on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
labels:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: angular/dev-infra/github-actions/commit-message-based-labels@fb9b744a4903d0d17e1fccfcdec531cb601b3e47
|
||||
- uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # tag=v3.0.0
|
||||
- uses: angular/dev-infra/github-actions/commit-message-based-labels@129a5ccbcc73d7ca960d8c166d7400bf2e94cd3d
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -5,11 +5,15 @@ on:
|
||||
# Run at 14:00 every day
|
||||
- cron: '0 14 * * *'
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
feature_triage:
|
||||
if: github.repository == 'angular/angular'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/feature-request@fb9b744a4903d0d17e1fccfcdec531cb601b3e47
|
||||
- uses: angular/dev-infra/github-actions/feature-request@129a5ccbcc73d7ca960d8c166d7400bf2e94cd3d
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -5,11 +5,15 @@ on:
|
||||
# Run at 16:00 every day
|
||||
- cron: '0 16 * * *'
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lock_closed:
|
||||
if: github.repository == 'angular/angular'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/lock-closed@fb9b744a4903d0d17e1fccfcdec531cb601b3e47
|
||||
- uses: angular/dev-infra/github-actions/lock-closed@129a5ccbcc73d7ca960d8c166d7400bf2e94cd3d
|
||||
with:
|
||||
lock-bot-key: ${{ secrets.LOCK_BOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -4,11 +4,16 @@ on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [
|
||||
main,
|
||||
# TODO(BRANCH_RENAME_CLEANUP): remove
|
||||
master,
|
||||
]
|
||||
workflow_dispatch:
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions: read-all
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
@@ -22,12 +27,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 'Checkout code'
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
||||
uses: actions/checkout@230611dbd0eb52da1e1f4f7bc8bb0c3a339fc8b7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Run analysis'
|
||||
uses: ossf/scorecard-action@c8416b0b2bf627c349ca92fc8e3de51a64b005cf # v1.0.2
|
||||
uses: ossf/scorecard-action@3662744abc9b750123cb8965fef31e3802d52da5
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@@ -36,7 +41,7 @@ jobs:
|
||||
|
||||
# Upload the results as artifacts.
|
||||
- name: 'Upload artifact'
|
||||
uses: actions/upload-artifact@82c141cc518b40d92cc801eee768e7aafc9c2fa2 # v2.3.1
|
||||
uses: actions/upload-artifact@2244c8200304ec9588bf9399eac622d9fadc28c4
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
@@ -44,6 +49,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: 'Upload to code-scanning'
|
||||
uses: github/codeql-action/upload-sarif@5f532563584d71fdef14ee64d17bafb34f751ce5 # v1.0.26
|
||||
uses: github/codeql-action/upload-sarif@040feefecff4ae0fc15c110822a5f96516b0629e
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
*.log
|
||||
node_modules
|
||||
|
||||
# CircleCI temporary file for cache key computation.
|
||||
# See `save_month_to_file` in `.circleci/config.yml`.
|
||||
month.txt
|
||||
|
||||
# Include when developing application packages.
|
||||
pubspec.lock
|
||||
.c9
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {CaretakerConfig} from '@angular/dev-infra-private/ng-dev/caretaker/config';
|
||||
import {CaretakerConfig} from '@angular/dev-infra-private/ng-dev';
|
||||
|
||||
/** The configuration for `ng-dev caretaker` commands. */
|
||||
export const caretaker: CaretakerConfig = {
|
||||
@@ -14,7 +14,7 @@ export const caretaker: CaretakerConfig = {
|
||||
{
|
||||
name: 'Initial Triage Queue',
|
||||
query: `is:open no:milestone`,
|
||||
}
|
||||
},
|
||||
],
|
||||
caretakerGroup: 'angular-caretaker',
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {CommitMessageConfig} from '@angular/dev-infra-private/ng-dev/commit-message/config';
|
||||
import {CommitMessageConfig} from '@angular/dev-infra-private/ng-dev';
|
||||
|
||||
/**
|
||||
* The configuration for `ng-dev commit-message` commands.
|
||||
@@ -36,5 +36,5 @@ export const commitMessage: CommitMessageConfig = {
|
||||
'upgrade',
|
||||
've',
|
||||
'zone.js',
|
||||
]
|
||||
],
|
||||
};
|
||||
|
||||
+4
-6
@@ -1,13 +1,11 @@
|
||||
import {FormatConfig} from '@angular/dev-infra-private/ng-dev/format/config';
|
||||
import {FormatConfig} from '@angular/dev-infra-private/ng-dev';
|
||||
|
||||
/**
|
||||
* Configuration for the `ng-dev format` command.
|
||||
*/
|
||||
export const format: FormatConfig = {
|
||||
'prettier': {
|
||||
'matchers': [
|
||||
'**/*.{yaml,yml}',
|
||||
]
|
||||
'matchers': ['**/*.{yaml,yml}'],
|
||||
},
|
||||
'clang-format': {
|
||||
'matchers': [
|
||||
@@ -31,7 +29,7 @@ export const format: FormatConfig = {
|
||||
'!packages/core/src/i18n/locale_en.ts',
|
||||
'!packages/common/locales/closure-locale.ts',
|
||||
'!packages/common/src/i18n/currencies.ts',
|
||||
]
|
||||
],
|
||||
},
|
||||
'buildifier': true
|
||||
'buildifier': true,
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import {GithubConfig} from '@angular/dev-infra-private/ng-dev/utils/config';
|
||||
import {GithubConfig} from '@angular/dev-infra-private/ng-dev';
|
||||
|
||||
/**
|
||||
* Github configuration for the `ng-dev` command. This repository is used as
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {PullRequestConfig} from '@angular/dev-infra-private/ng-dev/pr/config';
|
||||
import {PullRequestConfig} from '@angular/dev-infra-private/ng-dev';
|
||||
|
||||
/**
|
||||
* Configuration for the merge tool in `ng-dev`. This sets up the labels which
|
||||
@@ -19,5 +19,5 @@ export const pullRequest: PullRequestConfig = {
|
||||
// `dev-infra` and `docs-infra` are not affecting the public NPM packages. Similarly,
|
||||
// the `bazel` package is not considered part of the public API so that features
|
||||
// can land in patch branches.
|
||||
targetLabelExemptScopes: ['dev-infra', 'docs-infra', 'bazel']
|
||||
targetLabelExemptScopes: ['dev-infra', 'docs-infra', 'bazel'],
|
||||
};
|
||||
|
||||
+2
-8
@@ -1,4 +1,4 @@
|
||||
import {ReleaseConfig} from '@angular/dev-infra-private/ng-dev/release/config';
|
||||
import {ReleaseConfig} from '@angular/dev-infra-private/ng-dev';
|
||||
import {join} from 'path';
|
||||
|
||||
/** Configuration for the `ng-dev release` command. */
|
||||
@@ -30,13 +30,7 @@ export const release: ReleaseConfig = {
|
||||
return buildTargetPackages('dist/release-output', false, 'Release', /* isRelease */ true);
|
||||
},
|
||||
releaseNotes: {
|
||||
hiddenScopes: [
|
||||
'aio',
|
||||
'dev-infra',
|
||||
'docs-infra',
|
||||
'zone.js',
|
||||
'devtools',
|
||||
],
|
||||
hiddenScopes: ['aio', 'dev-infra', 'docs-infra', 'zone.js', 'devtools'],
|
||||
},
|
||||
releasePrLabels: ['comp: build & ci', 'action: merge', 'PullApprove: disable'],
|
||||
};
|
||||
|
||||
+9
-8
@@ -224,7 +224,7 @@ groups:
|
||||
users:
|
||||
- alxhub
|
||||
- crisbeto
|
||||
- jelbourn
|
||||
- ~jelbourn
|
||||
- jessicajaniuk
|
||||
|
||||
# =========================================================
|
||||
@@ -244,7 +244,7 @@ groups:
|
||||
'aio/content/guide/angular-compiler-options.md',
|
||||
'aio/content/guide/aot-compiler.md',
|
||||
'aio/content/guide/aot-metadata-errors.md',
|
||||
'aio/content/guide/template-typecheck.md '
|
||||
'aio/content/guide/template-typecheck.md'
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
@@ -395,6 +395,7 @@ groups:
|
||||
'aio/content/guide/template-expression-operators.md',
|
||||
'aio/content/examples/template-expression-operators/**',
|
||||
'aio/content/guide/pipes.md',
|
||||
'aio/content/guide/pipes-custom-data-trans.md',
|
||||
'aio/content/examples/pipes/**',
|
||||
'aio/content/images/guide/pipes/**',
|
||||
'aio/content/guide/providers.md',
|
||||
@@ -1038,12 +1039,12 @@ groups:
|
||||
'aio/content/guide/migration-module-with-providers.md',
|
||||
'aio/content/guide/static-query-migration.md',
|
||||
'aio/content/guide/update-to-latest-version.md',
|
||||
'aio/content/guide/ivy-compatibility.md',
|
||||
'aio/content/guide/ivy-compatibility-examples.md'
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- alxhub
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- jelbourn
|
||||
|
||||
# =========================================================
|
||||
@@ -1284,7 +1285,7 @@ groups:
|
||||
- alxhub
|
||||
- atscott
|
||||
- dylhunn
|
||||
- jelbourn
|
||||
- ~jelbourn
|
||||
- jessicajaniuk
|
||||
- pkozlowski-opensource
|
||||
reviews:
|
||||
@@ -1311,7 +1312,7 @@ groups:
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- dylhunn
|
||||
- jelbourn
|
||||
- ~jelbourn
|
||||
- jessicajaniuk
|
||||
- pkozlowski-opensource
|
||||
reviews:
|
||||
@@ -1338,7 +1339,7 @@ groups:
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- dylhunn
|
||||
- jelbourn
|
||||
- ~jelbourn
|
||||
- jessicajaniuk
|
||||
- pkozlowski-opensource
|
||||
|
||||
@@ -1363,5 +1364,5 @@ groups:
|
||||
- AndrewKushnir
|
||||
- andrewseguin
|
||||
- dgp1130
|
||||
- jelbourn
|
||||
- ~jelbourn
|
||||
- josephperrott
|
||||
|
||||
+10
-1
@@ -1,10 +1,12 @@
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
exports_files([
|
||||
"LICENSE",
|
||||
"karma-js.conf.js",
|
||||
"browser-providers.conf.js",
|
||||
"scripts/ci/track-payload-size.sh",
|
||||
"scripts/ci/bazel-payload-size.sh",
|
||||
"scripts/ci/payload-size.sh",
|
||||
"scripts/ci/payload-size.js",
|
||||
"package.json",
|
||||
@@ -56,3 +58,10 @@ config_setting(
|
||||
name = "stamp",
|
||||
values = {"stamp": "true"},
|
||||
)
|
||||
|
||||
nodejs_binary(
|
||||
name = "yarn_vendored",
|
||||
data = [".yarn/releases/yarn-1.22.17.cjs"],
|
||||
entry_point = ".yarn/releases/yarn-1.22.17.cjs",
|
||||
visibility = ["//integration:__subpackages__"],
|
||||
)
|
||||
|
||||
+163
-6561
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -53,6 +53,7 @@ Get started with Angular, learn the fundamentals and explore advanced topics on
|
||||
- [Server Side Rendering][ssr]
|
||||
- [Schematics][schematics]
|
||||
- [Lazy Loading][lazyloading]
|
||||
- [Animations][animations]
|
||||
|
||||
## Development Setup
|
||||
|
||||
@@ -157,3 +158,4 @@ Join the conversation and help the community.
|
||||
[stackoverflow]: https://stackoverflow.com/questions/tagged/angular
|
||||
[youtube]: https://youtube.com/angular
|
||||
[meetup]: https://www.meetup.com/find/?keywords=angular
|
||||
[animations]: https://angular.io/guide/animations
|
||||
|
||||
@@ -5,13 +5,16 @@ workspace(
|
||||
|
||||
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 = "ddb78717b802f8dd5d4c01c340ecdc007c8ced5c1df7db421d0df3d642ea0580",
|
||||
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/4.6.0/rules_nodejs-4.6.0.tar.gz"],
|
||||
sha256 = "3ceb1e5b5dcad5fa2ad8870a20201cfbb9c9c63cac4055c9ab370034c765297f",
|
||||
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.3.0/rules_nodejs-5.3.0.tar.gz"],
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_nodejs//:repositories.bzl", "build_bazel_rules_nodejs_dependencies")
|
||||
|
||||
build_bazel_rules_nodejs_dependencies()
|
||||
|
||||
# The PKG rules are needed to build tar packages for integration tests. The builtin
|
||||
# rule in `@bazel_tools` is not Windows compatible and outdated.
|
||||
http_archive(
|
||||
@@ -23,28 +26,38 @@ http_archive(
|
||||
],
|
||||
)
|
||||
|
||||
# Check the rules_nodejs version and download npm dependencies
|
||||
# Note: bazel (version 2 and after) will check the .bazelversion file so we don't need to
|
||||
# assert on that.
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "check_rules_nodejs_version", "node_repositories", "yarn_install")
|
||||
load("@rules_nodejs//nodejs:repositories.bzl", "nodejs_register_toolchains")
|
||||
|
||||
check_rules_nodejs_version(minimum_version_string = "2.2.0")
|
||||
|
||||
# Setup the Node.js toolchain
|
||||
node_repositories(
|
||||
nodejs_register_toolchains(
|
||||
name = "nodejs",
|
||||
node_version = "16.10.0",
|
||||
package_json = ["//:package.json"],
|
||||
)
|
||||
|
||||
# Download npm dependencies.
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "yarn_install")
|
||||
load("//integration:npm_package_archives.bzl", "npm_package_archives")
|
||||
|
||||
yarn_install(
|
||||
name = "npm",
|
||||
# Note that we add the postinstall script here so that the dependencies are re-installed
|
||||
# Note that we add the postinstall scripts here so that the dependencies are re-installed
|
||||
# when the postinstall patches are modified.
|
||||
data = ["//tools:postinstall-patches.js"],
|
||||
data = [
|
||||
"//:.yarn/releases/yarn-1.22.17.cjs",
|
||||
"//:.yarnrc",
|
||||
"//:scripts/puppeteer-chromedriver-versions.js",
|
||||
"//:scripts/webdriver-manager-update.js",
|
||||
"//tools:postinstall-patches.js",
|
||||
],
|
||||
# Currently disabled due to:
|
||||
# 1. Missing Windows support currently.
|
||||
# 2. Incompatibilites with the `ts_library` rule.
|
||||
exports_directories_only = False,
|
||||
manual_build_file_contents = npm_package_archives(),
|
||||
package_json = "//:package.json",
|
||||
# We prefer to symlink the `node_modules` to only maintain a single install.
|
||||
# See https://github.com/angular/dev-infra/pull/446#issuecomment-1059820287 for details.
|
||||
symlink_node_modules = True,
|
||||
yarn = "//:.yarn/releases/yarn-1.22.17.cjs",
|
||||
yarn_lock = "//:yarn.lock",
|
||||
)
|
||||
|
||||
@@ -64,7 +77,9 @@ browser_repositories()
|
||||
|
||||
load("@build_bazel_rules_nodejs//toolchains/esbuild:esbuild_repositories.bzl", "esbuild_repositories")
|
||||
|
||||
esbuild_repositories()
|
||||
esbuild_repositories(
|
||||
npm_repository = "npm",
|
||||
)
|
||||
|
||||
load("@rules_pkg//:deps.bzl", "rules_pkg_dependencies")
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
Everything in this folder is part of the documentation project. This includes:
|
||||
|
||||
* the web site for displaying the documentation
|
||||
* the web site for displaying the documentation.
|
||||
* the dgeni configuration for converting source files to rendered files that can be viewed in the web site.
|
||||
* the tooling for setting up examples for development; and generating live-example and zip files from the examples.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Image metadata and config
|
||||
FROM debian:buster
|
||||
FROM debian:bullseye
|
||||
|
||||
LABEL name="angular.io PR preview" \
|
||||
description="This image implements the PR preview functionality for angular.io." \
|
||||
@@ -72,29 +72,29 @@ RUN mkdir /var/log/aio
|
||||
|
||||
|
||||
# Add extra package sources
|
||||
RUN apt-get update -y && apt-get install -y curl=7.64.0-4+deb10u1
|
||||
RUN curl --silent --show-error --location https://deb.nodesource.com/setup_12.x | bash -
|
||||
RUN apt-get update -y && apt-get install -y curl
|
||||
RUN curl --silent --show-error --location https://deb.nodesource.com/setup_14.x | bash -
|
||||
RUN curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
|
||||
|
||||
# Install packages
|
||||
# NOTE: Some packages (such as `nginx`, `nodejs`, `openssl`) make older versions unavailable on the
|
||||
# repositories, so we cannot pin to specific versions for these packages :(
|
||||
# See for example:
|
||||
# NOTE: Many packages make older versions unavailable on the repositories, so we cannot pin to
|
||||
# specific versions :(
|
||||
# See, for example:
|
||||
# - https://github.com/nodesource/distributions/issues/33
|
||||
# - https://askubuntu.com/questions/715104/how-can-i-downgrade-openssl-via-apt-get
|
||||
RUN apt-get update -y && apt-get install -y \
|
||||
cron=3.0pl1-134+deb10u1 \
|
||||
dnsmasq=2.80-1 \
|
||||
nano=3.2-3 \
|
||||
cron \
|
||||
dnsmasq \
|
||||
nano \
|
||||
nginx \
|
||||
nodejs \
|
||||
openssl \
|
||||
rsyslog=8.1901.0-1 \
|
||||
vim=2:8.1.0875-5 \
|
||||
yarn=1.22.4-1
|
||||
RUN yarn global add pm2@4.4.0
|
||||
rsyslog \
|
||||
vim \
|
||||
yarn=1.22.18-1
|
||||
RUN yarn global add pm2@5.2.0
|
||||
|
||||
|
||||
# Set up log rotation
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Imports
|
||||
import fetch from 'node-fetch';
|
||||
import fetch, {RequestInit} from 'node-fetch';
|
||||
|
||||
import {assertNotMissingOrEmpty} from './utils';
|
||||
|
||||
// Constants
|
||||
const CIRCLE_CI_API_URL = 'https://circleci.com/api/v1.1/project/github';
|
||||
const CIRCLE_CI_API_URL = 'https://circleci.com/api/v2/project/gh';
|
||||
|
||||
// Interfaces - Types
|
||||
export interface ArtifactInfo {
|
||||
@@ -32,8 +32,6 @@ export interface BuildInfo {
|
||||
* A Helper that can interact with the CircleCI API.
|
||||
*/
|
||||
export class CircleCiApi {
|
||||
private tokenParam = `circle-token=${this.circleCiToken}`;
|
||||
|
||||
/**
|
||||
* Construct a helper that can interact with the CircleCI REST API.
|
||||
* @param githubOrg The Github organisation whose repos we want to access in CircleCI (e.g.
|
||||
@@ -51,6 +49,15 @@ export class CircleCiApi {
|
||||
assertNotMissingOrEmpty('circleCiToken', circleCiToken);
|
||||
}
|
||||
|
||||
public async fetchFromCircleCi(url: string, params: RequestInit = {}) {
|
||||
params.headers = {...params.headers, 'Circle-Token': this.circleCiToken};
|
||||
const response = await fetch(url, params);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`${url}: ${response.status} - ${response.statusText}`);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the info for a build from the CircleCI API
|
||||
* @param buildNumber The CircleCI build number that generated the artifact.
|
||||
@@ -58,11 +65,8 @@ export class CircleCiApi {
|
||||
*/
|
||||
public async getBuildInfo(buildNumber: number): Promise<BuildInfo> {
|
||||
try {
|
||||
const baseUrl = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`;
|
||||
const response = await fetch(`${baseUrl}?${this.tokenParam}`);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`${baseUrl}: ${response.status} - ${response.statusText}`);
|
||||
}
|
||||
const url = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/job/${buildNumber}`;
|
||||
const response = await this.fetchFromCircleCi(url);
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
throw new Error(`CircleCI build info request failed (${(error as Error).message})`);
|
||||
@@ -77,9 +81,9 @@ export class CircleCiApi {
|
||||
public async getBuildArtifactUrl(buildNumber: number, artifactPath: string): Promise<string> {
|
||||
const baseUrl = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`;
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/artifacts?${this.tokenParam}`);
|
||||
const artifacts = await response.json() as ArtifactResponse;
|
||||
const artifact = artifacts.find(item => item.path === artifactPath);
|
||||
const response = await this.fetchFromCircleCi(`${baseUrl}/artifacts`);
|
||||
const artifacts = await response.json() as {items: ArtifactResponse};
|
||||
const artifact = artifacts.items.find(item => item.path === artifactPath);
|
||||
if (!artifact) {
|
||||
throw new Error(`Missing artifact (${artifactPath}) for CircleCI build: ${buildNumber}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as fs from 'fs';
|
||||
import fetch from 'node-fetch';
|
||||
import {dirname} from 'path';
|
||||
import {mkdir} from 'shelljs';
|
||||
import {promisify} from 'util';
|
||||
@@ -63,11 +62,7 @@ export class BuildRetriever {
|
||||
await new Promise(resolve => fs.exists(outPath, exists => resolve(exists)));
|
||||
if (!downloadExists) {
|
||||
const url = await this.api.getBuildArtifactUrl(buildNum, artifactPath);
|
||||
const response = await fetch(url, {size: this.downloadSizeLimit});
|
||||
if (response.status !== 200) {
|
||||
throw new PreviewServerError(
|
||||
response.status, `Error ${response.status} - ${response.statusText}`);
|
||||
}
|
||||
const response = await this.api.fetchFromCircleCi(url, {size: this.downloadSizeLimit});
|
||||
const buffer = await response.buffer();
|
||||
mkdir('-p', dirname(outPath));
|
||||
await promisify(fs.writeFile)(outPath, buffer);
|
||||
|
||||
@@ -4,7 +4,7 @@ import {CircleCiApi} from '../../lib/common/circle-ci-api';
|
||||
const ORG = 'testorg';
|
||||
const REPO = 'testrepo';
|
||||
const TOKEN = 'xxxx';
|
||||
const BASE_URL = `https://circleci.com/api/v1.1/project/github/${ORG}/${REPO}`;
|
||||
const BASE_URL = `https://circleci.com/api/v2/project/gh/${ORG}/${REPO}`;
|
||||
|
||||
describe('CircleCIApi', () => {
|
||||
describe('constructor()', () => {
|
||||
@@ -24,6 +24,18 @@ describe('CircleCIApi', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchFromCircleCI', () => {
|
||||
it('should include the authentication token in the headers on every request', async () => {
|
||||
const api = new CircleCiApi(ORG, REPO, TOKEN);
|
||||
const request = nock(BASE_URL)
|
||||
.get('/')
|
||||
.matchHeader('Circle-Token', TOKEN)
|
||||
.reply(200);
|
||||
await api.fetchFromCircleCi(`${BASE_URL}/`);
|
||||
request.done();
|
||||
})
|
||||
})
|
||||
|
||||
describe('getBuildInfo', () => {
|
||||
it('should make a request to the CircleCI API for the given build number', async () => {
|
||||
const api = new CircleCiApi(ORG, REPO, TOKEN);
|
||||
@@ -31,7 +43,7 @@ describe('CircleCIApi', () => {
|
||||
const expectedBuildInfo: any = { org: ORG, repo: REPO, build_num: buildNum };
|
||||
|
||||
const request = nock(BASE_URL)
|
||||
.get(`/${buildNum}?circle-token=${TOKEN}`)
|
||||
.get(`/job/${buildNum}`)
|
||||
.reply(200, expectedBuildInfo);
|
||||
|
||||
const buildInfo = await api.getBuildInfo(buildNum);
|
||||
@@ -43,17 +55,17 @@ describe('CircleCIApi', () => {
|
||||
const api = new CircleCiApi(ORG, REPO, TOKEN);
|
||||
const buildNum = 12345;
|
||||
const errorMessage = 'Invalid request';
|
||||
const request = nock(BASE_URL).get(`/${buildNum}?circle-token=${TOKEN}`);
|
||||
const request = nock(BASE_URL).get(`/job/${buildNum}`);
|
||||
|
||||
request.replyWithError(errorMessage);
|
||||
await expectAsync(api.getBuildInfo(buildNum)).toBeRejectedWithError(
|
||||
`CircleCI build info request failed ` +
|
||||
`(request to ${BASE_URL}/${buildNum}?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
|
||||
`(request to ${BASE_URL}/job/${buildNum} failed, reason: ${errorMessage})`);
|
||||
|
||||
request.reply(404, errorMessage);
|
||||
await expectAsync(api.getBuildInfo(buildNum)).toBeRejectedWithError(
|
||||
`CircleCI build info request failed ` +
|
||||
`(request to ${BASE_URL}/${buildNum}?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
|
||||
`(request to ${BASE_URL}/job/${buildNum} failed, reason: ${errorMessage})`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,8 +77,8 @@ describe('CircleCIApi', () => {
|
||||
const artifact1: any = { path: 'some/path/1', url: 'https://url/1' };
|
||||
const artifact2: any = { path: 'some/path/2', url: 'https://url/2' };
|
||||
const request = nock(BASE_URL)
|
||||
.get(`/${buildNum}/artifacts?circle-token=${TOKEN}`)
|
||||
.reply(200, [artifact0, artifact1, artifact2]);
|
||||
.get(`/${buildNum}/artifacts`)
|
||||
.reply(200, {items: [artifact0, artifact1, artifact2]});
|
||||
|
||||
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/1')).toBeResolvedTo('https://url/1');
|
||||
request.done();
|
||||
@@ -77,17 +89,17 @@ describe('CircleCIApi', () => {
|
||||
const api = new CircleCiApi(ORG, REPO, TOKEN);
|
||||
const buildNum = 12345;
|
||||
const errorMessage = 'Invalid request';
|
||||
const request = nock(BASE_URL).get(`/${buildNum}/artifacts?circle-token=${TOKEN}`);
|
||||
const request = nock(BASE_URL).get(`/${buildNum}/artifacts`);
|
||||
|
||||
request.replyWithError(errorMessage);
|
||||
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/1')).toBeRejectedWithError(
|
||||
`CircleCI artifact URL request failed ` +
|
||||
`(request to ${BASE_URL}/${buildNum}/artifacts?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
|
||||
`(request to ${BASE_URL}/${buildNum}/artifacts failed, reason: ${errorMessage})`);
|
||||
|
||||
request.reply(404, errorMessage);
|
||||
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/1')).toBeRejectedWithError(
|
||||
`CircleCI artifact URL request failed ` +
|
||||
`(request to ${BASE_URL}/${buildNum}/artifacts?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
|
||||
`(request to ${BASE_URL}/${buildNum}/artifacts failed, reason: ${errorMessage})`);
|
||||
});
|
||||
|
||||
it('should throw an error if the response does not contain the specified artifact', async () => {
|
||||
@@ -97,8 +109,8 @@ describe('CircleCIApi', () => {
|
||||
const artifact1: any = { path: 'some/path/1', url: 'https://url/1' };
|
||||
const artifact2: any = { path: 'some/path/2', url: 'https://url/2' };
|
||||
nock(BASE_URL)
|
||||
.get(`/${buildNum}/artifacts?circle-token=${TOKEN}`)
|
||||
.reply(200, [artifact0, artifact1, artifact2]);
|
||||
.get(`/${buildNum}/artifacts`)
|
||||
.reply(200, {items: [artifact0, artifact1, artifact2]});
|
||||
|
||||
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/3')).toBeRejectedWithError(
|
||||
`CircleCI artifact URL request failed ` +
|
||||
|
||||
+1
-1
@@ -160,7 +160,7 @@ describe('BuildRetriever', () => {
|
||||
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(404, 'No such artifact');
|
||||
|
||||
await expectAsync(retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH)).
|
||||
toBeRejectedWithError('CircleCI artifact download failed (Error 404 - Not Found)');
|
||||
toBeRejectedWithError(`CircleCI artifact download failed (${BASE_URL}/some/path/build.zip: 404 - Not Found)`);
|
||||
|
||||
artifactRequest.done();
|
||||
});
|
||||
|
||||
@@ -25,8 +25,8 @@ Necessary secrets:
|
||||
- Generate new token with the `public_repo` scope.
|
||||
|
||||
2. `CIRCLE_CI_TOKEN`
|
||||
- Visit https://circleci.com/gh/angular/angular/edit#api.
|
||||
- Create an API token with `Build Artifacts` scope.
|
||||
- Visit https://app.circleci.com/settings/user/tokens.
|
||||
- Create a user API token.
|
||||
|
||||
|
||||
## Save secrets on the VM
|
||||
|
||||
+34
-17
@@ -1,5 +1,6 @@
|
||||
|
||||
# Persistent disk cache
|
||||
|
||||
Angular CLI saves a number of cachable operations on disk by default.
|
||||
|
||||
When you re-run the same build, the build system restores the state of the previous build and re-uses previously performed operations, which decreases the time taken to build and test your applications and libraries.
|
||||
@@ -7,9 +8,10 @@ When you re-run the same build, the build system restores the state of the previ
|
||||
To amend the default cache settings, add the `cli.cache` object to your [Workspace Configuration](guide/workspace-config).
|
||||
The object goes under `cli.cache` at the top level of the file, outside the `projects` sections.
|
||||
|
||||
```json
|
||||
<code-example format="json" language="json">
|
||||
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"cli": {
|
||||
"cache": {
|
||||
@@ -18,27 +20,35 @@ The object goes under `cli.cache` at the top level of the file, outside the `pro
|
||||
},
|
||||
"projects": {}
|
||||
}
|
||||
```
|
||||
|
||||
</code-example>
|
||||
|
||||
For more information, see [cache options](guide/workspace-config#cache-options).
|
||||
|
||||
### Enabling and disabling the cache
|
||||
Caching is enabled by default. To disable caching run the following command:
|
||||
|
||||
```bash
|
||||
Caching is enabled by default.
|
||||
To disable caching run the following command:
|
||||
|
||||
<code-example format="shell" language="shell">
|
||||
|
||||
ng config cli.cache.enabled false
|
||||
```
|
||||
|
||||
</code-example>
|
||||
|
||||
To re-enable caching, set `cli.cache.enabled` to `true`.
|
||||
|
||||
### Cache environments
|
||||
|
||||
By default, disk cache is only enabled for local environments.
|
||||
|
||||
To enable caching for all environments, run the following command:
|
||||
|
||||
```bash
|
||||
<code-example format="shell" language="shell">
|
||||
|
||||
ng config cli.cache.environment all
|
||||
```
|
||||
|
||||
</code-example>
|
||||
|
||||
For more information, see `environment` in [cache options](guide/workspace-config#cache-options).
|
||||
|
||||
@@ -50,11 +60,14 @@ The Angular CLI checks for the presence and value of the `CI` environment variab
|
||||
|
||||
### Cache path
|
||||
|
||||
By default, `.angular/cache` is used as a base directory to store cache results. To change this path, run the following command:
|
||||
By default, `.angular/cache` is used as a base directory to store cache results.
|
||||
To change this path, run the following command:
|
||||
|
||||
<code-example format="shell" language="shell">
|
||||
|
||||
```bash
|
||||
ng config cli.cache.path ".cache/ng"
|
||||
```
|
||||
|
||||
</code-example>
|
||||
|
||||
### Clearing the cache
|
||||
|
||||
@@ -62,14 +75,18 @@ To clear the cache, run one of the following commands.
|
||||
|
||||
To clear the cache on Unix-based operating systems:
|
||||
|
||||
```bash
|
||||
<code-example format="shell" language="shell">
|
||||
|
||||
rm -rf .angular/cache
|
||||
```
|
||||
|
||||
</code-example>
|
||||
|
||||
To clear the cache on Windows:
|
||||
|
||||
```bash
|
||||
rmdir /s /q .angular\cache
|
||||
```
|
||||
<code-example format="shell" language="shell">
|
||||
|
||||
For more information, see [rm command](https://man7.org/linux/man-pages/man1/rm.1.html) and [rmdir command](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/rmdir).
|
||||
rmdir /s /q .angular\cache
|
||||
|
||||
</code-example>
|
||||
|
||||
For more information, see [rm command](https://man7.org/linux/man-pages/man1/rm.1.html) and [rmdir command](https://docs.microsoft.com/windows-server/administration/windows-commands/rmdir).
|
||||
|
||||
+46
-24
@@ -7,30 +7,36 @@ The Angular CLI is a command-line interface tool that you use to initialize, dev
|
||||
Major versions of Angular CLI follow the supported major version of Angular, but minor versions can be released separately.
|
||||
|
||||
Install the CLI using the `npm` package manager:
|
||||
<code-example language="bash">
|
||||
npm install -g @angular/cli
|
||||
|
||||
<code-example format="shell" language="shell">
|
||||
|
||||
npm install -g @angular/cli
|
||||
|
||||
</code-example>
|
||||
|
||||
For details about changes between versions, and information about updating from previous releases,
|
||||
see the Releases tab on GitHub: https://github.com/angular/angular-cli/releases
|
||||
For details about changes between versions, and information about updating from previous releases, see the Releases tab on GitHub: https://github.com/angular/angular-cli/releases
|
||||
|
||||
## Basic workflow
|
||||
|
||||
Invoke the tool on the command line through the `ng` executable.
|
||||
Online help is available on the command line.
|
||||
Enter the following to list commands or options for a given command (such as [generate](cli/generate)) with a short description.
|
||||
Enter the following to list commands or options for a given command \(such as [generate](cli/generate)\) with a short description.
|
||||
|
||||
<code-example format="shell" language="shell">
|
||||
|
||||
<code-example language="bash">
|
||||
ng help
|
||||
ng generate --help
|
||||
|
||||
</code-example>
|
||||
|
||||
To create, build, and serve a new, basic Angular project on a development server, go to the parent directory of your new workspace use the following commands:
|
||||
|
||||
<code-example language="bash">
|
||||
<code-example format="shell" language="shell">
|
||||
|
||||
ng new my-first-project
|
||||
cd my-first-project
|
||||
ng serve
|
||||
|
||||
</code-example>
|
||||
|
||||
In your browser, open http://localhost:4200/ to see the new application run.
|
||||
@@ -38,9 +44,10 @@ When you use the [ng serve](cli/serve) command to build an application and serve
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
When you run `ng new my-first-project` a new folder, named `my-first-project`, will be created in the current working directory. Since you want to be able to create files inside that folder, make sure you have sufficient rights in the current working directory before running the command.
|
||||
When you run `ng new my-first-project` a new folder, named `my-first-project`, will be created in the current working directory.
|
||||
Since you want to be able to create files inside that folder, make sure you have sufficient rights in the current working directory before running the command.
|
||||
|
||||
If the current working directory is not the right place for your project, you can change to a more appropriate directory by running `cd <path-to-other-directory>` first.
|
||||
If the current working directory is not the right place for your project, you can change to a more appropriate directory by running `cd <path-to-other-directory>`.
|
||||
|
||||
</div>
|
||||
|
||||
@@ -58,7 +65,7 @@ You can edit the generated files directly, or add to and modify them using CLI c
|
||||
Use the [ng generate](cli/generate) command to add new files for additional components and services, and code for new pipes, directives, and so on.
|
||||
Commands such as [add](cli/add) and [generate](cli/generate), which create or operate on applications and libraries, must be executed from within a workspace or project folder.
|
||||
|
||||
* See more about the [Workspace file structure](guide/file-structure).
|
||||
* See more about the [Workspace file structure](guide/file-structure).
|
||||
|
||||
### Workspace and project configuration
|
||||
|
||||
@@ -66,10 +73,15 @@ A single workspace configuration file, `angular.json`, is created at the top lev
|
||||
This is where you can set per-project defaults for CLI command options, and specify configurations to use when the CLI builds a project for different targets.
|
||||
|
||||
The [ng config](cli/config) command lets you set and retrieve configuration values from the command line, or you can edit the `angular.json` file directly.
|
||||
Note that option names in the configuration file must use [camelCase](guide/glossary#case-types), while option names supplied to commands can use either camelCase or dash-case.
|
||||
|
||||
* See more about [Workspace Configuration](guide/workspace-config).
|
||||
* See the [complete schema](https://github.com/angular/angular-cli/wiki/angular-workspace) for `angular.json`.
|
||||
<div class="alert is-helpful">
|
||||
|
||||
**NOTE**: <br />
|
||||
Option names in the configuration file must use [camelCase](guide/glossary#case-types), while option names supplied to commands must be dash-case.
|
||||
|
||||
</div>
|
||||
|
||||
* See more about [Workspace Configuration](guide/workspace-config).
|
||||
|
||||
## CLI command-language syntax
|
||||
|
||||
@@ -77,34 +89,44 @@ Command syntax is shown as follows:
|
||||
|
||||
`ng` *commandNameOrAlias* *requiredArg* [*optionalArg*] `[options]`
|
||||
|
||||
* Most commands, and some options, have aliases. Aliases are shown in the syntax statement for each command.
|
||||
* Most commands, and some options, have aliases.
|
||||
Aliases are shown in the syntax statement for each command.
|
||||
|
||||
* Option names are prefixed with a double dash (--).
|
||||
Option aliases are prefixed with a single dash (-).
|
||||
* Option names are prefixed with a double dash \(`--`\) characters.
|
||||
Option aliases are prefixed with a single dash \(`-`\) character.
|
||||
Arguments are not prefixed.
|
||||
For example:
|
||||
<code-example language="bash">
|
||||
ng build my-app -c production
|
||||
|
||||
<code-example format="shell" language="shell">
|
||||
|
||||
ng build my-app -c production
|
||||
|
||||
</code-example>
|
||||
|
||||
* Typically, the name of a generated artifact can be given as an argument to the command or specified with the --name option.
|
||||
* Typically, the name of a generated artifact can be given as an argument to the command or specified with the `--name` option.
|
||||
|
||||
* Argument and option names can be given in either
|
||||
[camelCase or dash-case](guide/glossary#case-types).
|
||||
`--myOptionName` is equivalent to `--my-option-name`.
|
||||
* Argument and option names can be given in either [camelCase or dash-case](guide/glossary#case-types).
|
||||
`--myOptionName` is equivalent to `--my-option-name`.
|
||||
|
||||
### Boolean options
|
||||
|
||||
Boolean options have two forms: `--this-option` sets the flag to `true`, `--no-this-option` sets it to `false`.
|
||||
If neither option is supplied, the flag remains in its default state, as listed in the reference documentation.
|
||||
|
||||
|
||||
### Relative paths
|
||||
|
||||
Options that specify files can be given as absolute paths, or as paths relative to the current working directory, which is generally either the workspace or project root.
|
||||
|
||||
### Schematics
|
||||
|
||||
The [ng generate](cli/generate) and [ng add](cli/add) commands take as an argument the artifact or library to be generated or added to the current project.
|
||||
The [ng generate](cli/generate) and [ng add](cli/add) commands take, as an argument, the artifact or library to be generated or added to the current project.
|
||||
In addition to any general options, each artifact or library defines its own options in a *schematic*.
|
||||
Schematic options are supplied to the command in the same format as immediate command options.
|
||||
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
<!-- end links -->
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
@@ -20,14 +20,18 @@ This ID is a string that looks like `UA-123456-12`.
|
||||
You can choose to use a descriptive string as the key value, or be assigned a random key when you run the CLI command.
|
||||
For example, the following command adds a configuration key named "tracking".
|
||||
|
||||
<code-example language="sh">
|
||||
<code-example format="shell" language="shell">
|
||||
|
||||
ng config --global cli.analyticsSharing.tracking UA-123456-12
|
||||
|
||||
</code-example>
|
||||
|
||||
To turn off this feature, run the following command:
|
||||
|
||||
<code-example language="sh">
|
||||
<code-example format="shell" language="shell">
|
||||
|
||||
ng config --global cli.analyticsSharing undefined
|
||||
|
||||
</code-example>
|
||||
|
||||
## Per user tracking
|
||||
@@ -35,12 +39,24 @@ ng config --global cli.analyticsSharing undefined
|
||||
You can add a custom user ID to the global configuration, in order to identify unique usage of commands and flags.
|
||||
If that user enables CLI analytics for their own project, your analytics display tracks and labels their individual usage.
|
||||
|
||||
<code-example language="sh">
|
||||
<code-example format="shell" language="shell">
|
||||
|
||||
ng config --global cli.analyticsSharing.uuid SOME_USER_NAME
|
||||
|
||||
</code-example>
|
||||
|
||||
To generate a new random user ID, run the following command:
|
||||
|
||||
<code-example language="sh">
|
||||
<code-example format="shell" language="shell">
|
||||
|
||||
ng config --global cli.analyticsSharing.uuid ""
|
||||
|
||||
</code-example>
|
||||
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
<!-- end links -->
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
@@ -4,17 +4,36 @@
|
||||
@shortDescription Expression has changed after it was checked
|
||||
|
||||
@description
|
||||
Angular throws an `ExpressionChangedAfterItHasBeenCheckedError` when an expression value has been changed after change detection has completed. Angular only throws this error in development mode.
|
||||
|
||||
In development mode, Angular performs an additional check after each change detection run, to ensure the bindings haven’t changed. This catches errors where the view is left in an inconsistent state. This can occur, for example, if a method or getter returns a different value each time it is called, or if a child component changes values on its parent. If either of these occur, this is a sign that change detection is not stabilized. Angular throws the error to ensure data is always reflected correctly in the view, which prevents erratic UI behavior or a possible infinite loop.
|
||||
Angular throws an `ExpressionChangedAfterItHasBeenCheckedError` when an expression value has been changed after change detection has completed.
|
||||
Angular only throws this error in development mode.
|
||||
|
||||
This error commonly occurs when you’ve added template expressions or begun to implement lifecycle hooks like `ngAfterViewInit` or `ngOnChanges`. It is also common when dealing with loading status and asynchronous operations, or a child component changes its parent bindings.
|
||||
In development mode, Angular performs an additional check after each change detection run, to ensure the bindings haven't changed.
|
||||
This catches errors where the view is left in an inconsistent state.
|
||||
This can occur, for example, if a method or getter returns a different value each time it is called, or if a child component changes values on its parent.
|
||||
If either of these occur, this is a sign that change detection is not stabilized
|
||||
Angular throws the error to ensure data is always reflected correctly in the view, which prevents erratic UI behavior or a possible infinite loop.
|
||||
|
||||
This error commonly occurs when you've added template expressions or have begun to implement lifecycle hooks like `ngAfterViewInit` or `ngOnChanges`.
|
||||
It is also common when dealing with loading status and asynchronous operations, or when a child component changes its parent bindings.
|
||||
|
||||
@debugging
|
||||
The [source maps](https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Use_a_source_map) generated by the CLI are very useful when debugging. Navigate up the call stack until you find a template expression where the value displayed in the error has changed.
|
||||
|
||||
Ensure that there are no changes to the bindings in the template after change detection is run. This often means refactoring to use the correct [component lifecycle hook](guide/lifecycle-hooks) for your use case. If the issue exists within `ngAfterViewInit`, the recommended solution is to use a constructor or `ngOnInit` to set initial values, or use `ngAfterContentInit` for other value bindings.
|
||||
The [source maps](https://developer.mozilla.org/docs/Tools/Debugger/How_to/Use_a_source_map) generated by the CLI are very useful when debugging.
|
||||
Navigate up the call stack until you find a template expression where the value displayed in the error has changed.
|
||||
|
||||
Ensure that there are no changes to the bindings in the template after change detection is run.
|
||||
This often means refactoring to use the correct [component lifecycle hook](guide/lifecycle-hooks) for your use case.
|
||||
If the issue exists within `ngAfterViewInit`, the recommended solution is to use a constructor or `ngOnInit` to set initial values, or use `ngAfterContentInit` for other value bindings.
|
||||
|
||||
If you are binding to methods in the view, ensure that the invocation does not update any of the other bindings in the template.
|
||||
|
||||
Read more about which solution is right for you in ['Everything you need to know about the "ExpressionChangedAfterItHasBeenCheckedError" error'](https://indepth.dev/posts/1001/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error) and why this is useful at ['Angular Debugging "Expression has changed after it was checked": Simple Explanation (and Fix)'](https://blog.angular-university.io/angular-debugging/).
|
||||
Read more about which solution is right for you in ['Everything you need to know about the "ExpressionChangedAfterItHasBeenCheckedError" error'](https://indepth.dev/posts/1001/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error) and why this is useful at ['Angular Debugging "Expression has changed after it was checked": Simple Explanation (and Fix)'](https://blog.angular-university.io/angular-debugging).
|
||||
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
<!-- end links -->
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
@@ -4,9 +4,23 @@
|
||||
@shortDescription Circular dependency in DI detected while instantiating a provider
|
||||
|
||||
@description
|
||||
A cyclic dependency exists when a [dependency of a service](guide/hierarchical-dependency-injection) directly or indirectly depends on the service itself. For example, if `UserService` depends on `EmployeeService`, which also depends on `UserService`. Angular will have to instantiate `EmployeeService` to create `UserService`, which depends on `UserService`, itself.
|
||||
|
||||
A cyclic dependency exists when a [dependency of a service](guide/hierarchical-dependency-injection) directly or indirectly depends on the service itself.
|
||||
For example, if `UserService` depends on `EmployeeService`, which also depends on `UserService`.
|
||||
Angular will have to instantiate `EmployeeService` to create `UserService`, which depends on `UserService`, itself.
|
||||
|
||||
@debugging
|
||||
Use the call stack to determine where the cyclical dependency exists. You will be able to see if any child dependencies rely on the original file by [mapping out](guide/dependency-injection-in-action) the component, module, or service’s dependencies and identify the loop causing the problem.
|
||||
|
||||
Break this loop (or circle) of dependency to resolve this error. This most commonly means removing or refactoring the dependencies to not be reliant on one another.
|
||||
Use the call stack to determine where the cyclical dependency exists.
|
||||
You will be able to see if any child dependencies rely on the original file by [mapping out](guide/dependency-injection-in-action) the component, module, or service's dependencies, and identifying the loop causing the problem.
|
||||
|
||||
Break this loop \(or circle\) of dependency to resolve this error.
|
||||
This most commonly means removing or refactoring the dependencies to not be reliant on one another.
|
||||
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
<!-- end links -->
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
@@ -4,17 +4,30 @@
|
||||
@shortDescription No provider for {token} found!
|
||||
|
||||
@description
|
||||
|
||||
You see this error when you try to inject a service but have not declared a corresponding provider. A provider is a mapping that supplies a value that you can inject into the constructor of a class in your application.
|
||||
|
||||
Read more on providers in our [Dependency Injection guide](guide/dependency-injection).
|
||||
|
||||
@debugging
|
||||
Work backwards from the object where the error states that a [provider](guide/architecture-services) is missing: `No provider for ${this}!`. This is commonly thrown in [services](tutorial/toh-pt4), which require non-existing providers.
|
||||
|
||||
Work backwards from the object where the error states that a [provider](guide/architecture-services) is missing: `No provider for ${this}!`.
|
||||
This is commonly thrown in [services](tutorial/toh-pt4), which require non-existing providers.
|
||||
|
||||
To fix the error ensure that your service is registered in the list of providers of an `NgModule` or has the `@Injectable` decorator with a `providedIn` property at top.
|
||||
|
||||
The most common solution is to add a provider in `@Injectable` using `providedIn`:
|
||||
|
||||
```typescript
|
||||
@Injectable({ providedIn: 'app' })
|
||||
```
|
||||
<code-example format="typescript" language="typescript">
|
||||
|
||||
@Injectable({ providedIn: 'app' })
|
||||
|
||||
</code-example>
|
||||
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
<!-- end links -->
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
@@ -7,15 +7,25 @@
|
||||
Two or more components use the same [element selector](guide/component-overview#specifying-a-components-css-selector). Because there can only be a single component associated with an element, selectors must be unique strings to prevent ambiguity for Angular.
|
||||
|
||||
@debugging
|
||||
Use the element name from the error message to search for places where you’re using the same [selector declaration](guide/architecture-components) in your codebase:
|
||||
Use the element name from the error message to search for places where you're using the same [selector declaration](guide/architecture-components) in your codebase:
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
<code-example format="typescript" language="typescript">
|
||||
|
||||
@Component({
|
||||
selector: 'YOUR_STRING',
|
||||
...
|
||||
…
|
||||
})
|
||||
```
|
||||
|
||||
</code-example>
|
||||
|
||||
Ensure that each component has a unique CSS selector. This will guarantee that Angular renders the component you expect.
|
||||
|
||||
If you’re having trouble finding multiple components with this selector tag name, check for components from imported component libraries, such as Angular Material. Make sure you're following the [best practices](guide/styleguide#component-selectors) for your selectors to prevent collisions.
|
||||
If you're having trouble finding multiple components with this selector tag name, check for components from imported component libraries, such as Angular Material. Make sure you're following the [best practices](guide/styleguide#component-selectors) for your selectors to prevent collisions.
|
||||
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
<!-- end links -->
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
@shortDescription Export not found!
|
||||
|
||||
@description
|
||||
Angular can’t find a directive with `{{ PLACEHOLDER }}` export name. The export name is specified in the `exportAs` property of the directive decorator. This is common when using FormsModule or Material modules in templates, and you’ve forgotten to [import the corresponding modules](guide/sharing-ngmodules).
|
||||
|
||||
Angular can't find a directive with `{{ PLACEHOLDER }}` export name.
|
||||
The export name is specified in the `exportAs` property of the directive decorator.
|
||||
This is common when using FormsModule or Material modules in templates and you've forgotten to [import the corresponding modules](guide/sharing-ngmodules).
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
@@ -13,20 +16,30 @@ This is the runtime equivalent of a common compiler error [NG8003: No directive
|
||||
</div>
|
||||
|
||||
@debugging
|
||||
|
||||
Use the export name to trace the templates or modules using this export.
|
||||
|
||||
Ensure that all dependencies are [properly imported and declared in your NgModules](guide/sharing-ngmodules). For example, if the export not found is `ngForm`, we need to import `FormsModule` and declare it in the list of imports in `*.module.ts` to resolve the error.
|
||||
Ensure that all dependencies are [properly imported and declared in your NgModules](guide/sharing-ngmodules).
|
||||
For example, if the export not found is `ngForm`, we need to import `FormsModule` and declare it in the list of imports in `*.module.ts` to resolve the error.
|
||||
|
||||
```typescript
|
||||
import { FormsModule } from '@angular/forms';
|
||||
<code-example format="typescript" language="typescript">
|
||||
|
||||
@NgModule({
|
||||
...
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
@NgModule({
|
||||
…
|
||||
imports: [
|
||||
FormsModule,
|
||||
…
|
||||
```
|
||||
…
|
||||
|
||||
</code-example>
|
||||
|
||||
If you recently added an import, you may need to restart your server to see these changes.
|
||||
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
<!-- end links -->
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
@@ -4,15 +4,26 @@
|
||||
@shortDescription Pipe not found!
|
||||
|
||||
@description
|
||||
Angular can't find a pipe with this name.
|
||||
The pipe referenced in the template has not been named or declared properly.
|
||||
|
||||
Angular can't find a pipe with this name.
|
||||
The pipe referenced in the template has not been named or declared properly.
|
||||
A [pipe](guide/pipes) must be either declared or imported in the `NgModule` where it is used, and the name used in a template must match the name defined in the pipe decorator.
|
||||
|
||||
@debugging
|
||||
|
||||
Use the pipe name to trace the templates or modules where this pipe is declared and used.
|
||||
|
||||
To resolve this error, ensure that:
|
||||
- A local custom pipe is uniquely named in the pipe's decorator, and declared in the `NgModule`, or
|
||||
- A pipe from another `NgModule` is added to the imports of the `NgModule` where it is used.
|
||||
|
||||
* A local custom pipe is uniquely named in the pipe's decorator, and declared in the `NgModule`, or
|
||||
* A pipe from another `NgModule` is added to the imports of the `NgModule` where it is used
|
||||
|
||||
If you recently added an import or declaration, you may need to restart your server to see these changes.
|
||||
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
<!-- end links -->
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
@@ -3,27 +3,42 @@
|
||||
@shortDescription Decorator argument is not an object literal
|
||||
|
||||
@description
|
||||
|
||||
To make the metadata extraction in the Angular compiler faster, the decorators `@NgModule`, `@Pipe`, `@Component`, `@Directive`, and `@Injectable` accept only object literals as arguments.
|
||||
|
||||
This is an [intentional change in Ivy](https://github.com/angular/angular/issues/30840#issuecomment-498869540), which enforces stricter argument requirements for decorators than View Engine. Ivy requires this approach because it compiles decorators by moving the expressions into other locations in the class output.
|
||||
This is an [intentional change in Ivy](https://github.com/angular/angular/issues/30840#issuecomment-498869540), which enforces stricter argument requirements for decorators than View Engine.
|
||||
Ivy requires this approach because it compiles decorators by moving the expressions into other locations in the class output.
|
||||
|
||||
@debugging
|
||||
|
||||
Move all declarations:
|
||||
|
||||
```typescript
|
||||
const moduleDefinition = {...}
|
||||
<code-example format="typescript" language="typescript">
|
||||
|
||||
@NgModule(moduleDefinition)
|
||||
const moduleDefinition = {…}
|
||||
|
||||
@NgModule(moduleDefinition)
|
||||
export class AppModule {
|
||||
constructor() {}
|
||||
}
|
||||
```
|
||||
|
||||
</code-example>
|
||||
|
||||
into the decorator:
|
||||
|
||||
```typescript
|
||||
@NgModule({...})
|
||||
<code-example format="typescript" language="typescript">
|
||||
|
||||
@NgModule({…})
|
||||
export class AppModule {
|
||||
constructor() {}
|
||||
}
|
||||
```
|
||||
|
||||
</code-example>
|
||||
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
<!-- end links -->
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
@@ -3,9 +3,21 @@
|
||||
@shortDescription No suitable injection token for parameter
|
||||
|
||||
@description
|
||||
|
||||
There is no injection token for a constructor parameter at compile time. [InjectionTokens](api/core/InjectionToken) are tokens that can be used in a Dependency Injection Provider.
|
||||
|
||||
@debugging
|
||||
Look at the parameter that throws the error and all uses of the class. This error is commonly thrown when a constructor defines parameters with primitive types like `string`, `number`, `boolean`, and `Object`.
|
||||
|
||||
Use the `@Injectable` method or `@Inject` decorator from `@angular/core` to ensure that the type you are injecting is reified (has a runtime representation). Make sure to add a provider to this decorator so that you do not throw [NG0201: No Provider Found](errors/NG0201).
|
||||
Look at the parameter that throws the error, and all uses of the class.
|
||||
This error is commonly thrown when a constructor defines parameters with primitive types such as `string`, `number`, `boolean`, and `Object`.
|
||||
|
||||
Use the `@Injectable` method or `@Inject` decorator from `@angular/core` to ensure that the type you are injecting is reified \(has a runtime representation\).
|
||||
Make sure to add a provider to this decorator so that you do not throw [NG0201: No Provider Found](errors/NG0201).
|
||||
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
<!-- end links -->
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
@@ -3,30 +3,39 @@
|
||||
@shortDescription Component selector does not match shadow DOM requirements
|
||||
|
||||
@description
|
||||
|
||||
The selector of a component using `ViewEncapsulation.ShadowDom` doesn't match the custom element tag name requirements.
|
||||
|
||||
In order for a tag name to be considered a valid custom element name, it has to:
|
||||
* Be in lower case.
|
||||
* Contain a hyphen.
|
||||
* Start with a letter (a-z).
|
||||
|
||||
* Be in lower case.
|
||||
* Contain a hyphen.
|
||||
* Start with a letter \(a-z\).
|
||||
|
||||
@debugging
|
||||
|
||||
Rename your component's selector so that it matches the requirements.
|
||||
|
||||
**Before:**
|
||||
```typescript
|
||||
@Component({
|
||||
|
||||
<code-example format="typescript" language="typescript">
|
||||
|
||||
@Component({
|
||||
selector: 'comp',
|
||||
encapsulation: ViewEncapsulation.ShadowDom
|
||||
...
|
||||
…
|
||||
})
|
||||
```
|
||||
|
||||
</code-example>
|
||||
|
||||
**After:**
|
||||
```typescript
|
||||
@Component({
|
||||
|
||||
<code-example format="typescript" language="typescript">
|
||||
|
||||
@Component({
|
||||
selector: 'app-comp',
|
||||
encapsulation: ViewEncapsulation.ShadowDom
|
||||
...
|
||||
…
|
||||
})
|
||||
```
|
||||
|
||||
</code-example>
|
||||
|
||||
@@ -4,54 +4,59 @@
|
||||
|
||||
@description
|
||||
|
||||
A component, directive or pipe that is referenced by this component would require the compiler
|
||||
to add an import that would lead to a cycle of imports. For example, consider a scenario where
|
||||
a `ParentComponent` references a `ChildComponent` in its template:
|
||||
A component, directive, or pipe that is referenced by this component would require the compiler to add an import that would lead to a cycle of imports.
|
||||
For example, consider a scenario where a `ParentComponent` references a `ChildComponent` in its template:
|
||||
|
||||
<code-example path="errors/cyclic-imports/parent.component.ts" header="parent.component.ts"></code-example>
|
||||
<code-example header="parent.component.ts" path="errors/cyclic-imports/parent.component.ts"></code-example>
|
||||
|
||||
<code-example path="errors/cyclic-imports/child.component.ts" header="child.component.ts"></code-example>
|
||||
<code-example header="child.component.ts" path="errors/cyclic-imports/child.component.ts"></code-example>
|
||||
|
||||
There is already an import from `child.component.ts` to `parent.component.ts` since the `ChildComponent`
|
||||
references the `ParentComponent` in its constructor.
|
||||
There is already an import from `child.component.ts` to `parent.component.ts` since the `ChildComponent` references the `ParentComponent` in its constructor.
|
||||
|
||||
But note that the parent component's template contains `<child></child>`. The generated code for this
|
||||
template must therefore contain a reference to the `ChildComponent` class. In order to make this reference
|
||||
the compiler would have to add an import from `parent.component.ts` to `child.component.ts`, which would
|
||||
cause an import cycle:
|
||||
<div class="alert is-helpful">
|
||||
|
||||
```
|
||||
parent.component.ts -> child.component.ts -> parent.component.ts
|
||||
```
|
||||
**NOTE**: <br />
|
||||
The parent component's template contains `<child></child>`.
|
||||
The generated code for this template must therefore contain a reference to the `ChildComponent` class.
|
||||
In order to make this reference, the compiler would have to add an import from `parent.component.ts` to `child.component.ts`, which would cause an import cycle:
|
||||
|
||||
<code-example format="none" language="none">
|
||||
|
||||
parent.component.ts -> child.component.ts -> parent.component.ts
|
||||
|
||||
</code-example>
|
||||
|
||||
</div>
|
||||
|
||||
### Remote Scoping
|
||||
|
||||
To avoid adding imports that create cycles, additional code is added to the `NgModule` class where
|
||||
the component is declared that wires up the dependencies. This is known as "remote scoping".
|
||||
|
||||
To avoid adding imports that create cycles, additional code is added to the `NgModule` class where the component that wires up the dependencies is declared.
|
||||
This is known as "remote scoping".
|
||||
|
||||
### Libraries
|
||||
|
||||
Unfortunately, "remote scoping" code is side-effectful, which prevents tree shaking, and cannot
|
||||
be used in libraries. So when building libraries using the `"compilationMode": "partial"` setting,
|
||||
any component that would require a cyclic import will cause this `NG3003` compiler error to be raised.
|
||||
|
||||
Unfortunately, "remote scoping" code is side-effectful —which prevents tree shaking— and cannot be used in libraries.
|
||||
So when building libraries using the `"compilationMode": "partial"` setting, any component that would require a cyclic import will cause this `NG3003` compiler error to be raised.
|
||||
|
||||
@debugging
|
||||
|
||||
The cycle that would be generated is shown as part of the error message. For example:
|
||||
The cycle that would be generated is shown as part of the error message.
|
||||
For example:
|
||||
|
||||
<code-example hideCopy="true">
|
||||
<span class="nocode">The component ChildComponent is used in the template but importing it would create a cycle:
|
||||
/parent.component.ts -> /child.component.ts -> /parent.component.ts</span>
|
||||
|
||||
The component ChildComponent is used in the template but importing it would create a cycle:
|
||||
/parent.component.ts -> /child.component.ts -> /parent.component.ts
|
||||
|
||||
</code-example>
|
||||
|
||||
Use this to identify how the referenced component, pipe or directive has a dependency back to the
|
||||
component being compiled. Here are some ideas for fixing the problem:
|
||||
Use this to identify how the referenced component, pipe, or directive has a dependency back to the component being compiled.
|
||||
Here are some ideas for fixing the problem:
|
||||
|
||||
* Try to re-arrange your dependencies to avoid the cycle. For example using an intermediate interface
|
||||
that is stored in an independent file that can be imported to both dependent files without
|
||||
causing an import cycle.
|
||||
* Move the classes that reference each other into the same file, to avoid any imports between them.
|
||||
* Convert import statements to type-only imports (using `import type` syntax) if the imported declarations
|
||||
are only used as types, as type-only imports do not contribute to cycles.
|
||||
* Try to rearrange your dependencies to avoid the cycle.
|
||||
For example, using an intermediate interface that is stored in an independent file that can be imported to both dependent files without causing an import cycle.
|
||||
|
||||
* Move the classes that reference each other into the same file, to avoid any imports between them.
|
||||
* Convert import statements to type-only imports \(using `import type` syntax\) if the imported declarations are only used as types, as type-only imports do not contribute to cycles.
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
@shortDescription Invalid @NgModule() metadata
|
||||
|
||||
@description
|
||||
|
||||
This error represents the import or export of an `@NgModule()` that doesn't have valid metadata.
|
||||
|
||||
@debugging
|
||||
|
||||
The library might have been processed with `ngcc`.
|
||||
If this is the case, try removing and reinstalling `node_modules`.
|
||||
This error is likely due to the library being published for Angular Ivy, which cannot be used in this View Engine application.
|
||||
|
||||
@@ -3,19 +3,30 @@
|
||||
@shortDescription Unknown HTML element or component
|
||||
|
||||
@description
|
||||
|
||||
One or more elements cannot be resolved during compilation because the element is not defined by the HTML spec, or there is no component or directive with such element selector.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
This is the compiler equivalent of a common runtime error `NG0304: '${tagName}' is not a known element: …`.
|
||||
This is the compiler equivalent of a common runtime error `NG0304: '${tagName}' is not a known element: …`.
|
||||
|
||||
</div>
|
||||
|
||||
@debugging
|
||||
|
||||
Use the element name in the error to find the file(s) where the element is being used.
|
||||
|
||||
Check that the name and selector are correct. If the component is from a different module or import, check that the component is exported from its origin module and imported into the correct `*.modules.ts` file, and declared in the imports list.
|
||||
Check that the name and selector are correct.
|
||||
If the component is from a different module or import, check that the component is exported from its origin module and imported into the correct `*.modules.ts` file, and declared in the imports list.
|
||||
|
||||
When using custom elements or web components, ensure that you add [`CUSTOM_ELEMENTS_SCHEMA`](api/core/CUSTOM_ELEMENTS_SCHEMA) to the application module.
|
||||
|
||||
If this does not resolve the error, check the imported libraries for any recent changes to the exports and properties you are using, and restart your server.
|
||||
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
<!-- end links -->
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
@@ -4,13 +4,27 @@
|
||||
@shortDescription Unknown attribute or input
|
||||
|
||||
@description
|
||||
|
||||
An attribute or property cannot be resolved during compilation.
|
||||
|
||||
This error arises when attempting to bind to a property that does not exist. Any property binding must correspond to either:
|
||||
* A native property on the HTML element, or
|
||||
* An `@Input()` property of a component or directive applied to the element.
|
||||
This error arises when attempting to bind to a property that does not exist.
|
||||
Any property binding must correspond to either:
|
||||
|
||||
The runtime error for this is `NG0304: '${tagName}' is not a known element: …’`.
|
||||
* A native property on the HTML element, or
|
||||
* An `@Input()` property of a component or directive applied to the element.
|
||||
|
||||
The runtime error for this is `NG0304: '${tagName}' is not a known element: …'`.
|
||||
|
||||
@debugging
|
||||
Look at documentation for the specific [binding syntax](guide/binding-syntax) used. This is usually a typo or incorrect import. There may also be a missing direction with property selector ‘name’ or missing input.
|
||||
|
||||
Look at documentation for the specific [binding syntax](guide/binding-syntax) used.
|
||||
This is usually a typo or incorrect import.
|
||||
There may also be a missing direction with property selector 'name' or missing input.
|
||||
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
<!-- end links -->
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
@shortDescription No directive found with export
|
||||
|
||||
@description
|
||||
Angular can’t find a directive with `{{ PLACEHOLDER }}` export name. This is common with a missing import or a missing [`exportAs`](api/core/Directive#exportAs) on a directive.
|
||||
|
||||
Angular can't find a directive with `{{ PLACEHOLDER }}` export name.
|
||||
This is common with a missing import or a missing [`exportAs`](api/core/Directive#exportAs) on a directive.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
@@ -14,18 +15,30 @@ This is the compiler equivalent of a common runtime error [NG0301: Export Not Fo
|
||||
</div>
|
||||
|
||||
@debugging
|
||||
|
||||
Use the string name of the export not found to trace the templates or modules using this export.
|
||||
|
||||
Ensure that all dependencies are properly imported and declared in our Modules. For example, if the export not found is `ngForm`, we will need to import `FormsModule` and declare it in our list of imports in `*.module.ts` to resolve the missing export error.
|
||||
Ensure that all dependencies are properly imported and declared in our Modules.
|
||||
For example, if the export not found is `ngForm`, we will need to import `FormsModule` and declare it in our list of imports in `*.module.ts` to resolve the missing export error.
|
||||
|
||||
```typescript
|
||||
import { FormsModule } from '@angular/forms';
|
||||
<code-example format="typescript" language="typescript">
|
||||
|
||||
@NgModule({
|
||||
...
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
@NgModule({
|
||||
…
|
||||
imports: [
|
||||
FormsModule,
|
||||
…
|
||||
```
|
||||
…
|
||||
|
||||
</code-example>
|
||||
|
||||
If you recently added an import, you will need to restart your server to see these changes.
|
||||
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
<!-- end links -->
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// #docplaster ...
|
||||
// #docplaster …
|
||||
// #docregion webpack-config
|
||||
import linkerPlugin from '@angular/compiler-cli/linker/babel';
|
||||
|
||||
|
||||
@@ -8,8 +8,11 @@ import * as auto from './auto.po';
|
||||
import * as filterStagger from './filter-stagger.po';
|
||||
import * as heroGroups from './hero-groups';
|
||||
import { getLinkById, sleepFor } from './util';
|
||||
import { getComponentSection, getToggleButton } from './querying.po';
|
||||
|
||||
describe('Animation Tests', () => {
|
||||
const routingAnimationDuration = 350;
|
||||
|
||||
const openCloseHref = getLinkById('open-close');
|
||||
const statusSliderHref = getLinkById('status');
|
||||
const toggleHref = getLinkById('toggle');
|
||||
@@ -17,6 +20,9 @@ describe('Animation Tests', () => {
|
||||
const autoHref = getLinkById('auto');
|
||||
const filterHref = getLinkById('heroes');
|
||||
const heroGroupsHref = getLinkById('hero-groups');
|
||||
const queryingHref = getLinkById('querying');
|
||||
|
||||
const newPageSleepFor = (ms = 0) => sleepFor(ms + routingAnimationDuration);
|
||||
|
||||
beforeAll(() => browser.get(''));
|
||||
|
||||
@@ -26,7 +32,7 @@ describe('Animation Tests', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
await openCloseHref.click();
|
||||
await sleepFor();
|
||||
await newPageSleepFor(300);
|
||||
});
|
||||
|
||||
it('should be open', async () => {
|
||||
@@ -82,7 +88,7 @@ describe('Animation Tests', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
await statusSliderHref.click();
|
||||
await sleepFor(2000);
|
||||
await newPageSleepFor(2000);
|
||||
});
|
||||
|
||||
it('should be inactive with a blue background', async () => {
|
||||
@@ -123,7 +129,7 @@ describe('Animation Tests', () => {
|
||||
describe('Toggle Animations Component', () => {
|
||||
beforeAll(async () => {
|
||||
await toggleHref.click();
|
||||
await sleepFor();
|
||||
await newPageSleepFor();
|
||||
});
|
||||
|
||||
it('should disabled animations on the child element', async () => {
|
||||
@@ -141,7 +147,7 @@ describe('Animation Tests', () => {
|
||||
describe('Enter/Leave Component', () => {
|
||||
beforeAll(async () => {
|
||||
await enterLeaveHref.click();
|
||||
await sleepFor(100);
|
||||
await newPageSleepFor(100);
|
||||
});
|
||||
|
||||
it('should attach a flyInOut trigger to the list of items', async () => {
|
||||
@@ -167,7 +173,7 @@ describe('Animation Tests', () => {
|
||||
describe('Auto Calculation Component', () => {
|
||||
beforeAll(async () => {
|
||||
await autoHref.click();
|
||||
await sleepFor(0);
|
||||
await newPageSleepFor();
|
||||
});
|
||||
|
||||
it('should attach a shrinkOut trigger to the list of items', async () => {
|
||||
@@ -191,7 +197,7 @@ describe('Animation Tests', () => {
|
||||
describe('Filter/Stagger Component', () => {
|
||||
beforeAll(async () => {
|
||||
await filterHref.click();
|
||||
await sleepFor();
|
||||
await newPageSleepFor();
|
||||
});
|
||||
|
||||
it('should attach a filterAnimations trigger to the list container', async () => {
|
||||
@@ -218,7 +224,7 @@ describe('Animation Tests', () => {
|
||||
describe('Hero Groups Component', () => {
|
||||
beforeAll(async () => {
|
||||
await heroGroupsHref.click();
|
||||
await sleepFor(300);
|
||||
await newPageSleepFor(400);
|
||||
});
|
||||
|
||||
it('should attach a flyInOut trigger to the list of items', async () => {
|
||||
@@ -242,4 +248,49 @@ describe('Animation Tests', () => {
|
||||
await browser.wait(async () => await heroesList.count() < total, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Querying Component', () => {
|
||||
const queryingAnimationDuration = 2500;
|
||||
|
||||
beforeAll(async () => {
|
||||
await queryingHref.click();
|
||||
await newPageSleepFor(queryingAnimationDuration);
|
||||
});
|
||||
|
||||
it('should toggle the section', async () => {
|
||||
const toggleButton = getToggleButton();
|
||||
const section = getComponentSection();
|
||||
|
||||
expect(await section.isPresent()).toBe(true);
|
||||
|
||||
// toggling off
|
||||
await toggleButton.click();
|
||||
await newPageSleepFor(queryingAnimationDuration);
|
||||
expect(await section.isPresent()).toBe(false);
|
||||
|
||||
// toggling on
|
||||
await toggleButton.click();
|
||||
await newPageSleepFor(queryingAnimationDuration);
|
||||
expect(await section.isPresent()).toBe(true);
|
||||
await newPageSleepFor(queryingAnimationDuration);
|
||||
});
|
||||
|
||||
it(`should disable the button for the animation's duration`, async () => {
|
||||
const toggleButton = getToggleButton();
|
||||
expect(await toggleButton.isEnabled()).toBe(true);
|
||||
|
||||
// toggling off
|
||||
await toggleButton.click();
|
||||
expect(await toggleButton.isEnabled()).toBe(false);
|
||||
await newPageSleepFor(queryingAnimationDuration);
|
||||
expect(await toggleButton.isEnabled()).toBe(true);
|
||||
|
||||
// toggling on
|
||||
await toggleButton.click();
|
||||
expect(await toggleButton.isEnabled()).toBe(false);
|
||||
await newPageSleepFor(queryingAnimationDuration);
|
||||
expect(await toggleButton.isEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { by } from 'protractor';
|
||||
import { locate } from './util';
|
||||
|
||||
export function getComponent() {
|
||||
return by.css('app-querying');
|
||||
}
|
||||
|
||||
export function getToggleButton() {
|
||||
const toggleButton = () => by.className('toggle');
|
||||
return locate(getComponent(), toggleButton());
|
||||
}
|
||||
|
||||
export function getComponentSection() {
|
||||
const findSection = () => by.css('section');
|
||||
return locate(getComponent(), findSection());
|
||||
}
|
||||
@@ -39,11 +39,10 @@ export const slideInAnimation =
|
||||
]),
|
||||
query(':enter', [
|
||||
animate('300ms ease-out', style({ left: '0%' }))
|
||||
])
|
||||
]),
|
||||
]),
|
||||
query(':enter', animateChild()),
|
||||
]),
|
||||
transition('* <=> FilterPage', [
|
||||
transition('* <=> *', [
|
||||
style({ position: 'relative' }),
|
||||
query(':enter, :leave', [
|
||||
style({
|
||||
@@ -59,13 +58,13 @@ export const slideInAnimation =
|
||||
query(':leave', animateChild()),
|
||||
group([
|
||||
query(':leave', [
|
||||
animate('200ms ease-out', style({ left: '100%' }))
|
||||
animate('200ms ease-out', style({ left: '100%', opacity: 0 }))
|
||||
]),
|
||||
query(':enter', [
|
||||
animate('300ms ease-out', style({ left: '0%' }))
|
||||
])
|
||||
]),
|
||||
query('@*', animateChild())
|
||||
]),
|
||||
query(':enter', animateChild()),
|
||||
])
|
||||
// #enddocregion query
|
||||
]);
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
nav a {
|
||||
padding: .7rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: .3rem;
|
||||
}
|
||||
|
||||
form {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
<a id="heroes" routerLink="/heroes" routerLinkActive="active">Filter/Stagger</a>
|
||||
<a id="hero-groups" routerLink="/hero-groups" routerLinkActive="active">Hero Groups</a>
|
||||
<a id="insert-remove" routerLink="/insert-remove" routerLinkActive="active">Insert/Remove</a>
|
||||
|
||||
<a id="querying" routerLink="/querying" routerLinkActive="active">Querying</a>
|
||||
</nav>
|
||||
|
||||
<!-- #docregion route-animations-outlet -->
|
||||
<div [@routeAnimations]="prepareRoute(outlet)">
|
||||
<router-outlet #outlet="outlet"></router-outlet>
|
||||
<div [@routeAnimations]="getRouteAnimationData()">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<!-- #enddocregion route-animations-outlet -->
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from '@angular/animations';
|
||||
|
||||
// #enddocregion imports
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { ChildrenOutletContexts, RouterOutlet } from '@angular/router';
|
||||
import { slideInAnimation } from './animations';
|
||||
|
||||
// #docregion decorator, toggle-app-animations, define
|
||||
@@ -34,12 +34,13 @@ export class AppComponent {
|
||||
public animationsDisabled = false;
|
||||
// #enddocregion toggle-app-animations
|
||||
|
||||
// #docregion prepare-router-outlet
|
||||
prepareRoute(outlet: RouterOutlet) {
|
||||
return outlet?.activatedRouteData?.['animation'];
|
||||
}
|
||||
// #docregion get-route-animations-data
|
||||
constructor(private contexts: ChildrenOutletContexts) {}
|
||||
|
||||
// #enddocregion prepare-router-outlet
|
||||
getRouteAnimationData() {
|
||||
return this.contexts.getContext('primary')?.route?.snapshot?.data?.['animation'];
|
||||
}
|
||||
// #enddocregion get-route-animations-data
|
||||
|
||||
toggleAnimations() {
|
||||
this.animationsDisabled = !this.animationsDisabled;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { HeroListAutoComponent } from './hero-list-auto.component';
|
||||
import { HomeComponent } from './home.component';
|
||||
import { AboutComponent } from './about.component';
|
||||
import { InsertRemoveComponent } from './insert-remove.component';
|
||||
import { QueryingComponent } from './querying.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
@@ -28,17 +29,61 @@ import { InsertRemoveComponent } from './insert-remove.component';
|
||||
BrowserAnimationsModule,
|
||||
RouterModule.forRoot([
|
||||
{ path: '', pathMatch: 'full', redirectTo: '/enter-leave' },
|
||||
{ path: 'open-close', component: OpenClosePageComponent },
|
||||
{ path: 'status', component: StatusSliderPageComponent },
|
||||
{ path: 'toggle', component: ToggleAnimationsPageComponent },
|
||||
{ path: 'heroes', component: HeroListPageComponent,
|
||||
data: { animation: 'FilterPage' } },
|
||||
{ path: 'hero-groups', component: HeroListGroupPageComponent },
|
||||
{ path: 'enter-leave', component: HeroListEnterLeavePageComponent },
|
||||
{ path: 'auto', component: HeroListAutoCalcPageComponent },
|
||||
{ path: 'insert-remove', component: InsertRemoveComponent},
|
||||
{ path: 'home', component: HomeComponent, data: { animation: 'HomePage' } },
|
||||
{ path: 'about', component: AboutComponent, data: { animation: 'AboutPage' } },
|
||||
{
|
||||
path: 'open-close',
|
||||
component: OpenClosePageComponent,
|
||||
data: { animation: 'openClosePage' }
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
component: StatusSliderPageComponent,
|
||||
data: { animation: 'statusPage' }
|
||||
},
|
||||
{
|
||||
path: 'toggle',
|
||||
component: ToggleAnimationsPageComponent,
|
||||
data: { animation: 'togglePage' }
|
||||
},
|
||||
{
|
||||
path: 'heroes',
|
||||
component: HeroListPageComponent,
|
||||
data: { animation: 'filterPage' }
|
||||
},
|
||||
{
|
||||
path: 'hero-groups',
|
||||
component: HeroListGroupPageComponent,
|
||||
data: { animation: 'heroGroupPage' }
|
||||
},
|
||||
{
|
||||
path: 'enter-leave',
|
||||
component: HeroListEnterLeavePageComponent,
|
||||
data: { animation: 'enterLeavePage' }
|
||||
},
|
||||
{
|
||||
path: 'auto',
|
||||
component: HeroListAutoCalcPageComponent,
|
||||
data: { animation: 'autoPage' }
|
||||
},
|
||||
{
|
||||
path: 'insert-remove',
|
||||
component: InsertRemoveComponent,
|
||||
data: { animation: 'insertRemovePage' }
|
||||
},
|
||||
{
|
||||
path: 'querying',
|
||||
component: QueryingComponent,
|
||||
data: { animation: 'queryingPage' }
|
||||
},
|
||||
{
|
||||
path: 'home',
|
||||
component: HomeComponent,
|
||||
data: { animation: 'HomePage' }
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
data: { animation: 'AboutPage' }
|
||||
},
|
||||
])
|
||||
],
|
||||
// #enddocregion route-animation-data
|
||||
@@ -59,6 +104,7 @@ import { InsertRemoveComponent } from './insert-remove.component';
|
||||
HeroListAutoComponent,
|
||||
HomeComponent,
|
||||
InsertRemoveComponent,
|
||||
QueryingComponent,
|
||||
AboutComponent
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
section {
|
||||
border: 1px solid black;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
section > * {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
color: black;
|
||||
background-color: #DDD;
|
||||
}
|
||||
|
||||
.hero .badge {
|
||||
display: inline-block;
|
||||
font-size: small;
|
||||
color: white;
|
||||
padding: 0.5rem;
|
||||
background-color: #3d5157;
|
||||
margin-right: .8em;
|
||||
border-radius: 4px 0 0 4px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.hero .name {
|
||||
height: min-content;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
Component,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
trigger,
|
||||
style,
|
||||
animate,
|
||||
transition,
|
||||
group,
|
||||
query,
|
||||
animateChild,
|
||||
keyframes
|
||||
} from '@angular/animations';
|
||||
|
||||
import { HEROES } from './mock-heroes';
|
||||
|
||||
@Component({
|
||||
selector: 'app-querying',
|
||||
template: `
|
||||
<nav>
|
||||
<button class="toggle" (click)="show = !show" [disabled]="toggleDisabled">Toggle View</button>
|
||||
</nav>
|
||||
<section *ngIf="show" @query (@query.start)="toggleDisabled = true" (@query.done)="toggleDisabled = false">
|
||||
<p>I am a simple child element</p>
|
||||
<p *ngIf="show">I am a child element that enters and leaves with its parent</p>
|
||||
<p @animateMe>I am a child element with an animation trigger</p>
|
||||
<div class="hero">
|
||||
<span class="badge">{{ hero.id }}</span>
|
||||
<span class="name">{{ hero.name }} <small>(heroes are always animated!)</small></span>
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
styleUrls: ['./querying.component.css'],
|
||||
animations: [
|
||||
trigger('query', [
|
||||
transition(':enter', [
|
||||
style({ height: 0 }),
|
||||
group([
|
||||
animate(500, style({ height: '*' })),
|
||||
query(':enter', [
|
||||
style({ opacity: 0, transform: 'scale(0)'}),
|
||||
animate(2000, style({ opacity: 1, transform: 'scale(1)' }))
|
||||
]),
|
||||
query('.hero', [
|
||||
style({ transform: 'translateX(-100%)'}),
|
||||
animate('.7s 500ms ease-in', style({ transform: 'translateX(0)' }))
|
||||
]),
|
||||
]),
|
||||
query('@animateMe', animateChild()),
|
||||
]),
|
||||
transition(':leave', [
|
||||
style({ height: '*' }),
|
||||
query('@animateMe', animateChild()),
|
||||
group([
|
||||
animate('500ms 500ms', style({ height: '0', padding: '0' })),
|
||||
query(':leave', [
|
||||
style({ opacity: 1, transform: 'scale(1)'}),
|
||||
animate('1s', style({ opacity: 0, transform: 'scale(0)' }))
|
||||
]),
|
||||
query('.hero', [
|
||||
style({ transform: 'translateX(0)'}),
|
||||
animate('.7s ease-out', style({ transform: 'translateX(-100%)' }))
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
trigger('animateMe', [
|
||||
transition('* <=> *', animate('500ms cubic-bezier(.68,-0.73,.26,1.65)', keyframes([
|
||||
style({ backgroundColor: "transparent", color: '*', offset: 0 }),
|
||||
style({ backgroundColor: "blue", color: 'white', offset: 0.2 }),
|
||||
style({ backgroundColor: "transparent", color: '*', offset: 1 })
|
||||
])))
|
||||
]),
|
||||
]
|
||||
})
|
||||
export class QueryingComponent {
|
||||
toggleDisabled = false;
|
||||
show = true;
|
||||
hero = HEROES[0];
|
||||
}
|
||||
@@ -25,7 +25,6 @@
|
||||
background-color: #EEE;
|
||||
margin: .5em;
|
||||
padding: 0;
|
||||
height: 1.6em;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// #docregion
|
||||
import { Component, Input, OnDestroy, AfterViewInit, ViewChild } from '@angular/core';
|
||||
import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
|
||||
import { AdDirective } from './ad.directive';
|
||||
import { AdItem } from './ad-item';
|
||||
@@ -17,7 +17,7 @@ import { AdComponent } from './ad.component';
|
||||
// #enddocregion ad-host
|
||||
})
|
||||
// #docregion class
|
||||
export class AdBannerComponent implements AfterViewInit, OnDestroy {
|
||||
export class AdBannerComponent implements OnInit, OnDestroy {
|
||||
@Input() ads: AdItem[] = [];
|
||||
|
||||
currentAdIndex = -1;
|
||||
@@ -25,7 +25,7 @@ export class AdBannerComponent implements AfterViewInit, OnDestroy {
|
||||
@ViewChild(AdDirective, {static: true}) adHost!: AdDirective;
|
||||
interval: number|undefined;
|
||||
|
||||
ngAfterViewInit() {
|
||||
ngOnInit(): void {
|
||||
this.loadComponent();
|
||||
this.getAds();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.checkboxes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.checkboxes > label {
|
||||
flex: 0 0 max-content;
|
||||
margin: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
section:not(:last-of-type) {
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
@@ -1,29 +1,46 @@
|
||||
<h1>HttpClient Example</h1>
|
||||
<div>
|
||||
<input type="checkbox" id="heroes" [checked]="showHeroes" (click)="toggleHeroes()">
|
||||
<label for="heroes">Heroes</label>
|
||||
<div class="checkboxes">
|
||||
<label>
|
||||
<input type="checkbox" [checked]="showHeroes" (click)="toggleHeroes()">
|
||||
Heroes
|
||||
</label>
|
||||
|
||||
<input type="checkbox" id="config" [checked]="showConfig" (click)="toggleConfig()">
|
||||
<label for="config">Config</label>
|
||||
<label>
|
||||
<input type="checkbox" [checked]="showConfig" (click)="toggleConfig()">
|
||||
Config
|
||||
</label>
|
||||
|
||||
<input type="checkbox" id="downloader" [checked]="showDownloader" (click)="toggleDownloader()">
|
||||
<label for="downloader">Downloader</label>
|
||||
<label>
|
||||
<input type="checkbox" [checked]="showDownloader" (click)="toggleDownloader()">
|
||||
Downloader
|
||||
</label>
|
||||
|
||||
<input type="checkbox" id="uploader" [checked]="showUploader" (click)="toggleUploader()">
|
||||
<label for="uploader">Uploader</label>
|
||||
<label>
|
||||
<input type="checkbox" [checked]="showUploader" (click)="toggleUploader()">
|
||||
Uploader
|
||||
</label>
|
||||
|
||||
<input type="checkbox" id="search" [checked]="showSearch" (click)="toggleSearch()">
|
||||
<label for="search">Search</label>
|
||||
<label>
|
||||
<input type="checkbox" id="search" [checked]="showSearch" (click)="toggleSearch()">
|
||||
Search
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<app-heroes *ngIf="showHeroes"></app-heroes>
|
||||
<hr />
|
||||
<app-messages></app-messages>
|
||||
<hr />
|
||||
<app-config *ngIf="showConfig"></app-config>
|
||||
<hr />
|
||||
<app-downloader *ngIf="showDownloader"></app-downloader>
|
||||
<hr />
|
||||
<app-uploader *ngIf="showUploader"></app-uploader>
|
||||
<hr />
|
||||
<app-package-search *ngIf="showSearch"></app-package-search>
|
||||
<section>
|
||||
<app-messages></app-messages>
|
||||
</section>
|
||||
<section *ngIf="showHeroes">
|
||||
<app-heroes></app-heroes>
|
||||
</section>
|
||||
<section *ngIf="showConfig">
|
||||
<app-config></app-config>
|
||||
</section>
|
||||
<section *ngIf="showDownloader">
|
||||
<app-downloader></app-downloader>
|
||||
</section>
|
||||
<section *ngIf="showUploader">
|
||||
<app-uploader></app-uploader>
|
||||
</section>
|
||||
<section *ngIf="showSearch">
|
||||
<app-package-search></app-package-search>
|
||||
</section>
|
||||
|
||||
@@ -2,7 +2,8 @@ import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html'
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
showHeroes = true;
|
||||
@@ -16,4 +17,4 @@ export class AppComponent {
|
||||
toggleDownloader() { this.showDownloader = !this.showDownloader; }
|
||||
toggleUploader() { this.showUploader = !this.showUploader; }
|
||||
toggleSearch() { this.showSearch = !this.showSearch; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,10 +28,10 @@ export class ConfigComponent {
|
||||
showConfig() {
|
||||
this.configService.getConfig()
|
||||
// #enddocregion v1, v2
|
||||
.subscribe(
|
||||
(data: Config) => this.config = { ...data }, // success path
|
||||
error => this.error = error // error path
|
||||
);
|
||||
.subscribe({
|
||||
next: (data: Config) => this.config = { ...data }, // success path
|
||||
error: error => this.error = error, // error path
|
||||
});
|
||||
}
|
||||
|
||||
showConfig_v1() {
|
||||
@@ -69,7 +69,7 @@ export class ConfigComponent {
|
||||
}
|
||||
// #enddocregion showConfigResponse
|
||||
makeError() {
|
||||
this.configService.makeIntentionalError().subscribe(null, error => this.error = error );
|
||||
this.configService.makeIntentionalError().subscribe({ error: error => this.error = error.message });
|
||||
}
|
||||
|
||||
getType(val: any): string {
|
||||
|
||||
@@ -82,8 +82,7 @@ export class ConfigService {
|
||||
`Backend returned code ${error.status}, body was: `, error.error);
|
||||
}
|
||||
// Return an observable with a user-facing error message.
|
||||
return throwError(
|
||||
'Something bad happened; please try again later.');
|
||||
return throwError(() => new Error('Something bad happened; please try again later.'));
|
||||
}
|
||||
// #enddocregion handleError
|
||||
|
||||
|
||||
@@ -9,57 +9,71 @@
|
||||
}
|
||||
|
||||
.heroes li {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.heroes li:hover {
|
||||
left: .1em;
|
||||
}
|
||||
|
||||
.heroes button.delete {
|
||||
color: black;
|
||||
display: block;
|
||||
font-size: 1.2rem;
|
||||
display: grid;
|
||||
grid-template-columns: min-content max-content 1fr max-content max-content;
|
||||
grid-template-areas: "badge name . edit delete";
|
||||
margin: 1rem 0;
|
||||
align-items: center;
|
||||
background-color: #eee;
|
||||
border-radius: 0 4px 4px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.heroes a:hover {
|
||||
color: #2c3a41;
|
||||
background-color: #e6e6e6;
|
||||
.heroes li .badge {
|
||||
grid-area: badge;
|
||||
}
|
||||
|
||||
.heroes li .name {
|
||||
grid-area: name;
|
||||
}
|
||||
|
||||
.heroes li button {
|
||||
margin: 0.3rem;
|
||||
padding: 0.3rem 0.8rem;
|
||||
}
|
||||
|
||||
.heroes li button.edit {
|
||||
grid-area: edit;
|
||||
}
|
||||
|
||||
.heroes li button.delete {
|
||||
grid-area: delete;
|
||||
}
|
||||
|
||||
.heroes li button:hover, .heros li button:focus {
|
||||
color: #eee;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.heroes .badge {
|
||||
padding: .5em .6em;
|
||||
padding: 0.5rem 0.6rem;
|
||||
color: white;
|
||||
background-color: #435B60;
|
||||
min-width: 16px;
|
||||
margin-right: .8em;
|
||||
margin-right: 0.8rem;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
.heroes button.edit {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
padding: 0;
|
||||
margin-right: 0;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
button.delete {
|
||||
right: -8px;
|
||||
top: 5px;
|
||||
background-color: gray;
|
||||
color: white;
|
||||
padding: 5px 8px;
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
.heroes input {
|
||||
max-width: 12rem;
|
||||
padding: .25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.heroes li {
|
||||
grid-template-columns: min-content 1fr 1fr;
|
||||
grid-template-rows: 2rem max-content;
|
||||
grid-template-areas:
|
||||
"badge name name"
|
||||
"badge edit delete";
|
||||
}
|
||||
|
||||
.heroes li .name {
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
.heroes li button {
|
||||
justify-self: center;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,29 @@
|
||||
<h2>Heroes</h2>
|
||||
<div class="search">
|
||||
<label for="hero-name">Hero name: </label>
|
||||
<input type="text" #heroName id="hero-name">
|
||||
<input type="text" [(ngModel)]="heroName" id="hero-name">
|
||||
|
||||
<!-- (click) passes input value to add() and then clears the input -->
|
||||
<button type="button" (click)="add(heroName.value); heroName.value=''">
|
||||
Add hero
|
||||
</button>
|
||||
<button type="button" (click)="search(heroName.value)">
|
||||
<button type="button" [disabled]="!heroName"
|
||||
(click)="add(heroName)">Add hero</button>
|
||||
<button type="button" (click)="search(heroName)">
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="heroes">
|
||||
<li *ngFor="let hero of heroes">
|
||||
<button type="button" class="edit" (click)="edit(hero)">
|
||||
<span class="badge">{{ hero.id || -1 }}</span>
|
||||
<span *ngIf="hero!==editHero">{{hero.name}}</span>
|
||||
<input type="text"
|
||||
*ngIf="hero===editHero"
|
||||
[(ngModel)]="hero.name"
|
||||
(blur)="update()"
|
||||
(keyup.enter)="update()">
|
||||
<span class="badge">{{ hero.id || -1 }}</span>
|
||||
<span *ngIf="hero!==editHero; else editInput" class="name">{{hero.name}}</span>
|
||||
<ng-template #editInput>
|
||||
<input #heroEditInput type="text" class="name" [value]="editHero!.name" (blur)="edit(heroEditInput.value)" (keydown.enter)="edit(heroEditInput.value)"/>
|
||||
</ng-template>
|
||||
<button *ngIf="hero!==editHero" type="button" class="edit" title="edit hero"
|
||||
(click)="editHero = hero">
|
||||
Edit
|
||||
</button>
|
||||
<button type="button" class="delete" title="delete hero"
|
||||
(click)="delete(hero)">x</button>
|
||||
(click)="delete(hero)">
|
||||
Delete
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
|
||||
import { Hero } from './hero';
|
||||
import { HeroesService } from './heroes.service';
|
||||
@@ -12,9 +12,17 @@ import { HeroesService } from './heroes.service';
|
||||
export class HeroesComponent implements OnInit {
|
||||
heroes: Hero[] = [];
|
||||
editHero: Hero | undefined; // the hero currently being edited
|
||||
heroName = '';
|
||||
|
||||
constructor(private heroesService: HeroesService) {}
|
||||
|
||||
@ViewChild('heroEditInput')
|
||||
set heroEditInput(element: ElementRef<HTMLInputElement>) {
|
||||
if (element) {
|
||||
element.nativeElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.getHeroes();
|
||||
}
|
||||
@@ -55,8 +63,9 @@ export class HeroesComponent implements OnInit {
|
||||
*/
|
||||
}
|
||||
|
||||
edit(hero: Hero) {
|
||||
this.editHero = hero;
|
||||
edit(heroName: string) {
|
||||
this.update(heroName);
|
||||
this.editHero = undefined;
|
||||
}
|
||||
|
||||
search(searchTerm: string) {
|
||||
@@ -65,13 +74,15 @@ export class HeroesComponent implements OnInit {
|
||||
this.heroesService
|
||||
.searchHeroes(searchTerm)
|
||||
.subscribe(heroes => (this.heroes = heroes));
|
||||
} else {
|
||||
this.getHeroes();
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.editHero) {
|
||||
update(heroName: string) {
|
||||
if (heroName && this.editHero && this.editHero.name !== heroName) {
|
||||
this.heroesService
|
||||
.updateHero(this.editHero)
|
||||
.updateHero({...this.editHero, name: heroName})
|
||||
.subscribe(hero => {
|
||||
// replace the hero in the heroes list with update from server
|
||||
const ix = hero ? this.heroes.findIndex(h => h.id === hero.id) : -1;
|
||||
|
||||
@@ -53,10 +53,10 @@ describe('HeroesService', () => {
|
||||
|
||||
it('should return expected heroes (called once)', () => {
|
||||
|
||||
heroService.getHeroes().subscribe(
|
||||
heroes => expect(heroes).toEqual(expectedHeroes, 'should return expected heroes'),
|
||||
fail
|
||||
);
|
||||
heroService.getHeroes().subscribe({
|
||||
next: heroes => expect(heroes).toEqual(expectedHeroes, 'should return expected heroes'),
|
||||
error: fail,
|
||||
});
|
||||
|
||||
// HeroService should have made one request to GET heroes from expected URL
|
||||
const req = httpTestingController.expectOne(heroService.heroesUrl);
|
||||
@@ -68,10 +68,10 @@ describe('HeroesService', () => {
|
||||
|
||||
it('should be OK returning no heroes', () => {
|
||||
|
||||
heroService.getHeroes().subscribe(
|
||||
heroes => expect(heroes.length).toEqual(0, 'should have empty heroes array'),
|
||||
fail
|
||||
);
|
||||
heroService.getHeroes().subscribe({
|
||||
next: heroes => expect(heroes.length).toEqual(0, 'should have empty heroes array'),
|
||||
error: fail,
|
||||
});
|
||||
|
||||
const req = httpTestingController.expectOne(heroService.heroesUrl);
|
||||
req.flush([]); // Respond with no heroes
|
||||
@@ -80,10 +80,10 @@ describe('HeroesService', () => {
|
||||
// This service reports the error but finds a way to let the app keep going.
|
||||
it('should turn 404 into an empty heroes result', () => {
|
||||
|
||||
heroService.getHeroes().subscribe(
|
||||
heroes => expect(heroes.length).toEqual(0, 'should return empty heroes array'),
|
||||
fail
|
||||
);
|
||||
heroService.getHeroes().subscribe({
|
||||
next: heroes => expect(heroes.length).toEqual(0, 'should return empty heroes array'),
|
||||
error: fail,
|
||||
});
|
||||
|
||||
const req = httpTestingController.expectOne(heroService.heroesUrl);
|
||||
|
||||
@@ -96,10 +96,10 @@ describe('HeroesService', () => {
|
||||
|
||||
heroService.getHeroes().subscribe();
|
||||
heroService.getHeroes().subscribe();
|
||||
heroService.getHeroes().subscribe(
|
||||
heroes => expect(heroes).toEqual(expectedHeroes, 'should return expected heroes'),
|
||||
fail
|
||||
);
|
||||
heroService.getHeroes().subscribe({
|
||||
next: heroes => expect(heroes).toEqual(expectedHeroes, 'should return expected heroes'),
|
||||
error: fail,
|
||||
});
|
||||
|
||||
const requests = httpTestingController.match(heroService.heroesUrl);
|
||||
expect(requests.length).toEqual(3, 'calls to getHeroes()');
|
||||
@@ -119,10 +119,10 @@ describe('HeroesService', () => {
|
||||
|
||||
const updateHero: Hero = { id: 1, name: 'A' };
|
||||
|
||||
heroService.updateHero(updateHero).subscribe(
|
||||
data => expect(data).toEqual(updateHero, 'should return the hero'),
|
||||
fail
|
||||
);
|
||||
heroService.updateHero(updateHero).subscribe({
|
||||
next: data => expect(data).toEqual(updateHero, 'should return the hero'),
|
||||
error: fail,
|
||||
});
|
||||
|
||||
// HeroService should have made one request to PUT hero
|
||||
const req = httpTestingController.expectOne(heroService.heroesUrl);
|
||||
@@ -139,10 +139,10 @@ describe('HeroesService', () => {
|
||||
it('should turn 404 error into return of the update hero', () => {
|
||||
const updateHero: Hero = { id: 1, name: 'A' };
|
||||
|
||||
heroService.updateHero(updateHero).subscribe(
|
||||
data => expect(data).toEqual(updateHero, 'should return the update hero'),
|
||||
fail
|
||||
);
|
||||
heroService.updateHero(updateHero).subscribe({
|
||||
next: data => expect(data).toEqual(updateHero, 'should return the update hero'),
|
||||
error: fail,
|
||||
});
|
||||
|
||||
const req = httpTestingController.expectOne(heroService.heroesUrl);
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
:host {
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
background-color: rgb(255, 245, 187);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<div *ngIf="messageService.messages.length">
|
||||
<h2>Messages</h2>
|
||||
<h2>Messages</h2>
|
||||
<ng-container *ngIf="messageService.messages.length">
|
||||
<button type="button" class="clear" (click)="messageService.clear()">clear</button>
|
||||
<br>
|
||||
<ol>
|
||||
<li *ngFor='let message of messageService.messages'> {{message}} </li>
|
||||
</ol>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@@ -3,7 +3,8 @@ import { MessageService } from '../message.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-messages',
|
||||
templateUrl: './messages.component.html'
|
||||
templateUrl: './messages.component.html',
|
||||
styleUrls: ['./messages.component.css']
|
||||
})
|
||||
export class MessagesComponent {
|
||||
constructor(public messageService: MessageService) {}
|
||||
|
||||
@@ -97,13 +97,13 @@ describe('HttpClient testing', () => {
|
||||
|
||||
// Make three requests in a row
|
||||
httpClient.get<Data[]>(testUrl)
|
||||
.subscribe(d => expect(d.length).toEqual(0, 'should have no data'));
|
||||
.subscribe(d => expect(d.length).withContext('should have no data').toEqual(0));
|
||||
|
||||
httpClient.get<Data[]>(testUrl)
|
||||
.subscribe(d => expect(d).toEqual([testData[0]], 'should be one element array'));
|
||||
.subscribe(d => expect(d).withContext('should be one element array').toEqual([testData[0]]));
|
||||
|
||||
httpClient.get<Data[]>(testUrl)
|
||||
.subscribe(d => expect(d).toEqual(testData, 'should be expected data'));
|
||||
.subscribe(d => expect(d).withContext('should be expected data').toEqual(testData));
|
||||
|
||||
// #docregion multi-request
|
||||
// get all pending requests that match the given URL
|
||||
@@ -121,13 +121,13 @@ describe('HttpClient testing', () => {
|
||||
it('can test for 404 error', () => {
|
||||
const emsg = 'deliberate 404 error';
|
||||
|
||||
httpClient.get<Data[]>(testUrl).subscribe(
|
||||
data => fail('should have failed with the 404 error'),
|
||||
(error: HttpErrorResponse) => {
|
||||
expect(error.status).toEqual(404, 'status');
|
||||
expect(error.error).toEqual(emsg, 'message');
|
||||
}
|
||||
);
|
||||
httpClient.get<Data[]>(testUrl).subscribe({
|
||||
next: () => fail('should have failed with the 404 error'),
|
||||
error: (error: HttpErrorResponse) => {
|
||||
expect(error.status).withContext('status').toEqual(404);
|
||||
expect(error.error).withContext('message').toEqual(emsg);
|
||||
},
|
||||
});
|
||||
|
||||
const req = httpTestingController.expectOne(testUrl);
|
||||
|
||||
@@ -142,13 +142,13 @@ describe('HttpClient testing', () => {
|
||||
// at network level. e.g. Connection timeout, DNS error, offline, etc.
|
||||
const mockError = new ProgressEvent('error');
|
||||
|
||||
httpClient.get<Data[]>(testUrl).subscribe(
|
||||
data => fail('should have failed with the network error'),
|
||||
(error: HttpErrorResponse) => {
|
||||
httpClient.get<Data[]>(testUrl).subscribe({
|
||||
next: () => fail('should have failed with the network error'),
|
||||
error: (error: HttpErrorResponse) => {
|
||||
expect(error.error).toBe(mockError);
|
||||
done();
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const req = httpTestingController.expectOne(testUrl);
|
||||
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
"i18n": {
|
||||
"sourceLocale": "en-US",
|
||||
"locales": {
|
||||
"fr": "src/locale/messages.fr.xlf",
|
||||
// #enddocregion locale-config
|
||||
"baseHref": ""
|
||||
// #docregion locale-config
|
||||
"fr": {
|
||||
"translation": "src/locale/messages.fr.xlf",
|
||||
// #enddocregion locale-config
|
||||
"baseHref": ""
|
||||
// #docregion locale-config
|
||||
}
|
||||
}
|
||||
},
|
||||
// #docregion build-production-french
|
||||
@@ -42,13 +44,8 @@
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["src/styles.css"],
|
||||
"scripts": [],
|
||||
// #docregion missing-translation-error
|
||||
"i18nMissingTranslation": "error"
|
||||
@@ -80,6 +77,7 @@
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"localize": false,
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
@@ -89,9 +87,7 @@
|
||||
},
|
||||
// #docregion build-single-locale, build-production-french
|
||||
"fr": {
|
||||
"localize": [
|
||||
"fr"
|
||||
]
|
||||
"localize": ["fr"]
|
||||
}
|
||||
},
|
||||
// #enddocregion build-single-locale, build-production-french
|
||||
@@ -111,7 +107,7 @@
|
||||
},
|
||||
// #docregion build-single-locale, build-production-french
|
||||
"fr": {
|
||||
"browserTarget": "angular.io-example:build:fr"
|
||||
"browserTarget": "angular.io-example:build:development,fr"
|
||||
}
|
||||
},
|
||||
// #enddocregion build-single-locale, build-production-french
|
||||
@@ -132,13 +128,8 @@
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["src/styles.css"],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@
|
||||
<!-- #docregion formarrayname -->
|
||||
<div formArrayName="aliases">
|
||||
<h2>Aliases</h2>
|
||||
<button type="button" (click)="addAlias()" type="button">+ Add another alias</button>
|
||||
<button type="button" (click)="addAlias()">+ Add another alias</button>
|
||||
|
||||
<div *ngFor="let alias of aliases.controls; let i=index">
|
||||
<!-- The repeated alias template -->
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// #docplaster
|
||||
import {Injectable, NgModule} from '@angular/core';
|
||||
import {Title} from '@angular/platform-browser';
|
||||
import {
|
||||
Resolve,
|
||||
RouterModule,
|
||||
RouterStateSnapshot,
|
||||
Routes,
|
||||
TitleStrategy,
|
||||
} from '@angular/router'; // CLI imports router
|
||||
|
||||
// #docregion page-title
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'first-component',
|
||||
title: 'First component',
|
||||
component: FirstComponent, // this is the component with the <router-outlet> in the template
|
||||
children: [
|
||||
{
|
||||
path: 'child-a', // child route path
|
||||
title: ResolvedChildATitle,
|
||||
component: ChildAComponent, // child route component that the router renders
|
||||
},
|
||||
{
|
||||
path: 'child-b',
|
||||
title: 'child b',
|
||||
component: ChildBComponent, // another child route component that the router renders
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class ResolvedChildATitle implements Resolve<string> {
|
||||
resolve() {
|
||||
return Promise.resolve('child a');
|
||||
}
|
||||
}
|
||||
// #enddocregion page-title
|
||||
|
||||
|
||||
// #docregion custom-page-title
|
||||
export class TemplatePageTitleStrategy extends TitleStrategy {
|
||||
constructor(private readonly title: Title) {
|
||||
super();
|
||||
}
|
||||
|
||||
override updateTitle(routerState: RouterStateSnapshot) {
|
||||
const title = this.buildTitle(routerState);
|
||||
if (title !== undefined) {
|
||||
this.title.setTitle(`My Application | ${title}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule],
|
||||
providers: [
|
||||
{provide: TitleStrategy, useClass: TemplatePageTitleStrategy},
|
||||
]
|
||||
})
|
||||
export class AppRoutingModule {
|
||||
}
|
||||
// #enddocregion custom-page-title
|
||||
@@ -4,6 +4,6 @@
|
||||
<a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
|
||||
<a routerLink="/heroes" routerLinkActive="active">Heroes</a>
|
||||
</nav>
|
||||
<div [@routeAnimation]="getAnimationData(routerOutlet)">
|
||||
<router-outlet #routerOutlet="outlet"></router-outlet>
|
||||
<div [@routeAnimation]="getAnimationData()">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
@@ -2,7 +2,7 @@
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
// #docregion animation-imports
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { ChildrenOutletContexts } from '@angular/router';
|
||||
import { slideInAnimation } from './animations';
|
||||
|
||||
@Component({
|
||||
@@ -14,8 +14,10 @@ import { slideInAnimation } from './animations';
|
||||
// #enddocregion animation-imports
|
||||
// #docregion function-binding
|
||||
export class AppComponent {
|
||||
getAnimationData(outlet: RouterOutlet) {
|
||||
return outlet?.activatedRouteData?.['animation'];
|
||||
constructor(private contexts: ChildrenOutletContexts) {}
|
||||
|
||||
getAnimationData() {
|
||||
return this.contexts.getContext('primary')?.route?.snapshot?.data?.['animation'];
|
||||
}
|
||||
}
|
||||
// #enddocregion function-binding
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<!-- #enddocregion contact-link -->
|
||||
</nav>
|
||||
<!-- #docregion outlets -->
|
||||
<div [@routeAnimation]="getAnimationData(routerOutlet)">
|
||||
<router-outlet #routerOutlet="outlet"></router-outlet>
|
||||
<div [@routeAnimation]="getAnimationData()">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<router-outlet name="popup"></router-outlet>
|
||||
<!-- #enddocregion outlets -->
|
||||
@@ -6,7 +6,7 @@
|
||||
<a routerLink="/admin" routerLinkActive="active">Admin</a>
|
||||
<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
|
||||
</nav>
|
||||
<div [@routeAnimation]="getAnimationData(routerOutlet)">
|
||||
<router-outlet #routerOutlet="outlet"></router-outlet>
|
||||
<div [@routeAnimation]="getAnimationData()">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<router-outlet name="popup"></router-outlet>
|
||||
@@ -7,7 +7,7 @@
|
||||
<a routerLink="/login" routerLinkActive="active">Login</a>
|
||||
<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
|
||||
</nav>
|
||||
<div [@routeAnimation]="getAnimationData(routerOutlet)">
|
||||
<router-outlet #routerOutlet="outlet"></router-outlet>
|
||||
<div [@routeAnimation]="getAnimationData()">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<router-outlet name="popup"></router-outlet>
|
||||
@@ -8,8 +8,8 @@
|
||||
<a routerLink="/login" routerLinkActive="active">Login</a>
|
||||
<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
|
||||
</nav>
|
||||
<div [@routeAnimation]="getAnimationData(routerOutlet)">
|
||||
<router-outlet #routerOutlet="outlet"></router-outlet>
|
||||
<div [@routeAnimation]="getRouteAnimationData()">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<router-outlet name="popup"></router-outlet>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// #docplaster
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { ChildrenOutletContexts } from '@angular/router';
|
||||
import { slideInAnimation } from './animations';
|
||||
|
||||
@Component({
|
||||
@@ -11,7 +11,9 @@ import { slideInAnimation } from './animations';
|
||||
animations: [ slideInAnimation ]
|
||||
})
|
||||
export class AppComponent {
|
||||
getAnimationData(outlet: RouterOutlet) {
|
||||
return outlet?.activatedRouteData?.['animation'];
|
||||
constructor(private contexts: ChildrenOutletContexts) {}
|
||||
|
||||
getRouteAnimationData() {
|
||||
return this.contexts.getContext('primary')?.route?.snapshot?.data?.['animation'];
|
||||
}
|
||||
}
|
||||
|
||||
-6
@@ -41,12 +41,6 @@ export function myService(options: MyServiceSchema): Rule {
|
||||
// #enddocregion workspace
|
||||
|
||||
// #docregion project-info
|
||||
// #docregion project-fallback
|
||||
if (!options.project && typeof workspace.extensions.defaultProject === 'string') {
|
||||
options.project = workspace.extensions.defaultProject;
|
||||
}
|
||||
// #enddocregion project-fallback
|
||||
|
||||
const project = (options.project != null) ? workspace.projects.get(options.project) : null;
|
||||
if (!project) {
|
||||
throw new SchematicsException(`Invalid project name: ${options.project}`);
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
|
||||
|
||||
// #docregion sw-update
|
||||
@Injectable()
|
||||
export class LogUpdateService {
|
||||
|
||||
constructor(updates: SwUpdate) {
|
||||
updates.available.subscribe(event => {
|
||||
console.log('current version is', event.current);
|
||||
console.log('available version is', event.available);
|
||||
});
|
||||
updates.activated.subscribe(event => {
|
||||
console.log('old version was', event.previous);
|
||||
console.log('new version is', event.current);
|
||||
updates.versionUpdates.subscribe(evt => {
|
||||
switch (evt.type) {
|
||||
case 'VERSION_DETECTED':
|
||||
console.log(`Downloading new app version: ${evt.version.hash}`);
|
||||
break;
|
||||
case 'VERSION_READY':
|
||||
console.log(`Current app version: ${evt.currentVersion.hash}`);
|
||||
console.log(`New app version ready for use: ${evt.latestVersion.hash}`);
|
||||
break;
|
||||
case 'VERSION_INSTALLATION_FAILED':
|
||||
console.log(`Failed to install app version '${evt.version.hash}': ${evt.error}`);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
background-color: #EEE;
|
||||
margin: .5em;
|
||||
padding: 0;
|
||||
height: 1.6em;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
background-color: #EEE;
|
||||
margin: .5em;
|
||||
padding: 0;
|
||||
height: 1.6em;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -16,6 +16,28 @@ body, input[type="text"], button {
|
||||
color: #333;
|
||||
font-family: Cambria, Georgia, serif;
|
||||
}
|
||||
button {
|
||||
background-color: #eee;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: black;
|
||||
font-size: 1.2rem;
|
||||
padding: 1rem;
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
button:hover {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
button:disabled {
|
||||
background-color: #eee;
|
||||
color: #aaa;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
/* everywhere else */
|
||||
* {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
background-color: #EEE;
|
||||
margin: .5em;
|
||||
padding: 0;
|
||||
height: 1.6em;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
background-color: #EEE;
|
||||
margin: .5em;
|
||||
padding: 0;
|
||||
height: 1.6em;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
background-color: #EEE;
|
||||
margin: .5em;
|
||||
padding: 0;
|
||||
height: 1.6em;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -1,34 +1,39 @@
|
||||
This is the Angular Phonecat application adjusted to fit our boilerplate project
|
||||
structure.
|
||||
This is the Angular Phonecat application adjusted to fit our boilerplate project structure.
|
||||
|
||||
The following changes from vanilla Phonecat are applied:
|
||||
|
||||
* The TypeScript config file shown in the guide is `tsconfig.ajs.json` instead
|
||||
of the default, because we don't want to enable `noImplicitAny` for migration.
|
||||
* Karma config for unit tests is in karma.conf.ajs.js because the boilerplate
|
||||
Karma config is not compatible with the way AngularJS tests need to be run.
|
||||
The shell script run-unit-tests.sh can be used to run the unit tests.
|
||||
* Instead of using Bower, AngularJS and its dependencies are fetched from a CDN
|
||||
in index.html and karma.conf.ajs.js.
|
||||
* E2E tests have been moved to the parent directory, where `gulp run-e2e-tests` can
|
||||
discover and run them along with all the other examples.
|
||||
* Most of the phone JSON and image data removed in the interest of keeping
|
||||
repo weight down. Keeping enough to retain testability of the app.
|
||||
* The TypeScript config file shown in the guide is `tsconfig.ajs.json` instead of the default, because we don't want to enable `noImplicitAny` for migration.
|
||||
* Karma config for unit tests is in karma.conf.ajs.js because the boilerplate Karma config is not compatible with the way AngularJS tests need to be run.
|
||||
The shell script run-unit-tests.sh can be used to run the unit tests.
|
||||
|
||||
* Instead of using Bower, AngularJS and its dependencies are fetched from a CDN in index.html and karma.conf.ajs.js.
|
||||
* E2E tests have been moved to the parent directory, where `gulp run-e2e-tests` can discover and run them along with all the other examples.
|
||||
* Most of the phone JSON and image data removed in the interest of keeping repo weight down. Keeping enough to retain testability of the app.
|
||||
|
||||
## Running the app
|
||||
|
||||
Start like any example
|
||||
|
||||
npm run start
|
||||
```shell
|
||||
npm run start
|
||||
```
|
||||
|
||||
You'll find the app under the /app path: http://localhost:3002/app/index.html
|
||||
You'll find the app under the /app path:
|
||||
|
||||
```http
|
||||
http://localhost:3002/app/index.html
|
||||
```
|
||||
|
||||
## Running unit tests
|
||||
|
||||
./run-unit-tests.sh
|
||||
```shell
|
||||
./run-unit-tests.sh
|
||||
```
|
||||
|
||||
## Running E2E tests
|
||||
|
||||
Like for any example (at the project root):
|
||||
Like for any example \(at the project root\):
|
||||
|
||||
gulp run-e2e-tests --filter=phonecat-1
|
||||
```shell
|
||||
gulp run-e2e-tests --filter=phonecat-1
|
||||
```
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
This is the Angular Phonecat application adjusted to fit our boilerplate project
|
||||
structure.
|
||||
This is the Angular Phonecat application adjusted to fit our boilerplate project structure.
|
||||
|
||||
The following changes from vanilla Phonecat are applied:
|
||||
|
||||
* Karma config for unit tests is in karma.conf.ajs.js because the boilerplate
|
||||
Karma config is not compatible with the way AngularJS tests need to be run.
|
||||
The shell script run-unit-tests.sh can be used to run the unit tests.
|
||||
* Also for the Karma shim, there is a `karma-test-shim.1.js` file which isn't
|
||||
used but is shown in the test appendix.
|
||||
* Instead of using Bower, AngularJS and its dependencies are fetched from a CDN
|
||||
in index.html and karma.conf.ajs.js.
|
||||
* E2E tests have been moved to the parent directory, where `run-e2e-tests` can
|
||||
discover and run them along with all the other examples.
|
||||
* Most of the phone JSON and image data removed in the interest of keeping
|
||||
repo weight down. Keeping enough to retain testability of the app.
|
||||
* Karma config for unit tests is in karma.conf.ajs.js because the boilerplate Karma config is not compatible with the way AngularJS tests need to be run.
|
||||
The shell script run-unit-tests.sh can be used to run the unit tests.
|
||||
|
||||
* Also for the Karma shim, there is a `karma-test-shim.1.js` file which isn't used but is shown in the test appendix.
|
||||
* Instead of using Bower, AngularJS and its dependencies are fetched from a CDN in index.html and karma.conf.ajs.js.
|
||||
* E2E tests have been moved to the parent directory, where `run-e2e-tests` can discover and run them along with all the other examples.
|
||||
* Most of the phone JSON and image data removed in the interest of keeping repo weight down. Keeping enough to retain testability of the app.
|
||||
|
||||
## Running the app
|
||||
|
||||
Start like any example
|
||||
|
||||
npm run start
|
||||
```shell
|
||||
npm run start
|
||||
```
|
||||
|
||||
## Running unit tests
|
||||
|
||||
./run-unit-tests.sh
|
||||
```shell
|
||||
./run-unit-tests.sh
|
||||
```
|
||||
|
||||
## Running E2E tests
|
||||
|
||||
Like for any example (at the project root):
|
||||
Like for any example \(at the project root\):
|
||||
|
||||
gulp run-e2e-tests --filter=phonecat-2
|
||||
```shell
|
||||
gulp run-e2e-tests --filter=phonecat-2
|
||||
```
|
||||
|
||||
@@ -3,19 +3,22 @@ structure.
|
||||
|
||||
The following changes from vanilla Phonecat are applied:
|
||||
|
||||
* E2E tests have been moved to the parent directory, where `run-e2e-tests` can
|
||||
discover and run them along with all the other examples.
|
||||
* Most of the phone JSON and image data removed in the interest of keeping
|
||||
repo weight down. Keeping enough to retain testability of the app.
|
||||
* E2E tests have been moved to the parent directory, where `run-e2e-tests` can discover and run them along with all the other examples.
|
||||
* Most of the phone JSON and image data removed in the interest of keeping repo weight down.
|
||||
Keeping enough to retain testability of the app.
|
||||
|
||||
## Running the app
|
||||
|
||||
Start like any example
|
||||
|
||||
npm run start
|
||||
```shell
|
||||
npm run start
|
||||
```
|
||||
|
||||
## Running E2E tests
|
||||
|
||||
Like for any example (at the project root):
|
||||
Like for any example \(at the project root\):
|
||||
|
||||
gulp run-e2e-tests --filter=phonecat-3
|
||||
```shell
|
||||
gulp run-e2e-tests --filter=phonecat-3
|
||||
```
|
||||
|
||||
@@ -2,37 +2,42 @@
|
||||
|
||||
@description
|
||||
|
||||
This diagnostic detects a backwards "banana-in-box" syntax for
|
||||
[two-way bindings](guide/two-way-binding).
|
||||
This diagnostic detects a backwards "banana-in-box" syntax for [two-way bindings](guide/two-way-binding).
|
||||
|
||||
```html
|
||||
<div ([var])="otherVar"></div>
|
||||
```
|
||||
<code-example format="html" language="html">
|
||||
|
||||
<div ([var])="otherVar"></div>
|
||||
|
||||
</code-example>
|
||||
|
||||
## What's wrong with that?
|
||||
|
||||
As it stands, `([var])` is actually an [event binding](guide/event-binding) with
|
||||
the name `[var]`. The author likely intended this as a two-way binding to a variable named
|
||||
`var` but as written this binding has no effect.
|
||||
As it stands, `([var])` is actually an [event binding](guide/event-binding) with the name `[var]`.
|
||||
The author likely intended this as a two-way binding to a variable named `var` but, as written, this binding has no effect.
|
||||
|
||||
## What should I do instead?
|
||||
|
||||
Fix the typo. As the name suggests, the banana `(` should go *inside* the box `[]`. In this case:
|
||||
Fix the typo.
|
||||
As the name suggests, the banana `(` should go *inside* the box `[]`.
|
||||
In this case:
|
||||
|
||||
```html
|
||||
<div [(var)]="otherVar"></div>
|
||||
```
|
||||
<code-example format="html" language="html">
|
||||
|
||||
<div [(var)]="otherVar"></div>
|
||||
|
||||
</code-example>
|
||||
|
||||
## Configuration requirements
|
||||
|
||||
[`strictTemplates`](/guide/template-typecheck#strict-mode) must be enabled for any extended
|
||||
diagnostic to emit. `invalidBananaInBox` has no additional requirements beyond `strictTemplates`.
|
||||
[`strictTemplates`](guide/template-typecheck#strict-mode) must be enabled for any extended diagnostic to emit.
|
||||
`invalidBananaInBox` has no additional requirements beyond `strictTemplates`.
|
||||
|
||||
## What if I can't avoid this?
|
||||
|
||||
This diagnostic can be disabled by editing the project's `tsconfig.json` file:
|
||||
|
||||
```json
|
||||
<code-example format="json" language="json">
|
||||
|
||||
{
|
||||
"angularCompilerOptions": {
|
||||
"extendedDiagnostics": {
|
||||
@@ -42,6 +47,15 @@ This diagnostic can be disabled by editing the project's `tsconfig.json` file:
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</code-example>
|
||||
|
||||
See [extended diagnostic configuration](extended-diagnostics#configuration) for more info.
|
||||
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
<!-- end links -->
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
@@ -2,81 +2,96 @@
|
||||
|
||||
@description
|
||||
|
||||
This diagnostic detects a useless nullish coalescing operator (`??`) in Angular templates.
|
||||
Specifically, it looks for operations where the input is not "nullable", meaning its type does not
|
||||
include `null` or `undefined`. For such values, the right side of the `??` will never be used.
|
||||
This diagnostic detects a useless nullish coalescing operator \(`??`\) characters in Angular templates.
|
||||
Specifically, it looks for operations where the input is not "nullable", meaning its type does not include `null` or `undefined`.
|
||||
For such values, the right side of the `??` will never be used.
|
||||
|
||||
```typescript
|
||||
import {Component} from '@angular/core';
|
||||
<code-example format="typescript" language="typescript">
|
||||
|
||||
@Component({
|
||||
// Template displays `foo` if present, falls back to 'bar' if it is `null`
|
||||
// or `undefined`.
|
||||
template: `<div>{{ foo ?? 'bar' }}</div>`,
|
||||
// ...
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
// Template displays `foo` if present, falls back to 'bar' if it is `null`
|
||||
// or `undefined`.
|
||||
template: `
|
||||
<div>
|
||||
{{ foo ?? 'bar' }}
|
||||
</div>
|
||||
`,
|
||||
// …
|
||||
})
|
||||
class MyComponent {
|
||||
// `foo` is declared as a `string` which *cannot* be `null` or `undefined`.
|
||||
// `foo` is declared as a `string` which *cannot* be `null` or `undefined`.
|
||||
foo: string = 'test';
|
||||
}
|
||||
```
|
||||
|
||||
</code-example>
|
||||
|
||||
## What's wrong with that?
|
||||
|
||||
Using the nullish coalescing operator with a non-nullable input has no effect and is indicative of a
|
||||
discrepancy between the allowed type of a value and how it is presented in the template. A developer
|
||||
might reasonably assume that the right side of the nullish coalescing operator is displayed in some
|
||||
case, but it will never actually be displayed. This can lead to confusion about the expected output
|
||||
of the program.
|
||||
Using the nullish coalescing operator with a non-nullable input has no effect and is indicative of a discrepancy between the allowed type of a value and how it is presented in the template.
|
||||
A developer might reasonably assume that the right side of the nullish coalescing operator is displayed in some case, but it will never actually be displayed.
|
||||
This can lead to confusion about the expected output of the program.
|
||||
|
||||
## What should I do instead?
|
||||
|
||||
Update the template and declared type to be in sync. Double check the type of the input and confirm
|
||||
whether it is actually expected to be nullable.
|
||||
Update the template and declared type to be in sync.
|
||||
Double-check the type of the input and confirm whether it is actually expected to be nullable.
|
||||
|
||||
If the input should be nullable, add `null` or `undefined` to its type to indicate this.
|
||||
|
||||
```typescript
|
||||
import {Component} from '@angular/core';
|
||||
<code-example format="typescript" language="typescript">
|
||||
|
||||
@Component({
|
||||
template: `<div>{{ foo ?? 'bar' }}</div>`,
|
||||
// ...
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div>
|
||||
{{ foo ?? 'bar' }}
|
||||
<l/div>
|
||||
`,
|
||||
// …
|
||||
})
|
||||
class MyComponent {
|
||||
// `foo` is now nullable. If it is ever set to `null`, 'bar' will be displayed.
|
||||
foo: string | null = 'test';
|
||||
// `foo` is now nullable. If it is ever set to `null`, 'bar' will be displayed.
|
||||
foo: string | null = 'test';
|
||||
}
|
||||
```
|
||||
|
||||
If the input should *not* be nullable, delete the `??` operator and its right operand, since the
|
||||
value is guaranteed by TypeScript to always be non-nullable.
|
||||
</code-example>
|
||||
|
||||
```typescript
|
||||
import {Component} from '@angular/core';
|
||||
If the input should *not* be nullable, delete the `??` operator and its right operand, since the value is guaranteed by TypeScript to always be non-nullable.
|
||||
|
||||
@Component({
|
||||
// Template always displays `foo`, which is guaranteed to never be `null` or
|
||||
// `undefined`.
|
||||
template: `<div>{{ foo }}</div>`,
|
||||
// ...
|
||||
<code-example format="typescript" language="typescript">
|
||||
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
// Template always displays `foo`, which is guaranteed to never be `null` or
|
||||
// `undefined`.
|
||||
template: `
|
||||
<div>
|
||||
{{ foo }}
|
||||
</div>
|
||||
`,
|
||||
// …
|
||||
})
|
||||
class MyComponent {
|
||||
foo: string = 'test';
|
||||
}
|
||||
```
|
||||
|
||||
</code-example>
|
||||
|
||||
## Configuration requirements
|
||||
|
||||
[`strictTemplates`](/guide/template-typecheck#strict-mode) must be enabled for any extended
|
||||
diagnostic to emit. [`strictNullChecks`](/guide/template-typecheck#strict-null-checks) must also be
|
||||
enabled to emit any `nullishCoalescingNotNullable` diagnostics.
|
||||
[`strictTemplates`](guide/template-typecheck#strict-mode) must be enabled for any extended diagnostic to emit.
|
||||
[`strictNullChecks`](guide/template-typecheck#strict-null-checks) must also be enabled to emit any `nullishCoalescingNotNullable` diagnostics.
|
||||
|
||||
## What if I can't avoid this?
|
||||
|
||||
This diagnostic can be disabled by editing the project's `tsconfig.json` file:
|
||||
|
||||
```json
|
||||
<code-example format="json" language="json">
|
||||
|
||||
{
|
||||
"angularCompilerOptions": {
|
||||
"extendedDiagnostics": {
|
||||
@@ -86,6 +101,15 @@ This diagnostic can be disabled by editing the project's `tsconfig.json` file:
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</code-example>
|
||||
|
||||
See [extended diagnostic configuration](extended-diagnostics#configuration) for more info.
|
||||
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
<!-- end links -->
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
# Extended Diagnostics
|
||||
|
||||
There are many coding patterns that are technically valid to the compiler or runtime, but which may
|
||||
have complex nuances or caveats. These patterns may not have the intended effect expected by a
|
||||
developer, which often leads to bugs. The Angular compiler includes "extended diagnostics" which
|
||||
identify many of these patterns to warn developers about the potential issues and enforce common
|
||||
best practices within a codebase.
|
||||
There are many coding patterns that are technically valid to the compiler or runtime, but which may have complex nuances or caveats.
|
||||
These patterns may not have the intended effect expected by a developer, which often leads to bugs.
|
||||
The Angular compiler includes "extended diagnostics" which identify many of these patterns, in order to warn developers about the potential issues and enforce common best practices within a codebase.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Currently, Angular supports the following extended diagnostics:
|
||||
|
||||
* [NG8101 - `invalidBananaInBox`](extended-diagnostics/NG8101)
|
||||
* [NG8102 - `nullishCoalescingNotNullable`](extended-diagnostics/NG8102)
|
||||
* [NG8101 - `invalidBananaInBox`](extended-diagnostics/NG8101)
|
||||
* [NG8102 - `nullishCoalescingNotNullable`](extended-diagnostics/NG8102)
|
||||
|
||||
## Configuration
|
||||
|
||||
Extended diagnostics are warnings by default and do not block compilation. Each diagnostic can be
|
||||
configured as either:
|
||||
Extended diagnostics are warnings by default and do not block compilation.
|
||||
Each diagnostic can be configured as either:
|
||||
|
||||
* `warning` (default) - The compiler emits the diagnostic as a warning but does not block
|
||||
compilation. The compiler will still exit with status code 0, even if warnings are emitted.
|
||||
* `error` - The compiler emits the diagnostic as an error and fails the compilation. The compiler
|
||||
will exit with a non-zero status code if one or more errors are emitted.
|
||||
* `suppress` - The compiler does *not* emit the diagnostic at all.
|
||||
* `warning` \(default\) - The compiler emits the diagnostic as a warning but does not block compilation.
|
||||
The compiler will still exit with status code 0, even if warnings are emitted.
|
||||
|
||||
* `error` - The compiler emits the diagnostic as an error and fails the compilation.
|
||||
The compiler will exit with a non-zero status code if one or more errors are emitted.
|
||||
|
||||
* `suppress` - The compiler does *not* emit the diagnostic at all.
|
||||
|
||||
Check severity can be configured in the project's `tsconfig.json` file:
|
||||
|
||||
```jsonc
|
||||
<code-example format="jsonc" language="jsonc">
|
||||
|
||||
{
|
||||
"angularCompilerOptions": {
|
||||
"extendedDiagnostics": {
|
||||
@@ -36,50 +37,53 @@ Check severity can be configured in the project's `tsconfig.json` file:
|
||||
"invalidBananaInBox": "suppress"
|
||||
},
|
||||
|
||||
// The category to use for any diagnostics not listed in `checks` above.
|
||||
// The category to use for any diagnostics not listed in `checks` above.
|
||||
"defaultCategory": "error"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `checks` field maps the name of individual diagnostics to their associated category. See
|
||||
[Diagnostics](#diagnostics) for a complete list of extended diagnostics and the name to use for
|
||||
configuring them.
|
||||
</code-example>
|
||||
|
||||
The `defaultCategory` field is used for any diagnostics that are not explicitly listed under
|
||||
`checks`. If not set, such diagnostics will be treated as `warning`.
|
||||
The `checks` field maps the name of individual diagnostics to their associated category.
|
||||
See [Diagnostics](#diagnostics) for a complete list of extended diagnostics and the name to use for configuring them.
|
||||
|
||||
Extended diagnostics will emit when [`strictTemplates`](/guide/template-typecheck#strict-mode) is
|
||||
enabled. This is required to allow the compiler to better understand Angular template types and
|
||||
provide accurate and meaningful diagnostics.
|
||||
The `defaultCategory` field is used for any diagnostics that are not explicitly listed under `checks`.
|
||||
If not set, such diagnostics will be treated as `warning`.
|
||||
|
||||
Extended diagnostics will emit when [`strictTemplates`](guide/template-typecheck#strict-mode) is enabled.
|
||||
This is required to allow the compiler to better understand Angular template types and provide accurate and meaningful diagnostics.
|
||||
|
||||
## Semantic Versioning
|
||||
|
||||
The Angular team intends to add or enable new extended diagnostics in **minor** versions of Angular
|
||||
(see [semver](https://docs.npmjs.com/about-semantic-versioning)). This means that upgrading Angular
|
||||
may show new warnings in your existing codebase. This enables the team to deliver features more
|
||||
quickly and to make extended diagnostics more accessible to developers.
|
||||
The Angular team intends to add or enable new extended diagnostics in **minor** versions of Angular (see [semver](https://docs.npmjs.com/about-semantic-versioning)).
|
||||
This means that upgrading Angular may show new warnings in your existing codebase.
|
||||
This enables the team to deliver features more quickly and to make extended diagnostics more accessible to developers.
|
||||
|
||||
However, setting `"defaultCategory": "error"` will promote such warnings to hard errors. This can
|
||||
cause a minor version upgrade to introduce compilation errors, which may be seen as a semver
|
||||
non-compliant breaking change. Any new diagnostics can be suppressed or demoted to warnings via the
|
||||
above [configuration](#configuration), so the impact of a new diagnostic should be minimal to
|
||||
projects that treat extended diagnostics as errors by default. Defaulting to error is a very
|
||||
powerful tool; just be aware of this semver caveat when deciding if `error` is the right default for
|
||||
your project.
|
||||
However, setting `"defaultCategory": "error"` will promote such warnings to hard errors.
|
||||
This can cause a minor version upgrade to introduce compilation errors, which may be seen as a semver non-compliant breaking change.
|
||||
Any new diagnostics can be suppressed or demoted to warnings via the above [configuration](#configuration), so the impact of a new diagnostic should be minimal to
|
||||
projects that treat extended diagnostics as errors by default.
|
||||
Defaulting to error is a very powerful tool; just be aware of this semver caveat when deciding if `error` is the right default for your project.
|
||||
|
||||
## New Diagnostics
|
||||
|
||||
The Angular team is always open to suggestions about new diagnostics that could be added. Extended
|
||||
diagnostics should generally:
|
||||
The Angular team is always open to suggestions about new diagnostics that could be added.
|
||||
Extended diagnostics should generally:
|
||||
|
||||
* Detect a common, non-obvious developer mistake with Angular templates.
|
||||
* Clearly articulate why this pattern can lead to bugs or unintended behavior.
|
||||
* Suggest one or more clear solutions.
|
||||
* Have a low (preferrably zero) false-positive rate.
|
||||
* Apply to the vast majority of Angular applications (not specific to an unofficial library).
|
||||
* Improve program correctness or performance (not style, that responsibility falls to a linter).
|
||||
* Detect a common, non-obvious developer mistake with Angular templates
|
||||
* Clearly articulate why this pattern can lead to bugs or unintended behavior
|
||||
* Suggest one or more clear solutions
|
||||
* Have a low \(preferrably zero\) false-positive rate
|
||||
* Apply to the vast majority of Angular applications \(not specific to an unofficial library\)
|
||||
* Improve program correctness or performance \(not style, that responsibility falls to a linter\)
|
||||
|
||||
If you have an idea for a compiler check which fits these criteria, consider filing a
|
||||
[feature request](https://github.com/angular/angular/issues/new?template=2-feature-request.yaml).
|
||||
If you have an idea for a compiler check which fits these criteria, consider filing a [feature request](https://github.com/angular/angular/issues/new?template=2-feature-request.yaml).
|
||||
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
<!-- end links -->
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
@@ -1,65 +1,70 @@
|
||||
# Accessibility in Angular
|
||||
|
||||
The web is used by a wide variety of people, including those who have visual or motor impairments.
|
||||
A variety of assistive technologies are available that make it much easier for these groups to
|
||||
interact with web-based software applications.
|
||||
A variety of assistive technologies are available that make it much easier for these groups to interact with web-based software applications.
|
||||
In addition, designing an application to be more accessible generally improves the user experience for all users.
|
||||
|
||||
For an in-depth introduction to issues and techniques for designing accessible applications, see the [Accessibility](https://developers.google.com/web/fundamentals/accessibility/#what_is_accessibility) section of the Google's [Web Fundamentals](https://developers.google.com/web/fundamentals/).
|
||||
For an in-depth introduction to issues and techniques for designing accessible applications, see the [Accessibility](https://developers.google.com/web/fundamentals/accessibility/#what_is_accessibility) section of the Google's [Web Fundamentals](https://developers.google.com/web/fundamentals).
|
||||
|
||||
This page discusses best practices for designing Angular applications that
|
||||
work well for all users, including those who rely on assistive technologies.
|
||||
This page discusses best practices for designing Angular applications that work well for all users, including those who rely on assistive technologies.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
For the sample application that this page describes, see the <live-example></live-example>.
|
||||
For the sample application that this page describes, see the <live-example></live-example>.
|
||||
|
||||
</div>
|
||||
|
||||
## Accessibility attributes
|
||||
|
||||
Building accessible web experience often involves setting [ARIA attributes](https://developers.google.com/web/fundamentals/accessibility/semantics-aria)
|
||||
to provide semantic meaning where it might otherwise be missing.
|
||||
Building accessible web experience often involves setting [ARIA attributes](https://developers.google.com/web/fundamentals/accessibility/semantics-aria) to provide semantic meaning where it might otherwise be missing.
|
||||
Use [attribute binding](guide/attribute-binding) template syntax to control the values of accessibility-related attributes.
|
||||
|
||||
When binding to ARIA attributes in Angular, you must use the `attr.` prefix, as the ARIA
|
||||
specification depends specifically on HTML attributes rather than properties of DOM elements.
|
||||
When binding to ARIA attributes in Angular, you must use the `attr.` prefix, as the ARIA specification depends specifically on HTML attributes rather than properties of DOM elements.
|
||||
|
||||
```html
|
||||
<!-- Use attr. when binding to an ARIA attribute -->
|
||||
<button [attr.aria-label]="myActionLabel">...</button>
|
||||
```
|
||||
<code-example format="html" language="html">
|
||||
|
||||
Note that this syntax is only necessary for attribute _bindings_.
|
||||
Static ARIA attributes require no extra syntax.
|
||||
<!-- Use 'attr.' when binding to an ARIA attribute -->
|
||||
<button [attr.aria-label]="myActionLabel">…</button>
|
||||
|
||||
```html
|
||||
<!-- Static ARIA attributes require no extra syntax -->
|
||||
<button aria-label="Save document">...</button>
|
||||
```
|
||||
</code-example>
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
By convention, HTML attributes use lowercase names (`tabindex`), while properties use camelCase names (`tabIndex`).
|
||||
**NOTE** <br />
|
||||
This syntax is only necessary for attribute *bindings*.
|
||||
Static ARIA attributes require no extra syntax.
|
||||
|
||||
See the [Binding syntax](guide/binding-syntax#html-attribute-vs-dom-property) guide for more background on the difference between attributes and properties.
|
||||
<code-example format="html" language="html">
|
||||
|
||||
<!-- Static ARIA attributes require no extra syntax -->
|
||||
<button aria-label="Save document">…</button>
|
||||
|
||||
</code-example>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
By convention, HTML attributes use lowercase names \(`tabindex`\), while properties use camelCase names \(`tabIndex`\).
|
||||
|
||||
See the [Binding syntax](guide/binding-syntax#html-attribute-vs-dom-property) guide for more background on the difference between attributes and properties.
|
||||
|
||||
</div>
|
||||
|
||||
## Angular UI components
|
||||
|
||||
The [Angular Material](https://material.angular.io/) library, which is maintained by the Angular team, is a suite of reusable UI components that aims to be fully accessible.
|
||||
The [Angular Material](https://material.angular.io) library, which is maintained by the Angular team, is a suite of reusable UI components that aims to be fully accessible.
|
||||
The [Component Development Kit (CDK)](https://material.angular.io/cdk/categories) includes the `a11y` package that provides tools to support various areas of accessibility.
|
||||
For example:
|
||||
|
||||
* `LiveAnnouncer` is used to announce messages for screen-reader users using an `aria-live` region. See the W3C documentation for more information on [aria-live regions](https://www.w3.org/WAI/PF/aria-1.1/states_and_properties#aria-live).
|
||||
* `LiveAnnouncer` is used to announce messages for screen-reader users using an `aria-live` region.
|
||||
See the W3C documentation for more information on [aria-live regions](https://www.w3.org/WAI/PF/aria-1.1/states_and_properties#aria-live).
|
||||
|
||||
* The `cdkTrapFocus` directive traps Tab-key focus within an element. Use it to create accessible experience for components like modal dialogs, where focus must be constrained.
|
||||
* The `cdkTrapFocus` directive traps Tab-key focus within an element.
|
||||
Use it to create accessible experience for components such as modal dialogs, where focus must be constrained.
|
||||
|
||||
For full details of these and other tools, see the [Angular CDK accessibility overview](https://material.angular.io/cdk/a11y/overview).
|
||||
|
||||
|
||||
### Augmenting native elements
|
||||
|
||||
Native HTML elements capture a number of standard interaction patterns that are important to accessibility.
|
||||
@@ -68,17 +73,15 @@ When authoring Angular components, you should re-use these native elements direc
|
||||
For example, instead of creating a custom element for a new variety of button, create a component that uses an attribute selector with a native `<button>` element.
|
||||
This most commonly applies to `<button>` and `<a>`, but can be used with many other types of element.
|
||||
|
||||
You can see examples of this pattern in Angular Material: [`MatButton`](https://github.com/angular/components/blob/50d3f29b6dc717b512dbd0234ce76f4ab7e9762a/src/material/button/button.ts#L67-L69), [`MatTabNav`](https://github.com/angular/components/blob/50d3f29b6dc717b512dbd0234ce76f4ab7e9762a/src/material/tabs/tab-nav-bar/tab-nav-bar.ts#L139), [`MatTable`](https://github.com/angular/components/blob/50d3f29b6dc717b512dbd0234ce76f4ab7e9762a/src/material/table/table.ts#L22).
|
||||
You can see examples of this pattern in Angular Material:
|
||||
[`MatButton`](https://github.com/angular/components/blob/50d3f29b6dc717b512dbd0234ce76f4ab7e9762a/src/material/button/button.ts#L67-L69), [`MatTabNav`](https://github.com/angular/components/blob/50d3f29b6dc717b512dbd0234ce76f4ab7e9762a/src/material/tabs/tab-nav-bar/tab-nav-bar.ts#L139), and [`MatTable`](https://github.com/angular/components/blob/50d3f29b6dc717b512dbd0234ce76f4ab7e9762a/src/material/table/table.ts#L22).
|
||||
|
||||
### Using containers for native elements
|
||||
|
||||
Sometimes using the appropriate native element requires a container element.
|
||||
For example, the native `<input>` element cannot have children, so any custom text entry components need
|
||||
to wrap an `<input>` with additional elements.
|
||||
While you might just include the `<input>` in your custom component's template,
|
||||
this makes it impossible for users of the component to set arbitrary properties and attributes to the input element.
|
||||
Instead, create a container component that uses content projection to include the native control in the
|
||||
component's API.
|
||||
For example, the native `<input>` element cannot have children, so any custom text entry components need to wrap an `<input>` with additional elements.
|
||||
While you might just include the `<input>` in your custom component's template, this makes it impossible for users of the component to set arbitrary properties and attributes to the input element.
|
||||
Instead, create a container component that uses content projection to include the native control in the component's API.
|
||||
|
||||
You can see [`MatFormField`](https://material.angular.io/components/form-field/overview) as an example of this pattern.
|
||||
|
||||
@@ -86,65 +89,61 @@ You can see [`MatFormField`](https://material.angular.io/components/form-field/o
|
||||
|
||||
The following example shows how to make a progress bar accessible by using host binding to control accessibility-related attributes.
|
||||
|
||||
* The component defines an accessibility-enabled element with both the standard HTML attribute `role`, and ARIA attributes. The ARIA attribute `aria-valuenow` is bound to the user's input.
|
||||
* The component defines an accessibility-enabled element with both the standard HTML attribute `role`, and ARIA attributes.
|
||||
The ARIA attribute `aria-valuenow` is bound to the user's input.
|
||||
|
||||
<code-example path="accessibility/src/app/progress-bar.component.ts" header="src/app/progress-bar.component.ts" region="progressbar-component"></code-example>
|
||||
<code-example header="src/app/progress-bar.component.ts" path="accessibility/src/app/progress-bar.component.ts" region="progressbar-component"></code-example>
|
||||
|
||||
* In the template, the `aria-label` attribute ensures that the control is accessible to screen readers.
|
||||
|
||||
* In the template, the `aria-label` attribute ensures that the control is accessible to screen readers.
|
||||
|
||||
<code-example path="accessibility/src/app/app.component.html" header="src/app/app.component.html" region="template"></code-example>
|
||||
|
||||
<code-example header="src/app/app.component.html" path="accessibility/src/app/app.component.html" region="template"></code-example>
|
||||
|
||||
## Routing and focus management
|
||||
|
||||
Tracking and controlling [focus](https://developers.google.com/web/fundamentals/accessibility/focus/) in a UI is an important consideration in designing for accessibility.
|
||||
Tracking and controlling [focus](https://developers.google.com/web/fundamentals/accessibility/focus) in a UI is an important consideration in designing for accessibility.
|
||||
When using Angular routing, you should decide where page focus goes upon navigation.
|
||||
|
||||
To avoid relying solely on visual cues, you need to make sure your routing code updates focus after page navigation.
|
||||
Use the `NavigationEnd` event from the `Router` service to know when to update
|
||||
focus.
|
||||
Use the `NavigationEnd` event from the `Router` service to know when to update focus.
|
||||
|
||||
The following example shows how to find and focus the main content header in the DOM after navigation.
|
||||
|
||||
```ts
|
||||
<code-example format="typescript" language="typescript">
|
||||
|
||||
router.events.pipe(filter(e => e instanceof NavigationEnd)).subscribe(() => {
|
||||
const mainHeader = document.querySelector('#main-content-header')
|
||||
router.events.pipe(filter(e => e instanceof NavigationEnd)).subscribe(() => {
|
||||
const mainHeader = document.querySelector('#main-content-header')
|
||||
if (mainHeader) {
|
||||
mainHeader.focus();
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
In a real application, the element that receives focus will depend on your specific
|
||||
application structure and layout.
|
||||
</code-example>
|
||||
|
||||
In a real application, the element that receives focus will depend on your specific application structure and layout.
|
||||
The focused element should put users in a position to immediately move into the main content that has just been routed into view.
|
||||
You should avoid situations where focus returns to the `body` element after a route change.
|
||||
|
||||
|
||||
## Additional resources
|
||||
|
||||
* [Accessibility - Google Web Fundamentals](https://developers.google.com/web/fundamentals/accessibility)
|
||||
|
||||
* [ARIA specification and authoring practices](https://www.w3.org/TR/wai-aria/)
|
||||
|
||||
* [Material Design - Accessibility](https://material.io/design/usability/accessibility.html)
|
||||
|
||||
* [Smashing Magazine](https://www.smashingmagazine.com/search/?q=accessibility)
|
||||
|
||||
* [Inclusive Components](https://inclusive-components.design/)
|
||||
|
||||
* [Accessibility Resources and Code Examples](https://dequeuniversity.com/resources/)
|
||||
|
||||
* [W3C - Web Accessibility Initiative](https://www.w3.org/WAI/people-use-web/)
|
||||
|
||||
* [Rob Dodson A11ycasts](https://www.youtube.com/watch?v=HtTyRajRuyY)
|
||||
|
||||
* [Angular ESLint](https://github.com/angular-eslint/angular-eslint#functionality) provides linting rules that can help you make sure your code meets accessibility standards.
|
||||
* [Accessibility - Google Web Fundamentals](https://developers.google.com/web/fundamentals/accessibility)
|
||||
* [ARIA specification and authoring practices](https://www.w3.org/TR/wai-aria)
|
||||
* [Material Design - Accessibility](https://material.io/design/usability/accessibility.html)
|
||||
* [Smashing Magazine](https://www.smashingmagazine.com/search/?q=accessibility)
|
||||
* [Inclusive Components](https://inclusive-components.design)
|
||||
* [Accessibility Resources and Code Examples](https://dequeuniversity.com/resources)
|
||||
* [W3C - Web Accessibility Initiative](https://www.w3.org/WAI/people-use-web)
|
||||
* [Rob Dodson A11ycasts](https://www.youtube.com/watch?v=HtTyRajRuyY)
|
||||
* [Angular ESLint](https://github.com/angular-eslint/angular-eslint#functionality) provides linting rules that can help you make sure your code meets accessibility standards.
|
||||
|
||||
Books
|
||||
|
||||
* "A Web for Everyone: Designing Accessible User Experiences", Sarah Horton and Whitney Quesenbery
|
||||
* "A Web for Everyone: Designing Accessible User Experiences", Sarah Horton and Whitney Quesenbery
|
||||
* "Inclusive Design Patterns", Heydon Pickering
|
||||
|
||||
* "Inclusive Design Patterns", Heydon Pickering
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
<!-- end links -->
|
||||
|
||||
@reviewed 2022-02-28
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user