mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
201 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf70bf6d4f | |||
| b4c4b42222 | |||
| 2643d0a45d | |||
| 55e41d876d | |||
| 1a09d3e44b | |||
| 2820425e38 | |||
| 06ce49aeeb | |||
| d6070afdc2 | |||
| 37e7f2d933 | |||
| 81018d7562 | |||
| 6997c525b5 | |||
| d75eaa7375 | |||
| 630e906747 | |||
| 25788b2919 | |||
| f52eee8eb2 | |||
| 5f2a33ac58 | |||
| 753792856d | |||
| c8f87c8eff | |||
| fb10abcf3e | |||
| a3136e00f5 | |||
| fcdae578fe | |||
| 51a5baace3 | |||
| 7d8b3f4968 | |||
| 427705e6c4 | |||
| 2bfd28c396 | |||
| d3c9e436a9 | |||
| aeb7d0395e | |||
| e63e3db80a | |||
| 5f433f6b5b | |||
| 2a10050bea | |||
| 5122729c95 | |||
| 481198af92 | |||
| 1e4c91ca8e | |||
| b732961fc3 | |||
| 966ce9790a | |||
| 334fe60c17 | |||
| 5e6955a06f | |||
| 96141c6ad6 | |||
| 781bb91f43 | |||
| 82e8e5e043 | |||
| a16e3b8fe1 | |||
| 39a3e34e03 | |||
| aedd8de7dd | |||
| 68648ee684 | |||
| b8f447e611 | |||
| 70d8c2f75a | |||
| c3d901eacf | |||
| cc99ba4744 | |||
| 3498efc0e1 | |||
| 59074207a6 | |||
| 17cde6bd5b | |||
| 996cb4bcd0 | |||
| 663703946e | |||
| 6e6452d604 | |||
| 4ccd845081 | |||
| 6e4d8f65cf | |||
| 5810827de4 | |||
| 03127fc870 | |||
| bf1f9a0897 | |||
| 3b46066ff0 | |||
| b0b004e3ff | |||
| ffd25fcfbe | |||
| a31dcb993f | |||
| 5b4a5eb0e9 | |||
| 7b40d74fb4 | |||
| 82712f80df | |||
| 65d69157cb | |||
| 684891a63e | |||
| cc57b24881 | |||
| f6f3783a3d | |||
| febbca5124 | |||
| 38c6fa1cee | |||
| c700418ab8 | |||
| 867b380f64 | |||
| 975cb05819 | |||
| 2579592be6 | |||
| 7ce89a2e94 | |||
| 4051930b8c | |||
| 0de326271b | |||
| a734ca3e15 | |||
| c2fac37717 | |||
| 6d3c497c9d | |||
| 37aad5aa04 | |||
| 1122afd656 | |||
| 9fc808c129 | |||
| 608f5e7cd1 | |||
| f0ff388731 | |||
| 4721208d5a | |||
| fcfa173270 | |||
| dcf6b01f0d | |||
| 8ce22ebae3 | |||
| 137195cd26 | |||
| af9cd84c13 | |||
| c14c7cc191 | |||
| f56b6555bd | |||
| 7bb4f9126b | |||
| ab1e8c8667 | |||
| b1a3925794 | |||
| 7842d06bbd | |||
| 23db70f2d0 | |||
| d2343f2ca8 | |||
| 055448e9c6 | |||
| 3fd41a14cd | |||
| ba46365abd | |||
| 9cc424019e | |||
| 16e25aa01b | |||
| 68d6fae734 | |||
| 3ef0295572 | |||
| 19930f3ffa | |||
| a258823071 | |||
| cc9f5cb3aa | |||
| d140dd2d41 | |||
| 587093c3a5 | |||
| dc4f77c851 | |||
| c99624d078 | |||
| e8a36a7b81 | |||
| 5c4cf07f4d | |||
| 646fe3c5fb | |||
| a5e98aa338 | |||
| 5c36fc784f | |||
| 04c6574280 | |||
| e6b301caa2 | |||
| 403833557b | |||
| 97a40958b5 | |||
| dbd761f528 | |||
| dcd1add06f | |||
| 8081fdd22d | |||
| 6e5b865710 | |||
| 0c7c852ee7 | |||
| a06140a2ca | |||
| 20d62603c2 | |||
| 14941b9b79 | |||
| 0ad738658e | |||
| b7732f79db | |||
| 554305976b | |||
| 6f4a5c325d | |||
| 546a8e52dd | |||
| 02894382f3 | |||
| fbd5d0e7b5 | |||
| 5af3642f65 | |||
| 03fc92d448 | |||
| 76860b54b9 | |||
| fb296e4dd7 | |||
| 157415831c | |||
| 6208865206 | |||
| 0606eca25e | |||
| 4fe88cf9af | |||
| 94a361b543 | |||
| bf0f80e5d6 | |||
| b0396e7164 | |||
| 7f0563eb6e | |||
| 6dc2b22426 | |||
| 36f434e49d | |||
| 4b0ef5acfb | |||
| a1e923e2f4 | |||
| 39ace8664b | |||
| 2c65ea9b40 | |||
| 3dbfabcafe | |||
| cf463a6f4f | |||
| a43c0772ea | |||
| bd4d2bbc1e | |||
| 4a2492380c | |||
| 4b46819ed1 | |||
| 7a090eee96 | |||
| da68bead56 | |||
| 543b98e1f8 | |||
| 03619ada72 | |||
| d8e5316eca | |||
| 77efeb5933 | |||
| 252b111d1e | |||
| 1c41633483 | |||
| 9f977e0f34 | |||
| 3e19890967 | |||
| 074fd4a5ec | |||
| 094d733654 | |||
| 2fff344cd6 | |||
| 951ef93aa6 | |||
| f30a49e3de | |||
| c48e1cc4ed | |||
| 198834b731 | |||
| 6a8c030ab1 | |||
| 232a8c1b8d | |||
| 8f944e0752 | |||
| af0c8b8fa8 | |||
| ee19395672 | |||
| 096fdd0f59 | |||
| 9998bee5f4 | |||
| 95387047fd | |||
| 899e51ad8e | |||
| 0ba2b5e5c3 | |||
| 9f490da7e2 | |||
| b95b5b55d8 | |||
| 26781fd0fe | |||
| f79fd35042 | |||
| dfa772d686 | |||
| c7c0723c7f | |||
| e01549b26b | |||
| cf62063174 | |||
| c0c6d1d892 | |||
| d1fa6cd7d9 | |||
| 1472515d68 |
@@ -16,8 +16,6 @@ integration/cli-hello-world/node_modules
|
||||
integration/cli-hello-world-ivy-i18n/node_modules
|
||||
integration/cli-hello-world-lazy/node_modules
|
||||
integration/cli-hello-world-mocha/node_modules
|
||||
integration/cli-signal-inputs/node_modules
|
||||
integration/defer/node_modules
|
||||
integration/dynamic-compiler/node_modules
|
||||
integration/forms/node_modules
|
||||
integration/injectable-def/node_modules
|
||||
@@ -38,4 +36,3 @@ integration/typings_test_rxjs7/node_modules
|
||||
integration/typings_test_ts49/node_modules
|
||||
integration/typings_test_ts50/node_modules
|
||||
integration/typings_test_ts51/node_modules
|
||||
integration/typings_test_ts52/node_modules
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Enable debugging tests with --config=debug
|
||||
test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test_strategy=exclusive --test_timeout=9999 --nocache_test_results --spawn_strategy=local
|
||||
test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test_strategy=exclusive --test_timeout=9999 --nocache_test_results
|
||||
|
||||
# Do not attempt to de-flake locally.
|
||||
# On CI we might set this to `3` to run with deflaking.
|
||||
@@ -177,9 +177,6 @@ test:saucelabs --flaky_test_attempts=1
|
||||
|
||||
# --ng_perf will ask the Ivy compiler to produce performance results for each build.
|
||||
build --flag_alias=ng_perf=//packages/compiler-cli:ng_perf
|
||||
# --adev_fast will run adev build/serve in a faster mode, skipping things like prerendering
|
||||
# for local development.
|
||||
build --flag_alias=fast_adev=//adev:fast_build_mode
|
||||
|
||||
####################################################
|
||||
# User bazel configuration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Image metadata and config.
|
||||
# Ideally, the Node.js version should match what we use on CI.
|
||||
# See `executors > default-executor` in `.circleci/config.yml`.
|
||||
FROM cimg/node:18.13.0@sha256:45d0ee279f522c4562a464df71108104cc9b2187205333ffd0707d4f05589731
|
||||
FROM cimg/node:16.13-browsers@sha256:6c7a82701665dda03e92d682360fa2e4358e07dab8d1b6a7e48f7d36e3d98117
|
||||
|
||||
|
||||
LABEL name="Angular dev environment" \
|
||||
|
||||
@@ -4,7 +4,7 @@ description: 'Installs the dependencies using Yarn'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
|
||||
- uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3
|
||||
with:
|
||||
path: |
|
||||
./node_modules/
|
||||
|
||||
@@ -68,6 +68,7 @@ merge:
|
||||
|
||||
# list of PR statuses that need to be successful
|
||||
requiredStatuses:
|
||||
- 'ci/circleci: build'
|
||||
- 'google-internal-tests'
|
||||
- 'pullapprove'
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
# This workflow builds the previews for pull requests when a certain label is applied.
|
||||
# The actual deployment happens as part of a dedicated second workflow to avoid security
|
||||
# issues where the building would otherwise occur in an authorized context where secrets
|
||||
# could be leaked. More details can be found here:
|
||||
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
|
||||
|
||||
name: Build adev for preview deployment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [synchronize, labeled]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
adev-build:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event.action == 'labeled' && github.event.label.name == 'adev: preview') ||
|
||||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'adev: preview'))
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev to ensure it continues to work
|
||||
run: yarn bazel build --config=aio_local_deps //adev:build
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
artifact-build-revision: '${{github.event.pull_request.head.sha}}'
|
||||
deploy-directory: './dist/bin/adev/build/browser'
|
||||
@@ -1,50 +0,0 @@
|
||||
# This workflow runs whenever the ADEV build workflow has completed. Deployment happens
|
||||
# as part of a dedicated second workflow to avoid security issues where the building would
|
||||
# otherwise occur in an authorized context where secrets could be leaked.
|
||||
#
|
||||
# More details can be found here:
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
|
||||
|
||||
name: Deploying adev preview to Firebase
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Build adev for preview deployment']
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
# Needed in order to be able to comment on the pull request.
|
||||
pull-requests: write
|
||||
# Needed in order to checkout the repository
|
||||
contents: read
|
||||
# Needed in order to retrieve the artifacts from the previous job
|
||||
actions: read
|
||||
|
||||
env:
|
||||
PREVIEW_PROJECT: ng-dev-previews
|
||||
PREVIEW_SITE: ng-dev-previews-fw
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
token: '${{secrets.GITHUB_TOKEN}}'
|
||||
|
||||
- name: Configure Firebase deploy target
|
||||
working-directory: ./
|
||||
run: |
|
||||
# We can use `npx` as the Firebase deploy actions uses it too.
|
||||
npx -y firebase-tools@latest target:clear --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs
|
||||
npx -y firebase-tools@latest target:apply --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs ${{env.PREVIEW_SITE}}
|
||||
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
github-token: '${{secrets.GITHUB_TOKEN}}'
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
firebase-config-dir: './adev'
|
||||
firebase-public-dir: './adev/build/browser'
|
||||
firebase-project-id: '${{env.PREVIEW_PROJECT}}'
|
||||
firebase-service-key: '${{secrets.FIREBASE_PREVIEW_SERVICE_TOKEN}}'
|
||||
@@ -0,0 +1,42 @@
|
||||
# This workflow builds the AIO previews for pull requests when a certain label is applied.
|
||||
# The actual deployment happens as part of a dedicated second workflow to avoid security
|
||||
# issues where the building would otherwise occur in an authorized context where secrets
|
||||
# could be leaked. More details can be found here:
|
||||
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
|
||||
|
||||
name: Build AIO app for preview deployment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [synchronize, labeled]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
aio-build:
|
||||
runs-on: ubuntu-latest
|
||||
# We only want to build and deploy the AIO app if the `aio: preview` label has been
|
||||
# added, or if the label is already applied and new changes have been made in the PR.
|
||||
if: |
|
||||
(github.event.action == 'labeled' && github.event.label.name == 'aio: preview') ||
|
||||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'aio: preview'))
|
||||
steps:
|
||||
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
with:
|
||||
bazelrc: ./.bazelrc.user
|
||||
|
||||
# Build the web package. Note: We run Bazel from a low-resource Github action container,
|
||||
# so we manually need to instruct Bazel to run more actions concurrently as by default
|
||||
# the number of concurrent actions is determined based on the host resources.
|
||||
- run: bazel build //aio:build --jobs=32 --announce_rc --verbose_failures
|
||||
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
with:
|
||||
workflow-artifact-name: 'aio'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
artifact-build-revision: '${{github.event.pull_request.head.sha}}'
|
||||
deploy-directory: './dist/bin/aio/build'
|
||||
@@ -0,0 +1,44 @@
|
||||
# This workflow runs whenever the AIO build workflow has completed. Deployment happens
|
||||
# as part of a dedicated second workflow to avoid security issues where the building would
|
||||
# otherwise occur in an authorized context where secrets could be leaked.
|
||||
#
|
||||
# More details can be found here:
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
|
||||
|
||||
name: Deploying AIO preview to Firebase
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Build AIO app for preview deployment']
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
# Needed in order to be able to comment on the pull request.
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
PREVIEW_PROJECT: ng-dev-previews
|
||||
PREVIEW_SITE: ng-dev-previews-fw
|
||||
|
||||
jobs:
|
||||
aio-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||
|
||||
- name: Configure Firebase deploy target
|
||||
working-directory: aio/
|
||||
run: |
|
||||
# We can use `npx` as the Firebase deploy actions uses it too.
|
||||
npx -y firebase-tools@latest target:clear --project ${{env.PREVIEW_PROJECT}} hosting aio
|
||||
npx -y firebase-tools@latest target:apply --project ${{env.PREVIEW_PROJECT}} hosting aio ${{env.PREVIEW_SITE}}
|
||||
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
with:
|
||||
github-token: '${{secrets.GITHUB_TOKEN}}'
|
||||
workflow-artifact-name: 'aio'
|
||||
firebase-config-dir: './aio/'
|
||||
firebase-public-dir: './aio/dist'
|
||||
firebase-project-id: '${{env.PREVIEW_PROJECT}}'
|
||||
firebase-service-key: '${{secrets.FIREBASE_PREVIEW_SERVICE_TOKEN}}'
|
||||
@@ -13,9 +13,9 @@ jobs:
|
||||
assistant_to_the_branch_manager:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@c83e99a12397014162531ca125c94549db55dd84
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
exit: true
|
||||
|
||||
# Indicate that the benchmark command was received.
|
||||
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3
|
||||
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3
|
||||
with:
|
||||
comment-id: ${{github.event.comment.id}}
|
||||
token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}'
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
- uses: alessbell/pull-request-comment-branch@aad01d65d6982b8eacabed5e9a684cd8ceb98da6 # v1.1
|
||||
id: comment-branch
|
||||
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
with:
|
||||
# Specify repository as the PR branch might be from a fork.
|
||||
repository: ${{steps.comment-branch.outputs.head_owner}}/${{steps.comment-branch.outputs.head_repo}}
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
with:
|
||||
bazelrc: ./.bazelrc.user
|
||||
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
id: benchmark
|
||||
name: Running benchmark
|
||||
|
||||
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3
|
||||
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3
|
||||
with:
|
||||
issue-number: ${{github.event.issue.number}}
|
||||
token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}'
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1befe6d24fd5cab818e7f8bada40ab364781b8a8
|
||||
with:
|
||||
cache-node-modules: true
|
||||
# Checking out the pull request commit is intended here as we need to run the changed code tests.
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1befe6d24fd5cab818e7f8bada40ab364781b8a8
|
||||
- name: Starting Saucelabs tunnel service
|
||||
run: ./tools/saucelabs/sauce-service.sh run &
|
||||
# Build test fixtures for a test that rely on Bazel-generated fixtures. Note that disabling
|
||||
@@ -62,3 +62,31 @@ jobs:
|
||||
run: KARMA_WEB_TEST_MODE=SL_REQUIRED yarn karma start ./karma-js.conf.js --single-run
|
||||
- name: Stop Saucelabs tunnel service
|
||||
run: ./tools/saucelabs/sauce-service.sh stop
|
||||
|
||||
bazel-saucelabs:
|
||||
runs-on: ubuntu-latest-4core
|
||||
env:
|
||||
JOBS: 2
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@1befe6d24fd5cab818e7f8bada40ab364781b8a8
|
||||
with:
|
||||
cache-node-modules: true
|
||||
# Checking out the pull request commit is intended here as we need to run the changed code tests.
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@1befe6d24fd5cab818e7f8bada40ab364781b8a8
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba9b4487ced515e5b4d87edd681a3bd9792444d6
|
||||
- name: Set up Sauce Tunnel Daemon
|
||||
run: yarn bazel run //tools/saucelabs-daemon/background-service -- $JOBS &
|
||||
env:
|
||||
SAUCE_TUNNEL_IDENTIFIER: angular-framework-${{ github.run_number }}
|
||||
SAUCE_USERNAME: ${{ vars.SAUCE_USERNAME }}
|
||||
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
|
||||
- name: Run all saucelabs bazel tests
|
||||
run: |
|
||||
TESTS=$(./node_modules/.bin/bazelisk query --output label '(kind(karma_web_test, ...) intersect attr("tags", "saucelabs", ...)) except attr("tags", "fixme-saucelabs", ...)')
|
||||
yarn bazel test --config=saucelabs --jobs=$JOBS ${TESTS}
|
||||
|
||||
+114
-85
@@ -23,7 +23,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
@@ -35,6 +35,8 @@ jobs:
|
||||
run: yarn install --frozen-lockfile --cwd aio
|
||||
- name: Check code lint
|
||||
run: yarn -s tslint
|
||||
- name: Check code lint in aio
|
||||
run: yarn -s --cwd aio lint
|
||||
- name: Check for circular dependencies
|
||||
run: yarn -s ts-circular-deps:check
|
||||
- name: Validate pull approve configuration
|
||||
@@ -58,13 +60,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Run unit tests
|
||||
@@ -72,36 +74,123 @@ jobs:
|
||||
- name: Test build
|
||||
run: yarn devtools:build:chrome
|
||||
|
||||
framework-win:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile --network-timeout 100000
|
||||
- name: Test all windows CI targets
|
||||
run: bazel test --test_tag_filters="-browser:chromium-local" //packages/compiler-cli/...
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@45de46d9ba0e0689b7a846fd31ec9e241807ca71
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@45de46d9ba0e0689b7a846fd31ec9e241807ca71
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@45de46d9ba0e0689b7a846fd31ec9e241807ca71
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile --network-timeout 100000
|
||||
- name: Run CI tests for framework
|
||||
run: yarn test:ci
|
||||
|
||||
adev:
|
||||
aio:
|
||||
runs-on: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@45de46d9ba0e0689b7a846fd31ec9e241807ca71
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
./aio/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@45de46d9ba0e0689b7a846fd31ec9e241807ca71
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@45de46d9ba0e0689b7a846fd31ec9e241807ca71
|
||||
- name: Install node modules for aio
|
||||
run: yarn install --cwd aio --frozen-lockfile
|
||||
- name: Run AIO tests with upstream packages
|
||||
run: yarn --cwd aio test:ci
|
||||
- name: Check generated bundle sizes
|
||||
run: yarn --cwd aio payload-size
|
||||
|
||||
aio-local:
|
||||
runs-on:
|
||||
labels: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@45de46d9ba0e0689b7a846fd31ec9e241807ca71
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
./aio/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@45de46d9ba0e0689b7a846fd31ec9e241807ca71
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@45de46d9ba0e0689b7a846fd31ec9e241807ca71
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev to ensure it continues to work
|
||||
run: yarn bazel test --config=aio_local_deps //adev:test
|
||||
run: yarn install --cwd aio --frozen-lockfile
|
||||
- name: Run AIO tests with local packages
|
||||
run: yarn --cwd aio test-local:ci
|
||||
- name: Check generated bundle sizes
|
||||
run: yarn --cwd aio payload-size aio-local
|
||||
|
||||
aio-deploy:
|
||||
needs: [aio]
|
||||
if: needs.aio.result == 'success' && github.event_name == 'push'
|
||||
runs-on:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@45de46d9ba0e0689b7a846fd31ec9e241807ca71
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
./aio/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@45de46d9ba0e0689b7a846fd31ec9e241807ca71
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@45de46d9ba0e0689b7a846fd31ec9e241807ca71
|
||||
- name: Install node modules for aio
|
||||
run: yarn install --cwd aio --frozen-lockfile
|
||||
- name: Set the stable branch environment variable
|
||||
id: latest_version
|
||||
run: |
|
||||
echo "CI_STABLE_BRANCH=$(npm info @angular/core dist-tags.latest | sed -r 's/^\s*([0-9]+\.[0-9]+)\.[0-9]+.*$/\1.x/')" >> $GITHUB_OUTPUT
|
||||
echo "CI_BRANCH=$(echo ${{ github.event.ref }} | cut -d '/' -f 3)" >> $GITHUB_OUTPUT
|
||||
- name: Deploy aio to production
|
||||
env:
|
||||
CI_AIO_MIN_PWA_SCORE: '95'
|
||||
CI_COMMIT: ${{ github.event.after }}
|
||||
CI_REPO_NAME: 'angular'
|
||||
CI_PULL_REQUEST: 'false'
|
||||
CI_REPO_OWNER: 'angular'
|
||||
CI_BUILD_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
CI_STABLE_BRANCH: ${{ steps.latest_version.outputs.CI_STABLE_BRANCH }}
|
||||
CI_BRANCH: ${{ steps.latest_version.outputs.CI_BRANCH }}
|
||||
CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN: ${{ secrets.AIO_DEPLOY_TOKEN }}
|
||||
CI_SECRET_PAYLOAD_FIREBASE_TOKEN: ${{ secrets.AIO_PAYLOAD_TOKEN }}
|
||||
run: yarn --cwd aio deploy-production
|
||||
- name: Notify about failed deployment
|
||||
if: ${{ failure() }}
|
||||
uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v=v1.24.0
|
||||
with:
|
||||
channel-id: 'C07DT5M6V,CKV1F72BG'
|
||||
slack-message: 'Deploy to aio job failed for ${{ github.base_ref }} branch failed on build ${{ github.event.after }}: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.ANGULAR_ROBOT_SLACK_TOKEN }}
|
||||
|
||||
publish-snapshots:
|
||||
if: github.event_name == 'push'
|
||||
@@ -109,13 +198,13 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba9b4487ced515e5b4d87edd681a3bd9792444d6
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba9b4487ced515e5b4d87edd681a3bd9792444d6
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba9b4487ced515e5b4d87edd681a3bd9792444d6
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- run: echo "https://${{secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN}}:@github.com" > ${HOME}/.git_credentials
|
||||
@@ -127,7 +216,7 @@ jobs:
|
||||
labels: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@45de46d9ba0e0689b7a846fd31ec9e241807ca71
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
@@ -135,9 +224,9 @@ jobs:
|
||||
./packages/zone.js/node_modules
|
||||
./packages/zone.js/test/typings/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@45de46d9ba0e0689b7a846fd31ec9e241807ca71
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@45de46d9ba0e0689b7a846fd31ec9e241807ca71
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- run: |
|
||||
@@ -145,87 +234,27 @@ jobs:
|
||||
//packages/zone.js/bundles:zone.umd.js \
|
||||
//packages/zone.js:npm_package \
|
||||
//packages/zone.js/test/closure:closure_js \
|
||||
//packages/zone.js:zone_externs
|
||||
|
||||
- run: |
|
||||
rm -Rf packages/zone.js/build
|
||||
rm -Rf packages/zone.js/test/extra/*.umd.js
|
||||
|
||||
mkdir -p packages/zone.js/build/
|
||||
mkdir -p packages/zone.js/build/test/
|
||||
mkdir -p packages/zone.js/test/
|
||||
|
||||
cp dist/bin/packages/zone.js/bundles/zone.umd.js packages/zone.js/build/zone.umd.js
|
||||
cp dist/bin/packages/zone.js/npm_package/bundles/zone-mix.umd.js ./packages/zone.js/test/extra/
|
||||
cp dist/bin/packages/zone.js/npm_package/bundles/zone-patch-electron.umd.js ./packages/zone.js/test/extra/
|
||||
cp dist/bin/packages/zone.js/test/closure/zone.closure.mjs ./packages/zone.js/build/test/zone.closure.mjs
|
||||
cp dist/bin/packages/zone.js/zone_externs.js ./packages/zone.js/build/zone_externs.js
|
||||
|
||||
# Install
|
||||
- run: yarn --cwd packages/zone.js install --frozen-lockfile --non-interactive
|
||||
# Run zone.js tools tests
|
||||
- run: yarn --cwd packages/zone.js promisetest
|
||||
- run: yarn --cwd packages/zone.js promisefinallytest
|
||||
- run: yarn --cwd packages/zone.js jest:test
|
||||
- run: yarn --cwd packages/zone.js jest:nodetest
|
||||
- run: yarn --cwd packages/zone.js electrontest
|
||||
- run: yarn --cwd packages/zone.js closuretest
|
||||
- run: yarn --cwd packages/zone.js/test/typings install --frozen-lockfile --non-interactive
|
||||
- run: yarn --cwd packages/zone.js/test/typings test
|
||||
|
||||
bazel-saucelabs:
|
||||
# TODO: Replace legacy job with Bazel saucelabs job.
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest-4core
|
||||
env:
|
||||
JOBS: 2
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
# Checking out the pull request commit is intended here as we need to run the changed code tests.
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Set up Sauce Tunnel Daemon
|
||||
run: yarn bazel run //tools/saucelabs-daemon/background-service -- $JOBS &
|
||||
env:
|
||||
SAUCE_TUNNEL_IDENTIFIER: angular-framework-${{ github.run_number }}
|
||||
SAUCE_USERNAME: ${{ vars.SAUCE_USERNAME }}
|
||||
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
|
||||
- name: Run all saucelabs bazel tests
|
||||
run: |
|
||||
TESTS=$(./node_modules/.bin/bazelisk query --output label '(kind(karma_web_test, ...) intersect attr("tags", "saucelabs", ...)) except attr("tags", "fixme-saucelabs", ...)')
|
||||
yarn bazel test --config=saucelabs --jobs=$JOBS ${TESTS}
|
||||
|
||||
adev-deploy:
|
||||
needs: [adev]
|
||||
if: needs.adev.result == 'success' && github.event_name == 'push' && github.ref_name == 'main'
|
||||
env:
|
||||
DEPLOY_PROJECT: angular-dev-site
|
||||
DEPLOY_SITE: angular-dev-site
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev to ensure it continues to work
|
||||
run: yarn bazel build --config=aio_local_deps //adev:build
|
||||
- name: Move generated project files into deployment directory
|
||||
run: cp -r dist/bin/adev/build adev/build
|
||||
- name: Setup firebase context
|
||||
run: |
|
||||
npx -y firebase-tools@latest target:clear --config adev/firebase.json --project ${{ env.DEPLOY_PROJECT }} hosting angular-docs
|
||||
npx -y firebase-tools@latest target:apply --config adev/firebase.json --project ${{ env.DEPLOY_PROJECT }} hosting angular-docs ${{ env.DEPLOY_SITE }}
|
||||
- name: Deploy to firebase
|
||||
uses: FirebaseExtended/action-hosting-deploy@120e124148ab7016bec2374e5050f15051255ba2 # v0.7.1
|
||||
with:
|
||||
entryPoint: './adev'
|
||||
firebaseServiceAccount: ${{ secrets.ANGULAR_DEV_SITE_DEPLOY }}
|
||||
projectId: ${{ env.DEPLOY_PROJECT }}
|
||||
channelId: live
|
||||
|
||||
@@ -12,14 +12,14 @@ jobs:
|
||||
labels:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: angular/dev-infra/github-actions/commit-message-based-labels@c83e99a12397014162531ca125c94549db55dd84
|
||||
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
- uses: angular/dev-infra/github-actions/commit-message-based-labels@9b1adb2ad5a049ea9985734f197dca43dd4d3f1b
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
post_approval_changes:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@c83e99a12397014162531ca125c94549db55dd84
|
||||
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@9b1adb2ad5a049ea9985734f197dca43dd4d3f1b
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Feature request triage bot
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 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@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
@@ -13,8 +13,8 @@ jobs:
|
||||
statuses: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@c83e99a12397014162531ca125c94549db55dd84
|
||||
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@9b1adb2ad5a049ea9985734f197dca43dd4d3f1b
|
||||
with:
|
||||
run-tests-guide-url: http://go/angular-g3sync-start
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -9,6 +9,6 @@ jobs:
|
||||
status:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/unified-status-check@c83e99a12397014162531ca125c94549db55dd84
|
||||
- uses: angular/dev-infra/github-actions/unified-status-check@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -22,15 +22,15 @@ jobs:
|
||||
version: ['https://next.angular.io/', 'https://angular.io/']
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
./aio/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
- name: Install node modules in aio
|
||||
run: yarn install --frozen-lockfile --cwd aio
|
||||
- name: Run basic e2e and deployment config tests.
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Run analysis'
|
||||
uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
|
||||
uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398 # v2.3.0
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
# Upload the results as artifacts.
|
||||
- name: 'Upload artifact'
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
@@ -47,6 +47,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: 'Upload to code-scanning'
|
||||
uses: github/codeql-action/upload-sarif@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5
|
||||
uses: github/codeql-action/upload-sarif@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
with:
|
||||
# Setting `persist-credentials: false` prevents the github-action account from being the
|
||||
# account that is attempted to be used for authentication, instead the remote is set to
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
env:
|
||||
ANGULAR_CLI_BUILDS_READONLY_GITHUB_TOKEN: ${{ secrets.ANGULAR_CLI_BUILDS_READONLY_GITHUB_TOKEN }}
|
||||
- name: Create a PR (if necessary)
|
||||
uses: angular/dev-infra/github-actions/create-pr-for-changes@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/create-pr-for-changes@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
with:
|
||||
branch-prefix: update-cli-help
|
||||
pr-title: 'docs: update Angular CLI help [${{github.ref_name}}]'
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Update the `events.json` file that powers the [AIO events page](https://angular.io/events) (if
|
||||
# necessary) and create a pull request.
|
||||
#
|
||||
# For more details on the overall process, see
|
||||
# [aio/scripts/generate-events/README.md](../../aio/scripts/generate-events/README.md).
|
||||
|
||||
name: Update AIO events
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
schedule:
|
||||
# Run every day at 15:00.
|
||||
- cron: '0 15 * * *'
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update_events:
|
||||
name: Update `events.json` (if necessary)
|
||||
if: github.repository == 'angular/angular'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
with:
|
||||
# Setting `persist-credentials: false` prevents the github-action account from being the
|
||||
# account that is attempted to be used for authentication, instead the remote is set to
|
||||
# an authenticated URL.
|
||||
persist-credentials: false
|
||||
# This is needed as otherwise the PR creation will fail with `shallow update not allowed` when the forked branch is not in sync.
|
||||
fetch-depth: 0
|
||||
- name: Generate `events.json`
|
||||
run: node aio/scripts/generate-events/index.mjs --ignore-invalid-dates
|
||||
- name: Create a PR (if necessary)
|
||||
uses: angular/dev-infra/github-actions/create-pr-for-changes@0109d498b0f6aae418ed4924a5e5c65695f0ac61
|
||||
with:
|
||||
branch-prefix: docs-update-events
|
||||
pr-title: 'docs: update events'
|
||||
pr-description: |
|
||||
Generated `events.json` with the latest events retrieved from the Firebase DB.
|
||||
pr-labels: |
|
||||
action: review
|
||||
area: docs
|
||||
target: patch
|
||||
angular-robot-token: ${{ secrets.ANGULAR_ROBOT_ACCESS_TOKEN }}
|
||||
@@ -57,7 +57,3 @@ baseline.json
|
||||
# Husky
|
||||
.husky/_
|
||||
aio/content/examples/.DS_Store
|
||||
|
||||
|
||||
# Ignore cache created with the Angular CLI.
|
||||
.angular/
|
||||
@@ -1,3 +1,6 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname $0)/_/husky.sh"
|
||||
|
||||
set +e
|
||||
|
||||
yarn -s ng-dev commit-message pre-commit-validate --file $1 2>/dev/null
|
||||
|
||||
+4
-1
@@ -1,8 +1,11 @@
|
||||
set +e
|
||||
#!/bin/sh
|
||||
. "$(dirname $0)/_/husky.sh"
|
||||
|
||||
set +e
|
||||
yarn -s ng-dev format staged 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "WARNING: failed to run file formatting (ng-dev format staged)"
|
||||
fi
|
||||
|
||||
|
||||
exit 0;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname $0)/_/husky.sh"
|
||||
|
||||
set +e
|
||||
|
||||
yarn -s ng-dev commit-message restore-commit-message-draft $1 $2 2>/dev/null
|
||||
|
||||
+1
-47
@@ -5,33 +5,7 @@ import {FormatConfig} from '@angular/ng-dev';
|
||||
*/
|
||||
export const format: FormatConfig = {
|
||||
'prettier': {
|
||||
'matchers': [
|
||||
'**/*.{yaml,yml}',
|
||||
'adev/**/*.{js,ts}',
|
||||
'devtools/**/*.{js,ts}',
|
||||
'tools/**/*.{js,ts}',
|
||||
'modules/**/*.{js,ts}',
|
||||
'scripts/**/*.{js,ts}',
|
||||
'packages/animations/**/*.{js,ts}',
|
||||
'packages/bazel/**/*.{js,ts}',
|
||||
'packages/benchpress/**/*.{js,ts}',
|
||||
'packages/common/**/*.{js,ts}',
|
||||
'packages/docs/**/*.{js,ts}',
|
||||
'packages/elements/**/*.{js,ts}',
|
||||
'packages/examples/**/*.{js,ts}',
|
||||
'packages/misc/**/*.{js,ts}',
|
||||
'packages/private/**/*.{js,ts}',
|
||||
'packages/router/**/*.{js,ts}',
|
||||
'packages/service-worker/**/*.{js,ts}',
|
||||
'packages/upgrade/**/*.{js,ts}',
|
||||
|
||||
// Do not format d.ts files as they are generated
|
||||
'!**/*.d.ts',
|
||||
// Both third_party and .yarn are directories containing copied code which should
|
||||
// not be modified.
|
||||
'!third_party/**',
|
||||
'!.yarn/**',
|
||||
],
|
||||
'matchers': ['**/*.{yaml,yml}'],
|
||||
},
|
||||
'clang-format': {
|
||||
'matchers': [
|
||||
@@ -55,26 +29,6 @@ export const format: FormatConfig = {
|
||||
'!packages/core/src/i18n/locale_en.ts',
|
||||
'!packages/common/locales/closure-locale.ts',
|
||||
'!packages/common/src/i18n/currencies.ts',
|
||||
// Temporarily disable formatting for adev
|
||||
'!adev/**',
|
||||
|
||||
// Migrated to prettier
|
||||
'!devtools/**/*.{js,ts}',
|
||||
'!tools/**/*.{js,ts}',
|
||||
'!modules/**/*.{js,ts}',
|
||||
'!scripts/**/*.{js,ts}',
|
||||
'!packages/animations/**/*.{js,ts}',
|
||||
'!packages/bazel/**/*.{js,ts}',
|
||||
'!packages/benchpress/**/*.{js,ts}',
|
||||
'!packages/common/**/*.{js,ts}',
|
||||
'!packages/docs/**/*.{js,ts}',
|
||||
'!packages/elements/**/*.{js,ts}',
|
||||
'!packages/examples/**/*.{js,ts}',
|
||||
'!packages/misc/**/*.{js,ts}',
|
||||
'!packages/private/**/*.{js,ts}',
|
||||
'!packages/router/**/*.{js,ts}',
|
||||
'!packages/service-worker/**/*.{js,ts}',
|
||||
'!packages/upgrade/**/*.{js,ts}',
|
||||
],
|
||||
},
|
||||
'buildifier': true,
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
{
|
||||
"syncedFilePatterns": [
|
||||
"LICENSE",
|
||||
"modules/benchmarks/**",
|
||||
"packages/**"
|
||||
],
|
||||
"separateFilePatterns": [
|
||||
"packages/core/primitives/**"
|
||||
],
|
||||
"syncedFilePatterns": ["LICENSE", "modules/benchmarks/**", "packages/**"],
|
||||
"alwaysExternalFilePatterns": [
|
||||
"packages/*",
|
||||
"packages/bazel/*",
|
||||
@@ -29,6 +22,7 @@
|
||||
"packages/docs/**",
|
||||
"packages/elements/schematics/**",
|
||||
"packages/examples/**",
|
||||
"packages/language-service/**",
|
||||
"packages/localize/**",
|
||||
"packages/private/**",
|
||||
"packages/service-worker/**",
|
||||
@@ -37,6 +31,8 @@
|
||||
"packages/http/**",
|
||||
"**/.gitignore",
|
||||
"**/.gitkeep",
|
||||
"**/yarn.lock",
|
||||
"**/package.json",
|
||||
"**/third_party/**",
|
||||
"**/tsconfig-build.json",
|
||||
"**/tsconfig-tsec.json",
|
||||
|
||||
@@ -17,9 +17,4 @@ export const pullRequest: PullRequestConfig = {
|
||||
// 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'],
|
||||
// enables specific validations during the pull request merge process
|
||||
validators: {
|
||||
assertEnforceTested: true,
|
||||
assertIsolatedSeparateFiles: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"tabs": false,
|
||||
"embeddedLanguageFormatting": "off",
|
||||
"singleQuote": true,
|
||||
"semicolon": true,
|
||||
"quoteProps": "preserve",
|
||||
|
||||
+27
-47
@@ -166,9 +166,8 @@ groups:
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- crisbeto
|
||||
- devversion
|
||||
- dylhunn
|
||||
- JoostK
|
||||
- dylhunn
|
||||
|
||||
# =========================================================
|
||||
# Framework: Migrations
|
||||
@@ -183,8 +182,6 @@ groups:
|
||||
- crisbeto
|
||||
- devversion
|
||||
- dylhunn
|
||||
- jessicajaniuk
|
||||
- AndrewKushnir
|
||||
|
||||
# =========================================================
|
||||
# Framework: Core
|
||||
@@ -314,7 +311,6 @@ groups:
|
||||
'aio/content/guide/pipes.md',
|
||||
'aio/content/guide/pipes-custom-data-trans.md',
|
||||
'aio/content/guide/pipes-overview.md',
|
||||
'aio/content/guide/pipe-precedence.md',
|
||||
'aio/content/guide/pipes-transform-data.md',
|
||||
'aio/content/guide/pipe-template.md',
|
||||
'aio/content/examples/pipes/**/{*,.*}',
|
||||
@@ -322,9 +318,7 @@ groups:
|
||||
'aio/content/guide/providers.md',
|
||||
'aio/content/examples/providers/**/{*,.*}',
|
||||
'aio/content/images/guide/providers/**/{*,.*}',
|
||||
'aio/content/guide/signal-queries.md',
|
||||
'aio/content/guide/singleton-services.md',
|
||||
'aio/content/guide/signal-inputs.md',
|
||||
'aio/content/guide/sharing-ngmodules.md',
|
||||
'aio/content/guide/standalone-components.md',
|
||||
'aio/content/guide/standalone-migration.md',
|
||||
@@ -342,12 +336,8 @@ groups:
|
||||
'aio/content/examples/view-encapsulation/**/{*,.*}',
|
||||
'aio/content/images/guide/view-encapsulation/**/{*,.*}',
|
||||
'aio/content/special-elements/**/{*,.*}',
|
||||
'aio/content/blocks/**/{*,.*}',
|
||||
'aio/content/guide/hydration.md',
|
||||
'aio/content/guide/defer.md',
|
||||
'aio/content/guide/signals.md',
|
||||
'aio/content/guide/control_flow.md',
|
||||
'aio/content/guide/model-inputs.md',
|
||||
'aio/content/examples/injection-token/**/{*,.*}',
|
||||
])
|
||||
reviewers:
|
||||
@@ -356,7 +346,6 @@ groups:
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- crisbeto
|
||||
- devversion
|
||||
- dylhunn
|
||||
- jessicajaniuk
|
||||
- pkozlowski-opensource
|
||||
@@ -526,8 +515,8 @@ groups:
|
||||
contains_any_globs(files, [
|
||||
'packages/platform-server/**/{*,.*}',
|
||||
'aio/content/guide/prerendering.md',
|
||||
'aio/content/guide/ssr.md',
|
||||
'aio/content/examples/ssr/**/{*,.*}',
|
||||
'aio/content/guide/universal.md',
|
||||
'aio/content/examples/universal/**/{*,.*}'
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
@@ -939,14 +928,12 @@ groups:
|
||||
'aio/content/guide/update-to-version-14.md',
|
||||
'aio/content/guide/update-to-version-15.md',
|
||||
'aio/content/guide/update-to-version-16.md',
|
||||
'aio/content/guide/update-to-version-17.md',
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- alxhub
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- jessicajaniuk
|
||||
- jelbourn
|
||||
|
||||
# =========================================================
|
||||
@@ -1063,30 +1050,6 @@ groups:
|
||||
- dylhunn
|
||||
- mgechev
|
||||
|
||||
# =========================================================
|
||||
# Docs: Angular Dev Site
|
||||
# =========================================================
|
||||
angular-dev:
|
||||
<<: *defaults
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
'adev/**/{.*,*}'
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- josephperrott
|
||||
- jelbourn
|
||||
- bencodezen
|
||||
- jessicajaniuk
|
||||
- dylhunn
|
||||
- AndrewKushnir
|
||||
- alxhub
|
||||
- crisbeto
|
||||
- atscott
|
||||
- pkozlowski-opensource
|
||||
- ~JeanMeche
|
||||
|
||||
# =========================================================
|
||||
# Docs-infra
|
||||
# =========================================================
|
||||
@@ -1164,7 +1127,6 @@ groups:
|
||||
- josephperrott
|
||||
- mgechev
|
||||
- twerske
|
||||
- ~JeanMeche
|
||||
|
||||
# =========================================================
|
||||
# Dev-infra
|
||||
@@ -1199,11 +1161,9 @@ groups:
|
||||
'tools/esm-interop/**/{*,.*}',
|
||||
'tools/gulp-tasks/**/{*,.*}',
|
||||
'tools/legacy-saucelabs/**/{*,.*}',
|
||||
'tools/manual_api_docs/**/{*,.*}',
|
||||
'tools/npm-patches/**/{*,.*}',
|
||||
'tools/rxjs/**/{*,.*}',
|
||||
'tools/saucelabs-daemon/**/{*,.*}',
|
||||
'tools/saucelabs/**/{*,.*}',
|
||||
'tools/saucelabs-daemon/**/{*,.*}',
|
||||
'tools/symbol-extractor/**/{*,.*}',
|
||||
'tools/testing/**/{*,.*}',
|
||||
'tools/tslint/**/{*,.*}',
|
||||
@@ -1397,6 +1357,26 @@ groups:
|
||||
author_value: 0 # The author of the PR cannot provide an approval for themself
|
||||
reviewed_for: ignored # All reviews apply to this group whether noted via Reviewed-for or not
|
||||
reviewers:
|
||||
teams:
|
||||
# Any member of the team can provide a review to perform the minimum review required
|
||||
- team
|
||||
users:
|
||||
- alan-agius4 # Alan Agius
|
||||
- AleksanderBodurri # Aleksander Bodurri
|
||||
- alxhub # Alex Rickabaugh
|
||||
- AndrewKushnir # Andrew Kushnir
|
||||
- andrewseguin # Andrew Seguin
|
||||
- atscott # Andrew Scott
|
||||
- clydin # Charles Lyding
|
||||
- crisbeto # Kristiyan Kostadinov
|
||||
- devversion # Paul Gschwendtner
|
||||
- dgp1130 # Doug Parker
|
||||
- dylhunn # Dylan Hunn
|
||||
- jelbourn # Jeremy Elbourn
|
||||
- jessicajaniuk # Jessica Janiuk
|
||||
- JiaLiPassion # Jia Li
|
||||
- JoostK # Joost Koehoorn
|
||||
- josephperrott # Joey Perrott
|
||||
- MarkTechson # Mark Thompson (Techson)
|
||||
- mgechev # Minko Gechev
|
||||
- mmalerba # Miles Malerba
|
||||
- pkozlowski-opensource # Pawel Kozlowski
|
||||
- twerske # Emma Twersky
|
||||
- zarend # Zach Arend
|
||||
|
||||
Vendored
+5
@@ -5,7 +5,12 @@
|
||||
// List of extensions which should be recommended for users of this workspace.
|
||||
"recommendations": [
|
||||
"BazelBuild.vscode-bazel",
|
||||
"gkalpak.aio-docs-utils",
|
||||
"ms-vscode.vscode-typescript-tslint-plugin",
|
||||
"xaver.clang-format",
|
||||
// The following extensions are useful when working on angular.io (i.e. inside the `aio/` directory).
|
||||
//"angular.ng-template",
|
||||
//"dbaeumer.vscode-eslint",
|
||||
//"errata-ai.vale-server",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ exports_files([
|
||||
"scripts/ci/payload-size.sh",
|
||||
"scripts/ci/payload-size.js",
|
||||
"package.json",
|
||||
"angular.json",
|
||||
])
|
||||
|
||||
filegroup(
|
||||
|
||||
-950
@@ -1,939 +1,3 @@
|
||||
<a name="17.3.0-rc.0"></a>
|
||||
# 17.3.0-rc.0 (2024-03-06)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5afa4f0ec1](https://github.com/angular/angular/commit/5afa4f0ec1b64b88ef875d48bd143e0f36e0a955) | fix | support `ModuleWithProviders` literal detection with `typeof` ([#54650](https://github.com/angular/angular/pull/54650)) |
|
||||
| [ae7dbe42de](https://github.com/angular/angular/commit/ae7dbe42de5779e578fb636938699b8ba85b33f6) | fix | unwrap expressions with type parameters in query read property ([#54647](https://github.com/angular/angular/pull/54647)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [fb540e169a](https://github.com/angular/angular/commit/fb540e169a78a61f38d611f538eea8fdb0971f1d) | feat | add migration for invalid two-way bindings ([#54630](https://github.com/angular/angular/pull/54630)) |
|
||||
| [c687b8f453](https://github.com/angular/angular/commit/c687b8f4531252cd1c3dfbb9a7bd42bdbe666a36) | feat | expose new `output()` API ([#54650](https://github.com/angular/angular/pull/54650)) |
|
||||
| [c809069f21](https://github.com/angular/angular/commit/c809069f213244afd0e2d803a6a43510b218e6f5) | feat | introduce `outputFromObservable()` interop function ([#54650](https://github.com/angular/angular/pull/54650)) |
|
||||
| [aff65fd1f4](https://github.com/angular/angular/commit/aff65fd1f4a61ed76a6f9b623852f197eb3500e4) | feat | introduce `outputToObservable` interop helper ([#54650](https://github.com/angular/angular/pull/54650)) |
|
||||
| [33a6fab094](https://github.com/angular/angular/commit/33a6fab094205cc74aceb916e45c8afa22293cf4) | fix | apply TestBed provider overrides to `@defer` dependencies ([#54667](https://github.com/angular/angular/pull/54667)) |
|
||||
| [78e69117f0](https://github.com/angular/angular/commit/78e69117f07e8ae11aab3abe0343df815b3649be) | fix | generic inference for signal inputs may break with `--strictFunctionTypes` ([#54652](https://github.com/angular/angular/pull/54652)) |
|
||||
| [7243c704cf](https://github.com/angular/angular/commit/7243c704cf8a4986fae419793027458e142658f0) | fix | return a readonly signal on `asReadonly`. ([#54706](https://github.com/angular/angular/pull/54706)) |
|
||||
| [ffad7b8ea9](https://github.com/angular/angular/commit/ffad7b8ea9d1286ddb3ee7a2dac7dd33fd76b3aa) | fix | untrack various core operations ([#54614](https://github.com/angular/angular/pull/54614)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c1c7384e02](https://github.com/angular/angular/commit/c1c7384e02becc623c6a42985f7178ca98137264) | feat | Add reusable types for router guards ([#54580](https://github.com/angular/angular/pull/54580)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.2.4"></a>
|
||||
# 17.2.4 (2024-03-06)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [917b9bdd2e](https://github.com/angular/angular/commit/917b9bdd2eec9cd6395adb0cbde979490bcc3a88) | fix | unwrap expressions with type parameters in query read property ([#54647](https://github.com/angular/angular/pull/54647)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [586cc24a10](https://github.com/angular/angular/commit/586cc24a102d51e7c42ecc839124353afe459073) | fix | apply TestBed provider overrides to `@defer` dependencies ([#54667](https://github.com/angular/angular/pull/54667)) |
|
||||
| [b558a01c84](https://github.com/angular/angular/commit/b558a01c84a1fe4f9e047e92d99d69f02f2ccc4f) | fix | generic inference for signal inputs may break with `--strictFunctionTypes` ([#54652](https://github.com/angular/angular/pull/54652)) |
|
||||
| [443e5f1591](https://github.com/angular/angular/commit/443e5f1591896909eab6ad157bb4b5db50a49f26) | fix | return a readonly signal on `asReadonly`. ([#54719](https://github.com/angular/angular/pull/54719)) |
|
||||
| [ffbafc7d4a](https://github.com/angular/angular/commit/ffbafc7d4a3c61226cb5c524b3ff42ed4f9f1d76) | fix | untrack various core operations ([#54614](https://github.com/angular/angular/pull/54614)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.2.3"></a>
|
||||
# 17.2.3 (2024-02-27)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1a526f2881](https://github.com/angular/angular/commit/1a526f28814429c05b003edbe40500b7069e1404) | perf | `AsyncPipe` should not call `markForCheck` on subscription ([#54554](https://github.com/angular/angular/pull/54554)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [2aefed8763](https://github.com/angular/angular/commit/2aefed87632293c5570e17653a7d270162ea35c3) | fix | catch function instance properties in interpolated signal diagnostic ([#54325](https://github.com/angular/angular/pull/54325)) |
|
||||
| [48aec63ee4](https://github.com/angular/angular/commit/48aec63ee48fe9d20c1f1565b044ec359100736d) | fix | identify aliased initializer functions ([#54480](https://github.com/angular/angular/pull/54480)) |
|
||||
| [daf7c611b2](https://github.com/angular/angular/commit/daf7c611b23797ba2dba24e20e26c344902e7bc7) | fix | identify aliased initializer functions ([#54609](https://github.com/angular/angular/pull/54609)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [57123524a2](https://github.com/angular/angular/commit/57123524a2e1481987eaf239d2ae7f1216291864) | fix | collect providers from NgModules while rendering `@defer` block ([#52881](https://github.com/angular/angular/pull/52881)) |
|
||||
| [79a32816dc](https://github.com/angular/angular/commit/79a32816dcb57e9b02ba6e18261c6dccd14b0b23) | fix | fix typo in injectors.svg file ([#54596](https://github.com/angular/angular/pull/54596)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [dbe673b027](https://github.com/angular/angular/commit/dbe673b02751b8bf12f6f6f79b843dd268d965e1) | fix | resolve infinite loop for a single line element with a long tag name and angle bracket on a new line ([#54588](https://github.com/angular/angular/pull/54588)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.3.0-next.1"></a>
|
||||
# 17.3.0-next.1 (2024-02-27)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [707bfc9b32](https://github.com/angular/angular/commit/707bfc9b326f321f302dccfdfebef7380c914eb5) | perf | `AsyncPipe` should not call `markForCheck` on subscription ([#54554](https://github.com/angular/angular/pull/54554)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1a6beae8a2](https://github.com/angular/angular/commit/1a6beae8a2bdcff27d4c1e402f98246a52247906) | feat | Enable template pipeline by default. ([#54571](https://github.com/angular/angular/pull/54571)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [12dc4d074e](https://github.com/angular/angular/commit/12dc4d074e63edaff626003ad6136a8d122b2ba6) | fix | account for as expression in docs extraction ([#54414](https://github.com/angular/angular/pull/54414)) |
|
||||
| [f578889ca2](https://github.com/angular/angular/commit/f578889ca2dbb22f2e050d4c4af6aa2d597bf70d) | fix | catch function instance properties in interpolated signal diagnostic ([#54325](https://github.com/angular/angular/pull/54325)) |
|
||||
| [f04ecc0cda](https://github.com/angular/angular/commit/f04ecc0cdaeb3a292a748b1ccc94ce70a573fc79) | fix | identify aliased initializer functions ([#54480](https://github.com/angular/angular/pull/54480)) |
|
||||
| [f5c566c079](https://github.com/angular/angular/commit/f5c566c0793eacf9ca146c8a6b8da15b0e8f4c4d) | fix | identify aliased initializer functions ([#54609](https://github.com/angular/angular/pull/54609)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [331b16efd2](https://github.com/angular/angular/commit/331b16efd2f5af876e6dc0ad2474ee7a87b00de5) | feat | add API to inject attributes on the host node ([#54604](https://github.com/angular/angular/pull/54604)) |
|
||||
| [974958913c](https://github.com/angular/angular/commit/974958913ca632971f878a045537472f2c99c665) | feat | support TypeScript 5.4 ([#54414](https://github.com/angular/angular/pull/54414)) |
|
||||
| [dcb9deb363](https://github.com/angular/angular/commit/dcb9deb3631ff4e839f738f2fb97ca6b894256ef) | fix | collect providers from NgModules while rendering `@defer` block ([#52881](https://github.com/angular/angular/pull/52881)) |
|
||||
| [ff40c9f762](https://github.com/angular/angular/commit/ff40c9f762b2c14870ac2859201ece9660087e79) | fix | fix typo in injectors.svg file ([#54596](https://github.com/angular/angular/pull/54596)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [71e0c7df69](https://github.com/angular/angular/commit/71e0c7df695f274a54d5ffb9dfc0587d06d3a953) | fix | resolve infinite loop for a single line element with a long tag name and angle bracket on a new line ([#54588](https://github.com/angular/angular/pull/54588)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.3.0-next.0"></a>
|
||||
# 17.3.0-next.0 (2024-02-21)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [da7fbb40f0](https://github.com/angular/angular/commit/da7fbb40f06e6e37504f69e7b335f8219f424de2) | fix | detect when the linker is working in unpublished angular and widen supported versions ([#54439](https://github.com/angular/angular/pull/54439)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.2.2"></a>
|
||||
# 17.2.2 (2024-02-21)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [d34e3298db](https://github.com/angular/angular/commit/d34e3298db2da9d3aa8f8e2c49e1d1aed849e7d2) | fix | image placeholder not removed in OnPush component ([#54515](https://github.com/angular/angular/pull/54515)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [6447c0eecc](https://github.com/angular/angular/commit/6447c0eecc15800417bb5bde2d273865c559b7d4) | fix | adding the inert property to the "SCHEMA" array ([#53148](https://github.com/angular/angular/pull/53148)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0a3edfb543](https://github.com/angular/angular/commit/0a3edfb5433e1b67f37d7b3f69a5c77b48eedce6) | fix | correctly detect deferred dependencies across scoped nodes ([#54499](https://github.com/angular/angular/pull/54499)) |
|
||||
| [790f4f7c26](https://github.com/angular/angular/commit/790f4f7c26474af2b1d81a4f3c761047bf920edf) | fix | use correct symbol name for default imported symbols in defer blocks ([#54495](https://github.com/angular/angular/pull/54495)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3bd5860c74](https://github.com/angular/angular/commit/3bd5860c749517c4d35850703c53a768138b7bde) | fix | properly execute content queries for root components ([#54457](https://github.com/angular/angular/pull/54457)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [bb57d34110](https://github.com/angular/angular/commit/bb57d34110ea8796f5c5f522b98dadf078e8ad8a) | fix | Fix cf migration regular expression to include underscores ([#54533](https://github.com/angular/angular/pull/54533)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3e31f1a34e](https://github.com/angular/angular/commit/3e31f1a34ee97a802a4032cd9e7816d683a51d5e) | fix | Clear internal transition when navigation finalizes ([#54261](https://github.com/angular/angular/pull/54261)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.2.1"></a>
|
||||
# 17.2.1 (2024-02-14)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7234824228](https://github.com/angular/angular/commit/7234824228df11249a5ebe01a6dee381be74e02e) | fix | fix broken version detection condition ([#54443](https://github.com/angular/angular/pull/54443)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.2.0"></a>
|
||||
# 17.2.0 (2024-02-14)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [03c3b3eb79](https://github.com/angular/angular/commit/03c3b3eb79ec061b0031d6ad7ba386d185c87d8d) | feat | add Netlify image loader ([#54311](https://github.com/angular/angular/pull/54311)) |
|
||||
| [f5c520b836](https://github.com/angular/angular/commit/f5c520b836c4545c7043649f28b3a0369c168747) | feat | add placeholder to NgOptimizedImage ([#53783](https://github.com/angular/angular/pull/53783)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [47e6e84101](https://github.com/angular/angular/commit/47e6e841016abfca0c1aa84051d82a04b3027617) | feat | Add a TSConfig option `useTemplatePipeline` ([#54057](https://github.com/angular/angular/pull/54057)) |
|
||||
| [66e940aebf](https://github.com/angular/angular/commit/66e940aebfd5a93944860a4e0dbd14e1072f80f2) | feat | scope selectors in @starting-style ([#53943](https://github.com/angular/angular/pull/53943)) |
|
||||
| [7b4d275f49](https://github.com/angular/angular/commit/7b4d275f494a64c38b61cea7045ba8b6e8447b78) | fix | Fix the template pipeline option ([#54148](https://github.com/angular/angular/pull/54148)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7e861c640e](https://github.com/angular/angular/commit/7e861c640edf90c5f8d4f7e091861d3d98cd49c0) | feat | generate extra imports for component local dependencies in local mode ([#53543](https://github.com/angular/angular/pull/53543)) |
|
||||
| [3263df23f2](https://github.com/angular/angular/commit/3263df23f2f4da722ef2c1a1dacfb0866498dd60) | feat | generate global imports in local compilation mode ([#53543](https://github.com/angular/angular/pull/53543)) |
|
||||
| [b774e22d8e](https://github.com/angular/angular/commit/b774e22d8e384f43e9cd8f5c55475d06e7f66988) | feat | make it configurable to generate alias reexports ([#53937](https://github.com/angular/angular/pull/53937)) |
|
||||
| [3e1384048e](https://github.com/angular/angular/commit/3e1384048eb76c92532ae19aa2883318121c00e8) | feat | support host directives for local compilation mode ([#53877](https://github.com/angular/angular/pull/53877)) |
|
||||
| [a592904c69](https://github.com/angular/angular/commit/a592904c691844d2c1aed00bd914edabef49f9b1) | fix | allow custom/duplicate decorators for `@Injectable` classes in local compilation mode ([#54139](https://github.com/angular/angular/pull/54139)) |
|
||||
| [4b1d948b36](https://github.com/angular/angular/commit/4b1d948b36285ec6d80dbe93e0b92133f9d4be94) | fix | consider the case of duplicate Angular decorators in local compilation diagnostics ([#54139](https://github.com/angular/angular/pull/54139)) |
|
||||
| [96bcf4fb12](https://github.com/angular/angular/commit/96bcf4fb1208d1f073784a2cde4a03553e905807) | fix | forbid custom/duplicate decorator when option `forbidOrphanComponents` is set ([#54139](https://github.com/angular/angular/pull/54139)) |
|
||||
| [64fa5715c6](https://github.com/angular/angular/commit/64fa5715c696101fba0b4f8623eaec0eadc5b159) | fix | generating extra imports in local compilation mode when cycle is introduced ([#53543](https://github.com/angular/angular/pull/53543)) |
|
||||
| [6c8b09468a](https://github.com/angular/angular/commit/6c8b09468a05a80cba3960861f0ab8d3bae80415) | fix | highlight the unresolved element in the @Component.styles array for the error LOCAL_COMPILATION_UNRESOLVED_CONST ([#54230](https://github.com/angular/angular/pull/54230)) |
|
||||
| [0970129e20](https://github.com/angular/angular/commit/0970129e20f77dc309f2b4f76f961b310124778c) | fix | show proper error for custom decorators in local compilation mode ([#53983](https://github.com/angular/angular/pull/53983)) |
|
||||
| [f39cb06418](https://github.com/angular/angular/commit/f39cb064183d984254bdf4e41b61d3dc9379738a) | fix | show specific error for unresolved @Directive.exportAs in local compilation mode ([#54230](https://github.com/angular/angular/pull/54230)) |
|
||||
| [f3851b5945](https://github.com/angular/angular/commit/f3851b59459a1d9c214ace3db5a716d51c1f93c7) | fix | show specific error for unresolved @HostBinding's argument in local compilation mode ([#54230](https://github.com/angular/angular/pull/54230)) |
|
||||
| [39ddd884e8](https://github.com/angular/angular/commit/39ddd884e826cc0be63fd0f7d7de20d642877ef9) | fix | show specific error for unresolved @HostListener's event name in local compilation mode ([#54230](https://github.com/angular/angular/pull/54230)) |
|
||||
| [5d633240fd](https://github.com/angular/angular/commit/5d633240fd5927c4318a9240e60c3a30b2333cee) | fix | show the correct message for the error LOCAL_COMPILATION_UNRESOLVED_CONST when an unresolved symbol used for @Component.styles ([#54230](https://github.com/angular/angular/pull/54230)) |
|
||||
| [58b8a232d6](https://github.com/angular/angular/commit/58b8a232d64f5fe3207c90c8145cab36e7e192c2) | fix | support jumping to definitions of signal-based inputs ([#54053](https://github.com/angular/angular/pull/54053)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [702ab28b4c](https://github.com/angular/angular/commit/702ab28b4c07a903c403a20af2ca287348b6afd0) | feat | add support for model inputs ([#54252](https://github.com/angular/angular/pull/54252)) |
|
||||
| [e95ef2cbc6](https://github.com/angular/angular/commit/e95ef2cbc6f850d8fe96218b74cff76cea947674) | feat | expose queries as signals ([#54283](https://github.com/angular/angular/pull/54283)) |
|
||||
| [656bc282e3](https://github.com/angular/angular/commit/656bc282e345c5e37a9189a0a4daa631e02c31bf) | fix | add toString implementation to signals ([#54002](https://github.com/angular/angular/pull/54002)) |
|
||||
| [62b87b4551](https://github.com/angular/angular/commit/62b87b4551d77815f58af152d1921de3733621ba) | fix | do not crash for signal query that does not have any matches ([#54353](https://github.com/angular/angular/pull/54353)) |
|
||||
| [4b96f370ee](https://github.com/angular/angular/commit/4b96f370eea08d2531cc54f65a651f94b504692d) | fix | expose model signal subcribe for type checking purposes ([#54357](https://github.com/angular/angular/pull/54357)) |
|
||||
| [744cb1e561](https://github.com/angular/angular/commit/744cb1e561d9b0cbffd20ad612d5ea9e2cf6b2e7) | fix | return the same children query results if there are no changes ([#54392](https://github.com/angular/angular/pull/54392)) |
|
||||
| [6d00115bf4](https://github.com/angular/angular/commit/6d00115bf45731b80178e11921df6ec1dc89efaf) | fix | show placeholder block on the server with immediate trigger ([#54394](https://github.com/angular/angular/pull/54394)) |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1c536250b6](https://github.com/angular/angular/commit/1c536250b6112c67a68159de50c4afe0dbc440f8) | fix | Use string body to generate transfer cache key. ([#54379](https://github.com/angular/angular/pull/54379)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.1.3"></a>
|
||||
# 17.1.3 (2024-02-08)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [bc4a6a9715](https://github.com/angular/angular/commit/bc4a6a9715547881ed8e65169a5aaebfd3188a7f) | fix | do not error due to multiple components named equally ([#54273](https://github.com/angular/angular/pull/54273)) |
|
||||
| [a997e08c6f](https://github.com/angular/angular/commit/a997e08c6f5c5321e5d18f3368ff0886fa133d59) | fix | handle default imports in defer blocks ([#53695](https://github.com/angular/angular/pull/53695)) |
|
||||
| [63a9027720](https://github.com/angular/angular/commit/63a9027720611002c6ee3b443a11e9feff213059) | fix | interpolatedSignalNotInvoked diagnostic for model signals ([#54338](https://github.com/angular/angular/pull/54338)) |
|
||||
| [40e1edc977](https://github.com/angular/angular/commit/40e1edc977fbe398adc535167f8ede2db8985656) | fix | properly catch fatal diagnostics in type checking ([#54309](https://github.com/angular/angular/pull/54309)) |
|
||||
| [9f6605d11b](https://github.com/angular/angular/commit/9f6605d11b7ee75f289b5a2ed69e201d65b038d8) | fix | support jumping to definitions of signal-based inputs ([#54233](https://github.com/angular/angular/pull/54233)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7df133dcc2](https://github.com/angular/angular/commit/7df133dcc243cd6b0f779fa35de7f916e6938301) | fix | `afterRender` hooks should allow updating state ([#54074](https://github.com/angular/angular/pull/54074)) |
|
||||
| [744e20641a](https://github.com/angular/angular/commit/744e20641a21d18c324bd9c157c8912d38741826) | fix | Fix possible infinite loop with `markForCheck` by partially reverting [#54074](https://github.com/angular/angular/pull/54074) ([#54329](https://github.com/angular/angular/pull/54329)) |
|
||||
| [0fb114274c](https://github.com/angular/angular/commit/0fb114274cead9f317a2fc902cc3a3f6b046e708) | fix | update imports to be compatible with rxjs 6 ([#54193](https://github.com/angular/angular/pull/54193)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [238f2a8bc9](https://github.com/angular/angular/commit/238f2a8bc9c46b0d08aff163349ecc1493441a69) | fix | Clear internal transition when navigation finalizes ([#54261](https://github.com/angular/angular/pull/54261)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.1.2"></a>
|
||||
# 17.1.2 (2024-01-31)
|
||||
###
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [ccddacf11d](https://github.com/angular/angular/commit/ccddacf11deaebeda12e1bdb6e93ec401397d352) | fix | cta clickability issue in adev homepage. ([#52905](https://github.com/angular/angular/pull/52905)) |
|
||||
### animations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [98d545fafa](https://github.com/angular/angular/commit/98d545fafa7fc3b1fb3ae049ce655e33ef9bd423) | fix | cleanup DOM elements when root view is removed with async animations ([#53033](https://github.com/angular/angular/pull/53033)) |
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [cdc5e39532](https://github.com/angular/angular/commit/cdc5e3953237a192beafd6330f9d9e36ede34f2c) | fix | The date pipe should return ISO format for week and week-year as intended in the unit test. ([#53879](https://github.com/angular/angular/pull/53879)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f12b01ec88](https://github.com/angular/angular/commit/f12b01ec88eaf18041c2e46335428627aa0d7744) | fix | Update type check block to fix control flow source mappings ([#53980](https://github.com/angular/angular/pull/53980)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c477e876e3](https://github.com/angular/angular/commit/c477e876e39495b855b096440d53cf1dd1ad33c6) | fix | change defer block fixture default behavior to playthrough ([#54088](https://github.com/angular/angular/pull/54088)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [8264382a6b](https://github.com/angular/angular/commit/8264382a6bf389fb3fca75fa2d6c0a2aa5a1e42f) | fix | error in standalone migration when non-array value is used as declarations in TestBed ([#54122](https://github.com/angular/angular/pull/54122)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.1.1"></a>
|
||||
# 17.1.1 (2024-01-24)
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f222bee8fa](https://github.com/angular/angular/commit/f222bee8fa037f437761e5f7f127f22f280e9154) | fix | revert commit that replaced `last` helper with native `Array.at(-1)` ([#54021](https://github.com/angular/angular/pull/54021)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.1.0"></a>
|
||||
# 17.1.0 (2024-01-17)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [79ff91a813](https://github.com/angular/angular/commit/79ff91a813e544929cb5eb5f9aab762a9f3d0435) | fix | allow TS jsDocParsingMode host option to be programmatically set ([#53126](https://github.com/angular/angular/pull/53126)) |
|
||||
| [5613051a8b](https://github.com/angular/angular/commit/5613051a8bd2626ae347292807b2bf21085c4c02) | fix | allow TS jsDocParsingMode host option to be programmatically set again ([#53292](https://github.com/angular/angular/pull/53292)) |
|
||||
| [df8a825910](https://github.com/angular/angular/commit/df8a825910951bebf34a4eede42f3ce5cd3e6fb7) | fix | project empty block root node ([#53620](https://github.com/angular/angular/pull/53620)) |
|
||||
| [478d622265](https://github.com/angular/angular/commit/478d6222650884478314985e3d5132587c4f670c) | fix | project empty block root node in template pipeline ([#53620](https://github.com/angular/angular/pull/53620)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [abdc7e4578](https://github.com/angular/angular/commit/abdc7e45786667e4283912024a641975f1917d97) | feat | support type-checking for generic signal inputs ([#53521](https://github.com/angular/angular/pull/53521)) |
|
||||
| [e620b3a724](https://github.com/angular/angular/commit/e620b3a724cb615af24b7779c0ab492d24efb8cc) | fix | add compiler option to disable control flow content projection diagnostic ([#53311](https://github.com/angular/angular/pull/53311)) |
|
||||
| [4c1d69e288](https://github.com/angular/angular/commit/4c1d69e2880f22745c820eee630d10071e4fa86b) | fix | add diagnostic for control flow that prevents content projection ([#53190](https://github.com/angular/angular/pull/53190)) |
|
||||
| [76ceebad04](https://github.com/angular/angular/commit/76ceebad047f62972654a8c934c77d8d02d9fa14) | fix | do not throw fatal error if extended type check fails ([#53896](https://github.com/angular/angular/pull/53896)) |
|
||||
| [1a6eaa0fea](https://github.com/angular/angular/commit/1a6eaa0fea1024b919e17ac9d2e8c07df7916de8) | fix | input transform in local compilation mode ([#53645](https://github.com/angular/angular/pull/53645)) |
|
||||
| [56a76d73e0](https://github.com/angular/angular/commit/56a76d73e037aeea1975808d5c51608fd23d4fa6) | fix | modify `getConstructorDependencies` helper to work with reflection host after the previous change ([#52215](https://github.com/angular/angular/pull/52215)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [863be4b698](https://github.com/angular/angular/commit/863be4b6981dc60ca0610b0e61d2ba1f5759e2a3) | feat | expose new `input` API for signal-based inputs ([#53872](https://github.com/angular/angular/pull/53872)) |
|
||||
| [94096c6ede](https://github.com/angular/angular/commit/94096c6ede67436a349ae07901f2bb418bf9f461) | feat | support TypeScript 5.3 ([#52572](https://github.com/angular/angular/pull/52572)) |
|
||||
| [69b384c0d1](https://github.com/angular/angular/commit/69b384c0d16f631741339d8757c32ef08260cfce) | fix | `SignalNode` reactive node incorrectly exposing unset field ([#53571](https://github.com/angular/angular/pull/53571)) |
|
||||
| [6f79507ea7](https://github.com/angular/angular/commit/6f79507ea7f272d8d09250e222ca831f407867d8) | fix | Change defer block fixture default behavior to playthrough ([#53956](https://github.com/angular/angular/pull/53956)) |
|
||||
| [32f908ab70](https://github.com/angular/angular/commit/32f908ab70f1b9ed3f92df1cae05ddde68932404) | fix | do not accidentally inherit input transforms when overridden ([#53571](https://github.com/angular/angular/pull/53571)) |
|
||||
| [bdd61c768a](https://github.com/angular/angular/commit/bdd61c768a28b56c68634b99c036986499829f45) | fix | replace assertion with more intentional error ([#52234](https://github.com/angular/angular/pull/52234)) |
|
||||
| [0daca457bb](https://github.com/angular/angular/commit/0daca457bb5bb6ffe14b7037264f8497eb5b3daf) | fix | TestBed should still use the microtask queue to schedule effects ([#53843](https://github.com/angular/angular/pull/53843)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5c1d441029](https://github.com/angular/angular/commit/5c1d4410298e20cb03d7a1ddf7931538b6a181b4) | feat | Add info property to `NavigationExtras` ([#53303](https://github.com/angular/angular/pull/53303)) |
|
||||
| [50d7916278](https://github.com/angular/angular/commit/50d79162785bb8d3e158a7a4a3733f4c75d3b127) | feat | Add router configuration to resolve navigation promise on error ([#48910](https://github.com/angular/angular/pull/48910)) |
|
||||
| [a5a9b408e2](https://github.com/angular/angular/commit/a5a9b408e2eb64dcf1d3ca16da4897649dd2fc34) | feat | Add transient info to RouterLink input ([#53784](https://github.com/angular/angular/pull/53784)) |
|
||||
| [726530a9af](https://github.com/angular/angular/commit/726530a9af9c8daf7295cc3548f24e70f380d70e) | feat | Allow `onSameUrlNavigation: 'ignore'` in `navigateByUrl` ([#52265](https://github.com/angular/angular/pull/52265)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.9"></a>
|
||||
# 17.0.9 (2024-01-10)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c22b513b3f](https://github.com/angular/angular/commit/c22b513b3f45c49baf4d6e571735aa4aa33b7845) | fix | remove unused parameters from the ngClass constructor ([#53831](https://github.com/angular/angular/pull/53831)) |
|
||||
| [bd9f89d1c8](https://github.com/angular/angular/commit/bd9f89d1c8e295f00ef3399c6bedca4e2ce0e89e) | fix | server-side rendering error when using in-memory scrolling ([#53683](https://github.com/angular/angular/pull/53683)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [92fd6cc42e](https://github.com/angular/angular/commit/92fd6cc42e0a217f4575404ea8e2af462f14ae18) | fix | generate less code for advance instructions ([#53845](https://github.com/angular/angular/pull/53845)) |
|
||||
| [6a41961fbd](https://github.com/angular/angular/commit/6a41961fbdf921f7a3ab82e92185eab751c0d153) | fix | ignore empty switch blocks ([#53776](https://github.com/angular/angular/pull/53776)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7309463697](https://github.com/angular/angular/commit/7309463697110d848781bfe81f04b6070c759928) | fix | interpolatedSignalNotInvoked diagnostic ([#53585](https://github.com/angular/angular/pull/53585)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [441db5123f](https://github.com/angular/angular/commit/441db5123f368c5fe3a505a79b97309e8400250f) | fix | `afterRender` hooks now only run on `ApplicationRef.tick` ([#52455](https://github.com/angular/angular/pull/52455)) |
|
||||
| [f9120d79cb](https://github.com/angular/angular/commit/f9120d79cb88a9f14c4baa6981f71a5afbd984e1) | fix | allow effect to be used inside an ErrorHandler ([#53713](https://github.com/angular/angular/pull/53713)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e92c86b77f](https://github.com/angular/angular/commit/e92c86b77ff7d400c034740ee0ad68acb626a22f) | fix | Fix empty switch case offset bug in cf migration ([#53839](https://github.com/angular/angular/pull/53839)) |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [91cb16fde9](https://github.com/angular/angular/commit/91cb16fde9ff68bcdc760428c47f4ebf3e476bd2) | fix | Do not delete global Event ([#53659](https://github.com/angular/angular/pull/53659)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.8"></a>
|
||||
# 17.0.8 (2023-12-21)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [de5c9ca8e9](https://github.com/angular/angular/commit/de5c9ca8e9a026ad752aab348bd137f647cc3cc9) | fix | correctly intercept index in loop tracking function ([#53604](https://github.com/angular/angular/pull/53604)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [d79489255a](https://github.com/angular/angular/commit/d79489255a48a55b136746856af0d8be51bbe665) | fix | avoid repeated work when parsing version ([#53598](https://github.com/angular/angular/pull/53598)) |
|
||||
| [513fee871e](https://github.com/angular/angular/commit/513fee871eb5d1c8a12bfe64878276b1d9c07705) | fix | tree shake version class ([#53598](https://github.com/angular/angular/pull/53598)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [eb7c29c7b6](https://github.com/angular/angular/commit/eb7c29c7b64a64853c7f7691912835240b62dfeb) | fix | cf migration - detect and error when result is invalid i18n nesting ([#53638](https://github.com/angular/angular/pull/53638)) |
|
||||
| [ed936ba0e9](https://github.com/angular/angular/commit/ed936ba0e9d5323095aa486dc58d8b088b4d64cc) | fix | cf migration - detect and error when result is invalid i18n nesting ([#53638](https://github.com/angular/angular/pull/53638)) ([#53639](https://github.com/angular/angular/pull/53639)) |
|
||||
| [5c2f2539e2](https://github.com/angular/angular/commit/5c2f2539e27bd18fd586c5977d853cd827f7b004) | fix | cf migration - ensure full check runs for all imports ([#53637](https://github.com/angular/angular/pull/53637)) |
|
||||
| [817dc1b27f](https://github.com/angular/angular/commit/817dc1b27fcf32db8f8d8417bdd46a1763460f11) | fix | cf migration - fix bug in attribute formatting ([#53636](https://github.com/angular/angular/pull/53636)) |
|
||||
| [7ac60bab9a](https://github.com/angular/angular/commit/7ac60bab9a4bb7a5af037a2de47339bd7837b157) | fix | cf migration - improve import declaration handling ([#53622](https://github.com/angular/angular/pull/53622)) |
|
||||
| [c3f85e51a9](https://github.com/angular/angular/commit/c3f85e51a970aa458b920835573fa4e392e6f909) | fix | cf migration - preserve indentation on attribute strings ([#53625](https://github.com/angular/angular/pull/53625)) |
|
||||
| [e73205ff5a](https://github.com/angular/angular/commit/e73205ff5ae4c382924266520f271b56972a0db6) | fix | cf migration - stop removing empty newlines from i18n blocks ([#53578](https://github.com/angular/angular/pull/53578)) |
|
||||
| [886aa7b2a9](https://github.com/angular/angular/commit/886aa7b2a99a301eb6e35dc1a59ef918f0bea348) | fix | Fix cf migration bug with parsing for loop conditions properly ([#53558](https://github.com/angular/angular/pull/53558)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0696ab6a5b](https://github.com/angular/angular/commit/0696ab6a5bea8acd6dafde488151150bf3332b79) | fix | Should not freeze original object used for route data ([#53635](https://github.com/angular/angular/pull/53635)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.7"></a>
|
||||
# 17.0.7 (2023-12-13)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [4fd5409090](https://github.com/angular/angular/commit/4fd5409090b5556ecde3b4e2e1f09955e71443f4) | fix | handle ambient types in input transform function ([#51474](https://github.com/angular/angular/pull/51474)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [a603338fe8](https://github.com/angular/angular/commit/a603338fe800a133c64b1573f2f805978c947725) | fix | generate less type checking code in for loops ([#53515](https://github.com/angular/angular/pull/53515)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [58ed76be93](https://github.com/angular/angular/commit/58ed76be937c23713e05609da3195ebafeb49794) | fix | Avoid refreshing a host view twice when using transplanted views ([#53021](https://github.com/angular/angular/pull/53021)) |
|
||||
| [c16b5e8290](https://github.com/angular/angular/commit/c16b5e82901b28b8ca0b72009c56aaeca68c58b0) | fix | Multiple subscribers to ApplicationRef.isStable should all see values ([#53541](https://github.com/angular/angular/pull/53541)) |
|
||||
| [17dbf8b8e2](https://github.com/angular/angular/commit/17dbf8b8e2a0979515d07341b4a869a78fab8583) | fix | remove signal equality check short-circuit ([#53446](https://github.com/angular/angular/pull/53446)) |
|
||||
| [5b4add27b6](https://github.com/angular/angular/commit/5b4add27b6d9d3ba55740818dc00659b6d462b87) | fix | update feature usage marker ([#53542](https://github.com/angular/angular/pull/53542)) |
|
||||
| [68d111c841](https://github.com/angular/angular/commit/68d111c8416dff43a83bb9de1241b4847d80eae7) | perf | avoid changes Observable creation on QueryList ([#53498](https://github.com/angular/angular/pull/53498)) |
|
||||
| [044cb553b4](https://github.com/angular/angular/commit/044cb553b4b8ed2f5f9a80131c5da84b3c964d9f) | perf | optimize memory allocation when reconcilling lists ([#52245](https://github.com/angular/angular/pull/52245)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [96ab999698](https://github.com/angular/angular/commit/96ab999698ca186b8e2b95177ea6f2c84e43628a) | fix | CF Migration - ensure bound ngIfElse cases ignore line breaks ([#53435](https://github.com/angular/angular/pull/53435)) |
|
||||
| [c9a1c6f1c7](https://github.com/angular/angular/commit/c9a1c6f1c7455431755dc31e00d72864931bf819) | fix | cf migration - undo changes when html fails to parse post migration ([#53530](https://github.com/angular/angular/pull/53530)) |
|
||||
| [b75aca1d74](https://github.com/angular/angular/commit/b75aca1d747e1bb55d4f3f6996bb2cb363e34f5b) | fix | CF migration only remove newlines of changed template content ([#53508](https://github.com/angular/angular/pull/53508)) |
|
||||
| [e88a12d5b3](https://github.com/angular/angular/commit/e88a12d5b3cf0e7c55c15c6820362d4d1e25ecaa) | fix | cf migration validate structure of ngswitch before migrating ([#53530](https://github.com/angular/angular/pull/53530)) |
|
||||
| [543df3dca5](https://github.com/angular/angular/commit/543df3dca52597aae298a01271337949d891dc65) | fix | ensure we do not overwrite prior template replacements in migration ([#53393](https://github.com/angular/angular/pull/53393)) |
|
||||
| [d232ea143f](https://github.com/angular/angular/commit/d232ea143f99fffc190c126d01920e965ffde3a3) | fix | fix cf migration import removal when errors occur ([#53502](https://github.com/angular/angular/pull/53502)) |
|
||||
### platform-browser
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [d5c631bf36](https://github.com/angular/angular/commit/d5c631bf3663b414b6d7734352804e594bfa2772) | fix | Get correct base path when using "." as base href when serving from the file:// protocol. ([#53547](https://github.com/angular/angular/pull/53547)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e750e4edcf](https://github.com/angular/angular/commit/e750e4edcfe812bcee82d4a2f53fd8acd8792f08) | fix | provide more actionable error message when route is not matched in production mode ([#53523](https://github.com/angular/angular/pull/53523)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.6"></a>
|
||||
# 17.0.6 (2023-12-06)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [a2e5f483f5](https://github.com/angular/angular/commit/a2e5f483f5a869c0cca205f092049e252a02b710) | fix | generate proper code for nullish coalescing in styling host bindings ([#53305](https://github.com/angular/angular/pull/53305)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [66ecf4c274](https://github.com/angular/angular/commit/66ecf4c2748b43b7e53af00499b153bbe70dd684) | fix | add compiler option to disable control flow content projection diagnostic ([#53387](https://github.com/angular/angular/pull/53387)) |
|
||||
| [74e6ce5d23](https://github.com/angular/angular/commit/74e6ce5d233c8763b6c437fab5d81d7b89ae6cd4) | fix | add diagnostic for control flow that prevents content projection ([#53387](https://github.com/angular/angular/pull/53387)) |
|
||||
| [6ec7a42b95](https://github.com/angular/angular/commit/6ec7a42b9578aa34a66bb7c81ac491bb18f98941) | fix | avoid conflicts with built-in global variables in for loop blocks ([#53319](https://github.com/angular/angular/pull/53319)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0a53f96094](https://github.com/angular/angular/commit/0a53f9609462fc59cf20c7fe1436d153c23e0412) | fix | cleanup signal consumers for all views ([#53351](https://github.com/angular/angular/pull/53351)) |
|
||||
| [4fc1581bbc](https://github.com/angular/angular/commit/4fc1581bbcd98e607eb2bbd9976c64c92a70d827) | fix | handle hydration of multiple nodes projected in a single slot ([#53270](https://github.com/angular/angular/pull/53270)) |
|
||||
| [14e66533ec](https://github.com/angular/angular/commit/14e66533ec49184723a66652253e9ae863a972e0) | fix | support hydration for cases when content is re-projected using ng-template ([#53304](https://github.com/angular/angular/pull/53304)) |
|
||||
| [8e366e8911](https://github.com/angular/angular/commit/8e366e8911434d5b91e83e215320caae72f6adf8) | fix | support swapping hydrated views in `@for` loops ([#53274](https://github.com/angular/angular/pull/53274)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [45064f1ae1](https://github.com/angular/angular/commit/45064f1ae1ceb9c5a1f1053077f77370e3e9fdb2) | fix | CF migration - ensure NgIfElse attributes are properly removed ([#53298](https://github.com/angular/angular/pull/53298)) |
|
||||
| [a6275cfa54](https://github.com/angular/angular/commit/a6275cfa54d680d6612a1c91e5d3f6b86b828fbc) | fix | CF Migration - Fix case of aliases on i18n ng-templates preventing removal ([#53299](https://github.com/angular/angular/pull/53299)) |
|
||||
| [58a96e0f50](https://github.com/angular/angular/commit/58a96e0f50f2d59376aa357188ad0792b39e4e70) | fix | CF Migration add support for ngIf with just a then ([#53297](https://github.com/angular/angular/pull/53297)) |
|
||||
| [26e40c7f89](https://github.com/angular/angular/commit/26e40c7f8916612034b03dae4f74e53b61d39d86) | fix | CF Migration fix missing alias for bound ngifs ([#53296](https://github.com/angular/angular/pull/53296)) |
|
||||
| [836aeba01d](https://github.com/angular/angular/commit/836aeba01db526676610802daff4e8ebca8cef1e) | fix | Change CF Migration ng-template placeholder generation and handling ([#53394](https://github.com/angular/angular/pull/53394)) |
|
||||
| [72d22ba7ee](https://github.com/angular/angular/commit/72d22ba7eea87e2873a765aa69b3d923b9d6cc6d) | fix | fix regexp for else and then in cf migration ([#53257](https://github.com/angular/angular/pull/53257)) |
|
||||
| [7a2facae8a](https://github.com/angular/angular/commit/7a2facae8af3240b21fc17857a770dad793b7b6d) | fix | handle aliases on bound ngIf migrations ([#53261](https://github.com/angular/angular/pull/53261)) |
|
||||
| [5104a89b30](https://github.com/angular/angular/commit/5104a89b3035fb07ce23e09974dc9998ef6932ca) | fix | handle nested ng-template replacement safely in CF migration ([#53368](https://github.com/angular/angular/pull/53368)) |
|
||||
| [2a4e3f5373](https://github.com/angular/angular/commit/2a4e3f5373dfbc2a1634d67e646c30d8bbe4fea8) | fix | handle templates outside of component in cf migration ([#53368](https://github.com/angular/angular/pull/53368)) |
|
||||
| [0db75ab5b1](https://github.com/angular/angular/commit/0db75ab5b1c8c79a0e7ca1d6094092f0cb3e3939) | fix | remove setting that removes comments in CF migration ([#53350](https://github.com/angular/angular/pull/53350)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [13ade13a15](https://github.com/angular/angular/commit/13ade13a15f0c5b5f782d2fda0f7a96b3c606198) | fix | Ensure canMatch guards run on wildcard routes ([#53239](https://github.com/angular/angular/pull/53239)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.5"></a>
|
||||
# 17.0.5 (2023-11-29)
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [6be88040d1](https://github.com/angular/angular/commit/6be88040d134b8c8d7efd2068296f68b8b7efbdd) | fix | avoid stale provider info when TestBed.overrideProvider is used ([#52918](https://github.com/angular/angular/pull/52918)) |
|
||||
| [dee50f1d78](https://github.com/angular/angular/commit/dee50f1d78d009f74edfa53f11fd6998b1af5441) | fix | inherit host directives ([#52992](https://github.com/angular/angular/pull/52992)) |
|
||||
| [07920d96d4](https://github.com/angular/angular/commit/07920d96d4e9831fd14ff01a3dd44af1017b28ca) | fix | Reattached views that are dirty from a signal update should refresh ([#53001](https://github.com/angular/angular/pull/53001)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [aab7fb8654](https://github.com/angular/angular/commit/aab7fb865486b94d6aabbe548894f8083f4e6cfe) | fix | Add ngForTemplate support to control flow migration ([#53076](https://github.com/angular/angular/pull/53076)) |
|
||||
| [dbd6f386ea](https://github.com/angular/angular/commit/dbd6f386ea81e7f29bc1cb34fb8efbba8c290ea8) | fix | allows colons in ngIf else cases to migrate ([#53076](https://github.com/angular/angular/pull/53076)) |
|
||||
| [5b9f896009](https://github.com/angular/angular/commit/5b9f896009d9c57c91d44e1cb7adf80c29fbfb4c) | fix | cf migration fix migrating empty switch default ([#53237](https://github.com/angular/angular/pull/53237)) |
|
||||
| [2b3d3b0fe1](https://github.com/angular/angular/commit/2b3d3b0fe10134739ec9982a776837a273ec8e1d) | fix | CF migration log warning when collection aliasing detected in `@for` ([#53238](https://github.com/angular/angular/pull/53238)) |
|
||||
| [dffeac8386](https://github.com/angular/angular/commit/dffeac8386bed73ef0b2337c33f122af9884bb82) | fix | cf migration removes unnecessary bound ngifelse attribute ([#53236](https://github.com/angular/angular/pull/53236)) |
|
||||
| [00cb3339ba](https://github.com/angular/angular/commit/00cb3339bab738100e4164339420f2492be91fd1) | fix | control flow migration formatting fixes ([#53076](https://github.com/angular/angular/pull/53076)) |
|
||||
| [c22af72f75](https://github.com/angular/angular/commit/c22af72f753b9ad3886561228a0cfd938a2a6de9) | fix | fix off by one issue with template removal in CF migration ([#53255](https://github.com/angular/angular/pull/53255)) |
|
||||
| [ba6d7fe018](https://github.com/angular/angular/commit/ba6d7fe0184dd1d9dbab088d767097948b78d9a4) | fix | fixes CF migration i18n ng-template offsets ([#53212](https://github.com/angular/angular/pull/53212)) |
|
||||
| [8f6affdd64](https://github.com/angular/angular/commit/8f6affdd64c6022c6a96fddac564c0ec05c5da9b) | fix | fixes control flow migration common module removal ([#53076](https://github.com/angular/angular/pull/53076)) |
|
||||
| [6ae408847c](https://github.com/angular/angular/commit/6ae408847ce5943ff34c7432a76533b69f08cb63) | fix | properly handle ngIfThen cases in CF migration ([#53256](https://github.com/angular/angular/pull/53256)) |
|
||||
| [0fcef65cea](https://github.com/angular/angular/commit/0fcef65cea8a7687afdf90e6e0e0c396f5792606) | fix | Update CF migration to skip templates with duplicate ng-template names ([#53204](https://github.com/angular/angular/pull/53204)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [91486aaf07](https://github.com/angular/angular/commit/91486aaf0754cb38bc9846f1d9532b1a0941f53a) | fix | Resolvers in different parts of the route tree should be able to execute together ([#52934](https://github.com/angular/angular/pull/52934)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.4"></a>
|
||||
# 17.0.4 (2023-11-20)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7f1c55755d](https://github.com/angular/angular/commit/7f1c55755d94444aa2c07fc62c276bb158e69f24) | fix | remove `load` on image once it fails to load ([#52990](https://github.com/angular/angular/pull/52990)) |
|
||||
| [fafcb0d23f](https://github.com/angular/angular/commit/fafcb0d23f1f687a2fe5c8349b916586ffadc375) | fix | scan images once page is loaded ([#52991](https://github.com/angular/angular/pull/52991)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [98376f2c09](https://github.com/angular/angular/commit/98376f2c09e9c28d1473123a2a1f4fb1c9d1cb1e) | fix | changed after checked error in for loops ([#52935](https://github.com/angular/angular/pull/52935)) |
|
||||
| [291deac663](https://github.com/angular/angular/commit/291deac6636a6f99a98dd0c9096ebe3b0547bb9e) | fix | generate i18n instructions for blocks ([#52958](https://github.com/angular/angular/pull/52958)) |
|
||||
| [49dca36880](https://github.com/angular/angular/commit/49dca36880a1c1c394533e8a94db9c5ef412ebd2) | fix | nested for loops incorrectly calculating computed variables ([#52931](https://github.com/angular/angular/pull/52931)) |
|
||||
| [f01b7183d2](https://github.com/angular/angular/commit/f01b7183d2064f41c0f5e30ee976cc91c15e06c5) | fix | produce placeholder for blocks in i18n bundles ([#52958](https://github.com/angular/angular/pull/52958)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f671f86ac2](https://github.com/angular/angular/commit/f671f86ac28d434b2fd492ef005749fe0275ece9) | fix | add diagnostic for control flow that prevents content projection ([#52726](https://github.com/angular/angular/pull/52726)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [db1a8ebdb4](https://github.com/angular/angular/commit/db1a8ebdb4da8673107ba4ba08c42d484b733c03) | fix | cleanup loading promise when no dependencies are defined ([#53031](https://github.com/angular/angular/pull/53031)) |
|
||||
| [31a1575334](https://github.com/angular/angular/commit/31a1575334ef78822d947ed858d8365ca5665f2f) | fix | handle local refs when `getDeferBlocks` is invoked in tests ([#52973](https://github.com/angular/angular/pull/52973)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [ac9cd6108f](https://github.com/angular/angular/commit/ac9cd6108f6fe25e9c7a11db9816c6e07d241515) | fix | control flow migration fails for async pipe with unboxing of observable ([#52756](https://github.com/angular/angular/pull/52756)) ([#52972](https://github.com/angular/angular/pull/52972)) |
|
||||
| [13bf5b7007](https://github.com/angular/angular/commit/13bf5b700739aadb2e5a210441fb815a8501ad65) | fix | Fixes control flow migration if then else case ([#53006](https://github.com/angular/angular/pull/53006)) |
|
||||
| [492ad4698a](https://github.com/angular/angular/commit/492ad4698aaef51a3d24ae90f617a2ba3fae901e) | fix | fixes migrations of nested switches in control flow ([#53010](https://github.com/angular/angular/pull/53010)) |
|
||||
| [0fad36eff2](https://github.com/angular/angular/commit/0fad36eff2b228baa3b8868810d4ac86eb6db459) | fix | tweaks to formatting in control flow migration ([#53058](https://github.com/angular/angular/pull/53058)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.3"></a>
|
||||
# 17.0.3 (2023-11-15)
|
||||
### animations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f5872c9921](https://github.com/angular/angular/commit/f5872c992181a2c231890b83f92ec03ec9606802) | fix | prevent the AsyncAnimationRenderer from calling the delegate when there is no element. ([#52570](https://github.com/angular/angular/pull/52570)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [6a1d4ed667](https://github.com/angular/angular/commit/6a1d4ed6670f5965a654e40997aa266a99925f50) | fix | handle non-container environment injector cases ([#52774](https://github.com/angular/angular/pull/52774)) |
|
||||
| [5de7575be8](https://github.com/angular/angular/commit/5de7575be83b9829e65ad245034ee7ab1d966044) | fix | reset cached scope for components that were overridden using TestBed ([#52916](https://github.com/angular/angular/pull/52916)) |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7c066a4af4](https://github.com/angular/angular/commit/7c066a4af4faae25ee722c19576c63c3833066ee) | fix | Use the response `content-type` to set the blob `type`. ([#52840](https://github.com/angular/angular/pull/52840)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [4e200bf13b](https://github.com/angular/angular/commit/4e200bf13b284fa89bbb0854cbb85dc8fe94d8bb) | fix | Add missing support for ngForOf ([#52903](https://github.com/angular/angular/pull/52903)) |
|
||||
| [d033540d0f](https://github.com/angular/angular/commit/d033540d0f874a7a05b79c00e3151ed076fa71c3) | fix | Add support for bound versions of NgIfElse and NgIfThenElse ([#52869](https://github.com/angular/angular/pull/52869)) |
|
||||
| [aa2d815648](https://github.com/angular/angular/commit/aa2d815648dbf3303cfe72bf976a4a87de406ee0) | fix | Add support for removing imports post migration ([#52763](https://github.com/angular/angular/pull/52763)) |
|
||||
| [3831942771](https://github.com/angular/angular/commit/38319427711f4dab4e4d64ff48aecc7727085031) | fix | Fixes issue with multiple if elses with same template ([#52863](https://github.com/angular/angular/pull/52863)) |
|
||||
| [e1f84a31dc](https://github.com/angular/angular/commit/e1f84a31dcac413251329c3b695a253234c6aae6) | fix | passed in paths will be respected in nx workspaces ([#52796](https://github.com/angular/angular/pull/52796)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.2"></a>
|
||||
# 17.0.2 (2023-11-09)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7a95cccf50](https://github.com/angular/angular/commit/7a95cccf50c01a3733c6015551f8864e246d9239) | fix | add interpolatedSignalNotInvoked to diagnostics ([#52687](https://github.com/angular/angular/pull/52687)) |
|
||||
| [a548c0333e](https://github.com/angular/angular/commit/a548c0333ecc993073ee7df054119a6fdde1d27b) | fix | incorrect inferred type of for loop implicit variables ([#52732](https://github.com/angular/angular/pull/52732)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [2cea80c6e2](https://github.com/angular/angular/commit/2cea80c6e21c113d12c38c4b3219c5f3f5944bd8) | fix | error code in image performance warning ([#52727](https://github.com/angular/angular/pull/52727)) |
|
||||
| [b16fc2610a](https://github.com/angular/angular/commit/b16fc2610a37b7407713e1e0018d92372f1349e9) | fix | limit rate of markers invocations ([#52742](https://github.com/angular/angular/pull/52742)) |
|
||||
| [44c48a4835](https://github.com/angular/angular/commit/44c48a48358c92c32301b578966a8e1ee9a867d8) | fix | properly update collection with repeated keys in `@for` ([#52697](https://github.com/angular/angular/pull/52697)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.1"></a>
|
||||
# 17.0.1 (2023-11-08)
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5c6f3f8ec0](https://github.com/angular/angular/commit/5c6f3f8ec0f1dd9b5505f3c94e654a675e75f147) | fix | Don't override the backend when using the InMemoryWebAPI ([#52425](https://github.com/angular/angular/pull/52425)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [70d30c28e0](https://github.com/angular/angular/commit/70d30c28e04f4ead51145e4e47df342492bfb336) | fix | Add support for ng-templates with i18n attributes ([#52597](https://github.com/angular/angular/pull/52597)) |
|
||||
| [4f125c5f9a](https://github.com/angular/angular/commit/4f125c5f9ae572a8216ec1fbb88f52e47b875e1e) | fix | Switches to multiple passes to fix several reported bugs ([#52592](https://github.com/angular/angular/pull/52592)) |
|
||||
|
||||
Web Frameworks: the internet frontier.<br/>
|
||||
These are the voyages of the framework Angular.<br/>
|
||||
Its continuing mission:<br/>
|
||||
To explore strange, new technologies.<br/>
|
||||
To seek out new users and new applications.<br/>
|
||||
To boldly go where no web framework has gone before.<br/>
|
||||
|
||||
In honor of v17.0.1
|
||||
|
||||
```
|
||||
______
|
||||
___.--------'------`---------.____
|
||||
_.---'----------------------------------`---.__
|
||||
.'___=]===========================================
|
||||
,-----------------------..__/.' >--.______ _______.---'
|
||||
]====================<==||(__) .' `------'
|
||||
`-----------------------`' ----.___--/
|
||||
/ /---' `/
|
||||
/_______(______________________/
|
||||
`-------------.--------------.'
|
||||
\________|_.-'
|
||||
```
|
||||
|
||||
Live long and prosper 🖖🏻
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.0"></a>
|
||||
# 17.0.0 (2023-11-08)
|
||||
|
||||
[Blog post "Angular v17 is now available"](http://goo.gle/angular-v17).
|
||||
|
||||
## Breaking Changes
|
||||
###
|
||||
- Node.js v16 support has been removed and the minimum support version has been bumped to 18.13.0.
|
||||
|
||||
Node.js v16 is planned to be End-of-Life on 2023-09-11. Angular will stop supporting Node.js v16 in Angular v17. For Node.js release schedule details, please see: https://github.com/nodejs/release#release-schedule
|
||||
### common
|
||||
- the NgSwitch directive now defaults to the === equality operator,
|
||||
migrating from the previously used == operator. NgSwitch expressions and / or
|
||||
individual condition values need adjusting to this stricter equality
|
||||
check. The added warning message should help pin-pointing NgSwitch
|
||||
usages where adjustments are needed.
|
||||
### core
|
||||
- Angular now requires `zone.js` version `~0.14.0`
|
||||
- Versions of TypeScript older than 5.2 are no longer supported.
|
||||
- The `mutate` method was removed from the `WritableSignal` interface and completely
|
||||
dropped from the public API surface. As an alternative, please use the `update` method and
|
||||
make immutable changes to the object.
|
||||
|
||||
Example before:
|
||||
|
||||
```typescript
|
||||
items.mutate(itemsArray => itemsArray.push(newItem));
|
||||
```
|
||||
|
||||
Example after:
|
||||
|
||||
```typescript
|
||||
items.update(itemsArray => [itemsArray, …newItem]);
|
||||
```
|
||||
- The `mutate` method was removed from the `WritableSignal` interface and completely
|
||||
dropped from the public API surface. As an alternative please use the update method and
|
||||
make immutable changes to the object.
|
||||
|
||||
Example before:
|
||||
|
||||
```typescript
|
||||
items.mutate(itemsArray => itemsArray.push(newItem));
|
||||
```
|
||||
|
||||
Example after:
|
||||
|
||||
```typescript
|
||||
items.update(itemsArray => [itemsArray, …newItem]);
|
||||
```
|
||||
- `OnPush` components that are created dynamically now
|
||||
only have their host bindings refreshed and `ngDoCheck run` during change
|
||||
detection if they are dirty.
|
||||
Previously, a bug in the change detection would result in the `OnPush`
|
||||
configuration of dynamically created components to be ignored when
|
||||
executing host bindings and the `ngDoCheck` function. This is
|
||||
rarely encountered but can happen if code has a handle on the
|
||||
`ComponentRef` instance and updates values read in the `OnPush`
|
||||
component template without then calling either `markForCheck` or
|
||||
`detectChanges` on that component's `ChangeDetectorRef`.
|
||||
### platform-browser
|
||||
- `REMOVE_STYLES_ON_COMPONENT_DESTROY` default value is now `true`. This causes CSS of components to be removed from the DOM when destroyed. You retain the previous behaviour by providing the `REMOVE_STYLES_ON_COMPONENT_DESTROY` injection token.
|
||||
|
||||
```ts
|
||||
import {REMOVE_STYLES_ON_COMPONENT_DESTROY} from '@angular/platform-browser';
|
||||
...
|
||||
providers: [{
|
||||
provide: REMOVE_STYLES_ON_COMPONENT_DESTROY,
|
||||
useValue: false,
|
||||
}]
|
||||
```
|
||||
- The `withNoDomReuse()` function was removed from the public API. If you need to disable hydration, you can exclude the `provideClientHydration()` call from provider list in your application (which would disable hydration features for the entire application) or use `ngSkipHydration` attribute to disable hydration for particular components. See this guide for additional information: https://angular.io/guide/hydration#how-to-skip-hydration-for-particular-components.
|
||||
### router
|
||||
- Absolute redirects no longer prevent further redirects.
|
||||
Route configurations may need to be adjusted to prevent infinite
|
||||
redirects where additional redirects were previously ignored after an
|
||||
absolute redirect occurred.
|
||||
- Routes with `loadComponent` would incorrectly cause
|
||||
child routes to inherit their data by default. The default
|
||||
`paramsInheritanceStrategy` is `emptyOnly`. If parent data should be
|
||||
inherited in child routes, this should be manually set to `always`.
|
||||
- `urlHandlingStrategy` has been removed from the Router public API.
|
||||
This should instead be configured through the provideRouter or RouterModule.forRoot APIs.
|
||||
- The following Router properties have been removed from
|
||||
the public API:
|
||||
|
||||
- canceledNavigationResolution
|
||||
- paramsInheritanceStrategy
|
||||
- titleStrategy
|
||||
- urlUpdateStrategy
|
||||
- malformedUriErrorHandler
|
||||
|
||||
These should instead be configured through the `provideRouter` or
|
||||
`RouterModule.forRoot` APIs.
|
||||
- The `setupTestingRouter` function has been removed. Use
|
||||
`RouterModule.forRoot` or `provideRouter` to setup the `Router` for
|
||||
tests instead.
|
||||
- `malformedUriErrorHandler` is no longer available in
|
||||
the `RouterModule.forRoot` options. URL parsing errors should instead be
|
||||
handled in the `UrlSerializer.parse` method.
|
||||
### zone.js
|
||||
- Deep and legacy `dist/` imports like `zone.js/bundles/zone-testing.js` and `zone.js/dist/zone` are no longer allowed. `zone-testing-bundle` and `zone-testing-node-bundle` are also no longer part of the package.
|
||||
|
||||
The proper way to import `zone.js` and `zone.js/testing` is:
|
||||
```js
|
||||
import 'zone.js';
|
||||
import 'zone.js/testing';
|
||||
```
|
||||
## Deprecations
|
||||
### animations
|
||||
- The `AnimationDriver.NOOP` symbol is deprecated, use `NoopAnimationDriver` instead.
|
||||
### core
|
||||
- `ChangeDetectorRef.checkNoChanges` is deprecated.
|
||||
|
||||
Test code should use `ComponentFixture` instead of `ChangeDetectorRef`.
|
||||
Application code should not call `ChangeDetectorRef.checkNoChanges` directly.
|
||||
- Swapping out the context object for `EmbeddedViewRef`
|
||||
is no longer supported. Support for this was introduced with v12.0.0, but
|
||||
this pattern is rarely used. There is no replacement, but you can use
|
||||
simple assignments in most cases, or `Object.assign` , or alternatively
|
||||
still replace the full object by using a `Proxy` (see `NgTemplateOutlet`
|
||||
as an example).
|
||||
|
||||
Also adds a warning if the deprecated
|
||||
- NgProbeToken
|
||||
|
||||
The `NgProbeToken` is not used internally since the transition from View Engine to Ivy. The token has no utility and can be removed from applications and libraries.
|
||||
###
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [59aa0634f4](https://github.com/angular/angular/commit/59aa0634f4d4694203f2a69c40017fe5a3962514) | build | remove support for Node.js v16 ([#51755](https://github.com/angular/angular/pull/51755)) |
|
||||
### animations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e753278faa](https://github.com/angular/angular/commit/e753278faae79a53e235e0d8e03f89555a712d80) | feat | Add the possibility of lazy loading animations code. ([#50738](https://github.com/angular/angular/pull/50738)) |
|
||||
| [698c058e1c](https://github.com/angular/angular/commit/698c058e1c975c573722407f4843a4a774ceb92a) | fix | remove code duplication between entry-points ([#51500](https://github.com/angular/angular/pull/51500)) |
|
||||
| [0598613950](https://github.com/angular/angular/commit/0598613950c76f4a13601c6942e30ab4ce1e3b67) | refactor | deprecation of `AnimationDriver.NOOP` ([#51843](https://github.com/angular/angular/pull/51843)) |
|
||||
### benchpress
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [2da3551a70](https://github.com/angular/angular/commit/2da3551a703ebef401d76a8e88e388437e851d85) | feat | report gc and render time spent in script ([#50771](https://github.com/angular/angular/pull/50771)) |
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [fe2fd7e1a8](https://github.com/angular/angular/commit/fe2fd7e1a898a4525c219065a6d0908988dfd7e2) | feat | make the warning for lazy-loaded lcp image an error ([#51748](https://github.com/angular/angular/pull/51748)) |
|
||||
| [dde3fdabbd](https://github.com/angular/angular/commit/dde3fdabbd24b48dd6afd120d23e92a3605eb04d) | feat | upgrade warning to logged error for lazy-loaded LCP images using NgOptimizedImage ([#52004](https://github.com/angular/angular/pull/52004)) |
|
||||
| [da056a1fe2](https://github.com/angular/angular/commit/da056a1fe2816299319fb3f87416316be2029479) | fix | add missing types field for @angular/common/locales of exports in package.json ([#52080](https://github.com/angular/angular/pull/52080)) |
|
||||
| [85843e8212](https://github.com/angular/angular/commit/85843e8212e99deb8b70f3d3f8dfe002b978cbb1) | fix | allow to specify only some properties of `DatePipeConfig` ([#51287](https://github.com/angular/angular/pull/51287)) |
|
||||
| [3bd85fb7b0](https://github.com/angular/angular/commit/3bd85fb7b0723ed807bca771e9fa95af60a3cfaf) | fix | apply fixed_srcset_width value only to fixed srcsets ([#52459](https://github.com/angular/angular/pull/52459)) |
|
||||
| [65b460448e](https://github.com/angular/angular/commit/65b460448ec5fdcee5aecca0cdc3cf498b0832cb) | fix | missing space in ngSwitch equality warning ([#52180](https://github.com/angular/angular/pull/52180)) |
|
||||
| [86c5e34601](https://github.com/angular/angular/commit/86c5e34601d7901a11688124aa902646524177eb) | fix | remove code duplication between entry-points ([#51500](https://github.com/angular/angular/pull/51500)) |
|
||||
| [28a5925f53](https://github.com/angular/angular/commit/28a5925f53790067d45f1f68d204a36088dbf5e3) | fix | use === operator to match NgSwitch cases ([#51504](https://github.com/angular/angular/pull/51504)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1934524a0c](https://github.com/angular/angular/commit/1934524a0c673fb65cd927c55c712f59446f9c93) | feat | add docs extraction for type aliases ([#52118](https://github.com/angular/angular/pull/52118)) |
|
||||
| [7f6d9a73ab](https://github.com/angular/angular/commit/7f6d9a73ab8b658d0d8148080dfefb2550bee6b4) | feat | expand class api doc extraction ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [a7fa25306f](https://github.com/angular/angular/commit/a7fa25306f8ce47d8aa330531382106efec55a55) | feat | extract api docs for interfaces ([#52006](https://github.com/angular/angular/pull/52006)) |
|
||||
| [7bfe20707f](https://github.com/angular/angular/commit/7bfe20707fedff7290e12356a1545644b436d41c) | feat | extract api for fn overloads and abtract classes ([#52040](https://github.com/angular/angular/pull/52040)) |
|
||||
| [c7daf7ea16](https://github.com/angular/angular/commit/c7daf7ea1692391f7cac8f794ed777887a2764af) | feat | extract directive docs info ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [e0b1bb33d7](https://github.com/angular/angular/commit/e0b1bb33d77babe881f77f52cb1b71e345f5696b) | feat | extract doc info for JsDoc ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [b9c70158ab](https://github.com/angular/angular/commit/b9c70158abecd81a5af512c8b4da685851cf159f) | feat | extract docs for accessors, rest params, and types ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [a24ae994a0](https://github.com/angular/angular/commit/a24ae994a0470fdac09a69937fd0580cff6c6d68) | feat | extract docs for top level functions and consts ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [2e41488296](https://github.com/angular/angular/commit/2e41488296879685b19dfba8d78037690347bda3) | feat | extract docs info for enums, pipes, and NgModules ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [34495b3533](https://github.com/angular/angular/commit/34495b35337892ab209d9955ff7fe2897a0c5d41) | feat | extract docs via exports ([#51828](https://github.com/angular/angular/pull/51828)) |
|
||||
| [7e82df45c5](https://github.com/angular/angular/commit/7e82df45c5bb72ec3dafaa07dc1eaa5d463b006c) | feat | initial skeleton for API doc extraction ([#51733](https://github.com/angular/angular/pull/51733)) |
|
||||
| [6795cccbbb](https://github.com/angular/angular/commit/6795cccbbbfc17bbf88fb8197aa172cca67fa2d2) | fix | account for type-only imports in defer blocks ([#52343](https://github.com/angular/angular/pull/52343)) |
|
||||
| [23bfa10ac8](https://github.com/angular/angular/commit/23bfa10ac809f6b27d32647210c52329f0e4262e) | fix | add diagnostic for inaccessible deferred trigger ([#51922](https://github.com/angular/angular/pull/51922)) |
|
||||
| [31295a3cf9](https://github.com/angular/angular/commit/31295a3cf907a61e7115d9039a83a232b263a676) | fix | allocating unnecessary slots in conditional instruction ([#51913](https://github.com/angular/angular/pull/51913)) |
|
||||
| [2aaddd3f64](https://github.com/angular/angular/commit/2aaddd3f64bb8891bb4bdcadf05d427a89338112) | fix | allow comments between switch cases ([#52449](https://github.com/angular/angular/pull/52449)) |
|
||||
| [ddd9df68bb](https://github.com/angular/angular/commit/ddd9df68bb2e907dd820f239aaf819425cb95df8) | fix | allow decimals in defer block time values ([#52433](https://github.com/angular/angular/pull/52433)) |
|
||||
| [7dbd47fb30](https://github.com/angular/angular/commit/7dbd47fb3015117c420f984181bfcb48e533525a) | fix | allow newlines in track and let expressions ([#52137](https://github.com/angular/angular/pull/52137)) |
|
||||
| [0eae992c4e](https://github.com/angular/angular/commit/0eae992c4e03b7c9039476e03b72e92d662293df) | fix | allow nullable values in for loop block ([#51997](https://github.com/angular/angular/pull/51997)) |
|
||||
| [073ebfe09e](https://github.com/angular/angular/commit/073ebfe09eccd5d01d27fcc46fc5d4465c1851ff) | fix | apply style on :host attributes in prod builds. ([#49118](https://github.com/angular/angular/pull/49118)) |
|
||||
| [81a287a79a](https://github.com/angular/angular/commit/81a287a79afc16d43c0fd24d7aea54be4414940a) | fix | avoid error in template parser for tag names that can occur in object prototype ([#52225](https://github.com/angular/angular/pull/52225)) |
|
||||
| [6c58252521](https://github.com/angular/angular/commit/6c582525217197dd777d5bb9e62d6aaa2c70a996) | fix | compilation error when for loop block expression contains new line ([#52447](https://github.com/angular/angular/pull/52447)) |
|
||||
| [9d19c8e317](https://github.com/angular/angular/commit/9d19c8e31752d211f575246282358b83afe90969) | fix | don't allocate variable to for loop expression ([#52158](https://github.com/angular/angular/pull/52158)) |
|
||||
| [9acd2ac98b](https://github.com/angular/angular/commit/9acd2ac98bc3b6ffc5a8d6c19f7290d05fe1f896) | fix | enable block syntax in the linker ([#51979](https://github.com/angular/angular/pull/51979)) |
|
||||
| [1d871c03a5](https://github.com/angular/angular/commit/1d871c03a523e10bb838cb0f9550595cfbd9d14d) | fix | forward referenced dependencies not identified as deferrable ([#52017](https://github.com/angular/angular/pull/52017)) |
|
||||
| [16ff08ec70](https://github.com/angular/angular/commit/16ff08ec70bfa192041ba050e550676e8d505a05) | fix | narrow the type of expressions in event listeners inside if blocks ([#52069](https://github.com/angular/angular/pull/52069)) |
|
||||
| [ac0d5dcfd6](https://github.com/angular/angular/commit/ac0d5dcfd6015ec4283ed1a5cf241f130f4c5cf5) | fix | narrow the type of expressions in event listeners inside switch blocks ([#52069](https://github.com/angular/angular/pull/52069)) |
|
||||
| [02edb43067](https://github.com/angular/angular/commit/02edb4306736e6f12e87a4164c17eca6cbdfe151) | fix | narrow the type of the aliased if block expression ([#51952](https://github.com/angular/angular/pull/51952)) |
|
||||
| [83067b3ef2](https://github.com/angular/angular/commit/83067b3ef257dbc7b1c20d50645615d19023ba51) | fix | ng-template directive invoke twice at the root of control flow ([#52515](https://github.com/angular/angular/pull/52515)) |
|
||||
| [17078a3fe1](https://github.com/angular/angular/commit/17078a3fe1e9b90e48952b6c12b6e6b774b97810) | fix | pipes used inside defer triggers not being picked up ([#52071](https://github.com/angular/angular/pull/52071)) |
|
||||
| [861ce3a7c5](https://github.com/angular/angular/commit/861ce3a7c574340a6164ad0de13f49bda3e172da) | fix | pipes using DI not working in blocks ([#52112](https://github.com/angular/angular/pull/52112)) |
|
||||
| [1f5039bbd6](https://github.com/angular/angular/commit/1f5039bbd6de8450e5511af00044ddd2f4314016) | fix | project control flow root elements into correct slot ([#52414](https://github.com/angular/angular/pull/52414)) |
|
||||
| [81c315ec6e](https://github.com/angular/angular/commit/81c315ec6ea37c55d951d3b38b6c551226173be5) | fix | template type checking not reporting diagnostics for incompatible type comparisons ([#52322](https://github.com/angular/angular/pull/52322)) |
|
||||
| [1beef49d80](https://github.com/angular/angular/commit/1beef49d80809fbb0e7c8e95f17096c39ac8940a) | fix | update the minVersion if component uses block syntax ([#51979](https://github.com/angular/angular/pull/51979)) |
|
||||
| [386e1e9500](https://github.com/angular/angular/commit/386e1e950033ad98661e5077a4f119df0e7b3008) | fix | work around TypeScript bug when narrowing switch statements ([#52110](https://github.com/angular/angular/pull/52110)) |
|
||||
| [e5bca43224](https://github.com/angular/angular/commit/e5bca432248add0a19102f6afeae145f1a33ee8a) | perf | further reduce bundle size using arrow functions ([#52010](https://github.com/angular/angular/pull/52010)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5b66330329](https://github.com/angular/angular/commit/5b66330329fd066a7c347f040a330b4c7f2a0a2b) | fix | allow non-array imports for standalone component in local compilation mode ([#51819](https://github.com/angular/angular/pull/51819)) |
|
||||
| [377a7abfda](https://github.com/angular/angular/commit/377a7abfda60a6ddd55a41531e3653bcad78b0a2) | fix | bypass static resolving of the component's changeDetection field in local compilation mode ([#51848](https://github.com/angular/angular/pull/51848)) |
|
||||
| [19c3dc18d3](https://github.com/angular/angular/commit/19c3dc18d3c0cfd83efec2c8f81b40860d570346) | fix | fix NgModule injector def in local compilation mode when imports/exports are non-array expressions ([#51819](https://github.com/angular/angular/pull/51819)) |
|
||||
| [11bb19cafc](https://github.com/angular/angular/commit/11bb19cafcf447b7ce6ade146d431a43c3e2c682) | fix | handle nested qualified names in ctor injection in local compilation mode ([#51947](https://github.com/angular/angular/pull/51947)) |
|
||||
| [f91f222b55](https://github.com/angular/angular/commit/f91f222b55f249089d267c72a9c0ab5b09d7c932) | fix | resolve component encapsulation enum in local compilation mode ([#51848](https://github.com/angular/angular/pull/51848)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [59b6ec6be8](https://github.com/angular/angular/commit/59b6ec6be8cc464a6e74449ac7e0f2554ac7392b) | docs | Deprecate `ChangeDetectorRef.checkNoChanges` ([#52431](https://github.com/angular/angular/pull/52431)) |
|
||||
| [4f04d1cdab](https://github.com/angular/angular/commit/4f04d1cdab2fc5217566c0c01b7df10c74a93afa) | feat | add new list reconcilation algorithm ([#51980](https://github.com/angular/angular/pull/51980)) |
|
||||
| [c7127b98b5](https://github.com/angular/angular/commit/c7127b98b555449f99e24a81c828ab7b1c6c4a4e) | feat | add schematic to escape block syntax characters ([#51905](https://github.com/angular/angular/pull/51905)) |
|
||||
| [50275e58b8](https://github.com/angular/angular/commit/50275e58b80acfc52239908a6c61523e99f6731c) | feat | Add schematic to migrate control flow syntax ([#52035](https://github.com/angular/angular/pull/52035)) |
|
||||
| [81b67aa987](https://github.com/angular/angular/commit/81b67aa98767078aebae22150d3441372772c28f) | feat | add support for zone.js 0.14.0 ([#51774](https://github.com/angular/angular/pull/51774)) |
|
||||
| [048f400efc](https://github.com/angular/angular/commit/048f400efc75011e888ea25d214a0211f91b96d4) | feat | add warnings for oversized images and lazy-lcp ([#51846](https://github.com/angular/angular/pull/51846)) |
|
||||
| [93675dc797](https://github.com/angular/angular/commit/93675dc797cb9f897c19fe298455dec52b900113) | feat | conditional built-in control flow ([#51346](https://github.com/angular/angular/pull/51346)) |
|
||||
| [4427e1ebc2](https://github.com/angular/angular/commit/4427e1ebc29f5541cfe6a404f212de4359441812) | feat | create function to assert not running inside reactive context ([#52049](https://github.com/angular/angular/pull/52049)) |
|
||||
| [e23aaa7d75](https://github.com/angular/angular/commit/e23aaa7d75efdd52be4dd7ca9267bc60d36059c2) | feat | drop support for older TypeScript versions ([#51792](https://github.com/angular/angular/pull/51792)) |
|
||||
| [43e6fb0606](https://github.com/angular/angular/commit/43e6fb0606e15584dcb4478ad4eaa8e825dda83e) | feat | enable block syntax ([#51994](https://github.com/angular/angular/pull/51994)) |
|
||||
| [3cbb2a8ecf](https://github.com/angular/angular/commit/3cbb2a8ecf202c814c37ab241ce9f57fb574692e) | feat | implement deferred block interaction triggers ([#51830](https://github.com/angular/angular/pull/51830)) |
|
||||
| [8be2c48b7c](https://github.com/angular/angular/commit/8be2c48b7cda5e99f3d1efa9f26eb2615fea6a8b) | feat | implement new block syntax ([#51891](https://github.com/angular/angular/pull/51891)) |
|
||||
| [a54713c831](https://github.com/angular/angular/commit/a54713c8316787eea160cfdb7f2778a087fe59ed) | feat | implement ɵgetInjectorMetadata debug API ([#51900](https://github.com/angular/angular/pull/51900)) |
|
||||
| [5b88d136af](https://github.com/angular/angular/commit/5b88d136affdaa35e7015c00281b86cae040321b) | feat | mark core signal APIs as stable ([#51821](https://github.com/angular/angular/pull/51821)) |
|
||||
| [8eef694def](https://github.com/angular/angular/commit/8eef694def3dc660779168925a380179c7e30993) | feat | Provide a diagnostic for missing Signal invocation in template interpolation. ([#49660](https://github.com/angular/angular/pull/49660)) |
|
||||
| [40113f653c](https://github.com/angular/angular/commit/40113f653c2468315e1dea64f17e545840cc5e22) | feat | Remove deprecated `CompilerOptions.useJit` and`CompilerOptions.missingTranslation`. ([#49672](https://github.com/angular/angular/pull/49672)) |
|
||||
| [68ba798ae3](https://github.com/angular/angular/commit/68ba798ae3551b789a86d46b0a3bb61d42ef3f87) | feat | revamp the runtime error message for orphan components to include full component info ([#51919](https://github.com/angular/angular/pull/51919)) |
|
||||
| [1a4aee7e49](https://github.com/angular/angular/commit/1a4aee7e49074e3bc3f3099bff88eaee9b2ab255) | feat | show runtime error for orphan component rendering ([#52061](https://github.com/angular/angular/pull/52061)) |
|
||||
| [687b96186c](https://github.com/angular/angular/commit/687b96186c7da731927e55e714061ea2de718505) | feat | support deferred hover triggers ([#51874](https://github.com/angular/angular/pull/51874)) |
|
||||
| [e2e3d69a27](https://github.com/angular/angular/commit/e2e3d69a277990ab79aac7aae43cbdd398e13ed9) | feat | support deferred triggers with implicit triggers ([#51922](https://github.com/angular/angular/pull/51922)) |
|
||||
| [16f5fc40a4](https://github.com/angular/angular/commit/16f5fc40a49cee0d29711df1783f297ff30b5c6e) | feat | support deferred viewport triggers ([#51874](https://github.com/angular/angular/pull/51874)) |
|
||||
| [59387ee476](https://github.com/angular/angular/commit/59387ee476dff1a893a01fe5cbee3c95b93c0cdb) | feat | support styles and styleUrl as strings ([#51715](https://github.com/angular/angular/pull/51715)) |
|
||||
| [9cc52b9b85](https://github.com/angular/angular/commit/9cc52b9b85ffa5cb65c6886e81b5bff10dde8d52) | feat | support TypeScript 5.2 ([#51334](https://github.com/angular/angular/pull/51334)) |
|
||||
| [7d42dc3c02](https://github.com/angular/angular/commit/7d42dc3c023391e12ea607beb227fd4426e1694d) | feat | the new list reconciliation algorithm for built-in for ([#51980](https://github.com/angular/angular/pull/51980)) |
|
||||
| [935c1816fd](https://github.com/angular/angular/commit/935c1816fd04caab24be66f8ef67851c88d3d4da) | fix | add `rejectErrors` option to `toSignal` ([#52474](https://github.com/angular/angular/pull/52474)) |
|
||||
| [5411864c2e](https://github.com/angular/angular/commit/5411864c2e74b52e7df8022719f0fd792b50a849) | fix | adjust toSignal types to handle more common cases ([#51991](https://github.com/angular/angular/pull/51991)) |
|
||||
| [dcf18dc74c](https://github.com/angular/angular/commit/dcf18dc74c260253bbf394626beb712a831824f3) | fix | allow toSignal calls in reactive context ([#51831](https://github.com/angular/angular/pull/51831)) |
|
||||
| [dbffdc09c2](https://github.com/angular/angular/commit/dbffdc09c25c93868aa13ae368c9fd21a4c359fb) | fix | avoid duplicated code between entry-points (primary, testing, rxjs-interop) ([#51500](https://github.com/angular/angular/pull/51500)) |
|
||||
| [4f69d620d9](https://github.com/angular/angular/commit/4f69d620d94663592780b2875acbc2b1918775f9) | fix | deferred blocks not removing content immediately when animations are enabled ([#51971](https://github.com/angular/angular/pull/51971)) |
|
||||
| [df58c0b714](https://github.com/angular/angular/commit/df58c0b714e37152ddf81855ee31f93f9fa71e30) | fix | disallow `afterRender` in reactive contexts ([#52138](https://github.com/angular/angular/pull/52138)) |
|
||||
| [5d61221ed7](https://github.com/angular/angular/commit/5d61221ed7b4a5d1ef005183045d45238b19a446) | fix | disallow using `effect` inside reactive contexts ([#52138](https://github.com/angular/angular/pull/52138)) |
|
||||
| [99e7629159](https://github.com/angular/angular/commit/99e7629159afbb8550957a265c5bd75f7e13f052) | fix | do not remove used ng-template nodes in control flow migration ([#52186](https://github.com/angular/angular/pull/52186)) |
|
||||
| [c7ff9dff2c](https://github.com/angular/angular/commit/c7ff9dff2c14aba70e92b9e216a2d4d97d6ef71e) | fix | drop mutate function from the signals public API ([#51821](https://github.com/angular/angular/pull/51821)) |
|
||||
| [00128e3853](https://github.com/angular/angular/commit/00128e38538f12fe9bc72ede9b55149e0be5ead0) | fix | drop mutate function from the signals public API ([#51821](https://github.com/angular/angular/pull/51821)) ([#51986](https://github.com/angular/angular/pull/51986)) |
|
||||
| [ddef3ac9a4](https://github.com/angular/angular/commit/ddef3ac9a42677b900c998b2af7cd23a8213aa3a) | fix | effects wait for ngOnInit for their first run ([#52473](https://github.com/angular/angular/pull/52473)) |
|
||||
| [5ead7d412d](https://github.com/angular/angular/commit/5ead7d412d847c85176a321e58d12dcdfc0dab67) | fix | ensure a consumer drops all its stale producers ([#51722](https://github.com/angular/angular/pull/51722)) |
|
||||
| [1dd8558f82](https://github.com/angular/angular/commit/1dd8558f82aeb5f5819629d5e25b616343d27f1f) | fix | Ensure backwards-referenced transplanted views are refreshed ([#51854](https://github.com/angular/angular/pull/51854)) |
|
||||
| [50ad074505](https://github.com/angular/angular/commit/50ad074505c15d09fe5d85fb443d9a2775125f87) | fix | framework debug APIs getDependenciesForTokenInInjector and getInjectorMetadata ([#51719](https://github.com/angular/angular/pull/51719)) |
|
||||
| [80e7a0f8fa](https://github.com/angular/angular/commit/80e7a0f8fa13ad72a32b07cd1722efab3722fa49) | fix | guard usages of `performance.mark` ([#52505](https://github.com/angular/angular/pull/52505)) |
|
||||
| [b9ea2d6900](https://github.com/angular/angular/commit/b9ea2d690015dd5b919df239a5ded66f2969b97b) | fix | handle aliased index with no space in control flow migration ([#52444](https://github.com/angular/angular/pull/52444)) |
|
||||
| [ffe9b1fcc2](https://github.com/angular/angular/commit/ffe9b1fcc2ecb8dc0d36c7f9228ac1a052554eef) | fix | handle for alias with as in control flow migration ([#52183](https://github.com/angular/angular/pull/52183)) |
|
||||
| [e5720edb46](https://github.com/angular/angular/commit/e5720edb460a1bb51475c78d3bd442da52991a46) | fix | handle if alias in control flow migration ([#52181](https://github.com/angular/angular/pull/52181)) |
|
||||
| [4461cefa4f](https://github.com/angular/angular/commit/4461cefa4f8db21009ab10a2a53de664163a86d2) | fix | handle trackBy and aliased index in control flow migration ([#52423](https://github.com/angular/angular/pull/52423)) |
|
||||
| [7368b8aaeb](https://github.com/angular/angular/commit/7368b8aaeba2ef0972a8bb261208c7281e050bb9) | fix | host directive validation not picking up duplicate directives on component node ([#52073](https://github.com/angular/angular/pull/52073)) |
|
||||
| [696f003553](https://github.com/angular/angular/commit/696f003553a0ca6886329728511dd46761de909b) | fix | mutation bug in `getDependenciesFromInjectable` ([#52450](https://github.com/angular/angular/pull/52450)) |
|
||||
| [d487014785](https://github.com/angular/angular/commit/d48701478518d97a1fd5b4744963530494f93958) | fix | Remove no longer needed build rule related to removed migration ([#52143](https://github.com/angular/angular/pull/52143)) |
|
||||
| [4da08dc2ef](https://github.com/angular/angular/commit/4da08dc2ef439d3eced7199afb9a104cfd7b54cc) | fix | remove unnecessary migration ([#52141](https://github.com/angular/angular/pull/52141)) |
|
||||
| [384d7aacd0](https://github.com/angular/angular/commit/384d7aacd04dfbb951d9d4ab493759c12cf35645) | fix | replace assertion with more intentional error ([#52427](https://github.com/angular/angular/pull/52427)) |
|
||||
| [40bb45f329](https://github.com/angular/angular/commit/40bb45f3297359866cab39044dba06b3e809b096) | fix | Respect OnPush change detection strategy for dynamically created components ([#51356](https://github.com/angular/angular/pull/51356)) |
|
||||
| [3a19d6b743](https://github.com/angular/angular/commit/3a19d6b7437e1812ae70b3784fd6a8a185b330b1) | fix | run afterRender callbacks outside of the Angular zone ([#51385](https://github.com/angular/angular/pull/51385)) |
|
||||
| [a2ba5482c3](https://github.com/angular/angular/commit/a2ba5482c3032df808cb684444f76e2825a4fd36) | fix | use TNode instead of LView for mapping injector providers ([#52436](https://github.com/angular/angular/pull/52436)) |
|
||||
| [d5dad3eb4c](https://github.com/angular/angular/commit/d5dad3eb4cd837032da72899f0796c6d431cb2c9) | fix | viewport trigger deregistering callbacks multiple times ([#52115](https://github.com/angular/angular/pull/52115)) |
|
||||
| [8e4a7ab52b](https://github.com/angular/angular/commit/8e4a7ab52bc85172efd12e42304e1b8da446ff75) | perf | avoid repeated access to LContainer and trackBy calculation ([#52227](https://github.com/angular/angular/pull/52227)) |
|
||||
| [1dc14d9853](https://github.com/angular/angular/commit/1dc14d98539b9063b14c6463a534a4129b0a4643) | perf | avoid unnecessary callbacks in after render hooks ([#52292](https://github.com/angular/angular/pull/52292)) |
|
||||
| [e90694259e](https://github.com/angular/angular/commit/e90694259e31f264f05d4aa9ebe275638577ce1a) | perf | build-in for should update indexes only when views were added / removed ([#52051](https://github.com/angular/angular/pull/52051)) |
|
||||
| [1032c1e1a5](https://github.com/angular/angular/commit/1032c1e1a5f5de28f38ede1786cf973f8e8b7a53) | perf | cache LiveCollectionLContainerImpl ([#52227](https://github.com/angular/angular/pull/52227)) |
|
||||
| [685d01e106](https://github.com/angular/angular/commit/685d01e1065dad6dc52eaac9eb9527100994f5ce) | perf | chain template instructions ([#51546](https://github.com/angular/angular/pull/51546)) |
|
||||
| [88a0af64fd](https://github.com/angular/angular/commit/88a0af64fde58cbf71e8e2a22c39fabb8f0ee8fb) | perf | generate arrow functions for pure function calls ([#51668](https://github.com/angular/angular/pull/51668)) |
|
||||
| [37d627dbd4](https://github.com/angular/angular/commit/37d627dbd4083662b103de2e28102e6ff31a9192) | perf | minimze trackBy calculations ([#52227](https://github.com/angular/angular/pull/52227)) |
|
||||
| [3861a73135](https://github.com/angular/angular/commit/3861a73135ca9111c0ec10d52ee7db0a0e95f262) | perf | Update LView consumer to only mark component for check ([#52302](https://github.com/angular/angular/pull/52302)) |
|
||||
| [9b9e11fcaf](https://github.com/angular/angular/commit/9b9e11fcaf5d8d639ff1d7b8feddb01751b47e14) | refactor | deprecate allowing full context object to be replaced in `EmbeddedViewRef` ([#51887](https://github.com/angular/angular/pull/51887)) |
|
||||
| [ba9fc2419e](https://github.com/angular/angular/commit/ba9fc2419eee0d72c573463016a872a4b69f71c1) | refactor | deprecate the `NgProbeToken` ([#51396](https://github.com/angular/angular/pull/51396)) |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7dde42a5df](https://github.com/angular/angular/commit/7dde42a5dfdab30e9420708722e0bef9f1467d1f) | feat | allow customization of the HttpTransferCache. ([#52029](https://github.com/angular/angular/pull/52029)) |
|
||||
| [8156b3d4ec](https://github.com/angular/angular/commit/8156b3d4ec44a3b0489cc21763790a3be2969f7e) | fix | Don't override the backend when using the InMemoryWebAPI ([#52425](https://github.com/angular/angular/pull/52425)) |
|
||||
| [bd9e91ecf7](https://github.com/angular/angular/commit/bd9e91ecf7af877e4ecf08a16eda7b4e59707541) | perf | reduce data transfer when using HTTP caching ([#52347](https://github.com/angular/angular/pull/52347)) |
|
||||
### language-service
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [449830f24e](https://github.com/angular/angular/commit/449830f24e78ebd977ca3210ab3541912d959245) | feat | Complete inside @switch ([#52153](https://github.com/angular/angular/pull/52153)) |
|
||||
| [e2416a284f](https://github.com/angular/angular/commit/e2416a284ff086752c809689ef74588f02e5f0e4) | feat | Enable go to definition of styleUrl ([#51746](https://github.com/angular/angular/pull/51746)) |
|
||||
| [023a181ba5](https://github.com/angular/angular/commit/023a181ba5f489deb0a47bbc9b290621ad38304a) | feat | Implement outlining spans for control flow blocks ([#52062](https://github.com/angular/angular/pull/52062)) |
|
||||
| [7c052bb6ef](https://github.com/angular/angular/commit/7c052bb6efde580afc61d6c50e787353c103e3e1) | feat | Support autocompletion for blocks ([#52121](https://github.com/angular/angular/pull/52121)) |
|
||||
| [9d565cd6d6](https://github.com/angular/angular/commit/9d565cd6d682e5c86ee8d43e1ee1c0f8866eb274) | fix | Autocomplete block keywords in more cases ([#52198](https://github.com/angular/angular/pull/52198)) |
|
||||
### localize
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5a20a44c64](https://github.com/angular/angular/commit/5a20a44c64066e47894ca3cbe26327766ca89a42) | fix | ng-add schematics for application builder ([#51777](https://github.com/angular/angular/pull/51777)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f0da7c2e44](https://github.com/angular/angular/commit/f0da7c2e44a29c5a71cf4880388989d129f4c6e8) | feat | schematic to remove deprecated CompilerOptions properties ([#49672](https://github.com/angular/angular/pull/49672)) |
|
||||
| [965ce5a8c5](https://github.com/angular/angular/commit/965ce5a8c514237aa8e4c03a5e4b5527a1a19d96) | feat | Schematics for `TransferState`, `StateKey` and `makeStateKey` migration. ([#49594](https://github.com/angular/angular/pull/49594)) |
|
||||
| [09e905ad67](https://github.com/angular/angular/commit/09e905ad672294d36f36eb2728b3483ab2e729fc) | fix | account for separator characters inside strings ([#52525](https://github.com/angular/angular/pull/52525)) |
|
||||
| [4c878f90d2](https://github.com/angular/angular/commit/4c878f90d2cba689703d8def9dade3b7f8f6e8f1) | fix | Add support for nested structures inside a switch statement ([#52358](https://github.com/angular/angular/pull/52358)) |
|
||||
| [d7397fb29b](https://github.com/angular/angular/commit/d7397fb29b400f193a2cda7af70058ac39247e6f) | fix | Ensure control flow migration ignores new block syntax ([#52402](https://github.com/angular/angular/pull/52402)) |
|
||||
| [6a01d62b9d](https://github.com/angular/angular/commit/6a01d62b9df33732ea3dbef8de1385c8c0cb8a88) | fix | fix broken migration when no control flow is present ([#52399](https://github.com/angular/angular/pull/52399)) |
|
||||
| [9c2be715a3](https://github.com/angular/angular/commit/9c2be715a3f6ea6b1d0184f5d79f5a3dfef4b576) | fix | Fixes a bug in the ngFor pre-v5 alias translation ([#52531](https://github.com/angular/angular/pull/52531)) |
|
||||
| [54fed68bbf](https://github.com/angular/angular/commit/54fed68bbfed34a631df64ac6a32e99d2fade50a) | fix | Fixes the root level template offset in control flow migration ([#52355](https://github.com/angular/angular/pull/52355)) |
|
||||
| [57404d4723](https://github.com/angular/angular/commit/57404d4723d3634f2b5dfdc9af1af50d8f61da70) | fix | handle comma-separated syntax in ngFor ([#52525](https://github.com/angular/angular/pull/52525)) |
|
||||
| [54bc384661](https://github.com/angular/angular/commit/54bc384661ae7b884dbb822592e75df281aa02dd) | fix | handle nested classes in block entities migration ([#52309](https://github.com/angular/angular/pull/52309)) |
|
||||
| [c9b1ddff4d](https://github.com/angular/angular/commit/c9b1ddff4d16cca149ca2b1faae729bf454b61e5) | fix | handle nested classes in control flow migration ([#52309](https://github.com/angular/angular/pull/52309)) |
|
||||
| [6988a0070e](https://github.com/angular/angular/commit/6988a0070e9849b58738bba82d6f9eb9e3b27330) | fix | handle ngIf else condition with no whitespaces ([#52504](https://github.com/angular/angular/pull/52504)) |
|
||||
| [e40e55d902](https://github.com/angular/angular/commit/e40e55d902e09c36da4cf73b5ab31b6b65584518) | fix | Remove unhelpful parsing errors from the log ([#52401](https://github.com/angular/angular/pull/52401)) |
|
||||
| [c267f54bc3](https://github.com/angular/angular/commit/c267f54bc36e1c92f526071e2d78455daf8a588c) | fix | Update regex to better match ng-templates ([#52529](https://github.com/angular/angular/pull/52529)) |
|
||||
### platform-browser
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c340d6e044](https://github.com/angular/angular/commit/c340d6e0440bd982dff6f9f4f4229931c62d2c08) | feat | enable removal of styles on component destroy by default ([#51571](https://github.com/angular/angular/pull/51571)) |
|
||||
| [c5daa6ce77](https://github.com/angular/angular/commit/c5daa6ce776724d44c02cc97f1a349a85cb2a819) | feat | expose `EventManagerPlugin` in the public API. ([#49969](https://github.com/angular/angular/pull/49969)) |
|
||||
| [5b375d106f](https://github.com/angular/angular/commit/5b375d106f2e02afadad8f5a60c37558318ea091) | fix | Fire Animations events when using async animations. ([#52087](https://github.com/angular/angular/pull/52087)) |
|
||||
| [65786b2b96](https://github.com/angular/angular/commit/65786b2b96ba198034ff23bb14571a659a491b50) | fix | prevent duplicate stylesheets from being created ([#52019](https://github.com/angular/angular/pull/52019)) |
|
||||
| [75d610d420](https://github.com/angular/angular/commit/75d610d420ce3a1ec6429d79c72ec6ef6c2c9a10) | fix | set animation properties when using async animations. ([#52087](https://github.com/angular/angular/pull/52087)) |
|
||||
| [3c0577f991](https://github.com/angular/angular/commit/3c0577f99140b75688cb8ae969738325cc871548) | perf | disable styles of removed components instead of removing ([#51808](https://github.com/angular/angular/pull/51808)) |
|
||||
| [c9cde3ab10](https://github.com/angular/angular/commit/c9cde3ab103699bc3f941d8176ee0b0373fcf7e0) | perf | only append style element on creation ([#52237](https://github.com/angular/angular/pull/52237)) |
|
||||
| [dbc14eb41d](https://github.com/angular/angular/commit/dbc14eb41d540ab3f7509e41cdf64ac6fe33e13a) | refactor | remove `withNoDomReuse` function ([#52057](https://github.com/angular/angular/pull/52057)) |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0c66e2424c](https://github.com/angular/angular/commit/0c66e2424c84f92765c727a98f8d2199f4b1a809) | fix | resolve relative requests URL ([#52326](https://github.com/angular/angular/pull/52326)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1da28f4825](https://github.com/angular/angular/commit/1da28f482517ea53a18e4eb526c7c9708e6fcb55) | feat | Add callback to execute when a view transition is created ([#52002](https://github.com/angular/angular/pull/52002)) |
|
||||
| [73e4bf2ed2](https://github.com/angular/angular/commit/73e4bf2ed2471faf44a49b591e19a390d5867449) | feat | Add feature to support the View Transitions API ([#51314](https://github.com/angular/angular/pull/51314)) |
|
||||
| [86e91463af](https://github.com/angular/angular/commit/86e91463afc1f3d3d71a669fb2919f2b8bc5a1ca) | feat | Add option to skip the first view transition ([#51825](https://github.com/angular/angular/pull/51825)) |
|
||||
| [ce1b915868](https://github.com/angular/angular/commit/ce1b915868e654cdb679e9381db9d3bd3d68d5c4) | fix | Allow redirects after an absolute redirect ([#51731](https://github.com/angular/angular/pull/51731)) |
|
||||
| [37df395be0](https://github.com/angular/angular/commit/37df395be070a11b8cd84c0ff3af9290d15c4e9d) | fix | children of routes with loadComponent should not inherit parent data by default ([#52114](https://github.com/angular/angular/pull/52114)) |
|
||||
| [4dce8766f8](https://github.com/angular/angular/commit/4dce8766f8a3a33ffab0b3df5981ad209db42c77) | fix | Ensure newly resolved data is inherited by child routes ([#52167](https://github.com/angular/angular/pull/52167)) |
|
||||
| [f464e39364](https://github.com/angular/angular/commit/f464e39364da6436fc4b5a703f66fe7dee70818c) | fix | Ensure title observable gets latest values ([#51561](https://github.com/angular/angular/pull/51561)) |
|
||||
| [b2aff43621](https://github.com/angular/angular/commit/b2aff4362129feb746856fc3d0f8e73b1927a037) | fix | Remove `urlHandlingStrategy` from public Router properties ([#51631](https://github.com/angular/angular/pull/51631)) |
|
||||
| [c62e680098](https://github.com/angular/angular/commit/c62e680098a8c26fb2234336613185f7ab273483) | fix | Remove deprecated Router properties ([#51502](https://github.com/angular/angular/pull/51502)) |
|
||||
| [3c6258c85b](https://github.com/angular/angular/commit/3c6258c85b37535c1178e84509b7c9ed3a1359e4) | fix | Remove deprecated setupTestingRouter function ([#51826](https://github.com/angular/angular/pull/51826)) |
|
||||
| [0b3e6a41d0](https://github.com/angular/angular/commit/0b3e6a41d025997d2947125d875ac26ecd1b86d9) | fix | Remove malformedUriErrorHandler from `ExtraOptions` ([#51745](https://github.com/angular/angular/pull/51745)) |
|
||||
| [c03baed854](https://github.com/angular/angular/commit/c03baed8547c2c1da576307c708d2682dfdf3742) | fix | use DOCUMENT token instead of document directly in view transitions ([#51814](https://github.com/angular/angular/pull/51814)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.12"></a>
|
||||
# 16.2.12 (2023-11-02)
|
||||
### animations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [03f4050636](https://github.com/angular/angular/commit/03f4050636a385cf2e920b06e7ec94d6b5f32383) | fix | remove `finish` listener once player is destroyed ([#51136](https://github.com/angular/angular/pull/51136)) |
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e092184a5c](https://github.com/angular/angular/commit/e092184a5c3d98f4be329e4037c9039c1b420d75) | fix | apply fixed_srcset_width values only to fixed srcsets ([#52486](https://github.com/angular/angular/pull/52486)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [b3b4ae4c3a](https://github.com/angular/angular/commit/b3b4ae4c3ae5f307e68e3a57f84c9e2dfc9938fb) | fix | properly emit literal types in input coercion function arguments ([#52437](https://github.com/angular/angular/pull/52437)) |
|
||||
| [873c4f2454](https://github.com/angular/angular/commit/873c4f2454716520881f9d684364bdd6ab6ef722) | fix | use originally used module specifier for transform functions ([#52437](https://github.com/angular/angular/pull/52437)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.11"></a>
|
||||
# 16.2.11 (2023-10-25)
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [54ea3b65c3](https://github.com/angular/angular/commit/54ea3b65c35254b220054541f2fbc52384486156) | fix | emit provider configured event when a service is configured with `providedIn` ([#52365](https://github.com/angular/angular/pull/52365)) |
|
||||
| [78533324dc](https://github.com/angular/angular/commit/78533324dcda2e884fbe489aaa6b37fa9a6fadbc) | fix | get root and platform injector providers in special cases ([#52365](https://github.com/angular/angular/pull/52365)) |
|
||||
| [019a0f4c22](https://github.com/angular/angular/commit/019a0f4c225d6bb6f741b37bf272b62899fe1f69) | fix | load global utils before creating platform injector in the standalone case ([#52365](https://github.com/angular/angular/pull/52365)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [b79b4aca91](https://github.com/angular/angular/commit/b79b4aca914aefda1605fc8d069ac8d84283f1b3) | fix | `RouterTestingHarness` should throw if a component is expected but navigation fails ([#52357](https://github.com/angular/angular/pull/52357)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.10"></a>
|
||||
# 16.2.10 (2023-10-18)
|
||||
|
||||
@@ -961,15 +25,6 @@ Live long and prosper 🖖🏻
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="15.2.10"></a>
|
||||
# 15.2.10 (2023-10-04)
|
||||
### service-worker
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [9fe08968b8](https://github.com/angular/angular/commit/9fe08968b84b92da47ce91f5d2860b3aa23d2d2b) | fix | throw a critical error when handleFetch fail ([#51989](https://github.com/angular/angular/pull/51989)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.7"></a>
|
||||
# 16.2.7 (2023-09-27)
|
||||
### core
|
||||
@@ -1090,11 +145,6 @@ Live long and prosper 🖖🏻
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.1.9"></a>
|
||||
# 16.1.9 (2023-08-09)
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.1.8"></a>
|
||||
# 16.1.8 (2023-08-02)
|
||||
### compiler
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2024 Google LLC. https://angular.io/license
|
||||
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<h1 align="center">Angular - The modern web developer's platform</h1>
|
||||
|
||||
<p align="center">
|
||||
<img src="aio/src/assets/images/logos/angular/angular_renaissance.png" alt="angular-logo" width="120px" height="120px"/>
|
||||
<img src="aio/src/assets/images/logos/angular/angular.png" alt="angular-logo" width="120px" height="120px"/>
|
||||
<br>
|
||||
<em>Angular is a development platform for building mobile and desktop web applications
|
||||
<br> using TypeScript/JavaScript and other languages.</em>
|
||||
@@ -9,7 +9,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://angular.dev/"><strong>angular.dev</strong></a>
|
||||
<a href="https://www.angular.io"><strong>www.angular.io</strong></a>
|
||||
<br>
|
||||
</p>
|
||||
|
||||
@@ -61,25 +61,6 @@ Get started with Angular, learn the fundamentals and explore advanced topics on
|
||||
- [Lazy Loading][lazyloading]
|
||||
- [Animations][animations]
|
||||
|
||||
### Local Development
|
||||
|
||||
To contribute to Angular docs, you can setup a local environment with the following commands:
|
||||
|
||||
```bash
|
||||
# Clone Angular repo
|
||||
git clone https://github.com/angular/angular.git
|
||||
|
||||
# Navigate to project directory
|
||||
cd angular
|
||||
|
||||
# Install dependencies
|
||||
yarn
|
||||
|
||||
# Build and run local dev server
|
||||
# Note: Initial build will take some time
|
||||
yarn docs
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
@@ -148,7 +129,7 @@ Help us keep Angular open and inclusive. Please read and follow our [Code of Con
|
||||
|
||||
Join the conversation and help the community.
|
||||
|
||||
- [X (formerly Twitter)][X (formerly Twitter)]
|
||||
- [Twitter][twitter]
|
||||
- [Discord][discord]
|
||||
- [Gitter][gitter]
|
||||
- [YouTube][youtube]
|
||||
@@ -160,27 +141,27 @@ Join the conversation and help the community.
|
||||
**Love Angular? Give our repo a star :star: :arrow_up:.**
|
||||
|
||||
[contributing]: CONTRIBUTING.md
|
||||
[quickstart]: https://angular.dev/tutorials/learn-angular
|
||||
[quickstart]: https://angular.io/start
|
||||
[changelog]: CHANGELOG.md
|
||||
[ng]: https://angular.dev
|
||||
[documentation]: https://angular.dev/overview
|
||||
[ng]: https://angular.io
|
||||
[documentation]: https://angular.io/docs
|
||||
[angularmaterial]: https://material.angular.io/
|
||||
[cli]: https://angular.dev/tools/cli
|
||||
[architecture]: https://angular.dev/essentials
|
||||
[componentstemplates]: https://angular.dev/tutorials/learn-angular/components-in-angular
|
||||
[forms]: https://angular.dev/tutorials/learn-angular/forms
|
||||
[api]: https://angular.dev/api
|
||||
[angularelements]: https://angular.dev/guide/elements
|
||||
[ssr]: https://angular.dev/guide/ssr
|
||||
[schematics]: https://angular.dev/tools/cli/schematics
|
||||
[lazyloading]: https://angular.dev/guide/ngmodules/lazy-loading
|
||||
[cli]: https://cli.angular.io/
|
||||
[architecture]: https://angular.io/guide/architecture
|
||||
[componentstemplates]: https://angular.io/guide/displaying-data
|
||||
[forms]: https://angular.io/guide/forms-overview
|
||||
[api]: https://angular.io/api
|
||||
[angularelements]: https://angular.io/guide/elements
|
||||
[ssr]: https://angular.io/guide/universal
|
||||
[schematics]: https://angular.io/guide/schematics
|
||||
[lazyloading]: https://angular.io/guide/lazy-loading-ngmodules
|
||||
[node.js]: https://nodejs.org/
|
||||
[npm]: https://www.npmjs.com/get-npm
|
||||
[codeofconduct]: CODE_OF_CONDUCT.md
|
||||
[X (formerly Twitter)]: https://www.twitter.com/angular
|
||||
[twitter]: https://www.twitter.com/angular
|
||||
[discord]: https://discord.gg/angular
|
||||
[gitter]: https://gitter.im/angular/angular
|
||||
[stackoverflow]: https://stackoverflow.com/questions/tagged/angular
|
||||
[youtube]: https://youtube.com/angular
|
||||
[meetup]: https://www.meetup.com/find/?keywords=angular
|
||||
[animations]: https://angular.dev/guide/animations
|
||||
[animations]: https://angular.io/guide/animations
|
||||
|
||||
@@ -64,17 +64,7 @@ load("@rules_nodejs//nodejs:repositories.bzl", "nodejs_register_toolchains")
|
||||
|
||||
nodejs_register_toolchains(
|
||||
name = "nodejs",
|
||||
node_repositories = {
|
||||
"18.18.2-darwin_arm64": ("node-v18.18.2-darwin-arm64.tar.gz", "node-v18.18.2-darwin-arm64", "9f982cc91b28778dd8638e4f94563b0c2a1da7aba62beb72bd427721035ab553"),
|
||||
"18.18.2-darwin_amd64": ("node-v18.18.2-darwin-x64.tar.gz", "node-v18.18.2-darwin-x64", "5bb8da908ed590e256a69bf2862238c8a67bc4600119f2f7721ca18a7c810c0f"),
|
||||
"18.18.2-linux_arm64": ("node-v18.18.2-linux-arm64.tar.xz", "node-v18.18.2-linux-arm64", "2e630e18548627f61eaf573233da7949dc0a1df5eef3f486fa9820c5f6c121aa"),
|
||||
"18.18.2-linux_ppc64le": ("node-v18.18.2-linux-ppc64le.tar.xz", "node-v18.18.2-linux-ppc64le", "b0adff5cf5938266b711d6c724fb134d802e0dee40b3a3f73d162de1b3d11880"),
|
||||
"18.18.2-linux_s390x": ("node-v18.18.2-linux-s390x.tar.xz", "node-v18.18.2-linux-s390x", "c70ec2074b5e2b42c55bb4b8105418b67bf8a61c500d9376a07430dfcc341fdb"),
|
||||
"18.18.2-linux_amd64": ("node-v18.18.2-linux-x64.tar.xz", "node-v18.18.2-linux-x64", "75aba25ae76999309fc6c598efe56ce53fbfc221381a44a840864276264ab8ac"),
|
||||
"18.18.2-windows_amd64": ("node-v18.18.2-win-x64.zip", "node-v18.18.2-win-x64", "3bb0e51e579a41a22b3bf6cb2f3e79c03801aa17acbe0ca00fc555d1282e7acd"),
|
||||
},
|
||||
# We need at least Node 18.17 due to some transitive dependencies.
|
||||
node_version = "18.18.2",
|
||||
node_version = "18.13.0",
|
||||
)
|
||||
|
||||
# Download npm dependencies.
|
||||
@@ -88,9 +78,8 @@ yarn_install(
|
||||
data = [
|
||||
YARN_LABEL,
|
||||
"//:.yarnrc",
|
||||
"//:tools/npm-patches/@bazel+jasmine+5.8.1.patch",
|
||||
"//tools:postinstall-patches.js",
|
||||
"//tools/esm-interop:patches/npm/@angular+build-tooling+0.0.0-e0ec7b60641d7f6369be45d8d02663fd50f320be.patch",
|
||||
"//tools/esm-interop:patches/npm/@angular+build-tooling+0.0.0-8d4803573edc70b90a1134ffa996303d1dcc18a9.patch",
|
||||
"//tools/esm-interop:patches/npm/@bazel+concatjs+5.8.1.patch",
|
||||
"//tools/esm-interop:patches/npm/@bazel+esbuild+5.7.1.patch",
|
||||
"//tools/esm-interop:patches/npm/@bazel+protractor+5.7.1.patch",
|
||||
@@ -116,7 +105,6 @@ yarn_install(
|
||||
data = [
|
||||
YARN_LABEL,
|
||||
"//:.yarnrc",
|
||||
"//:tools/npm-patches/@bazel+jasmine+5.8.1.patch",
|
||||
"//aio:tools/cli-patches/bazel-architect-output.patch",
|
||||
"//aio:tools/cli-patches/patch.js",
|
||||
],
|
||||
@@ -206,10 +194,10 @@ cldr_xml_data_repository(
|
||||
# sass rules
|
||||
http_archive(
|
||||
name = "io_bazel_rules_sass",
|
||||
sha256 = "974f5e14c54d300ca63a67910ebfcea6d838d8811fefc61184f2872107152705",
|
||||
strip_prefix = "rules_sass-888dd9c02709e228b60a8b3e51e2b16b2ca5279d",
|
||||
sha256 = "7a90567717c3417a23ff4b21963a698a6e9aa669ac9e682121ea33f218a2816a",
|
||||
strip_prefix = "rules_sass-425ecafa8268bc013d684216363fe75bc802323e",
|
||||
urls = [
|
||||
"https://github.com/bazelbuild/rules_sass/archive/888dd9c02709e228b60a8b3e51e2b16b2ca5279d.zip",
|
||||
"https://github.com/bazelbuild/rules_sass/archive/425ecafa8268bc013d684216363fe75bc802323e.zip",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
load("//:packages.bzl", "link_packages")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
|
||||
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
# All source and configuration files required to build the docs app
|
||||
APPLICATION_FILES = [
|
||||
"angular.json",
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.json",
|
||||
"tsconfig.worker.json",
|
||||
] + glob(
|
||||
["src/**/*"],
|
||||
exclude = [
|
||||
"src/**/*.spec.ts",
|
||||
# Temporarily exclude generated sources produced by the non-bazel
|
||||
# build until the whole project is built by bazel and this directory
|
||||
# isn't needed.
|
||||
"src/generated/**/*",
|
||||
],
|
||||
)
|
||||
|
||||
TEST_FILES = APPLICATION_FILES + [
|
||||
"karma.conf.js",
|
||||
"tsconfig.spec.json",
|
||||
] + glob(
|
||||
["**/*.spec.ts"],
|
||||
)
|
||||
|
||||
APPLICATION_DEPS = link_packages([
|
||||
"@npm//@angular-devkit/build-angular",
|
||||
"@npm//@angular/animations",
|
||||
"@npm//@angular/cdk",
|
||||
"@npm//@angular/common",
|
||||
"@npm//@angular/compiler",
|
||||
"@npm//@angular/compiler-cli",
|
||||
"@npm//@angular/core",
|
||||
"@npm//@angular/docs",
|
||||
"@npm//@angular/forms",
|
||||
"@npm//@angular/material",
|
||||
"@npm//@angular/platform-browser",
|
||||
"@npm//@angular/platform-server",
|
||||
"@npm//@angular/router",
|
||||
"@npm//gsap",
|
||||
"@npm//marked",
|
||||
"@npm//ngx-progressbar",
|
||||
"@npm//ogl",
|
||||
"@npm//rxjs",
|
||||
"@npm//typescript",
|
||||
"//adev/src/assets/images",
|
||||
"//adev/src/assets/textures",
|
||||
"//adev/src/assets/previews",
|
||||
"//adev/src/assets:tutorials",
|
||||
"//adev/src/assets/icons",
|
||||
"//adev/src/assets:api",
|
||||
"//adev/src/assets:content",
|
||||
"@npm//@typescript/vfs",
|
||||
"@npm//@codemirror/state",
|
||||
"@npm//@codemirror/view",
|
||||
"@npm//@codemirror/language",
|
||||
"@npm//@codemirror/commands",
|
||||
"@npm//@codemirror/search",
|
||||
"@npm//@codemirror/autocomplete",
|
||||
"@npm//@codemirror/lint",
|
||||
"@npm//@codemirror/lang-html",
|
||||
"@npm//@codemirror/lang-angular",
|
||||
"@npm//@codemirror/lang-css",
|
||||
"@npm//@codemirror/lang-sass",
|
||||
"@npm//@codemirror/lang-javascript",
|
||||
"@npm//@lezer/highlight",
|
||||
"@npm//@lezer/javascript",
|
||||
"@npm//@lezer/common",
|
||||
"@npm//xterm",
|
||||
"@npm//xterm-addon-fit",
|
||||
"@npm//angular-split",
|
||||
])
|
||||
|
||||
TEST_DEPS = APPLICATION_DEPS + link_packages([
|
||||
"@npm//@angular/platform-browser-dynamic",
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium",
|
||||
"@npm//@types/jasmine",
|
||||
"@npm//@types/node",
|
||||
"@npm//assert",
|
||||
"@npm//jasmine",
|
||||
"@npm//jasmine-core",
|
||||
"@npm//karma-chrome-launcher",
|
||||
"@npm//karma-coverage",
|
||||
"@npm//karma-jasmine",
|
||||
"@npm//karma-jasmine-html-reporter",
|
||||
"//aio/tools:windows-chromium-path",
|
||||
])
|
||||
|
||||
copy_to_bin(
|
||||
name = "application_files_bin",
|
||||
srcs = APPLICATION_FILES,
|
||||
)
|
||||
|
||||
bool_flag(
|
||||
name = "fast_build_mode",
|
||||
build_setting_default = False,
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "fast",
|
||||
flag_values = {
|
||||
":fast_build_mode": "true",
|
||||
},
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "full",
|
||||
flag_values = {
|
||||
":fast_build_mode": "false",
|
||||
},
|
||||
)
|
||||
|
||||
config_based_architect_flags = select({
|
||||
":fast": ["--no-prerender"],
|
||||
":full": ["--prerender"],
|
||||
})
|
||||
|
||||
architect(
|
||||
name = "build",
|
||||
args = [
|
||||
"angular-dev:build",
|
||||
"--output-path=build",
|
||||
] + config_based_architect_flags,
|
||||
chdir = "$(RULEDIR)",
|
||||
data = APPLICATION_DEPS + [
|
||||
":application_files_bin",
|
||||
],
|
||||
# Network is required to inline fonts.
|
||||
exec_properties = ENABLE_NETWORK,
|
||||
output_dir = True,
|
||||
tags = [
|
||||
"no-remote-exec",
|
||||
],
|
||||
)
|
||||
|
||||
architect(
|
||||
name = "serve",
|
||||
args = [
|
||||
"angular-dev:serve",
|
||||
"--poll=1000",
|
||||
"--live-reload",
|
||||
"--watch",
|
||||
],
|
||||
chdir = package_name(),
|
||||
data = APPLICATION_DEPS + [
|
||||
":application_files_bin",
|
||||
],
|
||||
tags = [
|
||||
"no-remote-exec",
|
||||
],
|
||||
)
|
||||
|
||||
architect_test(
|
||||
name = "test",
|
||||
args = [
|
||||
"angular-dev:test",
|
||||
"--no-watch",
|
||||
],
|
||||
chdir = package_name(),
|
||||
data = TEST_DEPS + TEST_FILES,
|
||||
env = {
|
||||
"CHROME_BIN": "../$(CHROMIUM)",
|
||||
},
|
||||
toolchains = [
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
|
||||
],
|
||||
)
|
||||
@@ -1,104 +0,0 @@
|
||||
{
|
||||
"$schema": "../node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": ".",
|
||||
"projects": {
|
||||
"angular-dev": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss",
|
||||
"standalone": true,
|
||||
"changeDetection": "OnPush"
|
||||
}
|
||||
},
|
||||
"root": ".",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "adev",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"externalDependencies": [
|
||||
"path"
|
||||
],
|
||||
"outputPath": "dist/angular-dev",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"server": "src/main.server.ts",
|
||||
"polyfills": ["src/polyfills.ts", "zone.js"],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/robots.txt",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": ["@angular/docs/styles/global-styles.scss"],
|
||||
"scripts": [],
|
||||
"webWorkerTsConfig": "tsconfig.worker.json",
|
||||
"optimization": {
|
||||
"fonts": {
|
||||
// TODO(josephperrott): enabled inline scripts
|
||||
"inline": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "angular-dev:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "angular-dev:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"buildTarget": "web-ui:build",
|
||||
"headers": {
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "angular-dev:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": ["zone.js", "zone.js/testing"],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"include": ["src/app"],
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["@angular/docs/styles/global-styles.scss"],
|
||||
"scripts": [],
|
||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": "dca119a9-da31-47f7-a6cb-b60541037021"
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"hosting": {
|
||||
"target": "angular-docs",
|
||||
"public": "./build/browser",
|
||||
"ignore": ["**/.*"],
|
||||
"headers": [
|
||||
{
|
||||
"source": "assets/tutorials/common/*.jpg",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Resource-Policy",
|
||||
"value": "cross-origin"
|
||||
},
|
||||
{
|
||||
"key": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "*.[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].+(css|js)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public,max-age=31536000,immutable"
|
||||
},
|
||||
{
|
||||
"key": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/[0-9a-f][0-9a-f][0-9a-f].[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].js",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Embedder-Policy",
|
||||
"value": "require-corp"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/@(ngsw-worker.js|ngsw.json)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "no-cache"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/**",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Opener-Policy",
|
||||
"value": "same-origin"
|
||||
},
|
||||
|
||||
{
|
||||
"key": "Cross-Origin-Embedder-Policy",
|
||||
"value": "require-corp"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
const {getAdjustedChromeBinPathForWindows} = require('../aio/tools/windows-chromium-path');
|
||||
|
||||
process.env.CHROME_BIN = getAdjustedChromeBinPathForWindows();
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma'),
|
||||
{'reporter:jasmine-seed': ['type', JasmineSeedReporter]},
|
||||
],
|
||||
proxies: {
|
||||
'/dummy/image': 'src/assets/images/logos/angular/angular.png',
|
||||
},
|
||||
client: {
|
||||
clearContext: false, // leave Jasmine Spec Runner output visible in browser
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
random: true,
|
||||
seed: '',
|
||||
},
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true, // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/site'),
|
||||
subdir: '.',
|
||||
reporters: [{type: 'html'}, {type: 'text-summary'}],
|
||||
},
|
||||
reporters: ['progress', 'kjhtml', 'jasmine-seed'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
customLaunchers: {
|
||||
ChromeHeadlessNoSandbox: {
|
||||
base: 'ChromeHeadless',
|
||||
// See /integration/README.md#browser-tests for more info on these args
|
||||
flags: [
|
||||
'--no-sandbox',
|
||||
'--headless',
|
||||
'--disable-gpu',
|
||||
'--disable-dev-shm-usage',
|
||||
'--hide-scrollbars',
|
||||
'--mute-audio',
|
||||
],
|
||||
},
|
||||
},
|
||||
browsers: ['ChromeHeadlessNoSandbox'],
|
||||
browserNoActivityTimeout: 60000,
|
||||
singleRun: false,
|
||||
restartOnFileChange: true,
|
||||
});
|
||||
};
|
||||
|
||||
// Helpers
|
||||
function JasmineSeedReporter(baseReporterDecorator) {
|
||||
baseReporterDecorator(this);
|
||||
|
||||
this.onBrowserComplete = (browser, result) => {
|
||||
const seed = result.order && result.order.random && result.order.seed;
|
||||
if (seed) this.write(`${browser}: Randomized with seed ${seed}.\n`);
|
||||
};
|
||||
|
||||
this.onRunComplete = () => undefined;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
@defer (when isBrowser) {
|
||||
<adev-progress-bar />
|
||||
}
|
||||
<button (click)="focusFirstHeading()" class="adev-skip">Skip to main content</button>
|
||||
|
||||
<div class="adev-nav"></div>
|
||||
@if (displaySecondaryNav()) {
|
||||
<adev-secondary-navigation />
|
||||
}
|
||||
<div class="docs-app-main-content">
|
||||
<!--
|
||||
Avoid rendering cookies popup on the server,
|
||||
since there is no benefit of doing this and
|
||||
it requires additional state management.
|
||||
-->
|
||||
@defer (when isBrowser) {
|
||||
<docs-cookie-popup />
|
||||
|
||||
@defer (when displaySearchDialog(); prefetch on idle) {
|
||||
@if (displaySearchDialog()) {
|
||||
<docs-search-dialog (onClose)="displaySearchDialog.set(false)" />
|
||||
}
|
||||
}
|
||||
}
|
||||
<router-outlet />
|
||||
|
||||
@if (displayFooter()) {
|
||||
<footer adev-footer></footer>
|
||||
}
|
||||
</div>
|
||||
@@ -1,86 +0,0 @@
|
||||
@use '@angular/docs/styles/media-queries' as mq;
|
||||
|
||||
:host {
|
||||
max-width: 2560px;
|
||||
margin-inline: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
min-height: 100vh;
|
||||
|
||||
@include mq.for-tablet-landscape-down {
|
||||
flex-direction: column;
|
||||
}
|
||||
// If navs are open, render a blurry background over content
|
||||
&:has(.docs-nav-secondary--open),
|
||||
&:has(.adev-nav-primary--open) {
|
||||
.docs-app-main-content {
|
||||
&::after {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:has(.adev-home) {
|
||||
.adev-nav {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
@include mq.for-tablet-landscape-up {
|
||||
footer {
|
||||
margin-left: var(--primary-nav-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adev-skip {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
z-index: 1000;
|
||||
background: var(--primary-contrast);
|
||||
color: var(--page-background);
|
||||
border: 1px solid var(--vivid-pink);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
transform: translateY(-150%);
|
||||
transition: transform 0.3s ease-out;
|
||||
&:focus {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.docs-app-main-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
|
||||
// If navs are open, render a blurry background over content
|
||||
::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
backdrop-filter: blur(2px);
|
||||
background-color: color-mix(in srgb, var(--gray-1000) 5%, transparent);
|
||||
z-index: 50;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
@include mq.for-tablet {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: auto;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {AppComponent} from './app.component';
|
||||
import {provideRouter} from '@angular/router';
|
||||
import {routes} from './routes';
|
||||
import {Search, WINDOW} from '@angular/docs';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
const fakeSearch = {};
|
||||
const fakeWindow = {};
|
||||
|
||||
it('should create the app', () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideRouter(routes),
|
||||
{
|
||||
provide: WINDOW,
|
||||
useValue: fakeWindow,
|
||||
},
|
||||
{
|
||||
provide: Search,
|
||||
useValue: fakeSearch,
|
||||
},
|
||||
],
|
||||
});
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,133 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {DOCUMENT, isPlatformBrowser} from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
inject,
|
||||
NgZone,
|
||||
OnInit,
|
||||
PLATFORM_ID,
|
||||
signal,
|
||||
WritableSignal,
|
||||
} from '@angular/core';
|
||||
import {NavigationEnd, NavigationSkipped, Router, RouterLink, RouterOutlet} from '@angular/router';
|
||||
import {filter, map, skip} from 'rxjs/operators';
|
||||
import {
|
||||
CookiePopup,
|
||||
getActivatedRouteSnapshotFromRouter,
|
||||
IS_SEARCH_DIALOG_OPEN,
|
||||
SearchDialog,
|
||||
WINDOW,
|
||||
} from '@angular/docs';
|
||||
import {Footer} from './core/layout/footer/footer.component';
|
||||
import {Navigation} from './core/layout/navigation/navigation.component';
|
||||
import {SecondaryNavigation} from './core/layout/secondary-navigation/secondary-navigation.component';
|
||||
import {ProgressBarComponent} from './core/layout/progress-bar/progress-bar.component';
|
||||
import {ESCAPE, SEARCH_TRIGGER_KEY} from './core/constants/keys';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'adev-root',
|
||||
imports: [
|
||||
CookiePopup,
|
||||
Navigation,
|
||||
Footer,
|
||||
SecondaryNavigation,
|
||||
RouterOutlet,
|
||||
RouterLink,
|
||||
SearchDialog,
|
||||
ProgressBarComponent,
|
||||
],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly ngZone = inject(NgZone);
|
||||
private readonly router = inject(Router);
|
||||
private readonly window = inject(WINDOW);
|
||||
|
||||
currentUrl = signal('');
|
||||
displayFooter = signal(false);
|
||||
displaySecondaryNav = signal(false);
|
||||
displaySearchDialog: WritableSignal<boolean> = inject(IS_SEARCH_DIALOG_OPEN);
|
||||
|
||||
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setSearchDialogVisibilityOnKeyPress();
|
||||
this.closeSearchDialogOnNavigationSkipped();
|
||||
this.router.events
|
||||
.pipe(
|
||||
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
|
||||
map((event) => event.urlAfterRedirects),
|
||||
)
|
||||
.subscribe((url) => {
|
||||
this.currentUrl.set(url);
|
||||
this.setComponentsVisibility();
|
||||
this.displaySearchDialog.set(false);
|
||||
});
|
||||
|
||||
this.focusFirstHeadingOnRouteChange();
|
||||
}
|
||||
|
||||
focusFirstHeading(): void {
|
||||
if (!this.isBrowser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const h1 = this.document.querySelector<HTMLHeadingElement>('h1');
|
||||
h1?.focus();
|
||||
}
|
||||
|
||||
private setComponentsVisibility(): void {
|
||||
const activatedRoute = getActivatedRouteSnapshotFromRouter(this.router as any);
|
||||
|
||||
this.displaySecondaryNav.set(activatedRoute.data['displaySecondaryNav']);
|
||||
this.displayFooter.set(!activatedRoute.data['hideFooter']);
|
||||
}
|
||||
|
||||
private focusFirstHeadingOnRouteChange(): void {
|
||||
this.router.events
|
||||
.pipe(
|
||||
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
|
||||
// Skip first emission, cause on the initial load we would like to `Skip to main content` popup when it's focused
|
||||
skip(1),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.focusFirstHeading();
|
||||
});
|
||||
}
|
||||
|
||||
private setSearchDialogVisibilityOnKeyPress(): void {
|
||||
this.ngZone.runOutsideAngular(() => {
|
||||
this.window.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (event.key === SEARCH_TRIGGER_KEY && (event.metaKey || event.ctrlKey)) {
|
||||
this.ngZone.run(() => {
|
||||
event.preventDefault();
|
||||
this.displaySearchDialog.update((display) => !display);
|
||||
});
|
||||
}
|
||||
|
||||
if (event.key === ESCAPE && this.displaySearchDialog()) {
|
||||
this.ngZone.run(() => {
|
||||
event.preventDefault();
|
||||
this.displaySearchDialog.set(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private closeSearchDialogOnNavigationSkipped(): void {
|
||||
this.router.events.pipe(filter((event) => event instanceof NavigationSkipped)).subscribe(() => {
|
||||
this.displaySearchDialog.set(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {mergeApplicationConfig, ApplicationConfig} from '@angular/core';
|
||||
import {provideServerRendering} from '@angular/platform-server';
|
||||
import {appConfig} from './app.config';
|
||||
import {
|
||||
ReferenceScrollHandler,
|
||||
ReferenceScrollHandlerNoop,
|
||||
} from './features/references/services/reference-scroll-handler.service';
|
||||
|
||||
const serverConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideServerRendering(),
|
||||
{provide: ReferenceScrollHandler, useClass: ReferenceScrollHandlerNoop},
|
||||
],
|
||||
};
|
||||
|
||||
export const config = mergeApplicationConfig(appConfig, serverConfig);
|
||||
@@ -1,97 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
import {provideHttpClient, withFetch} from '@angular/common/http';
|
||||
import {
|
||||
ApplicationConfig,
|
||||
ENVIRONMENT_INITIALIZER,
|
||||
ErrorHandler,
|
||||
inject,
|
||||
provideZoneChangeDetection,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
DOCS_CONTENT_LOADER,
|
||||
ENVIRONMENT,
|
||||
EXAMPLE_VIEWER_CONTENT_LOADER,
|
||||
PREVIEWS_COMPONENTS,
|
||||
WINDOW,
|
||||
windowProvider,
|
||||
} from '@angular/docs';
|
||||
import {provideClientHydration} from '@angular/platform-browser';
|
||||
import {provideAnimationsAsync} from '@angular/platform-browser/animations/async';
|
||||
import {
|
||||
RouteReuseStrategy,
|
||||
Router,
|
||||
TitleStrategy,
|
||||
createUrlTreeFromSnapshot,
|
||||
provideRouter,
|
||||
withInMemoryScrolling,
|
||||
withViewTransitions,
|
||||
} from '@angular/router';
|
||||
import environment from './environment';
|
||||
import {PREVIEWS_COMPONENTS_MAP} from './../assets/previews/previews';
|
||||
import {ADevTitleStrategy} from './core/services/a-dev-title-strategy';
|
||||
import {AnalyticsService} from './core/services/analytics/analytics.service';
|
||||
import {ContentLoader} from './core/services/content-loader.service';
|
||||
import {CustomErrorHandler} from './core/services/errors-handling/error-handler';
|
||||
import {ExampleContentLoader} from './core/services/example-content-loader.service';
|
||||
import {ReuseTutorialsRouteStrategy} from './features/tutorial/tutorials-route-reuse-strategy';
|
||||
import {routes} from './routes';
|
||||
import {ReferenceScrollHandler} from './features/references/services/reference-scroll-handler.service';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideRouter(
|
||||
routes,
|
||||
withInMemoryScrolling({anchorScrolling: 'enabled', scrollPositionRestoration: 'enabled'}),
|
||||
withViewTransitions({
|
||||
onViewTransitionCreated: ({transition, to}) => {
|
||||
const router = inject(Router);
|
||||
const toTree = createUrlTreeFromSnapshot(to, []);
|
||||
// Skip the transition if the only thing changing is the fragment and queryParams
|
||||
if (
|
||||
router.isActive(toTree, {
|
||||
paths: 'exact',
|
||||
matrixParams: 'exact',
|
||||
fragment: 'ignored',
|
||||
queryParams: 'ignored',
|
||||
})
|
||||
) {
|
||||
transition.skipTransition();
|
||||
}
|
||||
},
|
||||
}),
|
||||
),
|
||||
provideClientHydration(),
|
||||
provideHttpClient(withFetch()),
|
||||
provideAnimationsAsync(),
|
||||
{provide: ENVIRONMENT, useValue: environment},
|
||||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
multi: true,
|
||||
useValue: () => inject(AnalyticsService),
|
||||
},
|
||||
{provide: ErrorHandler, useClass: CustomErrorHandler},
|
||||
{provide: PREVIEWS_COMPONENTS, useValue: PREVIEWS_COMPONENTS_MAP},
|
||||
{provide: DOCS_CONTENT_LOADER, useClass: ContentLoader},
|
||||
{provide: EXAMPLE_VIEWER_CONTENT_LOADER, useClass: ExampleContentLoader},
|
||||
{
|
||||
provide: RouteReuseStrategy,
|
||||
useClass: ReuseTutorialsRouteStrategy,
|
||||
},
|
||||
{
|
||||
provide: WINDOW,
|
||||
useFactory: (document: Document) => windowProvider(document),
|
||||
deps: [DOCUMENT],
|
||||
},
|
||||
{provide: TitleStrategy, useClass: ADevTitleStrategy},
|
||||
provideZoneChangeDetection({eventCoalescing: true}),
|
||||
ReferenceScrollHandler,
|
||||
],
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
export const PRIMARY_NAV_ID = 'primaryNav';
|
||||
export const SECONDARY_NAV_ID = 'secondaryNav';
|
||||
@@ -1,12 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
export const COMMAND = 'Command';
|
||||
export const CONTROL = 'Control';
|
||||
export const ESCAPE = 'Escape';
|
||||
export const SEARCH_TRIGGER_KEY = 'k';
|
||||
@@ -1,12 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
export const GITHUB = 'https://github.com/angular/angular';
|
||||
export const X = 'https://x.com/angular';
|
||||
export const MEDIUM = 'https://blog.angular.io';
|
||||
export const YOUTUBE = 'https://www.youtube.com/channel/UCbn1OgGei-DV7aSRo_HaAiw';
|
||||
@@ -1,28 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
// File contains the enums used across whole application.
|
||||
|
||||
// The enum with the prefixes of the main routes
|
||||
export enum PagePrefix {
|
||||
API = 'api',
|
||||
CLI = 'cli',
|
||||
DOCS = 'docs',
|
||||
HOME = '',
|
||||
PLAYGROUND = 'playground',
|
||||
REFERENCE = 'reference',
|
||||
TUTORIALS = 'tutorials',
|
||||
}
|
||||
|
||||
// The enum with the default pages for each main tab
|
||||
export enum DefaultPage {
|
||||
DOCS = 'overview',
|
||||
REFERENCE = 'api',
|
||||
TUTORIALS = 'tutorials',
|
||||
PLAYGROUND = 'playground',
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
<div class="adev-footer-container">
|
||||
<div class="adev-footer-columns">
|
||||
<div>
|
||||
<h2>Social Media</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a [href]="MEDIUM" title="Angular blog">Blog</a>
|
||||
</li>
|
||||
<li>
|
||||
<a [href]="X" title="X (formerly Twitter)">X (formerly Twitter)</a>
|
||||
</li>
|
||||
<li>
|
||||
<a [href]="YOUTUBE" title="YouTube">YouTube</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://discord.gg/angular"
|
||||
title="Join the discussions at Angular Community Discord server."
|
||||
>
|
||||
Discord
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a [href]="GITHUB" title="GitHub">GitHub</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://stackoverflow.com/questions/tagged/angular"
|
||||
title="Stack Overflow: where the community answers your technical Angular questions."
|
||||
>
|
||||
Stack Overflow
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Community</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/angular/angular/blob/main/CONTRIBUTING.md"
|
||||
title="Contribute to Angular"
|
||||
>
|
||||
Contribute
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/angular/code-of-conduct/blob/main/CODE_OF_CONDUCT.md"
|
||||
title="Treating each other with respect."
|
||||
>
|
||||
Code of Conduct
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/angular/angular/issues"
|
||||
title="Post issues and suggestions on github."
|
||||
>
|
||||
Report Issues
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://devlibrary.withgoogle.com/products/angular?sort=updated"
|
||||
title="Google's DevLibrary"
|
||||
>
|
||||
Google's DevLibrary
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://developers.google.com/community/experts/directory?specialization=angular"
|
||||
title="Angular Google Developer Experts"
|
||||
>
|
||||
Angular Google Developer Experts
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Resources</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a routerLink="/press-kit" title="Press contacts, logos, and branding.">Press Kit</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/roadmap" title="Roadmap">Roadmap</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Languages</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://angular.cn/" title="简体中文版">简体中文版</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://angular.tw/" title="正體中文版">正體中文版</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://angular.jp/" title="日本語版">日本語版</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://angular.kr/" title="한국어">한국어</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://angular-gr.web.app"
|
||||
title="Ελληνικά"
|
||||
>
|
||||
Ελληνικά
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p class="docs-license">
|
||||
Super-powered by Google ©2010-2024. Code licensed under an
|
||||
<a routerLink="/license" title="License text">MIT-style License</a>
|
||||
. Documentation licensed under
|
||||
<a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1,59 +0,0 @@
|
||||
.adev-footer-columns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 2rem;
|
||||
|
||||
@container footer (max-width: 600px) {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.adev-footer-container {
|
||||
container: footer / inline-size;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
padding: var(--layout-padding);
|
||||
padding-inline-end: 1rem;
|
||||
background-color: var(--page-background);
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
// this accounts for absolutely positioned TOC on the right
|
||||
@media only screen and (min-width: 1430px) {
|
||||
width: calc(100% - 195px - var(--layout-padding) * 3);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin-block-end: 1.75rem;
|
||||
letter-spacing: -0.00875rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.95rem;
|
||||
li {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--quaternary-contrast);
|
||||
font-weight: 300;
|
||||
transition: color 0.3s ease;
|
||||
&:hover {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
p.docs-license {
|
||||
transition: color 0.3s ease;
|
||||
color: var(--quaternary-contrast);
|
||||
font-weight: 300;
|
||||
grid-column: span 4;
|
||||
font-size: 0.75rem;
|
||||
margin-block-start: 2rem;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {WINDOW} from '@angular/docs';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {Footer} from './footer.component';
|
||||
|
||||
describe('Footer', () => {
|
||||
let component: Footer;
|
||||
let fixture: ComponentFixture<Footer>;
|
||||
const fakeWindow = {
|
||||
location: {
|
||||
origin: 'example-origin',
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [Footer, RouterTestingModule],
|
||||
providers: [
|
||||
{
|
||||
provide: WINDOW,
|
||||
useValue: fakeWindow,
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture = TestBed.createComponent(Footer);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {ChangeDetectionStrategy, Component} from '@angular/core';
|
||||
import {ExternalLink} from '@angular/docs';
|
||||
import {RouterLink} from '@angular/router';
|
||||
import {GITHUB, X, MEDIUM, YOUTUBE} from './../../constants/links';
|
||||
|
||||
@Component({
|
||||
selector: 'footer[adev-footer]',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ExternalLink, RouterLink],
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrls: ['./footer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class Footer {
|
||||
readonly GITHUB = GITHUB;
|
||||
readonly X = X;
|
||||
readonly YOUTUBE = YOUTUBE;
|
||||
readonly MEDIUM = MEDIUM;
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
@use '@angular/docs/styles/media-queries' as mq;
|
||||
|
||||
// Dark/Light mode switcher & social media icon buttons
|
||||
// ul
|
||||
.adev-mini-menu {
|
||||
padding: 0;
|
||||
color: var(--primary-contrast);
|
||||
background-color: var(--page-background);
|
||||
border: 1px solid var(--senary-contrast);
|
||||
border-radius: 0.25rem;
|
||||
z-index: var(--z-index-mini-menu);
|
||||
box-shadow: 10px 4px 40px 0 rgba(0, 0, 0, 0.075);
|
||||
|
||||
@include mq.for-tablet {
|
||||
top: 75px;
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
|
||||
// themes and versions
|
||||
button {
|
||||
padding: 1rem;
|
||||
min-width: 75px;
|
||||
min-height: 75px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
docs-icon {
|
||||
font-size: 1.5rem;
|
||||
color: var(--quaternary-contrast);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--senary-contrast);
|
||||
span,
|
||||
docs-icon {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// social links
|
||||
a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
min-width: 50px;
|
||||
svg {
|
||||
fill: var(--quaternary-contrast);
|
||||
transition: fill 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--senary-contrast);
|
||||
svg {
|
||||
fill: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--quaternary-contrast);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
&-open {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.adev-version-picker {
|
||||
top: 30px;
|
||||
left: 10px;
|
||||
position: absolute;
|
||||
bottom: auto;
|
||||
|
||||
li {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
li button {
|
||||
min-height: fit-content;
|
||||
}
|
||||
|
||||
@include mq.for-tablet {
|
||||
top: 30px;
|
||||
left: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
@use '@angular/docs/styles/media-queries' as mq;
|
||||
|
||||
.adev-nav-item {
|
||||
color: var(--quaternary-contrast);
|
||||
position: relative;
|
||||
width: 6.875rem;
|
||||
|
||||
@include mq.for-phone-only {
|
||||
width: 5.05rem;
|
||||
}
|
||||
|
||||
@include mq.for-tablet {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 2px;
|
||||
@include mq.for-tablet {
|
||||
width: auto;
|
||||
top: auto;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
// TODO: Make this gradient if the page is currently active, and black if child page active
|
||||
background-color: var(--primary-contrast);
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
transform-origin: center;
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
&:not(.adev-nav-item--logo) {
|
||||
@include mq.for-tablet {
|
||||
a,
|
||||
.adev-nav-button {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adev-nav-button {
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a,
|
||||
.adev-nav-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include mq.for-tablet {
|
||||
flex-direction: row;
|
||||
}
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding-block: 1.25rem;
|
||||
text-decoration: none;
|
||||
fill: var(--quaternary-contrast);
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
transition: fill 0.3s ease;
|
||||
}
|
||||
|
||||
&__label {
|
||||
margin: 0;
|
||||
font-size: 0.813; // 13px
|
||||
color: inherit;
|
||||
|
||||
abbr {
|
||||
font-size: 0.688; // 11px
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
color: var(--quaternary-contrast);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
span,
|
||||
abbr {
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
a,
|
||||
.adev-nav-button {
|
||||
fill: var(--primary-contrast);
|
||||
}
|
||||
span,
|
||||
abbr {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
i {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
&--active {
|
||||
&::before {
|
||||
opacity: 1;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
&:not(.adev-nav-item--logo) {
|
||||
path {
|
||||
fill: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
span,
|
||||
abbr,
|
||||
i {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,450 +0,0 @@
|
||||
<!-- Primary Nav -->
|
||||
<div
|
||||
[attr.id]="PRIMARY_NAV_ID"
|
||||
(docsClickOutside)="closeMobileNav()"
|
||||
[docsClickOutsideIgnore]="[SECONDARY_NAV_ID]"
|
||||
>
|
||||
<!-- Mobile nav bar -->
|
||||
<div class="adev-mobile-nav-bar">
|
||||
<!-- Logo icon -->
|
||||
<button
|
||||
type="button"
|
||||
class="adev-mobile-nav-button"
|
||||
aria-label="Toggle mobile navigation"
|
||||
(click)="openMobileNav($event)"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 223 236" width="32">
|
||||
<g clip-path="url(#2a)">
|
||||
<path
|
||||
fill="url(#2b)"
|
||||
d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
||||
/>
|
||||
<path
|
||||
fill="url(#2c)"
|
||||
d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="2b"
|
||||
x1="49.009"
|
||||
x2="225.829"
|
||||
y1="213.75"
|
||||
y2="129.722"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#E40035" />
|
||||
<stop offset=".24" stop-color="#F60A48" />
|
||||
<stop offset=".352" stop-color="#F20755" />
|
||||
<stop offset=".494" stop-color="#DC087D" />
|
||||
<stop offset=".745" stop-color="#9717E7" />
|
||||
<stop offset="1" stop-color="#6C00F5" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="2c"
|
||||
x1="41.025"
|
||||
x2="156.741"
|
||||
y1="28.344"
|
||||
y2="160.344"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#FF31D9" />
|
||||
<stop offset="1" stop-color="#FF5BE1" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<clipPath id="2a"><path fill="#fff" d="M0 0h223v236H0z" /></clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<docs-icon role="presentation">menu</docs-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
class="adev-nav-primary docs-scroll-hide"
|
||||
[class.adev-nav-primary--open]="isMobileNavigationOpened()"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="adev-close-nav"
|
||||
(click)="closeMobileNav()"
|
||||
aria-label="Close navigation"
|
||||
>
|
||||
<docs-icon role="presentation">close</docs-icon>
|
||||
</button>
|
||||
<!-- Top section of primary nav -->
|
||||
<ul class="adev-nav__top">
|
||||
<!-- Home / Angular logo -->
|
||||
<li
|
||||
class="adev-nav-item adev-nav-item--logo"
|
||||
[class.adev-nav-item--active]="activeRouteItem() === HOME_ROUTE"
|
||||
>
|
||||
<a aria-label="Angular homepage" routerLink="/">
|
||||
<!-- Logo Symbol -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 223 236" width="32">
|
||||
<g clip-path="url(#a)">
|
||||
<path
|
||||
fill="url(#b)"
|
||||
d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
||||
/>
|
||||
<path
|
||||
fill="url(#c)"
|
||||
d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="b"
|
||||
x1="49.009"
|
||||
x2="225.829"
|
||||
y1="213.75"
|
||||
y2="129.722"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#E40035" />
|
||||
<stop offset=".24" stop-color="#F60A48" />
|
||||
<stop offset=".352" stop-color="#F20755" />
|
||||
<stop offset=".494" stop-color="#DC087D" />
|
||||
<stop offset=".745" stop-color="#9717E7" />
|
||||
<stop offset="1" stop-color="#6C00F5" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="c"
|
||||
x1="41.025"
|
||||
x2="156.741"
|
||||
y1="28.344"
|
||||
y2="160.344"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#FF31D9" />
|
||||
<stop offset="1" stop-color="#FF5BE1" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h223v236H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<div class="adev-beta-version">Beta Docs</div>
|
||||
</a>
|
||||
|
||||
<!-- Version picker for v18+ -->
|
||||
<div class="adev-nav-item">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Select Angular version"
|
||||
role="menu"
|
||||
class="adev-version-button"
|
||||
[class.adev-mini-menu-open]="openedMenu === 'version-picker'"
|
||||
[cdkMenuTriggerFor]="docsVersionMiniMenu"
|
||||
[cdkMenuPosition]="miniMenuPositions"
|
||||
(cdkMenuClosed)="closeMenu()"
|
||||
(click)="openVersionMenu($event)"
|
||||
>
|
||||
{{ currentDocsVersion()?.displayName }}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="15"
|
||||
viewBox="0 -960 960 960"
|
||||
width="15"
|
||||
fill="inherit"
|
||||
>
|
||||
<path d="M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<ng-template #docsVersionMiniMenu>
|
||||
<ul class="adev-mini-menu adev-version-picker" cdkMenu>
|
||||
@for (item of versions(); track item) {
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
cdkMenuItem
|
||||
(click)="redirectToDocsVersion(item.url)"
|
||||
[attr.aria-label]="item.displayName"
|
||||
>
|
||||
<span>{{ item.displayName }}</span>
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</ng-template>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Search -->
|
||||
<li class="adev-nav-item">
|
||||
<button
|
||||
class="adev-nav-button"
|
||||
type="button"
|
||||
(click)="toggleSearchDialog($event)"
|
||||
title="Search docs"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="inherit"
|
||||
>
|
||||
<path
|
||||
d="M14.583 15.48 9.104 10a4.591 4.591 0 0 1-1.458.844 5.156 5.156 0 0 1-1.771.302c-1.5 0-2.77-.52-3.813-1.563C1.022 8.542.5 7.285.5 5.813c0-1.473.52-2.73 1.563-3.771C3.103 1 4.367.479 5.854.479 7.326.48 8.58 1 9.614 2.042c1.035 1.041 1.553 2.298 1.553 3.77 0 .598-.098 1.174-.292 1.73A5.287 5.287 0 0 1 10 9.104l5.5 5.459-.917.916ZM5.854 9.895c1.125 0 2.083-.4 2.875-1.198a3.95 3.95 0 0 0 1.188-2.885 3.95 3.95 0 0 0-1.188-2.886C7.938 2.13 6.98 1.73 5.854 1.73c-1.139 0-2.107.4-2.906 1.198-.799.799-1.198 1.76-1.198 2.886 0 1.125.4 2.086 1.198 2.885.799.799 1.767 1.198 2.906 1.198Z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="adev-nav-item__label adev-search-desktop"
|
||||
[attr.aria-label]="'Open search dialog with ' + searchTitle"
|
||||
>
|
||||
<kbd>{{ searchLabel }}</kbd>
|
||||
<kbd>K</kbd>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<!-- Docs -->
|
||||
<li class="adev-nav-item" [class.adev-nav-item--active]="activeRouteItem() === DOCS_ROUTE">
|
||||
<a [routerLink]="DOCS_ROUTE">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="18"
|
||||
fill="inherit"
|
||||
>
|
||||
<path
|
||||
d="M3.645 13.792h6.708v-1.25H3.645v1.25Zm0-3.542h6.708V9H3.645v1.25Zm-2.063 7.083a1.2 1.2 0 0 1-.875-.375 1.2 1.2 0 0 1-.375-.875V1.917a1.2 1.2 0 0 1 .375-.875 1.2 1.2 0 0 1 .875-.375h7.52l4.563 4.562v10.854a1.2 1.2 0 0 1-.375.875 1.2 1.2 0 0 1-.875.375H1.582ZM8.478 5.792V1.917H1.582v14.166h10.833V5.792H8.478Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="adev-nav-item__label">Docs</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Tutorials -->
|
||||
<li
|
||||
class="adev-nav-item"
|
||||
[class.adev-nav-item--active]="activeRouteItem() === TUTORIALS_ROUTE"
|
||||
>
|
||||
<a [routerLink]="TUTORIALS_ROUTE">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="10"
|
||||
fill="inherit"
|
||||
>
|
||||
<path
|
||||
d="m5.668 10-5-5 5-5 1.187 1.188L3.022 5.02l3.813 3.812L5.668 10Zm6.667 0-1.188-1.188L14.98 4.98l-3.812-3.812L12.335 0l5 5-5 5Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="adev-nav-item__label">Tutorials</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Playground -->
|
||||
<li
|
||||
class="adev-nav-item"
|
||||
[class.adev-nav-item--active]="activeRouteItem() === PLAYGROUND_ROUTE"
|
||||
>
|
||||
<a [routerLink]="PLAYGROUND_ROUTE">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
|
||||
<path
|
||||
d="M450.001-611.691v-32.386q-39.385-9.923-64.692-41.897-25.308-31.975-25.308-74.025 0-49.922 35.038-84.96 35.039-35.038 84.961-35.038t84.961 35.038q35.038 35.038 35.038 84.96 0 42.05-25.308 74.025-25.307 31.974-64.692 41.897v32.386l273.846 157.538q17.173 9.912 26.663 26.582 9.491 16.671 9.491 36.495v62.152q0 19.824-9.491 36.495-9.49 16.67-26.663 26.582L516.154-111.771q-17.203 9.846-36.217 9.846t-36.091-9.846L176.155-265.847q-17.173-9.912-26.663-26.582-9.491-16.671-9.491-36.495v-62.152q0-19.824 9.491-36.495 9.49-16.67 26.663-26.582l273.846-157.538Zm-6.155 364.537L200-387.461v58.537q0 3.078 1.539 5.962 1.538 2.885 4.615 4.808l267.692 154.692q3.077 1.923 6.154 1.923t6.154-1.923l267.692-154.692q3.077-1.923 4.615-4.808 1.539-2.884 1.539-5.962v-58.537L516.154-247.154q-17.203 9.847-36.217 9.847t-36.091-9.847Zm6.155-162.847V-542.77L250.46-427.691l223.386 128.846q3.077 1.924 6.154 1.924t6.154-1.924l223.001-128.846L509.999-542.77v132.769h-59.998ZM480-699.999q25 0 42.5-17.5t17.5-42.5q0-25-17.5-42.5t-42.5-17.5q-25 0-42.5 17.5t-17.5 42.5q0 25 17.5 42.5t42.5 17.5Zm-2.308 538.46Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="adev-nav-item__label">Playground</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- API Ref -->
|
||||
<li
|
||||
class="adev-nav-item"
|
||||
[class.adev-nav-item--active]="activeRouteItem() === REFERENCE_ROUTE"
|
||||
>
|
||||
<a [routerLink]="REFERENCE_ROUTE">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="18"
|
||||
fill="inherit"
|
||||
>
|
||||
<path
|
||||
d="M3.645 13.792h6.708v-1.25H3.645v1.25Zm0-3.542h6.708V9H3.645v1.25Zm-2.063 7.083a1.2 1.2 0 0 1-.875-.375 1.2 1.2 0 0 1-.375-.875V1.917a1.2 1.2 0 0 1 .375-.875 1.2 1.2 0 0 1 .875-.375h7.52l4.563 4.562v10.854a1.2 1.2 0 0 1-.375.875 1.2 1.2 0 0 1-.875.375H1.582ZM8.478 5.792V1.917H1.582v14.166h10.833V5.792H8.478Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="adev-nav-item__label">Reference</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Bottom section of primary nav -->
|
||||
<div class="adev-nav__bottom">
|
||||
<!-- Social Icons Dots -->
|
||||
<div class="adev-nav-item" [class.adev-nav-item--active]="openedMenu === 'social'">
|
||||
<button
|
||||
type="button"
|
||||
[cdkMenuTriggerFor]="socialMiniMenu"
|
||||
[cdkMenuPosition]="miniMenuPositions"
|
||||
aria-label="Open social media links"
|
||||
(cdkMenuClosed)="closeMenu()"
|
||||
(cdkMenuOpened)="openMenu('social')"
|
||||
>
|
||||
<docs-icon role="presentation">more_horiz</docs-icon>
|
||||
</button>
|
||||
|
||||
<ng-template #socialMiniMenu>
|
||||
<ul class="adev-mini-menu" cdkMenu>
|
||||
<li>
|
||||
<a
|
||||
[href]="YOUTUBE"
|
||||
cdkMenuItem
|
||||
title="Angular YouTube channel"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<!-- Youtube Icon -->
|
||||
<svg
|
||||
width="20"
|
||||
height="15"
|
||||
viewBox="0 0 20 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18.7556 2.94783C18.5803 1.98018 17.745 1.27549 16.7756 1.05549C15.325 0.747832 12.6403 0.527832 9.73563 0.527832C6.83266 0.527832 4.105 0.747832 2.65266 1.05549C1.685 1.27549 0.847969 1.93549 0.672656 2.94783C0.495625 4.04783 0.320312 5.58783 0.320312 7.56783C0.320312 9.54783 0.495625 11.0878 0.715625 12.1878C0.892656 13.1555 1.72797 13.8602 2.69563 14.0802C4.23563 14.3878 6.87563 14.6078 9.78031 14.6078C12.685 14.6078 15.325 14.3878 16.865 14.0802C17.8327 13.8602 18.668 13.2002 18.845 12.1878C19.0203 11.0878 19.2403 9.50314 19.285 7.56783C19.1956 5.58783 18.9756 4.04783 18.7556 2.94783ZM7.36031 10.6478V4.48783L12.728 7.56783L7.36031 10.6478Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
[href]="X"
|
||||
cdkMenuItem
|
||||
title="Angular X (formerly Twitter) profile"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<!-- X Icon -->
|
||||
<svg
|
||||
width="17"
|
||||
height="16"
|
||||
viewBox="0 0 17 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0.04145 0.04432l6.56351 8.77603L0 15.95564h1.48651l5.78263-6.24705 4.6722 6.24705h5.05865l-6.9328-9.26967L16.21504.04432h-1.48651l-5.32552 5.75341L5.1001.04432H.04145Zm2.18602 1.09497h2.32396l10.26221 13.72122h-2.32396L2.22747 1.13928Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
[href]="MEDIUM"
|
||||
cdkMenuItem
|
||||
title="Angular Medium blog"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<!-- Medium Icon -->
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7 6A7 7 0 107 20 7 7 0 107 6zM18 6.5A3 6.5 0 1018 19.5 3 6.5 0 1018 6.5zM23 8A1 5 0 1023 18 1 5 0 1023 8z"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a [href]="GITHUB" cdkMenuItem title="Angular Github" target="_blank" rel="noopener">
|
||||
<!-- Github Icon -->
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.59948 19.0428C7.59948 18.8069 7.59118 18.182 7.58656 17.3526C4.89071 17.9526 4.32164 16.0201 4.32164 16.0201C3.88087 14.8718 3.24533 14.5663 3.24533 14.5663C2.36518 13.9492 3.31179 13.9621 3.31179 13.9621C4.28471 14.0323 4.79656 14.9868 4.79656 14.9868C5.66102 16.5052 7.06456 16.0672 7.61748 15.8125C7.70564 15.17 7.95579 14.732 8.23271 14.4837C6.08056 14.2331 3.81764 13.3801 3.81764 9.57199C3.81764 8.48737 4.19564 7.6003 4.81548 6.90522C4.71625 6.65414 4.38302 5.64384 4.91056 4.27537C4.91056 4.27537 5.72471 4.00814 7.57594 5.29399C8.34856 5.07384 9.17795 4.96307 10.0027 4.95937C10.8256 4.96307 11.6546 5.07384 12.429 5.29399C14.2793 4.00814 15.0921 4.27537 15.0921 4.27537C15.621 5.64337 15.2883 6.65368 15.1881 6.90522C15.8093 7.6003 16.1841 8.48737 16.1841 9.57199C16.1841 13.3898 13.9179 14.2298 11.7589 14.4758C12.1073 14.7828 12.4166 15.3892 12.4166 16.3165C12.4166 17.6452 12.4041 18.7174 12.4041 19.0428C12.4041 19.3091 12.579 19.6178 13.071 19.5205C16.9193 18.2041 19.6936 14.4814 19.6936 10.0926C19.6936 4.60353 15.3538 0.154297 10.0009 0.154297C4.64887 0.154297 0.309021 4.60353 0.309021 10.0926C0.309483 14.4828 3.08656 18.2078 6.9381 19.5218C7.42225 19.6128 7.59948 19.3058 7.59948 19.0428Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</div>
|
||||
<!-- TODO: Refactor this and make it better. Accessible, animated, etc. -->
|
||||
<div class="adev-nav-item" [class.adev-nav-item--active]="openedMenu === 'theme-picker'">
|
||||
<button
|
||||
type="button"
|
||||
[cdkMenuTriggerFor]="themeMiniMenu"
|
||||
[cdkMenuPosition]="miniMenuPositions"
|
||||
aria-label="Open theme picker"
|
||||
(cdkMenuClosed)="closeMenu()"
|
||||
(cdkMenuOpened)="openMenu('theme-picker')"
|
||||
>
|
||||
<docs-icon role="presentation">
|
||||
@switch (theme()) { @case ('light') {
|
||||
{{ 'light_mode' }}
|
||||
} @case ('dark') {
|
||||
{{ 'dark_mode' }}
|
||||
} @case ('auto') {
|
||||
{{ 'routine' }}
|
||||
} }
|
||||
</docs-icon>
|
||||
</button>
|
||||
|
||||
<ng-template #themeMiniMenu>
|
||||
<ul class="adev-mini-menu" cdkMenu>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
cdkMenuItem
|
||||
(click)="setTheme('auto')"
|
||||
aria-label="Set default system theme"
|
||||
>
|
||||
<docs-icon class="docs-icon_high-contrast">routine</docs-icon>
|
||||
<span>System</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
cdkMenuItem
|
||||
(click)="setTheme('dark')"
|
||||
aria-label="Set dark theme"
|
||||
>
|
||||
<docs-icon class="docs-icon_high-contrast">dark_mode</docs-icon>
|
||||
<span>Dark</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
cdkMenuItem
|
||||
(click)="setTheme('light')"
|
||||
aria-label="Set light theme"
|
||||
>
|
||||
<docs-icon class="docs-icon_high-contrast">light_mode</docs-icon>
|
||||
<span>Light</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Tablet: Second horizontal nav layer which opens the secondary nav -->
|
||||
@if (activeRouteItem() === DOCS_ROUTE || activeRouteItem() === REFERENCE_ROUTE) {
|
||||
<div class="adev-secondary-tablet-bar">
|
||||
<button type="button" (click)="openMobileNav($event)">
|
||||
<docs-icon class="docs-icon_high-contrast">menu</docs-icon>
|
||||
@if (activeRouteItem() === DOCS_ROUTE) {
|
||||
<span>Docs</span>
|
||||
} @if (activeRouteItem() === REFERENCE_ROUTE) {
|
||||
<span>API</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -1,283 +0,0 @@
|
||||
@use '@angular/docs/styles/media-queries' as mq;
|
||||
|
||||
// primary nav
|
||||
:host {
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: var(--z-index-nav);
|
||||
|
||||
.adev-mobile-nav-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
// render tablet primary nav under blur when secondary is open
|
||||
@include mq.for-tablet {
|
||||
&:has(.adev-nav-primary--open) {
|
||||
z-index: 50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adev-mobile-nav-bar {
|
||||
display: none;
|
||||
@include mq.for-phone-only {
|
||||
display: flex;
|
||||
}
|
||||
gap: 0.75rem;
|
||||
backdrop-filter: blur(16px);
|
||||
background-color: color-mix(in srgb, var(--page-background) 70%, transparent);
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
padding-block: 0.75rem;
|
||||
padding-inline: var(--layout-padding);
|
||||
border-block-end: 1px solid var(--septenary-contrast);
|
||||
box-sizing: border-box;
|
||||
|
||||
transform: translateY(0);
|
||||
// mobile bar: enter with delay
|
||||
transition: transform 0.3s ease-out 0.6s;
|
||||
|
||||
&:has(+ .adev-nav-primary--open) {
|
||||
transform: translateY(-100%);
|
||||
// mobile bar: exit without delay
|
||||
transition: transform 0.3s ease-in;
|
||||
}
|
||||
|
||||
docs-icon {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
// First level navigation
|
||||
.adev-nav-primary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
max-height: 100vh;
|
||||
overflow: auto;
|
||||
backdrop-filter: blur(16px);
|
||||
background-color: color-mix(in srgb, var(--page-background) 70%, transparent);
|
||||
// render primary nav / mini menu above tablet secondary bar
|
||||
z-index: 250;
|
||||
position: relative;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
height: 100dvh;
|
||||
padding-block-start: 1rem;
|
||||
padding-block-end: 2rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
// div containing mobile close button and adev-nav_top
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@include mq.for-phone-only {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
background-color: var(--page-background);
|
||||
box-shadow: 10px 4px 3px 0 rgba(0, 0, 0, 0.001);
|
||||
|
||||
transform: translateX(-100%);
|
||||
// primary nav: exit delayed
|
||||
// TODO: should only be delayed if there is a secondary nav
|
||||
transition: transform 0.3s ease-in 0.38s;
|
||||
|
||||
&.adev-nav-primary--open {
|
||||
transform: translateX(0%);
|
||||
// primary nav: enter after mobile nav bar exits
|
||||
transition: transform 0.3s ease-out 0.1s;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce-motion) {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include mq.for-tablet {
|
||||
flex-direction: row;
|
||||
width: 100vw;
|
||||
padding-inline: calc(var(--layout-padding) - 1.25rem);
|
||||
height: auto;
|
||||
padding-block: 0;
|
||||
}
|
||||
|
||||
// Add a subtle border for the home page
|
||||
border-block-end: 1px solid var(--septenary-contrast);
|
||||
@include mq.for-tablet-up {
|
||||
border-inline-end: 1px solid var(--septenary-contrast);
|
||||
}
|
||||
@include mq.for-phone-only {
|
||||
border-inline-end: 1px solid var(--septenary-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
.adev-nav {
|
||||
&__top {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@include mq.for-tablet {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.adev-beta-version {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
// version dropdown button
|
||||
.adev-version-button {
|
||||
border: 1px solid var(--senary-contrast);
|
||||
border-radius: 0.25rem;
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.25rem;
|
||||
color: var(--quaternary-contrast);
|
||||
fill: var(--quaternary-contrast);
|
||||
transition: color 0.3s ease;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
|
||||
docs-icon {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
@include mq.for-phone-only {
|
||||
&.adev-mini-menu-open {
|
||||
&::after {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@include mq.for-tablet-landscape-up {
|
||||
&.adev-mini-menu-open {
|
||||
&::after {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mq.for-tablet {
|
||||
> li:first-of-type {
|
||||
padding-inline-start: 1.25rem;
|
||||
}
|
||||
|
||||
li {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__bottom {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
@include mq.for-tablet {
|
||||
flex-direction: row;
|
||||
margin-inline-end: 1.25rem;
|
||||
}
|
||||
|
||||
.adev-nav-item--active {
|
||||
button {
|
||||
docs-icon {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding-inline: 1rem;
|
||||
|
||||
@include mq.for-tablet {
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
|
||||
docs-icon {
|
||||
color: var(--quaternary-contrast);
|
||||
font-size: 1.5rem;
|
||||
@include mq.for-tablet-landscape-down {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
docs-icon {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adev-close-nav {
|
||||
display: none;
|
||||
@include mq.for-phone-only {
|
||||
display: block;
|
||||
}
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
|
||||
.adev-search-desktop {
|
||||
height: 1.375rem;
|
||||
text-transform: capitalize;
|
||||
@include mq.for-tablet-landscape-down {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.adev-sub-navigation-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.adev-secondary-tablet-bar {
|
||||
font-size: 0.875rem;
|
||||
backdrop-filter: blur(16px);
|
||||
background-color: color-mix(in srgb, var(--page-background) 70%, transparent);
|
||||
border-block-end: 1px solid var(--septenary-contrast);
|
||||
padding-block: 1rem;
|
||||
padding-inline: var(--layout-padding);
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
button {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
color: var(--primary-contrast);
|
||||
padding: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
@include mq.for-tablet-landscape-up {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@include mq.for-phone-only {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {Navigation} from './navigation.component';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {PagePrefix} from '../../enums/pages';
|
||||
import {Theme, ThemeManager} from '../../services/theme-manager.service';
|
||||
import {Version, signal} from '@angular/core';
|
||||
import {of} from 'rxjs';
|
||||
import {VersionManager} from '../../services/version-manager.service';
|
||||
import {Search, WINDOW} from '@angular/docs';
|
||||
|
||||
describe('Navigation', () => {
|
||||
let component: Navigation;
|
||||
let fixture: ComponentFixture<Navigation>;
|
||||
|
||||
const fakeThemeManager = {
|
||||
theme: signal<Theme>('dark'),
|
||||
setTheme: (theme: Theme) => {},
|
||||
themeChanged$: of(),
|
||||
};
|
||||
|
||||
const fakeVersionManager = {
|
||||
currentDocsVersion: signal('v17'),
|
||||
versions: signal<Version[]>([]),
|
||||
};
|
||||
|
||||
const fakeWindow = {};
|
||||
const fakeSearch = {};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Navigation, RouterTestingModule],
|
||||
providers: [
|
||||
{
|
||||
provide: WINDOW,
|
||||
useValue: fakeWindow,
|
||||
},
|
||||
{
|
||||
provide: Search,
|
||||
useValue: fakeSearch,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
TestBed.overrideProvider(ThemeManager, {useValue: fakeThemeManager});
|
||||
TestBed.overrideProvider(VersionManager, {useValue: fakeVersionManager});
|
||||
|
||||
fixture = TestBed.createComponent(Navigation);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should append active class to DOCS_ROUTE when DOCS_ROUTE is active', () => {
|
||||
component.activeRouteItem.set(PagePrefix.DOCS);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const docsLink = fixture.debugElement.query(By.css('a[href="/docs"]')).parent?.nativeElement;
|
||||
|
||||
expect(docsLink).toHaveClass('adev-nav-item--active');
|
||||
});
|
||||
|
||||
it('should not have active class when activeRouteItem is null', () => {
|
||||
component.activeRouteItem.set(null);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const docsLink = fixture.debugElement.query(By.css('a[href="/docs"]')).nativeElement;
|
||||
const referenceLink = fixture.debugElement.query(By.css('a[href="/reference"]')).nativeElement;
|
||||
|
||||
expect(docsLink).not.toHaveClass('adev-nav-item--active');
|
||||
expect(referenceLink).not.toHaveClass('adev-nav-item--active');
|
||||
});
|
||||
|
||||
it('should call themeManager.setTheme(dark) when user tries to set dark theme', () => {
|
||||
const openThemePickerButton = fixture.debugElement.query(
|
||||
By.css('button[aria-label^="Open theme picker"]'),
|
||||
).nativeElement;
|
||||
const setThemeSpy = spyOn(fakeThemeManager, 'setTheme');
|
||||
|
||||
openThemePickerButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
const setDarkModeButton = fixture.debugElement.query(
|
||||
By.css('button[aria-label="Set dark theme"]'),
|
||||
).nativeElement;
|
||||
|
||||
setDarkModeButton.click();
|
||||
|
||||
expect(setThemeSpy).toHaveBeenCalledOnceWith('dark');
|
||||
});
|
||||
});
|
||||
@@ -1,228 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {CdkMenu, CdkMenuItem, CdkMenuTrigger} from '@angular/cdk/menu';
|
||||
import {CommonModule, DOCUMENT, Location, isPlatformBrowser} from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DestroyRef,
|
||||
OnInit,
|
||||
PLATFORM_ID,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import {takeUntilDestroyed, toObservable} from '@angular/core/rxjs-interop';
|
||||
import {
|
||||
ClickOutside,
|
||||
NavigationList,
|
||||
NavigationState,
|
||||
WINDOW,
|
||||
IconComponent,
|
||||
getBaseUrlAfterRedirects,
|
||||
isApple,
|
||||
IS_SEARCH_DIALOG_OPEN,
|
||||
} from '@angular/docs';
|
||||
import {NavigationEnd, Router, RouterLink} from '@angular/router';
|
||||
import {filter, map, startWith} from 'rxjs/operators';
|
||||
import {DOCS_ROUTES, REFERENCE_ROUTES, TUTORIALS_ROUTES} from '../../../routes';
|
||||
import {GITHUB, MEDIUM, X, YOUTUBE} from '../../constants/links';
|
||||
import {PagePrefix} from '../../enums/pages';
|
||||
import {Theme, ThemeManager} from '../../services/theme-manager.service';
|
||||
import {VersionManager} from '../../services/version-manager.service';
|
||||
import {PRIMARY_NAV_ID, SECONDARY_NAV_ID} from '../../constants/element-ids';
|
||||
import {ConnectionPositionPair} from '@angular/cdk/overlay';
|
||||
import {COMMAND, CONTROL, SEARCH_TRIGGER_KEY} from '../../constants/keys';
|
||||
|
||||
type MenuType = 'social' | 'theme-picker' | 'version-picker';
|
||||
|
||||
@Component({
|
||||
selector: 'div.adev-nav',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink,
|
||||
NavigationList,
|
||||
ClickOutside,
|
||||
CdkMenu,
|
||||
CdkMenuItem,
|
||||
CdkMenuTrigger,
|
||||
IconComponent,
|
||||
],
|
||||
templateUrl: './navigation.component.html',
|
||||
styleUrls: ['./navigation.component.scss', './mini-menu.scss', './nav-item.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class Navigation implements OnInit {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
||||
private readonly navigationState = inject(NavigationState);
|
||||
private readonly router = inject(Router);
|
||||
private readonly location = inject(Location);
|
||||
private readonly themeManager = inject(ThemeManager);
|
||||
private readonly isSearchDialogOpen = inject(IS_SEARCH_DIALOG_OPEN);
|
||||
private readonly window = inject(WINDOW);
|
||||
private readonly versionManager = inject(VersionManager);
|
||||
|
||||
readonly DOCS_ROUTE = PagePrefix.DOCS;
|
||||
readonly HOME_ROUTE = PagePrefix.HOME;
|
||||
readonly PLAYGROUND_ROUTE = PagePrefix.PLAYGROUND;
|
||||
readonly REFERENCE_ROUTE = PagePrefix.REFERENCE;
|
||||
readonly TUTORIALS_ROUTE = PagePrefix.TUTORIALS;
|
||||
|
||||
readonly GITHUB = GITHUB;
|
||||
readonly X = X;
|
||||
readonly MEDIUM = MEDIUM;
|
||||
readonly YOUTUBE = YOUTUBE;
|
||||
|
||||
readonly PRIMARY_NAV_ID = PRIMARY_NAV_ID;
|
||||
readonly SECONDARY_NAV_ID = SECONDARY_NAV_ID;
|
||||
|
||||
miniMenuPositions = [
|
||||
new ConnectionPositionPair(
|
||||
{originX: 'end', originY: 'center'},
|
||||
{overlayX: 'start', overlayY: 'center'},
|
||||
),
|
||||
new ConnectionPositionPair(
|
||||
{originX: 'end', originY: 'top'},
|
||||
{overlayX: 'start', overlayY: 'top'},
|
||||
),
|
||||
];
|
||||
|
||||
readonly APPLE_SEARCH_LABEL = `⌘`;
|
||||
readonly DEFAULT_SEARCH_LABEL = `ctrl`;
|
||||
|
||||
activeRouteItem = this.navigationState.primaryActiveRouteItem;
|
||||
theme = this.themeManager.theme;
|
||||
openedMenu: MenuType | null = null;
|
||||
|
||||
currentDocsVersion = this.versionManager.currentDocsVersion;
|
||||
// Set the values of the search label and title only on the client, because the label is user-agent specific.
|
||||
searchLabel = this.isBrowser
|
||||
? isApple
|
||||
? this.APPLE_SEARCH_LABEL
|
||||
: this.DEFAULT_SEARCH_LABEL
|
||||
: '';
|
||||
searchTitle = this.isBrowser
|
||||
? isApple
|
||||
? `${COMMAND} ${SEARCH_TRIGGER_KEY.toUpperCase()}`
|
||||
: `${CONTROL} ${SEARCH_TRIGGER_KEY.toUpperCase()}`
|
||||
: '';
|
||||
versions = this.versionManager.versions;
|
||||
|
||||
isMobileNavigationOpened = this.navigationState.isMobileNavVisible;
|
||||
isMobileNavigationOpened$ = toObservable(this.isMobileNavigationOpened);
|
||||
primaryRouteChanged$ = toObservable(this.activeRouteItem);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.listenToRouteChange();
|
||||
this.preventToScrollContentWhenSecondaryNavIsOpened();
|
||||
this.closeMobileNavOnPrimaryRouteChange();
|
||||
}
|
||||
|
||||
redirectToDocsVersion(versionUrl: string): void {
|
||||
this.window.location.href = versionUrl;
|
||||
}
|
||||
|
||||
setTheme(theme: Theme): void {
|
||||
this.themeManager.setTheme(theme);
|
||||
}
|
||||
|
||||
openVersionMenu($event: MouseEvent): void {
|
||||
// It's required to avoid redirection to `home`
|
||||
$event.stopImmediatePropagation();
|
||||
$event.preventDefault();
|
||||
this.openMenu('version-picker');
|
||||
}
|
||||
|
||||
openMenu(menuType: MenuType): void {
|
||||
this.openedMenu = menuType;
|
||||
}
|
||||
|
||||
closeMenu(): void {
|
||||
this.openedMenu = null;
|
||||
}
|
||||
|
||||
openMobileNav($event: MouseEvent): void {
|
||||
$event.stopPropagation();
|
||||
this.navigationState.setMobileNavigationListVisibility(true);
|
||||
}
|
||||
|
||||
closeMobileNav(): void {
|
||||
this.navigationState.setMobileNavigationListVisibility(false);
|
||||
}
|
||||
|
||||
toggleSearchDialog(event: MouseEvent): void {
|
||||
event.stopPropagation();
|
||||
this.isSearchDialogOpen.update((isOpen) => !isOpen);
|
||||
}
|
||||
|
||||
private closeMobileNavOnPrimaryRouteChange(): void {
|
||||
this.primaryRouteChanged$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
this.closeMobileNav();
|
||||
});
|
||||
}
|
||||
|
||||
private listenToRouteChange(): void {
|
||||
this.router.events
|
||||
.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
map((event) => (event as NavigationEnd).urlAfterRedirects),
|
||||
)
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
//using location because router.url will only return "/" here
|
||||
startWith(this.location.path()),
|
||||
)
|
||||
.subscribe((url) => {
|
||||
this.setActivePrimaryRoute(getBaseUrlAfterRedirects(url, this.router));
|
||||
});
|
||||
}
|
||||
|
||||
// Set active route item, based on urlAfterRedirects.
|
||||
// First check if url starts with the main prefixes (docs, reference, tutorials).
|
||||
// (*) Docs navigation tree contains items which will navigate to /tutorials.
|
||||
// In that case after click on such link we should mark as active item, and display tutorials navigation tree.
|
||||
// If it's not starting with prefix then check if specific path exist in the array of defined routes
|
||||
// (*) Reference navigation tree contains items which are not start with prefix like /migrations or /errors.
|
||||
private setActivePrimaryRoute(urlAfterRedirects: string): void {
|
||||
if (urlAfterRedirects === '') {
|
||||
this.activeRouteItem.set(PagePrefix.HOME);
|
||||
} else if (urlAfterRedirects.startsWith(PagePrefix.DOCS)) {
|
||||
this.activeRouteItem.set(PagePrefix.DOCS);
|
||||
} else if (
|
||||
urlAfterRedirects.startsWith(PagePrefix.REFERENCE) ||
|
||||
urlAfterRedirects.startsWith(PagePrefix.API)
|
||||
) {
|
||||
this.activeRouteItem.set(PagePrefix.REFERENCE);
|
||||
} else if (urlAfterRedirects === PagePrefix.PLAYGROUND) {
|
||||
this.activeRouteItem.set(PagePrefix.PLAYGROUND);
|
||||
} else if (urlAfterRedirects.startsWith(PagePrefix.TUTORIALS)) {
|
||||
this.activeRouteItem.set(PagePrefix.TUTORIALS);
|
||||
} else if (DOCS_ROUTES.some((route) => route.path === urlAfterRedirects)) {
|
||||
this.activeRouteItem.set(PagePrefix.DOCS);
|
||||
} else if (REFERENCE_ROUTES.some((route) => route.path === urlAfterRedirects)) {
|
||||
this.activeRouteItem.set(PagePrefix.REFERENCE);
|
||||
} else if (TUTORIALS_ROUTES.some((route) => route.path === urlAfterRedirects)) {
|
||||
this.activeRouteItem.set(PagePrefix.TUTORIALS);
|
||||
} else {
|
||||
// Reset if no active route item could be found
|
||||
this.activeRouteItem.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
private preventToScrollContentWhenSecondaryNavIsOpened(): void {
|
||||
this.isMobileNavigationOpened$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((opened) => {
|
||||
if (opened) {
|
||||
this.document.body.style.overflowY = 'hidden';
|
||||
} else {
|
||||
this.document.body.style.removeProperty('overflow-y');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
|
||||
|
||||
import {PROGRESS_BAR_DELAY, ProgressBarComponent} from './progress-bar.component';
|
||||
import {RouterTestingHarness, RouterTestingModule} from '@angular/router/testing';
|
||||
|
||||
describe('ProgressBarComponent', () => {
|
||||
let component: ProgressBarComponent;
|
||||
let fixture: ComponentFixture<ProgressBarComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ProgressBarComponent, RouterTestingModule],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ProgressBarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should call progressBar.complete() on route change', fakeAsync(async () => {
|
||||
const progressBarCompleteSpy = spyOn(component.progressBar, 'complete');
|
||||
|
||||
const harness = await RouterTestingHarness.create();
|
||||
await harness.navigateByUrl('/');
|
||||
|
||||
tick(PROGRESS_BAR_DELAY);
|
||||
expect(progressBarCompleteSpy).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
@@ -1,89 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
OnInit,
|
||||
PLATFORM_ID,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import {isPlatformBrowser} from '@angular/common';
|
||||
import {NgProgressComponent} from 'ngx-progressbar';
|
||||
import {
|
||||
NavigationCancel,
|
||||
NavigationEnd,
|
||||
NavigationError,
|
||||
NavigationSkipped,
|
||||
NavigationStart,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import {filter, map, switchMap, take} from 'rxjs/operators';
|
||||
|
||||
/** Time to wait after navigation starts before showing the progress bar. This delay allows a small amount of time to skip showing the progress bar when a navigation is effectively immediate. 30ms is approximately the amount of time we can wait before a delay is perceptible.*/
|
||||
export const PROGRESS_BAR_DELAY = 30;
|
||||
|
||||
@Component({
|
||||
selector: 'adev-progress-bar',
|
||||
standalone: true,
|
||||
imports: [NgProgressComponent],
|
||||
template: `
|
||||
<ng-progress aria-label="Page load progress" />
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ProgressBarComponent implements OnInit {
|
||||
private readonly router = inject(Router);
|
||||
|
||||
@ViewChild(NgProgressComponent, {static: true}) progressBar!: NgProgressComponent;
|
||||
|
||||
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
||||
|
||||
ngOnInit() {
|
||||
this.setupPageNavigationDimming();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dims the main router-outlet content when navigating to a new page.
|
||||
*/
|
||||
private setupPageNavigationDimming() {
|
||||
if (!this.isBrowser) {
|
||||
return;
|
||||
}
|
||||
this.router.events
|
||||
.pipe(
|
||||
filter((e) => e instanceof NavigationStart),
|
||||
map(() => {
|
||||
// Only apply set the property if the navigation is not "immediate"
|
||||
return setTimeout(() => {
|
||||
this.progressBar.start();
|
||||
}, PROGRESS_BAR_DELAY);
|
||||
}),
|
||||
switchMap((timeoutId) => {
|
||||
return this.router.events.pipe(
|
||||
filter((e) => {
|
||||
return (
|
||||
e instanceof NavigationEnd ||
|
||||
e instanceof NavigationCancel ||
|
||||
e instanceof NavigationSkipped ||
|
||||
e instanceof NavigationError
|
||||
);
|
||||
}),
|
||||
take(1),
|
||||
map(() => timeoutId),
|
||||
);
|
||||
}),
|
||||
)
|
||||
.subscribe((timeoutId) => {
|
||||
// When the navigation finishes, prevent the navigating class from being applied in the timeout.
|
||||
clearTimeout(timeoutId);
|
||||
this.progressBar.complete();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<div [attr.id]="SECONDARY_NAV_ID" class="adev-secondary-nav-mask" [class.docs-nav-secondary--open]="isSecondaryNavVisible()" (docsClickOutside)="close()" [docsClickOutsideIgnore]="[PRIMARY_NAV_ID]">
|
||||
<div class="docs-nav-secondary docs-scroll-track-transparent" [style.transform]="translateX()" [style.transition]="transition">
|
||||
<!-- Secondary Nav -->
|
||||
@if (navigationItems && navigationItems.length > 0) {
|
||||
<docs-navigation-list
|
||||
[navigationItems]="navigationItems"
|
||||
[displayItemsToLevel]="maxVisibleLevelsOnSecondaryNav()"
|
||||
[expandableLevel]="maxVisibleLevelsOnSecondaryNav()"
|
||||
(linkClicked)="close()"
|
||||
/>
|
||||
}
|
||||
|
||||
<!-- Third, fourth and next levels of navigation -->
|
||||
@for (item of navigationItemsSlides(); track item; let level = $index) {
|
||||
<docs-navigation-list
|
||||
[@leaveAnimation]
|
||||
[collapsableLevel]="level + maxVisibleLevelsOnSecondaryNav()"
|
||||
[expandableLevel]="level + maxVisibleLevelsOnSecondaryNav() + 1"
|
||||
[navigationItems]="[item]"
|
||||
[displayItemsToLevel]="level + maxVisibleLevelsOnSecondaryNav() + 1"
|
||||
(linkClicked)="close()"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,72 +0,0 @@
|
||||
@use '@angular/docs/styles/media-queries' as mq;
|
||||
|
||||
:host {
|
||||
@include mq.for-tablet-landscape-up {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
// Render secondary nav below primary nav
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
@include mq.for-tablet-landscape-down {
|
||||
position: fixed;
|
||||
// Render secondary nav above mobile menu
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@include mq.for-phone-only {
|
||||
transform: translateX(0);
|
||||
z-index: 99;
|
||||
transition: transform 0.3s ease;
|
||||
&:has(.docs-nav-secondary--open) {
|
||||
transform: translateX(82px);
|
||||
transition: transform 0.3s ease-in 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .adev-secondary-nav-mask stays in the same place during nav animation, as a mask
|
||||
.adev-secondary-nav-mask {
|
||||
position: sticky;
|
||||
@include mq.for-tablet-landscape-down {
|
||||
position: absolute;
|
||||
}
|
||||
top: 0;
|
||||
overflow-x: hidden;
|
||||
min-width: var(--secondary-nav-width);
|
||||
border-inline-end: 1px solid var(--septenary-contrast);
|
||||
background-color: var(--page-background);
|
||||
z-index: var(--z-index-nav);
|
||||
transition: transform 0.45s ease;
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition: transform 0.45s ease, background-color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
@include mq.for-tablet-landscape-down {
|
||||
transform: translateX(-100%);
|
||||
|
||||
&.docs-nav-secondary--open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@include mq.for-phone-only {
|
||||
transform: translateX(-100%);
|
||||
// exit immediately
|
||||
transition: transform 0.45s ease-in;
|
||||
|
||||
&.docs-nav-secondary--open {
|
||||
transform: translateX(0);
|
||||
// enter delayed
|
||||
transition: transform 0.45s ease-out 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// secondary nav - translated on x to display levels
|
||||
.docs-nav-secondary {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: var(--secondary-nav-width);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {SecondaryNavigation} from './secondary-navigation.component';
|
||||
import {WINDOW} from '@angular/docs';
|
||||
|
||||
describe('SecondaryNavigation', () => {
|
||||
let component: SecondaryNavigation;
|
||||
let fixture: ComponentFixture<SecondaryNavigation>;
|
||||
|
||||
const fakeWindow = {
|
||||
location: {
|
||||
origin: 'example-origin',
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [SecondaryNavigation],
|
||||
providers: [
|
||||
{
|
||||
provide: WINDOW,
|
||||
useValue: fakeWindow,
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture = TestBed.createComponent(SecondaryNavigation);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,214 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DestroyRef,
|
||||
OnInit,
|
||||
PLATFORM_ID,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import {takeUntilDestroyed, toObservable} from '@angular/core/rxjs-interop';
|
||||
import {
|
||||
ClickOutside,
|
||||
NavigationItem,
|
||||
NavigationList,
|
||||
NavigationState,
|
||||
WINDOW,
|
||||
findNavigationItem,
|
||||
getBaseUrlAfterRedirects,
|
||||
getNavigationItemsTree,
|
||||
markExternalLinks,
|
||||
shouldReduceMotion,
|
||||
} from '@angular/docs';
|
||||
import {distinctUntilChanged, filter, map, skip, startWith} from 'rxjs/operators';
|
||||
import {SUB_NAVIGATION_DATA} from '../../../sub-navigation-data';
|
||||
import {PagePrefix} from '../../enums/pages';
|
||||
import {ActivatedRouteSnapshot, NavigationEnd, Router, RouterStateSnapshot} from '@angular/router';
|
||||
import {NgFor, NgIf, isPlatformBrowser} from '@angular/common';
|
||||
import {trigger, transition, style, animate} from '@angular/animations';
|
||||
import {PRIMARY_NAV_ID, SECONDARY_NAV_ID} from '../../constants/element-ids';
|
||||
|
||||
export const ANIMATION_DURATION = 500;
|
||||
|
||||
@Component({
|
||||
selector: 'adev-secondary-navigation',
|
||||
standalone: true,
|
||||
imports: [NavigationList, NgIf, NgFor, ClickOutside],
|
||||
templateUrl: './secondary-navigation.component.html',
|
||||
styleUrls: ['./secondary-navigation.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [
|
||||
trigger('leaveAnimation', [
|
||||
transition(':leave', [
|
||||
style({transform: 'translateX(0%)'}),
|
||||
animate(
|
||||
`${ANIMATION_DURATION}ms ${ANIMATION_DURATION}ms ease-out`,
|
||||
style({transform: 'translateX(100%)'}),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class SecondaryNavigation implements OnInit {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly navigationState = inject(NavigationState);
|
||||
private readonly platformId = inject(PLATFORM_ID);
|
||||
private readonly router = inject(Router);
|
||||
private readonly window = inject(WINDOW);
|
||||
|
||||
isSecondaryNavVisible = this.navigationState.isMobileNavVisible;
|
||||
primaryActiveRouteItem = this.navigationState.primaryActiveRouteItem;
|
||||
maxVisibleLevelsOnSecondaryNav = computed(() =>
|
||||
this.primaryActiveRouteItem() === PagePrefix.REFERENCE ? 1 : 2,
|
||||
);
|
||||
navigationItemsSlides = this.navigationState.expandedItems;
|
||||
navigationItems: NavigationItem[] | undefined;
|
||||
|
||||
translateX = computed(() => {
|
||||
const level = this.navigationState.expandedItems()?.length ?? 0;
|
||||
return `translateX(${-level * 100}%)`;
|
||||
});
|
||||
transition: string = '0ms';
|
||||
|
||||
readonly PRIMARY_NAV_ID = PRIMARY_NAV_ID;
|
||||
readonly SECONDARY_NAV_ID = SECONDARY_NAV_ID;
|
||||
|
||||
private readonly routeMap: Record<string, NavigationItem[]> = {
|
||||
[PagePrefix.REFERENCE]: getNavigationItemsTree(SUB_NAVIGATION_DATA.reference, (tree) =>
|
||||
markExternalLinks(tree, this.window.origin),
|
||||
),
|
||||
[PagePrefix.DOCS]: getNavigationItemsTree(SUB_NAVIGATION_DATA.docs, (tree) =>
|
||||
markExternalLinks(tree, this.window.origin),
|
||||
),
|
||||
};
|
||||
|
||||
private readonly primaryActiveRouteChanged$ = toObservable(this.primaryActiveRouteItem).pipe(
|
||||
distinctUntilChanged(),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
);
|
||||
|
||||
private readonly urlAfterRedirects$ = this.router.events.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
map((event) => (event as NavigationEnd).urlAfterRedirects),
|
||||
filter((url): url is string => url !== undefined),
|
||||
startWith(this.getInitialPath(this.router.routerState.snapshot)),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.navigationState.cleanExpandedState();
|
||||
this.listenToPrimaryRouteChange();
|
||||
this.setActiveRouteOnNavigationEnd();
|
||||
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
this.initSlideAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.navigationState.setMobileNavigationListVisibility(false);
|
||||
}
|
||||
|
||||
private setActiveRouteOnNavigationEnd(): void {
|
||||
this.urlAfterRedirects$.subscribe((url) => {
|
||||
const activeNavigationItem = this.getActiveNavigationItem(url);
|
||||
if (
|
||||
activeNavigationItem?.level &&
|
||||
activeNavigationItem.level <= this.maxVisibleLevelsOnSecondaryNav()
|
||||
) {
|
||||
this.navigationState.cleanExpandedState();
|
||||
} else if (activeNavigationItem) {
|
||||
/**
|
||||
* For the `Docs`, we don't expand the "level === 1" items because they are already displayed in the main navigation list.
|
||||
* Example:
|
||||
* In-depth Guides (level == 0)
|
||||
* Components (level == 1) -> Selectors, Styling, etc (level == 2)
|
||||
* Template Syntax (level == 1) -> Text interpolation, etc (level == 2)
|
||||
*
|
||||
* For the `Tutorials`, we display the navigation in the dropdown and it has flat structure (all items are displayed as items with level === 0).
|
||||
*
|
||||
* For the `Reference` we would like to give possibility to expand the "level === 1" items cause they are not visible in the first slide of navigation list.
|
||||
* Example:
|
||||
* API Reference (level == 0) -> Overview, Animations, common, etc (level == 1) -> API Package exports (level == 2)
|
||||
*/
|
||||
const shouldExpandItem = (node: NavigationItem): boolean =>
|
||||
!!node.level &&
|
||||
(this.primaryActiveRouteItem() === PagePrefix.REFERENCE
|
||||
? node.level > 0
|
||||
: node.level > 1);
|
||||
|
||||
// Skip expand when active item is API Reference homepage - `/api`.
|
||||
// It protect us from displaying second level of the navigation when user clicks on `Reference`,
|
||||
// Because in this situation we want to display the first level, which contains, in addition to the API Reference, also the CLI Reference, Error Encyclopedia etc.
|
||||
const skipExpandPredicateFn = (node: NavigationItem): boolean =>
|
||||
node.path === PagePrefix.API;
|
||||
|
||||
this.navigationState.expandItemHierarchy(
|
||||
activeNavigationItem,
|
||||
shouldExpandItem,
|
||||
skipExpandPredicateFn,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getActiveNavigationItem(url: string): NavigationItem | null {
|
||||
// set visible navigation items if not present
|
||||
this.setVisibleNavigationItems();
|
||||
|
||||
const activeNavigationItem = findNavigationItem(
|
||||
this.navigationItems!,
|
||||
(item) =>
|
||||
!!item.path &&
|
||||
getBaseUrlAfterRedirects(item.path, this.router) ===
|
||||
getBaseUrlAfterRedirects(url, this.router),
|
||||
);
|
||||
|
||||
this.navigationState.setActiveNavigationItem(activeNavigationItem);
|
||||
|
||||
return activeNavigationItem;
|
||||
}
|
||||
|
||||
private initSlideAnimation(): void {
|
||||
if (shouldReduceMotion()) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.transition = `${ANIMATION_DURATION}ms`;
|
||||
}, ANIMATION_DURATION);
|
||||
}
|
||||
|
||||
private setVisibleNavigationItems(): void {
|
||||
const routeMap = this.routeMap[this.primaryActiveRouteItem()!];
|
||||
this.navigationItems = routeMap
|
||||
? getNavigationItemsTree(routeMap, (item) => {
|
||||
item.isExpanded = this.primaryActiveRouteItem() === PagePrefix.DOCS && item.level === 1;
|
||||
})
|
||||
: [];
|
||||
}
|
||||
|
||||
private listenToPrimaryRouteChange(): void {
|
||||
// Fix: flicker of sub-navigation on init
|
||||
this.primaryActiveRouteChanged$.pipe(skip(1)).subscribe(() => {
|
||||
this.navigationState.cleanExpandedState();
|
||||
});
|
||||
}
|
||||
|
||||
private getInitialPath(routerState: RouterStateSnapshot): string {
|
||||
let route: ActivatedRouteSnapshot = routerState.root;
|
||||
|
||||
while (route.firstChild) {
|
||||
route = route.firstChild;
|
||||
}
|
||||
|
||||
return route.routeConfig?.path ?? '';
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {TITLE_SUFFIX, ADevTitleStrategy, DEFAULT_PAGE_TITLE} from './a-dev-title-strategy';
|
||||
import {Router, provideRouter} from '@angular/router';
|
||||
import {Title} from '@angular/platform-browser';
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({})
|
||||
class FakeComponent {}
|
||||
|
||||
describe('ADevTitleStrategy', () => {
|
||||
let service: ADevTitleStrategy;
|
||||
let router: Router;
|
||||
let title: Title;
|
||||
|
||||
const routes = [
|
||||
{path: 'first', data: {label: 'First'}, component: FakeComponent},
|
||||
{path: 'second', component: FakeComponent},
|
||||
{
|
||||
path: 'third',
|
||||
data: {label: 'Third'},
|
||||
component: FakeComponent,
|
||||
children: [{path: 'child', data: {label: 'Child'}, component: FakeComponent}],
|
||||
},
|
||||
{
|
||||
path: 'fourth',
|
||||
data: {label: 'Fourth'},
|
||||
component: FakeComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'child',
|
||||
data: {label: 'Overview', parent: {label: 'Fourth'}},
|
||||
component: FakeComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter(routes)],
|
||||
});
|
||||
service = TestBed.inject(ADevTitleStrategy);
|
||||
router = TestBed.inject(Router);
|
||||
title = TestBed.inject(Title);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should set '${TITLE_SUFFIX}' when route doesn't have defined label`, async () => {
|
||||
spyOn(title, 'setTitle');
|
||||
|
||||
await router.navigateByUrl('/second');
|
||||
service.updateTitle(router.routerState.snapshot);
|
||||
|
||||
expect(title.setTitle).toHaveBeenCalledOnceWith(TITLE_SUFFIX);
|
||||
});
|
||||
|
||||
it(`should set 'Third - ${TITLE_SUFFIX}' when route has defined label equal to 'Third'`, async () => {
|
||||
spyOn(title, 'setTitle');
|
||||
|
||||
await router.navigateByUrl('/third');
|
||||
service.updateTitle(router.routerState.snapshot);
|
||||
|
||||
expect(title.setTitle).toHaveBeenCalledOnceWith('Third • Angular');
|
||||
});
|
||||
|
||||
it(`shouldn't take label from the parent route when current route label is not equal to ${DEFAULT_PAGE_TITLE}`, async () => {
|
||||
spyOn(title, 'setTitle');
|
||||
|
||||
await router.navigateByUrl('/third/child');
|
||||
service.updateTitle(router.routerState.snapshot);
|
||||
|
||||
expect(title.setTitle).toHaveBeenCalledOnceWith('Child • Angular');
|
||||
});
|
||||
|
||||
it(`should take label from the parent route when current route label is equal to ${DEFAULT_PAGE_TITLE}`, async () => {
|
||||
spyOn(title, 'setTitle');
|
||||
|
||||
await router.navigateByUrl('/fourth/child');
|
||||
service.updateTitle(router.routerState.snapshot);
|
||||
|
||||
expect(title.setTitle).toHaveBeenCalledOnceWith('Fourth • Overview • Angular');
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Injectable} from '@angular/core';
|
||||
import {NavigationItem} from '@angular/docs';
|
||||
import {Title} from '@angular/platform-browser';
|
||||
import {ActivatedRouteSnapshot, RouterStateSnapshot, TitleStrategy} from '@angular/router';
|
||||
|
||||
export const ROUTE_TITLE_PROPERTY = 'label';
|
||||
export const ROUTE_PARENT_PROPERTY = 'parent';
|
||||
export const TITLE_SUFFIX = 'Angular';
|
||||
export const TITLE_SEPARATOR = ' • ';
|
||||
export const DEFAULT_PAGE_TITLE = 'Overview';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class ADevTitleStrategy extends TitleStrategy {
|
||||
constructor(private readonly title: Title) {
|
||||
super();
|
||||
}
|
||||
|
||||
override updateTitle(routerState: RouterStateSnapshot) {
|
||||
const title = this.buildTitle(routerState);
|
||||
|
||||
if (title !== undefined) {
|
||||
this.title.setTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
override buildTitle(snapshot: RouterStateSnapshot): string {
|
||||
let route: ActivatedRouteSnapshot = snapshot.root;
|
||||
|
||||
while (route.firstChild) {
|
||||
route = route.firstChild;
|
||||
}
|
||||
|
||||
const data = route.data as NavigationItem;
|
||||
const routeTitle = data.label ?? '';
|
||||
|
||||
const prefix =
|
||||
routeTitle.startsWith(DEFAULT_PAGE_TITLE) && data.parent
|
||||
? `${data.parent.label}${TITLE_SEPARATOR}`
|
||||
: '';
|
||||
|
||||
return !!routeTitle ? `${prefix}${routeTitle}${TITLE_SEPARATOR}${TITLE_SUFFIX}` : TITLE_SUFFIX;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* Formats an `ErrorEvent` to a human-readable string that can
|
||||
* be sent to Google Analytics.
|
||||
*/
|
||||
export function formatErrorEventForAnalytics(event: ErrorEvent): string {
|
||||
const {message, filename, colno, lineno, error} = event;
|
||||
|
||||
if (error instanceof Error) {
|
||||
return formatErrorForAnalytics(error);
|
||||
}
|
||||
|
||||
return `${stripErrorMessagePrefix(message)}\n${filename}:` + `${lineno || '?'}:${colno || '?'}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an `Error` to a human-readable string that can be sent
|
||||
* to Google Analytics.
|
||||
*/
|
||||
export function formatErrorForAnalytics(error: Error): string {
|
||||
let stack = '<no-stack>';
|
||||
|
||||
if (error.stack) {
|
||||
stack = stripErrorMessagePrefix(error.stack)
|
||||
// strip the message from the stack trace, if present
|
||||
.replace(error.message + '\n', '')
|
||||
// strip leading spaces
|
||||
.replace(/^ +/gm, '')
|
||||
// strip all leading "at " for each frame
|
||||
.replace(/^at /gm, '')
|
||||
// replace long urls with just the last segment: `filename:line:column`
|
||||
.replace(/(?: \(|@)http.+\/([^/)]+)\)?(?:\n|$)/gm, '@$1\n')
|
||||
// replace "eval code" in Edge
|
||||
.replace(/ *\(eval code(:\d+:\d+)\)(?:\n|$)/gm, '@???$1\n');
|
||||
}
|
||||
|
||||
return `${error.message}\n${stack}`;
|
||||
}
|
||||
|
||||
/** Strips the error message prefix from a message or stack trace. */
|
||||
function stripErrorMessagePrefix(input: string): string {
|
||||
return input.replace(/^(Uncaught )?Error: /, '');
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Injector} from '@angular/core';
|
||||
import {ENVIRONMENT, WINDOW} from '@angular/docs';
|
||||
import {AnalyticsService} from './analytics.service';
|
||||
|
||||
describe('AnalyticsService', () => {
|
||||
let service: AnalyticsService;
|
||||
let injector: Injector;
|
||||
let gtagSpy: jasmine.Spy;
|
||||
let gtagAppendNodeSpy: jasmine.Spy;
|
||||
let windowOnErrorHandler: (event: ErrorEvent) => void;
|
||||
|
||||
let mockWindow: any;
|
||||
|
||||
beforeEach(() => {
|
||||
gtagSpy = jasmine.createSpy('gtag');
|
||||
gtagAppendNodeSpy = jasmine.createSpy('gtag.js script head attach');
|
||||
|
||||
mockWindow = {
|
||||
name: 'Some name',
|
||||
document: {
|
||||
head: {appendChild: gtagAppendNodeSpy},
|
||||
createElement: (tag: string) => document.createElement(tag),
|
||||
querySelector: (_gtagIdSelector: string) => null,
|
||||
},
|
||||
addEventListener: (_name: string, handler: typeof windowOnErrorHandler) =>
|
||||
(windowOnErrorHandler = handler),
|
||||
};
|
||||
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{provide: ENVIRONMENT, useValue: {}},
|
||||
{provide: AnalyticsService, deps: [WINDOW]},
|
||||
{provide: WINDOW, useFactory: () => mockWindow, deps: []},
|
||||
],
|
||||
});
|
||||
|
||||
service = injector.get(AnalyticsService);
|
||||
|
||||
// The `gtag` function is attached to the `Window`, so we can spy on it
|
||||
// after the service has been initialized.
|
||||
gtagSpy = spyOn(mockWindow, 'gtag');
|
||||
});
|
||||
|
||||
describe('error reporting', () => {
|
||||
it('should subscribe to window uncaught errors and report them', () => {
|
||||
spyOn(service, 'reportError');
|
||||
|
||||
windowOnErrorHandler(
|
||||
new ErrorEvent('error', {
|
||||
error: new Error('Test Error'),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(service.reportError).toHaveBeenCalledTimes(1);
|
||||
expect(service.reportError).toHaveBeenCalledWith(
|
||||
jasmine.stringContaining('Test Error\n'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should report errors to analytics by dispatching `gtag` and `ga` events', () => {
|
||||
gtagSpy.calls.reset();
|
||||
|
||||
windowOnErrorHandler(
|
||||
new ErrorEvent('error', {
|
||||
error: new Error('Test Error'),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(gtagSpy).toHaveBeenCalledTimes(1);
|
||||
expect(gtagSpy).toHaveBeenCalledWith(
|
||||
'event',
|
||||
'exception',
|
||||
jasmine.objectContaining({
|
||||
description: jasmine.stringContaining('Test Error\n'),
|
||||
fatal: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,89 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
|
||||
import {WINDOW, ENVIRONMENT} from '@angular/docs';
|
||||
|
||||
import {formatErrorEventForAnalytics} from './analytics-format-error';
|
||||
|
||||
/** Extension of `Window` with potential Google Analytics fields. */
|
||||
interface WindowWithAnalytics extends Window {
|
||||
dataLayer?: any[];
|
||||
gtag?(...args: any[]): void;
|
||||
}
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
/**
|
||||
* Google Analytics Service - captures app behaviors and sends them to Google Analytics.
|
||||
*
|
||||
* Associates data wi`th properties determined from the environment configurations:
|
||||
* - Data is uploaded to our main Google Analytics 4+ property.
|
||||
*/
|
||||
export class AnalyticsService {
|
||||
private environment = inject(ENVIRONMENT);
|
||||
private window: WindowWithAnalytics = inject(WINDOW);
|
||||
|
||||
constructor() {
|
||||
this._installGlobalSiteTag();
|
||||
this._installWindowErrorHandler();
|
||||
}
|
||||
|
||||
reportError(description: string, fatal = true) {
|
||||
// Limit descriptions to maximum of 150 characters.
|
||||
// See: https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#exd.
|
||||
description = description.substring(0, 150);
|
||||
|
||||
this._gtag('event', 'exception', {description, fatal});
|
||||
}
|
||||
|
||||
sendEvent(name: string, parameters: Record<string, string | boolean | number>) {
|
||||
this._gtag('event', name, parameters);
|
||||
}
|
||||
|
||||
private _gtag(...args: any[]) {
|
||||
if (this.window.gtag) {
|
||||
this.window.gtag(...args);
|
||||
}
|
||||
}
|
||||
|
||||
private _installGlobalSiteTag() {
|
||||
const window = this.window;
|
||||
const url = `https://www.googletagmanager.com/gtag/js?id=${this.environment.googleAnalyticsId}`;
|
||||
|
||||
// Note: This cannot be an arrow function as `gtag.js` expects an actual `Arguments`
|
||||
// instance with e.g. `callee` to be set. Do not attempt to change this and keep this
|
||||
// as much as possible in sync with the tracking code snippet suggested by the Google
|
||||
// Analytics 4 web UI under `Data Streams`.
|
||||
window.dataLayer = this.window.dataLayer || [];
|
||||
window.gtag = function () {
|
||||
window.dataLayer?.push(arguments);
|
||||
};
|
||||
window.gtag('js', new Date());
|
||||
|
||||
// Configure properties before loading the script. This is necessary to avoid
|
||||
// loading multiple instances of the gtag JS scripts.
|
||||
window.gtag('config', this.environment.googleAnalyticsId);
|
||||
|
||||
// Only add the element if `gtag` is not loaded yet. It might already
|
||||
// be inlined into the `index.html` via SSR.
|
||||
if (window.document.querySelector('#gtag-script') === null) {
|
||||
const el = window.document.createElement('script');
|
||||
el.async = true;
|
||||
el.src = url as string;
|
||||
el.id = 'gtag-script';
|
||||
window.document.head.appendChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
private _installWindowErrorHandler() {
|
||||
this.window.addEventListener('error', (event) =>
|
||||
this.reportError(formatErrorEventForAnalytics(event), true),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ErrorHandler, Injector, VERSION} from '@angular/core';
|
||||
|
||||
import {ReportingErrorHandler} from './reporting-error-handler';
|
||||
import {AnalyticsService} from './analytics.service';
|
||||
|
||||
describe('ReportingErrorHandler service', () => {
|
||||
let handler: ReportingErrorHandler;
|
||||
let superHandler: jasmine.Spy;
|
||||
let reportErrorSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
superHandler = spyOn(ErrorHandler.prototype, 'handleError');
|
||||
reportErrorSpy = jasmine.createSpy('analytics report error');
|
||||
|
||||
const injector = Injector.create({
|
||||
providers: [
|
||||
{provide: AnalyticsService, useValue: {reportError: reportErrorSpy}},
|
||||
{provide: ErrorHandler, useClass: ReportingErrorHandler, deps: [AnalyticsService]},
|
||||
],
|
||||
});
|
||||
|
||||
handler = injector.get(ErrorHandler) as unknown as ReportingErrorHandler;
|
||||
});
|
||||
|
||||
describe('handleError', () => {
|
||||
it('should call the super class handleError', () => {
|
||||
const error = new Error();
|
||||
handler.handleError(error);
|
||||
expect(superHandler).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
it('should cope with the super handler throwing an error', () => {
|
||||
const error = new Error('initial error');
|
||||
superHandler.and.throwError('super handler error');
|
||||
handler.handleError(error);
|
||||
|
||||
expect(reportErrorSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Error from super handler is reported first
|
||||
expect(reportErrorSpy.calls.argsFor(0)[0]).toEqual(
|
||||
jasmine.stringContaining('super handler error\n'),
|
||||
);
|
||||
|
||||
// Then error from initial exception
|
||||
expect(reportErrorSpy.calls.argsFor(1)[0]).toEqual(
|
||||
jasmine.stringContaining(`[v${VERSION.full}] initial error\n`),
|
||||
);
|
||||
});
|
||||
|
||||
it('should augment an error object with version info', () => {
|
||||
const originalMessage = 'this is an error message';
|
||||
const error = new Error(originalMessage);
|
||||
|
||||
expect(error.message).toBe(originalMessage);
|
||||
// NOTE:
|
||||
// Intentionally not checking `error.stack` here, because accessing it causes the property to
|
||||
// be computed and may affect the observed behavior of `handleError()`.
|
||||
|
||||
handler.handleError(error);
|
||||
|
||||
expect(error.message).toBe(`[v${VERSION.full}] ${originalMessage}`);
|
||||
if (error.stack) {
|
||||
const expected = `Error: [v${VERSION.full}] ${originalMessage}`;
|
||||
const actual = error.stack.split('\n', 1)[0];
|
||||
expect(actual.startsWith(expected))
|
||||
.withContext(`Expected '${actual}' to start with '${expected}'.`)
|
||||
.toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
it('should send an error object to analytics', () => {
|
||||
const error = new Error('this is an error message');
|
||||
handler.handleError(error);
|
||||
expect(reportErrorSpy).toHaveBeenCalledWith(
|
||||
jasmine.stringContaining('this is an error message\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should send a non-error object to analytics', () => {
|
||||
const error = {reason: 'this is an error message'};
|
||||
handler.handleError(error);
|
||||
expect(reportErrorSpy).toHaveBeenCalledWith(JSON.stringify(error));
|
||||
});
|
||||
|
||||
it('should send a non-error object with circular references to analytics', () => {
|
||||
const error = {
|
||||
reason: 'this is an error message',
|
||||
get self() {
|
||||
return this;
|
||||
},
|
||||
toString() {
|
||||
return `{reason: ${this.reason}}`;
|
||||
},
|
||||
};
|
||||
handler.handleError(error);
|
||||
expect(reportErrorSpy).toHaveBeenCalledWith('{reason: this is an error message}');
|
||||
});
|
||||
|
||||
it('should send an error string to analytics (prefixed with version info)', () => {
|
||||
const error = 'this is an error message';
|
||||
handler.handleError(error);
|
||||
expect(reportErrorSpy).toHaveBeenCalledWith(`[v${VERSION.full}] ${error}`);
|
||||
});
|
||||
|
||||
it('should send a non-object, non-string error stringified to analytics', () => {
|
||||
handler.handleError(404);
|
||||
handler.handleError(false);
|
||||
handler.handleError(null);
|
||||
handler.handleError(undefined);
|
||||
|
||||
expect(reportErrorSpy.calls.allArgs()).toEqual([['404'], ['false'], ['null'], ['undefined']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ErrorHandler, Injectable, VERSION} from '@angular/core';
|
||||
import {formatErrorForAnalytics} from './analytics-format-error';
|
||||
import {AnalyticsService} from './analytics.service';
|
||||
|
||||
/**
|
||||
* Extend the default error handling to report errors to an external service - e.g Google Analytics.
|
||||
*
|
||||
* Errors outside the Angular application may also be handled by `window.onerror`.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ReportingErrorHandler extends ErrorHandler {
|
||||
constructor(private _analytics: AnalyticsService) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send error info to Google Analytics, in addition to the default handling.
|
||||
*
|
||||
* @param error Information about the error.
|
||||
*/
|
||||
override handleError(error: any) {
|
||||
const versionedError = this.prefixErrorWithVersion(error);
|
||||
|
||||
try {
|
||||
super.handleError(versionedError);
|
||||
} catch (e) {
|
||||
this.reportError(e);
|
||||
}
|
||||
this.reportError(versionedError);
|
||||
}
|
||||
|
||||
private prefixErrorWithVersion<T>(error: T): T {
|
||||
const prefix = `[v${VERSION.full}] `;
|
||||
|
||||
if (error instanceof Error) {
|
||||
const oldMessage = error.message;
|
||||
const oldStack = error.stack;
|
||||
|
||||
error.message = prefix + oldMessage;
|
||||
error.stack = oldStack?.replace(oldMessage, error.message);
|
||||
} else if (typeof error === 'string') {
|
||||
error = (prefix + error) as unknown as T;
|
||||
}
|
||||
// If it is a different type, omit the version to avoid altering the original `error` object.
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
private reportError(error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
this._analytics.reportError(formatErrorForAnalytics(error));
|
||||
} else {
|
||||
if (typeof error === 'object') {
|
||||
try {
|
||||
error = JSON.stringify(error);
|
||||
} catch {
|
||||
// Ignore the error and just let it be stringified.
|
||||
}
|
||||
}
|
||||
this._analytics.reportError(`${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {ContentLoader} from './content-loader.service';
|
||||
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||
|
||||
describe('ContentLoader', () => {
|
||||
let service: ContentLoader;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [ContentLoader],
|
||||
imports: [HttpClientTestingModule],
|
||||
});
|
||||
service = TestBed.inject(ContentLoader);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {DocContent, DocsContentLoader} from '@angular/docs';
|
||||
import {Router} from '@angular/router';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class ContentLoader implements DocsContentLoader {
|
||||
private readonly cache = new Map<string, Promise<DocContent | undefined>>();
|
||||
private readonly httpClient = inject(HttpClient);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
async getContent(path: string): Promise<DocContent | undefined> {
|
||||
// If the path does not end with a file extension, add `.md.html` as the default
|
||||
if (!path.match(/\.\w+$/)) {
|
||||
path += '.md.html';
|
||||
}
|
||||
if (!this.cache.has(path)) {
|
||||
try {
|
||||
this.cache.set(
|
||||
path,
|
||||
this.httpClient
|
||||
.get(`assets/content/${path}`, {
|
||||
responseType: 'text',
|
||||
})
|
||||
.pipe(map((contents) => ({contents, id: path})))
|
||||
.toPromise(),
|
||||
);
|
||||
} catch {
|
||||
this.router.navigateByUrl('/404');
|
||||
}
|
||||
}
|
||||
return this.cache.get(path)!;
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {DOCUMENT, isPlatformServer} from '@angular/common';
|
||||
import {ErrorHandler, PLATFORM_ID, inject} from '@angular/core';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
import {AnalyticsService} from '../analytics/analytics.service';
|
||||
import {ErrorSnackBar, ErrorSnackBarData} from './error-snack-bar';
|
||||
|
||||
export class CustomErrorHandler implements ErrorHandler {
|
||||
snackBar = inject(MatSnackBar);
|
||||
document = inject(DOCUMENT);
|
||||
isServer = isPlatformServer(inject(PLATFORM_ID));
|
||||
analyticsService = inject(AnalyticsService);
|
||||
|
||||
get isOnline(): boolean {
|
||||
if (this.isServer) return false;
|
||||
|
||||
const win = this.document.defaultView;
|
||||
return win?.navigator.onLine ?? true;
|
||||
}
|
||||
|
||||
handleError(error: any) {
|
||||
if (typeof error.message === 'string') {
|
||||
// Just looking at the first line of the error message (ignoring the call stack part),
|
||||
// which should contain a pattern that we are looking for.
|
||||
const firstLine = error.message.split('\n')[0];
|
||||
if (this.isOnline && firstLine?.match(/chunk-(.*?)\.js/)) {
|
||||
// Trying to load a chunk that doesn't exist anymore
|
||||
// Users should reload the app.
|
||||
this.openErrorSnackBar();
|
||||
this.analyticsService.reportError('Chunk loading error');
|
||||
}
|
||||
}
|
||||
|
||||
// We still want to log every error
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
openErrorSnackBar(): void {
|
||||
this.snackBar
|
||||
.openFromComponent(ErrorSnackBar, {
|
||||
panelClass: 'docs-invert-mode',
|
||||
data: {
|
||||
message: `Our docs have been updated, reload the page to see the latest.`,
|
||||
actionText: `Reload`,
|
||||
} satisfies ErrorSnackBarData,
|
||||
})
|
||||
.onAction()
|
||||
.subscribe(() => {
|
||||
this.document.location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Component, Inject} from '@angular/core';
|
||||
import {MAT_SNACK_BAR_DATA, MatSnackBarAction, MatSnackBarRef} from '@angular/material/snack-bar';
|
||||
|
||||
export interface ErrorSnackBarData {
|
||||
message: string;
|
||||
actionText?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'error-snack-bar',
|
||||
template: `
|
||||
{{ message }}
|
||||
<button
|
||||
class="docs-primary-btn"
|
||||
type="button"
|
||||
matSnackBarAction
|
||||
[attr.text]="actionText"
|
||||
(click)="snackBarRef.dismissWithAction()"
|
||||
>
|
||||
{{ actionText }}
|
||||
</button>
|
||||
`,
|
||||
imports: [MatSnackBarAction],
|
||||
standalone: true,
|
||||
styles: `:host { display: flex; align-items: center; button { margin-left: 16px }}`,
|
||||
})
|
||||
export class ErrorSnackBar {
|
||||
protected message: string;
|
||||
protected actionText?: string;
|
||||
|
||||
constructor(
|
||||
protected snackBarRef: MatSnackBarRef<ErrorSnackBar>,
|
||||
@Inject(MAT_SNACK_BAR_DATA) public data: ErrorSnackBarData,
|
||||
) {
|
||||
this.message = data.message;
|
||||
this.actionText = data.actionText;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {ExampleContentLoader} from './example-content-loader.service';
|
||||
import {PREVIEWS_COMPONENTS} from '@angular/docs';
|
||||
|
||||
describe('ExampleContentLoader', () => {
|
||||
let service: ExampleContentLoader;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [ExampleContentLoader, {provide: PREVIEWS_COMPONENTS, useValue: []}],
|
||||
});
|
||||
service = TestBed.inject(ExampleContentLoader);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Injectable, Type, inject} from '@angular/core';
|
||||
import {PREVIEWS_COMPONENTS} from '@angular/docs';
|
||||
|
||||
@Injectable()
|
||||
export class ExampleContentLoader {
|
||||
private readonly previewsComponents = inject(PREVIEWS_COMPONENTS);
|
||||
|
||||
loadPreview(id: string): Promise<Type<unknown>> {
|
||||
return this.previewsComponents[id]();
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {
|
||||
DestroyRef,
|
||||
ENVIRONMENT_INITIALIZER,
|
||||
EnvironmentInjector,
|
||||
Injectable,
|
||||
Injector,
|
||||
Provider,
|
||||
ProviderToken,
|
||||
Type,
|
||||
createEnvironmentInjector,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
|
||||
/**
|
||||
* inject a service asynchronously
|
||||
*
|
||||
* @param: injector. If the injector is a NodeInjector the loaded module will be destroyed alonside its injector
|
||||
*/
|
||||
export async function injectAsync<T>(
|
||||
injector: Injector,
|
||||
providerLoader: () => Promise<ProviderToken<T>>,
|
||||
): Promise<T> {
|
||||
const injectImpl = injector.get(InjectAsyncImpl);
|
||||
return injectImpl.get(injector, providerLoader);
|
||||
}
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
class InjectAsyncImpl<T> {
|
||||
private overrides = new WeakMap(); // no need to cleanup
|
||||
override<T>(type: Type<T>, mock: Type<unknown>) {
|
||||
this.overrides.set(type, mock);
|
||||
}
|
||||
|
||||
async get(injector: Injector, providerLoader: () => Promise<ProviderToken<T>>): Promise<T> {
|
||||
const type = await providerLoader();
|
||||
|
||||
// Check if we have overrides, O(1), low overhead
|
||||
if (this.overrides.has(type)) {
|
||||
const override = this.overrides.get(type);
|
||||
return new override();
|
||||
}
|
||||
|
||||
if (!(injector instanceof EnvironmentInjector)) {
|
||||
// this is the DestroyRef of the component
|
||||
const destroyRef = injector.get(DestroyRef);
|
||||
|
||||
// This is the parent injector of the injector we're creating
|
||||
const environmentInjector = injector.get(EnvironmentInjector);
|
||||
|
||||
// Creating an environment injector to destroy it afterwards
|
||||
const newInjector = createEnvironmentInjector([type as Provider], environmentInjector);
|
||||
|
||||
// Destroy the injector to trigger DestroyRef.onDestroy on our service
|
||||
destroyRef.onDestroy(() => {
|
||||
newInjector.destroy();
|
||||
});
|
||||
|
||||
// We want to create the new instance of our service with our new injector
|
||||
injector = newInjector;
|
||||
}
|
||||
|
||||
return injector.get(type)!;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to mock the lazy loaded module in `injectAsync`
|
||||
*
|
||||
* @usage
|
||||
* TestBed.configureTestingModule({
|
||||
* providers: [
|
||||
* mockAsyncProvider(SandboxService, fakeSandboxService)
|
||||
* ]
|
||||
* });
|
||||
*/
|
||||
export function mockAsyncProvider<T>(type: Type<T>, mock: Type<unknown>) {
|
||||
return [
|
||||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
multi: true,
|
||||
useValue: () => {
|
||||
inject(InjectAsyncImpl).override(type, mock);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {ThemeManager} from './theme-manager.service';
|
||||
import {LOCAL_STORAGE} from '@angular/docs';
|
||||
|
||||
describe('ThemeManager', () => {
|
||||
let service: ThemeManager;
|
||||
let localStorageSpy: jasmine.SpyObj<Storage>;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorageSpy = jasmine.createSpyObj<Storage>('localStorage', ['getItem', 'setItem']);
|
||||
localStorageSpy.getItem.and.returnValue(null);
|
||||
localStorageSpy.setItem.and.returnValue();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: LOCAL_STORAGE,
|
||||
useValue: localStorageSpy,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should set theme based on device preferences (auto) when user did not set theme manually', () => {
|
||||
localStorageSpy.getItem.and.returnValue(null);
|
||||
|
||||
service = TestBed.inject(ThemeManager);
|
||||
|
||||
expect(service.theme()).toBe('auto');
|
||||
});
|
||||
|
||||
it('should set theme based on stored user preferences (dark) when user already set theme manually', () => {
|
||||
localStorageSpy.getItem.and.returnValue('dark');
|
||||
|
||||
service = TestBed.inject(ThemeManager);
|
||||
|
||||
expect(service.theme()).toBe('dark');
|
||||
});
|
||||
|
||||
it('should set theme based on stored user preferences (light) when user already set theme manually', () => {
|
||||
localStorageSpy.getItem.and.returnValue('light');
|
||||
|
||||
service = TestBed.inject(ThemeManager);
|
||||
|
||||
expect(service.theme()).toBe('light');
|
||||
});
|
||||
|
||||
it('should set theme based on stored user preferences (auto) when user already set theme manually', () => {
|
||||
localStorageSpy.getItem.and.returnValue('auto');
|
||||
|
||||
service = TestBed.inject(ThemeManager);
|
||||
|
||||
expect(service.theme()).toBe('auto');
|
||||
});
|
||||
});
|
||||
@@ -1,94 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {DOCUMENT, isPlatformBrowser} from '@angular/common';
|
||||
import {Injectable, PLATFORM_ID, inject, signal} from '@angular/core';
|
||||
import {LOCAL_STORAGE} from '@angular/docs';
|
||||
import {Subject} from 'rxjs';
|
||||
|
||||
// Keep these constants in sync with the code in index.html
|
||||
|
||||
export const THEME_PREFERENCE_LOCAL_STORAGE_KEY = 'themePreference';
|
||||
export const DARK_MODE_CLASS_NAME = 'docs-dark-mode';
|
||||
export const LIGHT_MODE_CLASS_NAME = 'docs-light-mode';
|
||||
export const PREFERS_COLOR_SCHEME_DARK = '(prefers-color-scheme: dark)';
|
||||
|
||||
export type Theme = 'dark' | 'light' | 'auto';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ThemeManager {
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly localStorage = inject(LOCAL_STORAGE);
|
||||
private readonly platformId = inject(PLATFORM_ID);
|
||||
|
||||
readonly theme = signal<Theme | null>(this.getThemeFromLocalStorageValue());
|
||||
// Zoneless - it's required to notify that theme was changed. It could be removed when signal-based components will be available.
|
||||
readonly themeChanged$ = new Subject<void>();
|
||||
|
||||
constructor() {
|
||||
if (!isPlatformBrowser(this.platformId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadThemePreference();
|
||||
this.watchPreferredColorScheme();
|
||||
}
|
||||
|
||||
setTheme(theme: Theme): void {
|
||||
this.theme.set(theme);
|
||||
this.setThemeInLocalStorage();
|
||||
this.setThemeBodyClasses(theme === 'auto' ? preferredScheme() : theme);
|
||||
}
|
||||
|
||||
// 1. Read theme preferences stored in localStorage
|
||||
// 2. In case when there are no stored user preferences, then read them from device preferences.
|
||||
private loadThemePreference(): void {
|
||||
const savedUserPreference = this.getThemeFromLocalStorageValue();
|
||||
const useTheme = savedUserPreference ?? 'auto';
|
||||
|
||||
this.theme.set(useTheme);
|
||||
this.setThemeBodyClasses(useTheme === 'auto' ? preferredScheme() : useTheme);
|
||||
}
|
||||
|
||||
// Set theme classes on the body element
|
||||
private setThemeBodyClasses(theme: 'dark' | 'light'): void {
|
||||
const documentClassList = this.document.documentElement.classList;
|
||||
if (theme === 'dark') {
|
||||
documentClassList.add(DARK_MODE_CLASS_NAME);
|
||||
documentClassList.remove(LIGHT_MODE_CLASS_NAME);
|
||||
} else {
|
||||
documentClassList.add(LIGHT_MODE_CLASS_NAME);
|
||||
documentClassList.remove(DARK_MODE_CLASS_NAME);
|
||||
}
|
||||
this.themeChanged$.next();
|
||||
}
|
||||
|
||||
private getThemeFromLocalStorageValue(): Theme | null {
|
||||
const theme = this.localStorage?.getItem(THEME_PREFERENCE_LOCAL_STORAGE_KEY) as Theme | null;
|
||||
return theme ?? null;
|
||||
}
|
||||
|
||||
private setThemeInLocalStorage(): void {
|
||||
if (this.theme()) {
|
||||
this.localStorage?.setItem(THEME_PREFERENCE_LOCAL_STORAGE_KEY, this.theme()!);
|
||||
}
|
||||
}
|
||||
|
||||
private watchPreferredColorScheme() {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => {
|
||||
const preferredScheme = event.matches ? 'dark' : 'light';
|
||||
this.setThemeBodyClasses(preferredScheme);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function preferredScheme(): 'dark' | 'light' {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {VersionManager} from './version-manager.service';
|
||||
|
||||
describe('VersionManager', () => {
|
||||
let service: VersionManager;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(VersionManager);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,138 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Injectable, VERSION, computed, signal} from '@angular/core';
|
||||
|
||||
// TODO(josephperrott): extract this out of the file into a managed location.
|
||||
const VERSIONS_CONFIG = {
|
||||
currentVersion: 'stable',
|
||||
historicalVersionsLinkPattern: 'https://v{{version}}.angular.dev',
|
||||
mainVersions: [
|
||||
{
|
||||
version: 'stable',
|
||||
url: 'https://angular.dev',
|
||||
},
|
||||
{
|
||||
version: 'v16',
|
||||
url: 'https://v16.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v15',
|
||||
url: 'https://v15.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v14',
|
||||
url: 'https://v14.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v13',
|
||||
url: 'https://v13.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v12',
|
||||
url: 'https://v12.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v11',
|
||||
url: 'https://v11.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v10',
|
||||
url: 'https://v10.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v9',
|
||||
url: 'https://v9.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v8',
|
||||
url: 'https://v8.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v7',
|
||||
url: 'https://v7.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v6',
|
||||
url: 'https://v6.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v5',
|
||||
url: 'https://v5.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v4',
|
||||
url: 'https://v4.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v2',
|
||||
url: 'https://v2.angular.io/docs',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export interface Version {
|
||||
displayName: string;
|
||||
version: VersionMode;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type VersionMode = 'stable' | 'rc' | 'next' | number;
|
||||
|
||||
export const INITIAL_DOCS_VERSION = 17;
|
||||
export const VERSION_PATTERN_PLACEHOLDER = '{{version}}';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class VersionManager {
|
||||
versions = signal<Version[]>([
|
||||
...VERSIONS_CONFIG.mainVersions.map((item) =>
|
||||
this.mapToVersion(item as Pick<Version, 'url' | 'version'>),
|
||||
),
|
||||
...this.getHistoricalVersions(),
|
||||
]);
|
||||
|
||||
currentDocsVersion = computed(() => {
|
||||
return this.versions().find(
|
||||
(version) => version.version.toString() === VERSIONS_CONFIG.currentVersion,
|
||||
);
|
||||
});
|
||||
|
||||
private getHistoricalVersions(): Version[] {
|
||||
const historicalVersions: Version[] = [];
|
||||
for (let version = Number(VERSION.major) - 1; version >= INITIAL_DOCS_VERSION; version--) {
|
||||
historicalVersions.push({
|
||||
url: VERSIONS_CONFIG.historicalVersionsLinkPattern.replace(
|
||||
VERSION_PATTERN_PLACEHOLDER,
|
||||
version.toString(),
|
||||
),
|
||||
displayName: this.getVersion(version),
|
||||
version,
|
||||
});
|
||||
}
|
||||
return historicalVersions;
|
||||
}
|
||||
|
||||
private mapToVersion(value: Pick<Version, 'url' | 'version'>): Version {
|
||||
return {
|
||||
...value,
|
||||
displayName: this.getVersion(value.version),
|
||||
};
|
||||
}
|
||||
|
||||
private getVersion(versionMode: VersionMode): string {
|
||||
if (versionMode === 'stable') {
|
||||
return `v${VERSION.major}`;
|
||||
}
|
||||
if (Number.isInteger(versionMode)) {
|
||||
return `v${versionMode}`;
|
||||
}
|
||||
return versionMode.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
# EmbeddedEditor components, services and functionality
|
||||
|
||||
- [Scenarios](#scenarios)
|
||||
- [Loading a project](#loading-a-project)
|
||||
- [Updating the code](#updating-the-code)
|
||||
- [Creating a new file](#creating-a-new-file)
|
||||
- [Deleting a file](#deleting-a-file)
|
||||
- [Switching a project](#switching-a-project)
|
||||
- [Components and services](#components-and-services)
|
||||
|
||||
- [EmbeddedEditor](#EmbeddedEditor)
|
||||
- [CodeEditor](#CodeEditor)
|
||||
- [CodeMirrorEditor](#CodeMirrorEditor)
|
||||
- [TypeScript Web Worker](#typescript-web-worker)
|
||||
- [Preview](#Preview)
|
||||
- [Terminal](#Terminal)
|
||||
- [InteractiveTerminal](#InteractiveTerminal)
|
||||
- [Console](#Console)
|
||||
- [NodeRuntimeSandbox](#NodeRuntimeSandbox)
|
||||
- [NodeRuntimeState](#NodeRuntimeState)
|
||||
- [EmbeddedTutorialManager](#EmbeddedTutorialManager)
|
||||
- [EditorUiState](#EditorUiState)
|
||||
- [DownloadManager](#DownloadManager)
|
||||
- [AlertManager](#AlertManager)
|
||||
- [TypingsLoader](#TypingsLoader)
|
||||
|
||||
## External libraries
|
||||
|
||||
- [WebContainers API](https://webcontainers.io/)
|
||||
- [CodeMirror](https://codemirror.net/)
|
||||
- [@typescript/vfs](https://www.npmjs.com/package/@typescript/vfs)
|
||||
- [Xterm.js](https://xtermjs.org/)
|
||||
|
||||
## Notes
|
||||
|
||||
- See [scripts/tutorials/README.md](/scripts/tutorials/README.md) for more information about the tutorials script.
|
||||
- See [adev/src/content/tutorials/README.md](/adev/src/content/tutorials/README.md) for more information about the tutorials content.
|
||||
|
||||
---
|
||||
|
||||
## Scenarios
|
||||
|
||||
### Loading a project
|
||||
|
||||
1. The page responsible for the embedded editor lazy loads the [`EmbeddedEditor`](./embedded-editor.component.ts) component and the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts), then triggers the initialization of all components and services. The embedded editor is available in the following pages:
|
||||
|
||||
- homepage: https://angular.dev
|
||||
- playground: https://angular.dev/playground
|
||||
- tutorial pages: https://angular.dev/tutorials
|
||||
|
||||
2. The project assets are fetched by the [`EmbeddedTutorialManager`](./embedded-tutorial-manager.service.ts). Meanwhile:
|
||||
|
||||
- The code editor is initialized
|
||||
- The code editor initializes the TypeScript Web Worker, which initializes the "default file system map" using TypeScript's CDN.
|
||||
- The WebContainer is initialized
|
||||
- The terminal is initialized
|
||||
|
||||
3. The tutorial source code is mounted in the `WebContainer`'s filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts)
|
||||
4. The tutorial project dependencies are installed by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
|
||||
5. The development server is started by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts) and the types are loaded by the [`TypingsLoader`](./typings-loader.service.ts) service.
|
||||
6. The preview is loaded with the URL provided by the WebContainer API after the development server is started.
|
||||
7. The project is ready.
|
||||
|
||||
### Updating the code
|
||||
|
||||
1. The user update the code in the code editor.
|
||||
2. The code editor state is updated on real time, without debouncing so that the user can see the changes in the code editor and CodeMirror can handle the changes accordingly.
|
||||
3. At the same time, the changes are sent to the TypeScript web worker to provide diagnostics, autocomplete and type features as soon as possible.
|
||||
4. The code changes are debounced to be written in the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
|
||||
5. After the debounce time is reached, the code changes are written in the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts), then the user can see the changes in the preview.
|
||||
|
||||
### Creating a new file
|
||||
|
||||
1. The user clicks on the new file button.
|
||||
2. The new file tab is opened.
|
||||
3. The user types the new file name.
|
||||
4. If the file name is valid, the file is created in the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
|
||||
|
||||
- `..` is disallowed in the file name to prevent users to create files outside the `src` directory.
|
||||
|
||||
5. The file is added to the TypeScript virtual file system, allowing the TypeScript web worker to provide diagnostics, autocomplete and type features for the new file. Also, exports from the new file are available in other files.
|
||||
6. The new file is added as the last tab in the code editor and the new file can be edited.
|
||||
|
||||
Note: If the new file name matches a file that already exists but is hidden in the code editor, the content for that file will show up in the created file. An example for a file that always exists is `index.html`.
|
||||
|
||||
### Deleting a file
|
||||
|
||||
1. The user clicks on the delete file button.
|
||||
2. The file is deleted from the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
|
||||
3. The file is removed from the TypeScript virtual file system.
|
||||
4. The file is removed from the code editor tabs.
|
||||
|
||||
Note: Some files can't be deleted to prevent users to break the app, being `src/main.ts`and `src/index.html`
|
||||
|
||||
### Switching a project
|
||||
|
||||
The embedded editor considers a project change when the embedded editor was already initialized and the user changes the page in the following scenarios:
|
||||
|
||||
- Navigating through tutorial steps
|
||||
- Going from the homepage after the embedded editor is initialized to the playground
|
||||
- Going from a tutorial page to the playground
|
||||
- Going from a tutorial page to the homepage
|
||||
- Going from the playground to the homepage
|
||||
|
||||
When a project change is detected, the [`EmbeddedTutorialManager`](./embedded-tutorial-manager.service.ts) emits the `tutorialChanged` observable, which is listened in multiple sub-components and services, then each component/service performs the necessary operations to switch the project.
|
||||
|
||||
The following steps are executed on project change:
|
||||
|
||||
1. The new project files are fetched by the [`EmbeddedTutorialManager`](./embedded-tutorial-manager.service.ts).
|
||||
2. The new project files are mounted in the WebContainer filesystem.
|
||||
3. The TypeScript virtual filesystem is updated with the new files and contents.
|
||||
4. The previous project and new project files are compared.
|
||||
1. Files that are not available in the new project are deleted from the WebContainer filesystem.
|
||||
2. Files that have the same path and name have their content replaced on the previous step when the files are mounted.
|
||||
5. The previous project dependencies are compared with the new project dependencies.
|
||||
1. If there are differences, a `npm install` is triggered, hiding the preview and going to the install loading step.
|
||||
2. If there are no differences, the project is ready.
|
||||
6. Some states are resetted, for example the "reveal answer" state if the previous project was in the "reveal answer" state.
|
||||
|
||||
## Components and services
|
||||
|
||||
### [`EmbeddedEditor`](./embedded-editor.component.ts)
|
||||
|
||||
The embedded editor is the parent component that holds all the components and services that compose the embedded editor.
|
||||
|
||||
#### [`CodeEditor`](./code-editor/code-editor.component.ts)
|
||||
|
||||
The component that holds the code editor view and the code editor state.
|
||||
|
||||
##### [`CodeMirrorEditor`](./code-editor/code-mirror-editor.service.ts)
|
||||
|
||||
[CodeMirror](https://codemirror.net/) is the library used to handle the code editor.
|
||||
|
||||
The `CodeMirrorEditor` service manages the CodeMirror instance and all the interactions with the library used to handle the code editor.
|
||||
|
||||
- handle the file edits and the CodeMirror view and state
|
||||
- handle the current project files in the code editor
|
||||
- handle the file creations and deletions
|
||||
- handle the file changes
|
||||
- handle all the CodeMirror specific events and extensions
|
||||
|
||||
###### [TypeScript Web Worker](./code-editor/workers/typescript-vfs.worker.ts)
|
||||
|
||||
The TypeScript features are provided by the TypeScript web worker, that is initialized by the `CodeMirrorEditor` service.
|
||||
|
||||
The TypeScript web worker uses `@typescript/vfs` and the TypeScript language service to provide diagnostics, autocomplete and type features.
|
||||
|
||||
#### [`Preview`](./preview/preview.component.ts)
|
||||
|
||||
The preview component manages the `iframe` responsible for displaying the tutorial project preview, with the URL provided by the WebContainer API after the development server is started.
|
||||
|
||||
While the project is being initialized, the preview displays the loading state.
|
||||
|
||||
#### [`Terminal`](./terminal/terminal.component.ts)
|
||||
|
||||
[Xterm.js](https://xtermjs.org/) is the library used to handle the terminals.
|
||||
|
||||
The terminal component handles the Xterm.js instance for the console and for the interactive terminal.
|
||||
|
||||
##### [`InteractiveTerminal`](./terminal/interactive-terminal.ts)
|
||||
|
||||
The interactive terminal is the terminal where the user can interact with the terminal and run commands, supporting only commands for the Angular CLI.
|
||||
|
||||
##### Console
|
||||
|
||||
The console displays the output for `npm install` and `ng serve`.
|
||||
|
||||
#### [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts)
|
||||
|
||||
Responsible for managing the WebContainer instance and all communication with its API. This service handles:
|
||||
|
||||
- the WebContainer instance
|
||||
- all Node.js scripts
|
||||
- the WebContainer filesystem, mounting the tutorial project files, writing new content, deleting and creating files.
|
||||
- the terminal session, reading and processing user inputs.
|
||||
- the tutorial project dependencies, installing the dependencies.
|
||||
- the processes running inside the WebContainer, being the npm scripts to install the dependencies, run the development server and the user inputs for the `ng` CLI.
|
||||
|
||||
##### [`NodeRuntimeState`](./node-runtime-state.service.ts)
|
||||
|
||||
Manages the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts) loading and error state.
|
||||
|
||||
#### [`EmbeddedTutorialManager`](./embedded-tutorial-manager.service.ts)
|
||||
|
||||
Manages the tutorial assets, being responsible for fetching the tutorial source code and metadata.
|
||||
|
||||
The source code is mounted in the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
|
||||
|
||||
The metadata is used to manage the project, handle the project changes and the user interactivity with the app.
|
||||
|
||||
This service also handles the reveal answer and reset reveal answer feature.
|
||||
|
||||
#### [`EditorUiState`](./editor-ui-state.service.ts)
|
||||
|
||||
Manages the editor UI state, being responsible for handling the user interactions with the editor tabs, switching between the preview, the terminal and the console.
|
||||
|
||||
#### [`DownloadManager`](./download-manager.service.ts)
|
||||
|
||||
Responsible for handling the download button in the embedded editor, fetching the tutorial project files and generating a zip file with the project content.
|
||||
|
||||
#### [`AlertManager`](./alert-manager.service.ts)
|
||||
|
||||
Manage the alerts displayed in the embedded editor, being the out of memory alert when multiple tabs are opened, and unsupported enviroments alerts.
|
||||
|
||||
#### [`TypingsLoader`](./typings-loader.service.ts)
|
||||
|
||||
Manages the types definitions for the code editor.
|
||||
@@ -1,105 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {LOCAL_STORAGE, WINDOW, isMobile} from '@angular/docs';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
import {ErrorSnackBar, ErrorSnackBarData} from '../core/services/errors-handling/error-snack-bar';
|
||||
|
||||
export const MAX_RECOMMENDED_WEBCONTAINERS_INSTANCES = 3;
|
||||
export const WEBCONTAINERS_COUNTER_KEY = 'numberOfWebcontainers';
|
||||
|
||||
export enum AlertReason {
|
||||
OUT_OF_MEMORY,
|
||||
MOBILE,
|
||||
}
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class AlertManager {
|
||||
private readonly localStorage = inject(LOCAL_STORAGE);
|
||||
private readonly window = inject(WINDOW);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
|
||||
init(): void {
|
||||
this.listenToLocalStorageValuesChange();
|
||||
|
||||
this.increaseInstancesCounter();
|
||||
|
||||
this.decreaseInstancesCounterOnPageClose();
|
||||
|
||||
this.checkDevice();
|
||||
}
|
||||
|
||||
private listenToLocalStorageValuesChange(): void {
|
||||
this.window.addEventListener('storage', () => {
|
||||
const countOfRunningInstances = this.getStoredCountOfWebcontainerInstances();
|
||||
|
||||
this.validateRunningInstances(countOfRunningInstances);
|
||||
});
|
||||
}
|
||||
|
||||
// Increase count of the running instances of the webcontainers when user will boot the webcontainer
|
||||
private increaseInstancesCounter(): void {
|
||||
const countOfRunningInstances = this.getStoredCountOfWebcontainerInstances() + 1;
|
||||
|
||||
this.localStorage?.setItem(WEBCONTAINERS_COUNTER_KEY, countOfRunningInstances.toString());
|
||||
this.validateRunningInstances(countOfRunningInstances);
|
||||
}
|
||||
|
||||
// Decrease count of running instances of the webcontainers when user close the app.
|
||||
private decreaseInstancesCounterOnPageClose(): void {
|
||||
this.window.addEventListener('beforeunload', () => {
|
||||
const countOfRunningInstances = this.getStoredCountOfWebcontainerInstances() - 1;
|
||||
|
||||
this.localStorage?.setItem(WEBCONTAINERS_COUNTER_KEY, countOfRunningInstances.toString());
|
||||
this.validateRunningInstances(countOfRunningInstances);
|
||||
});
|
||||
}
|
||||
|
||||
private getStoredCountOfWebcontainerInstances(): number {
|
||||
const countStoredInLocalStorage = this.localStorage?.getItem(WEBCONTAINERS_COUNTER_KEY);
|
||||
|
||||
if (!countStoredInLocalStorage || Number.isNaN(countStoredInLocalStorage)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Number(countStoredInLocalStorage);
|
||||
}
|
||||
|
||||
private validateRunningInstances(countOfRunningInstances: number): void {
|
||||
if (countOfRunningInstances > MAX_RECOMMENDED_WEBCONTAINERS_INSTANCES) {
|
||||
this.openSnackBar(AlertReason.OUT_OF_MEMORY);
|
||||
}
|
||||
}
|
||||
|
||||
private checkDevice() {
|
||||
if (isMobile) {
|
||||
this.openSnackBar(AlertReason.MOBILE);
|
||||
}
|
||||
}
|
||||
|
||||
private openSnackBar(reason: AlertReason) {
|
||||
let message = '';
|
||||
switch (reason) {
|
||||
case AlertReason.OUT_OF_MEMORY:
|
||||
message = `Your browser is currently limiting the memory available to run the Angular Tutorials or Playground. If you have multiple tabs open with Tutorials or Playground, please close some of them and refresh this page.`;
|
||||
break;
|
||||
case AlertReason.MOBILE:
|
||||
message = `You are running the embedded editor in a mobile device, this may result in an Out of memory error.`;
|
||||
break;
|
||||
}
|
||||
|
||||
this.snackBar.openFromComponent(ErrorSnackBar, {
|
||||
panelClass: 'docs-invert-mode',
|
||||
data: {
|
||||
message,
|
||||
actionText: 'I understand',
|
||||
} satisfies ErrorSnackBarData,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
<!-- Code Editor Tabs -->
|
||||
<div class="docs-code-editor-tabs">
|
||||
<div class="adev-tabs-and-plus">
|
||||
<mat-tab-group animationDuration="0ms" mat-stretch-tabs="false">
|
||||
<!--
|
||||
Hint: we would like to keep only one instance of #codeEditorWrapper,
|
||||
that's why we're not placing this element as content of mat-tab, just to
|
||||
not call another time init method from CodeMirrorEditor service.
|
||||
-->
|
||||
@for (file of files(); track file) {
|
||||
<mat-tab #tab>
|
||||
<ng-template mat-tab-label>
|
||||
{{ file.filename.replace('src/', '') }}
|
||||
@if (tab.isActive && canDeleteFile(file.filename)) {
|
||||
<button
|
||||
class="docs-delete-file"
|
||||
aria-label="Delete file"
|
||||
(click)="deleteFile(file.filename)"
|
||||
>
|
||||
<docs-icon>delete</docs-icon>
|
||||
</button>
|
||||
}
|
||||
</ng-template>
|
||||
</mat-tab>
|
||||
} @if (isCreatingFile()) {
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<form (submit)="createFile($event)">
|
||||
<input
|
||||
name="new-file"
|
||||
class="adev-new-file-input"
|
||||
#createFileInput
|
||||
(keydown)="$event.stopPropagation()"
|
||||
/>
|
||||
</form>
|
||||
</ng-template>
|
||||
</mat-tab>
|
||||
}
|
||||
</mat-tab-group>
|
||||
|
||||
<button class="adev-add-file" (click)="onAddButtonClick()" aria-label="Add a new file">
|
||||
<docs-icon>add</docs-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="adev-editor-download-button"
|
||||
type="button"
|
||||
(click)="downloadCurrentCodeEditorState()"
|
||||
aria-label="Download current code in editor"
|
||||
>
|
||||
<docs-icon>download</docs-icon>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Code Editor -->
|
||||
<div #codeEditorWrapper class="adev-code-editor-wrapper"></div>
|
||||
|
||||
@if (displayErrorsBox()) {
|
||||
<div class="adev-inline-errors-box">
|
||||
<button type="button" (click)="closeErrorsBox()">
|
||||
<docs-icon class="docs-icon_high-contrast">close</docs-icon>
|
||||
</button>
|
||||
<ul>
|
||||
@for (error of errors(); track error) {
|
||||
<li>(line: {{ error.lineNumber }}:{{ error.characterPosition }}) {{ error.message }}</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
:host {
|
||||
// backgrounds
|
||||
--code-editor-selection-background: color-mix(
|
||||
in srgb,
|
||||
var(--selection-background) 5%,
|
||||
var(--octonary-contrast)
|
||||
);
|
||||
--code-editor-focused-selection-background: color-mix(
|
||||
in srgb,
|
||||
var(--selection-background) 12%,
|
||||
var(--octonary-contrast)
|
||||
);
|
||||
|
||||
// text base colors
|
||||
--code-editor-text-base-color: var(--primary-contrast);
|
||||
|
||||
// tooltips
|
||||
--code-editor-tooltip-background: color-mix(
|
||||
in srgb,
|
||||
var(--bright-blue),
|
||||
var(--page-background) 90%
|
||||
);
|
||||
--code-editor-tooltip-color: var(--primary-contrast);
|
||||
--code-editor-tooltip-border: 1px solid
|
||||
color-mix(in srgb, var(--bright-blue), var(--page-background) 70%);
|
||||
--code-editor-tooltip-border-radius: 0.25rem;
|
||||
|
||||
// autocomplete
|
||||
--code-editor-autocomplete-item-background: var(--senary-contrast);
|
||||
--code-editor-autocomplete-item-color: var(--primary-contrast);
|
||||
|
||||
// cursor
|
||||
--code-name: var(--primary-contrast);
|
||||
--code-editor-cursor-color: var(--code-name);
|
||||
--code-variable-name: var(--bright-blue);
|
||||
--code-property-name: var(--code-name);
|
||||
--code-definition-keyword: var(--electric-violet);
|
||||
|
||||
// comments
|
||||
--code-comment: var(--electric-violet);
|
||||
--code-line-comment: var(--symbolic-gray);
|
||||
--code-block-comment: var(--symbolic-brown);
|
||||
--code-doc-comment: var(--code-comment);
|
||||
|
||||
// keywords
|
||||
--code-keyword: var(--electric-violet);
|
||||
--code-modifier: var(--code-keyword);
|
||||
--code-operator-keyword: var(--code-keyword);
|
||||
--code-control-keyword: var(--code-keyword);
|
||||
--code-module-keyword: var(--code-keyword);
|
||||
|
||||
// structural
|
||||
--code-brace: var(--vivid-pink);
|
||||
|
||||
// Misc
|
||||
--code-bool: var(--bright-blue);
|
||||
--code-string: var(--orange-red);
|
||||
--code-regexp: var(--orange-red);
|
||||
|
||||
--code-tags: var(--bright-blue);
|
||||
--code-component: var(--primary-contrast);
|
||||
--code-type-name: var(--vivid-pink);
|
||||
--code-self: var(--orange-red);
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.docs-code-editor-tabs {
|
||||
display: flex;
|
||||
background: var(--octonary-contrast);
|
||||
border-block-end: 1px solid var(--senary-contrast);
|
||||
transition: background 0.3s ease, border 0.3s ease;
|
||||
}
|
||||
|
||||
.adev-tabs-and-plus {
|
||||
display: flex;
|
||||
width: calc(100% - 2.875rem);
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.adev-add-file,
|
||||
.docs-delete-file {
|
||||
docs-icon {
|
||||
color: var(--gray-400);
|
||||
transition: color 0.3s ease;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
&:hover {
|
||||
docs-icon {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.docs-delete-file {
|
||||
padding-inline-start: 0.1rem;
|
||||
padding-inline-end: 0;
|
||||
margin-block-start: 0.2rem;
|
||||
docs-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.adev-new-file-input {
|
||||
color: var(--primary-contrast);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
border-block-end: 1px solid var(--senary-contrast);
|
||||
background: transparent;
|
||||
outline: none;
|
||||
transition: color 0.3s ease;
|
||||
&:focus {
|
||||
background: transparent;
|
||||
border-block-end: 1px solid var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
.adev-code-editor-wrapper {
|
||||
// adjust height for terminal tabs
|
||||
// so that scroll bar & content do not render under tabs
|
||||
height: calc(100% - 49px);
|
||||
|
||||
&-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.adev-inline-errors-box {
|
||||
position: absolute;
|
||||
bottom: 0.5rem;
|
||||
left: 0.5rem;
|
||||
right: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
background-color: color-mix(in srgb, var(--bright-blue), var(--page-background) 90%);
|
||||
border: 1px solid color-mix(in srgb, var(--bright-blue), var(--page-background) 70%);
|
||||
border-radius: 0.25rem;
|
||||
|
||||
button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 0.25rem;
|
||||
|
||||
docs-icon {
|
||||
font-size: 1.25rem;
|
||||
color: var(--quaternary-contrast);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
docs-icon {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-inline: 1.25rem;
|
||||
color: var(--tertiary-contrast);
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.adev-editor-download-button {
|
||||
padding: 0;
|
||||
width: 2.875rem;
|
||||
border-radius: 0 0.19rem 0 0;
|
||||
background: var(--octonary-contrast);
|
||||
border-inline-start: 1px solid var(--senary-contrast);
|
||||
|
||||
docs-icon {
|
||||
color: var(--gray-400);
|
||||
transition: color 0.3s ease;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
docs-icon {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {HarnessLoader} from '@angular/cdk/testing';
|
||||
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
|
||||
import {ChangeDetectorRef, signal} from '@angular/core';
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {MatTabGroupHarness} from '@angular/material/tabs/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
import {EmbeddedTutorialManager} from '../embedded-tutorial-manager.service';
|
||||
|
||||
import {CodeEditor, REQUIRED_FILES} from './code-editor.component';
|
||||
import {CodeMirrorEditor} from './code-mirror-editor.service';
|
||||
import {FakeChangeDetectorRef} from '@angular/docs/testing';
|
||||
import {TutorialType} from '@angular/docs';
|
||||
|
||||
const files = [
|
||||
{filename: 'a', content: '', language: {} as any},
|
||||
{filename: 'b', content: '', language: {} as any},
|
||||
{filename: 'c', content: '', language: {} as any},
|
||||
...Array.from(REQUIRED_FILES).map((filename) => ({
|
||||
filename,
|
||||
content: '',
|
||||
language: {} as any,
|
||||
})),
|
||||
];
|
||||
|
||||
class FakeCodeMirrorEditor implements Partial<CodeMirrorEditor> {
|
||||
init(element: HTMLElement) {}
|
||||
changeCurrentFile(fileName: string) {}
|
||||
disable() {}
|
||||
files = signal(files);
|
||||
currentFile = signal(this.files()[0]);
|
||||
openFiles = this.files;
|
||||
}
|
||||
const codeMirrorEditorService = new FakeCodeMirrorEditor();
|
||||
const fakeChangeDetectorRef = new FakeChangeDetectorRef();
|
||||
|
||||
describe('CodeEditor', () => {
|
||||
let component: CodeEditor;
|
||||
let fixture: ComponentFixture<CodeEditor>;
|
||||
let loader: HarnessLoader;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CodeEditor, NoopAnimationsModule],
|
||||
providers: [
|
||||
{
|
||||
provide: CodeMirrorEditor,
|
||||
useValue: codeMirrorEditorService,
|
||||
},
|
||||
{
|
||||
provide: ChangeDetectorRef,
|
||||
useValue: fakeChangeDetectorRef,
|
||||
},
|
||||
{
|
||||
provide: EmbeddedTutorialManager,
|
||||
useValue: {
|
||||
tutorialChanged$: new BehaviorSubject(true),
|
||||
tutorialId: () => 'tutorial',
|
||||
tutorialFilesystemTree: () => ({'app.component.ts': ''}),
|
||||
commonFilesystemTree: () => ({'app.component.ts': ''}),
|
||||
openFiles: () => ['app.component.ts'],
|
||||
tutorialFiles: () => ({'app.component.ts': ''}),
|
||||
nextTutorial: () => 'next-tutorial',
|
||||
previousTutorial: () => 'previous-tutorial',
|
||||
type: () => TutorialType.EDITOR,
|
||||
title: () => 'Tutorial',
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CodeEditor);
|
||||
loader = TestbedHarnessEnvironment.loader(fixture);
|
||||
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should initialize the code editor service afterViewInit with the code editor wrapper element', () => {
|
||||
const codeMirrorEditorInitSpy = spyOn(codeMirrorEditorService, 'init');
|
||||
|
||||
component.ngAfterViewInit();
|
||||
|
||||
expect(codeMirrorEditorInitSpy).toHaveBeenCalledWith(
|
||||
component['codeEditorWrapperRef'].nativeElement,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render tabs based on filenames order', async () => {
|
||||
component.ngAfterViewInit();
|
||||
|
||||
const matTabGroup = await loader.getHarness(MatTabGroupHarness);
|
||||
const tabs = await matTabGroup.getTabs();
|
||||
const expectedLabels = files.map((file, index) => {
|
||||
const label = file.filename.replace('src/', '');
|
||||
if (index === 0) return `${label} delete`;
|
||||
return label;
|
||||
});
|
||||
|
||||
for (const [index, tab] of tabs.entries()) {
|
||||
const tabLabel = await tab.getLabel();
|
||||
|
||||
expect(tabLabel).toBe(expectedLabels[index]);
|
||||
}
|
||||
});
|
||||
|
||||
describe('Tabs selection', () => {
|
||||
let codeMirrorEditorChangeCurrentFileSpy: jasmine.Spy<(fileName: string) => void>;
|
||||
|
||||
beforeEach(() => {
|
||||
codeMirrorEditorChangeCurrentFileSpy = spyOn(codeMirrorEditorService, 'changeCurrentFile');
|
||||
component.ngAfterViewInit();
|
||||
});
|
||||
|
||||
it('should change file content when clicking on an unselected tab', async () => {
|
||||
const matTabGroup = await loader.getHarness(MatTabGroupHarness);
|
||||
const tabs = await matTabGroup.getTabs();
|
||||
|
||||
expect(await tabs[0].isSelected());
|
||||
await tabs[1].select();
|
||||
|
||||
expect(codeMirrorEditorChangeCurrentFileSpy).toHaveBeenCalledWith(files[1].filename);
|
||||
});
|
||||
|
||||
it('should not change file content when clicking on a selected tab', async () => {
|
||||
const matTabGroup = await loader.getHarness(MatTabGroupHarness);
|
||||
const tabs = await matTabGroup.getTabs();
|
||||
|
||||
expect(await tabs[0].isSelected());
|
||||
await tabs[0].select();
|
||||
|
||||
expect(codeMirrorEditorChangeCurrentFileSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should focused on a new tab when adding a new file', async () => {
|
||||
const button = fixture.debugElement.query(By.css('button.adev-add-file')).nativeElement;
|
||||
button.click();
|
||||
|
||||
const matTabGroup = await loader.getHarness(MatTabGroupHarness);
|
||||
const tabs = await matTabGroup.getTabs();
|
||||
|
||||
expect(await tabs[files.length].isSelected()).toBeTrue();
|
||||
});
|
||||
|
||||
it('should change file content when clicking on unselected tab while creating a new file', async () => {
|
||||
const button = fixture.debugElement.query(By.css('button.adev-add-file')).nativeElement;
|
||||
button.click();
|
||||
|
||||
const matTabGroup = await loader.getHarness(MatTabGroupHarness);
|
||||
const tabs = await matTabGroup.getTabs();
|
||||
await tabs[2].select();
|
||||
|
||||
expect(codeMirrorEditorChangeCurrentFileSpy).toHaveBeenCalledWith(files[2].filename);
|
||||
});
|
||||
|
||||
it('start creating a new file, select an existing tab, should not change file content when return back on a new file tab', async () => {
|
||||
const button = fixture.debugElement.query(By.css('button.adev-add-file')).nativeElement;
|
||||
button.click();
|
||||
|
||||
const matTabGroup = await loader.getHarness(MatTabGroupHarness);
|
||||
const tabs = await matTabGroup.getTabs();
|
||||
|
||||
await tabs[1].select();
|
||||
expect(codeMirrorEditorChangeCurrentFileSpy).toHaveBeenCalledWith(files[1].filename);
|
||||
|
||||
codeMirrorEditorChangeCurrentFileSpy.calls.reset();
|
||||
|
||||
await tabs[files.length].select();
|
||||
expect(codeMirrorEditorChangeCurrentFileSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow to delete a required file', async () => {
|
||||
const matTabGroup = await loader.getHarness(MatTabGroupHarness);
|
||||
const tabs = await matTabGroup.getTabs();
|
||||
|
||||
const requiredFilesTabIndexes = files
|
||||
.filter((file) => REQUIRED_FILES.has(file.filename))
|
||||
.map((file) => files.indexOf(file));
|
||||
|
||||
for (const tabIndex of requiredFilesTabIndexes) {
|
||||
const tab = tabs[tabIndex];
|
||||
|
||||
await tab.select();
|
||||
|
||||
expect(fixture.debugElement.query(By.css('[aria-label="Delete file"]'))).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,175 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {NgFor, NgIf} from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DestroyRef,
|
||||
ElementRef,
|
||||
OnDestroy,
|
||||
ViewChild,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {MatTabGroup, MatTabsModule} from '@angular/material/tabs';
|
||||
import {debounceTime, map} from 'rxjs';
|
||||
|
||||
import {TerminalType} from '../terminal/terminal-handler.service';
|
||||
import {EmbeddedTutorialManager} from '../embedded-tutorial-manager.service';
|
||||
|
||||
import {CodeMirrorEditor} from './code-mirror-editor.service';
|
||||
import {DiagnosticWithLocation, DiagnosticsState} from './services/diagnostics-state.service';
|
||||
import {DownloadManager} from '../download-manager.service';
|
||||
import {IconComponent} from '@angular/docs';
|
||||
|
||||
export const REQUIRED_FILES = new Set(['src/main.ts', 'src/index.html']);
|
||||
|
||||
@Component({
|
||||
selector: 'docs-tutorial-code-editor',
|
||||
standalone: true,
|
||||
templateUrl: './code-editor.component.html',
|
||||
styleUrls: ['./code-editor.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgIf, NgFor, MatTabsModule, IconComponent],
|
||||
})
|
||||
export class CodeEditor implements AfterViewInit, OnDestroy {
|
||||
@ViewChild('codeEditorWrapper') private codeEditorWrapperRef!: ElementRef<HTMLDivElement>;
|
||||
@ViewChild(MatTabGroup) private matTabGroup!: MatTabGroup;
|
||||
|
||||
private createFileInputRef?: ElementRef<HTMLInputElement>;
|
||||
@ViewChild('createFileInput') protected set setFileInputRef(
|
||||
element: ElementRef<HTMLInputElement>,
|
||||
) {
|
||||
if (element) {
|
||||
element.nativeElement.focus();
|
||||
this.createFileInputRef = element;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
private readonly codeMirrorEditor = inject(CodeMirrorEditor);
|
||||
private readonly diagnosticsState = inject(DiagnosticsState);
|
||||
private readonly downloadManager = inject(DownloadManager);
|
||||
private readonly embeddedTutorialManager = inject(EmbeddedTutorialManager);
|
||||
|
||||
private readonly errors$ = this.diagnosticsState.diagnostics$.pipe(
|
||||
// Display errors one second after code update
|
||||
debounceTime(1000),
|
||||
map((diagnosticsItem) =>
|
||||
diagnosticsItem
|
||||
.filter((item) => item.severity === 'error')
|
||||
.sort((a, b) =>
|
||||
a.lineNumber != b.lineNumber
|
||||
? a.lineNumber - b.lineNumber
|
||||
: a.characterPosition - b.characterPosition,
|
||||
),
|
||||
),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
);
|
||||
|
||||
readonly TerminalType = TerminalType;
|
||||
|
||||
readonly displayErrorsBox = signal<boolean>(false);
|
||||
readonly errors = signal<DiagnosticWithLocation[]>([]);
|
||||
readonly files = this.codeMirrorEditor.openFiles;
|
||||
readonly isCreatingFile = signal<boolean>(false);
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.codeMirrorEditor.init(this.codeEditorWrapperRef.nativeElement);
|
||||
this.listenToDiagnosticsChange();
|
||||
|
||||
this.listenToTabChange();
|
||||
this.setSelectedTabOnTutorialChange();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.codeMirrorEditor.disable();
|
||||
}
|
||||
|
||||
async downloadCurrentCodeEditorState(): Promise<void> {
|
||||
const name = this.embeddedTutorialManager.tutorialId();
|
||||
await this.downloadManager.downloadCurrentStateOfTheSolution(name);
|
||||
}
|
||||
|
||||
closeErrorsBox(): void {
|
||||
this.displayErrorsBox.set(false);
|
||||
}
|
||||
|
||||
canDeleteFile(filename: string) {
|
||||
return !REQUIRED_FILES.has(filename);
|
||||
}
|
||||
|
||||
async deleteFile(filename: string) {
|
||||
await this.codeMirrorEditor.deleteFile(filename);
|
||||
this.matTabGroup.selectedIndex = 0;
|
||||
}
|
||||
|
||||
onAddButtonClick() {
|
||||
this.isCreatingFile.set(true);
|
||||
this.matTabGroup.selectedIndex = this.files().length;
|
||||
}
|
||||
|
||||
async createFile(event: SubmitEvent) {
|
||||
if (!this.createFileInputRef) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const newFileInputValue = this.createFileInputRef.nativeElement.value;
|
||||
|
||||
if (newFileInputValue) {
|
||||
if (newFileInputValue.includes('..')) {
|
||||
alert('File name can not contain ".."');
|
||||
return;
|
||||
}
|
||||
|
||||
// src is hidden from users, here we manually add it to the new filename
|
||||
const newFile = 'src/' + newFileInputValue;
|
||||
|
||||
if (this.files().find(({filename}) => filename.includes(newFile))) {
|
||||
alert('File already exists');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.codeMirrorEditor.createFile(newFile);
|
||||
}
|
||||
|
||||
this.isCreatingFile.set(false);
|
||||
}
|
||||
|
||||
private listenToDiagnosticsChange(): void {
|
||||
this.errors$.subscribe((diagnostics) => {
|
||||
this.errors.set(diagnostics);
|
||||
this.displayErrorsBox.set(diagnostics.length > 0);
|
||||
});
|
||||
}
|
||||
|
||||
private setSelectedTabOnTutorialChange() {
|
||||
this.embeddedTutorialManager.tutorialChanged$
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(() => {
|
||||
// selected file on project change is always the first
|
||||
this.matTabGroup.selectedIndex = 0;
|
||||
});
|
||||
}
|
||||
|
||||
private listenToTabChange() {
|
||||
this.matTabGroup.selectedIndexChange
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((index) => {
|
||||
const selectedFile = this.files()[index];
|
||||
|
||||
if (selectedFile) {
|
||||
this.codeMirrorEditor.changeCurrentFile(selectedFile.filename);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {signal} from '@angular/core';
|
||||
import {TestBed, fakeAsync, tick} from '@angular/core/testing';
|
||||
import {BehaviorSubject, Subject} from 'rxjs';
|
||||
import {EditorState} from '@codemirror/state';
|
||||
import type {FileSystemTree} from '@webcontainer/api';
|
||||
|
||||
import {NodeRuntimeSandbox} from '../node-runtime-sandbox.service';
|
||||
import {EmbeddedTutorialManager} from '../embedded-tutorial-manager.service';
|
||||
|
||||
import {CodeMirrorEditor, EDITOR_CONTENT_CHANGE_DELAY_MILLIES} from './code-mirror-editor.service';
|
||||
import {TutorialConfig, TutorialMetadata} from '@angular/docs';
|
||||
|
||||
class FakeNodeRuntimeSandbox {
|
||||
async writeFile(path: string, content: string) {}
|
||||
createdFile$ = new Subject<void>();
|
||||
async readFile(path: string) {
|
||||
return Promise.resolve('content');
|
||||
}
|
||||
}
|
||||
|
||||
const files = ['1.ts', '2.ts', '3.ts'];
|
||||
// reverse the files list to test openFiles order
|
||||
const filesReverse = [...files].reverse();
|
||||
|
||||
export class FakeEmbeddedTutorialManager {
|
||||
commonFilesystemTree = signal<FileSystemTree | null>(null);
|
||||
previousTutorial = signal<string | undefined>(undefined);
|
||||
nextTutorial = signal<string | undefined>(undefined);
|
||||
tutorialId = signal<string>('fake-tutorial');
|
||||
tutorialFiles = signal<NonNullable<TutorialMetadata['tutorialFiles']>>(
|
||||
// use a reverse map to test openFiles order
|
||||
Object.fromEntries(filesReverse.map((file) => [file, ''])),
|
||||
);
|
||||
openFiles = signal<TutorialConfig['openFiles']>(files);
|
||||
tutorialFilesystemTree = signal<FileSystemTree>(
|
||||
Object.fromEntries(
|
||||
files.map((file) => [
|
||||
file,
|
||||
{
|
||||
file: {
|
||||
contents: this.tutorialFiles()![0],
|
||||
},
|
||||
},
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
private _shouldChangeTutorial$ = new BehaviorSubject<boolean>(false);
|
||||
tutorialChanged$ = this._shouldChangeTutorial$.asObservable();
|
||||
|
||||
fetchAndSetTutorialFiles(tutorial: string): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
fetchCommonFiles(): Promise<FileSystemTree> {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
}
|
||||
|
||||
describe('CodeMirrorEditor', () => {
|
||||
let service: CodeMirrorEditor;
|
||||
|
||||
const fakeNodeRuntimeSandbox = new FakeNodeRuntimeSandbox();
|
||||
const fakeEmbeddedTutorialManager = new FakeEmbeddedTutorialManager();
|
||||
|
||||
function dispatchDocumentChange(newContent: string) {
|
||||
for (let i = 0; i < newContent.length; i++) {
|
||||
service['_editorView']?.dispatch({
|
||||
changes: {from: i, insert: newContent[i]},
|
||||
selection: {anchor: i, head: 0},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CodeMirrorEditor,
|
||||
{
|
||||
provide: NodeRuntimeSandbox,
|
||||
useValue: fakeNodeRuntimeSandbox,
|
||||
},
|
||||
{
|
||||
provide: EmbeddedTutorialManager,
|
||||
useValue: fakeEmbeddedTutorialManager,
|
||||
},
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(CodeMirrorEditor);
|
||||
|
||||
const parentElement = document.createElement('div');
|
||||
service.init(parentElement);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should initialize the editor', () => {
|
||||
expect(service['_editorView']).toBeDefined();
|
||||
|
||||
expect(service.files()).toBeDefined();
|
||||
expect(service.currentFile()).toBeDefined();
|
||||
|
||||
const editorState = service['_editorStates'].get(service.currentFile().filename);
|
||||
expect(editorState).toBeInstanceOf(EditorState);
|
||||
});
|
||||
|
||||
it('should change the current file', () => {
|
||||
const newFilename = files.at(-1)!;
|
||||
service.changeCurrentFile(newFilename);
|
||||
expect(service.currentFile().filename).toEqual(newFilename);
|
||||
|
||||
const editorState = service['_editorStates'].get(newFilename);
|
||||
const editorContent = editorState?.doc.toString();
|
||||
const newFileContent = service.files().find((file) => file.filename === newFilename)?.content;
|
||||
|
||||
expect(editorContent).toBe(newFileContent);
|
||||
});
|
||||
|
||||
it('should update the current file content on change', async () => {
|
||||
const newContent = 'new content';
|
||||
|
||||
dispatchDocumentChange(newContent);
|
||||
|
||||
const updatedEditorState = service['_editorStates'].get(service.currentFile().filename);
|
||||
expect(updatedEditorState?.doc.toString()).toBe(newContent);
|
||||
|
||||
const fileContentOnFilesSignal = service
|
||||
.files()
|
||||
.find((file) => file.filename === service.currentFile().filename)?.content;
|
||||
|
||||
expect(fileContentOnFilesSignal).toBe(newContent);
|
||||
|
||||
expect(service.currentFile().content).toBe(newContent);
|
||||
});
|
||||
|
||||
it('should write the changed file content to the sandbox filesystem', fakeAsync(() => {
|
||||
const newContent = 'new content';
|
||||
|
||||
const nodeRuntimeSandboxSpy = spyOn(fakeNodeRuntimeSandbox, 'writeFile');
|
||||
|
||||
dispatchDocumentChange(newContent);
|
||||
|
||||
tick(EDITOR_CONTENT_CHANGE_DELAY_MILLIES);
|
||||
|
||||
expect(nodeRuntimeSandboxSpy).toHaveBeenCalledWith(service.currentFile().filename, newContent);
|
||||
}));
|
||||
|
||||
it('should add created file to code editor', async () => {
|
||||
const newFile = 'new-component.component.ts';
|
||||
|
||||
await service['addCreatedFileToCodeEditor'](newFile);
|
||||
|
||||
expect(service['embeddedTutorialManager'].tutorialFiles()[newFile]).toBeDefined();
|
||||
expect(service.files().find((file) => file.filename === newFile)).toBeDefined();
|
||||
});
|
||||
|
||||
it('should keep openFiles order', () => {
|
||||
service['setProjectFiles']();
|
||||
|
||||
for (const [index, openFile] of service['openFiles']().entries()) {
|
||||
expect(openFile.filename).toEqual(service['embeddedTutorialManager'].openFiles()[index]);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,458 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {DestroyRef, Injectable, inject, signal} from '@angular/core';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {Subject, Subscription, debounceTime, filter, map} from 'rxjs';
|
||||
|
||||
import {LRLanguage, LanguageSupport} from '@codemirror/language';
|
||||
import {EditorState, Transaction} from '@codemirror/state';
|
||||
import {EditorView, placeholder as placeholderExtension} from '@codemirror/view';
|
||||
|
||||
import {EmbeddedTutorialManager} from '../embedded-tutorial-manager.service';
|
||||
import {NodeRuntimeSandbox} from '../node-runtime-sandbox.service';
|
||||
import {TypingsLoader} from '../typings-loader.service';
|
||||
|
||||
import {FileAndContentRecord} from '@angular/docs';
|
||||
import {CODE_EDITOR_EXTENSIONS} from './constants/code-editor-extensions';
|
||||
import {LANGUAGES} from './constants/code-editor-languages';
|
||||
import {getAutocompleteExtension} from './extensions/autocomplete';
|
||||
import {getDiagnosticsExtension} from './extensions/diagnostics';
|
||||
import {getTooltipExtension} from './extensions/tooltip';
|
||||
import {DiagnosticsState} from './services/diagnostics-state.service';
|
||||
import {TsVfsWorkerActions} from './workers/enums/actions';
|
||||
import {CodeChangeRequest} from './workers/interfaces/code-change-request';
|
||||
import {ActionMessage} from './workers/interfaces/message';
|
||||
import {NodeRuntimeState} from '../node-runtime-state.service';
|
||||
|
||||
export interface EditorFile {
|
||||
filename: string;
|
||||
content: string;
|
||||
language: LanguageSupport | LRLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* The delay between the last typed character and the actual file save.
|
||||
* This is used to prevent saving the file on every keystroke.
|
||||
*
|
||||
* Important! this value is intentionally set a bit higher than it needs to be, because sending
|
||||
* changes too frequently to the web container appears to put it in a state where it stops picking
|
||||
* up changes. See issue #691 for context.
|
||||
*/
|
||||
export const EDITOR_CONTENT_CHANGE_DELAY_MILLIES = 500;
|
||||
|
||||
const INITIAL_STATES = {
|
||||
_editorView: null,
|
||||
|
||||
files: [],
|
||||
|
||||
currentFile: {
|
||||
filename: '',
|
||||
content: '',
|
||||
language: LANGUAGES['ts'],
|
||||
},
|
||||
|
||||
contentChangeListenerSubscription$: undefined,
|
||||
tutorialChangeListener$: undefined,
|
||||
createdFile$: undefined,
|
||||
};
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class CodeMirrorEditor {
|
||||
// TODO: handle files created by the user, e.g. after running `ng generate component`
|
||||
files = signal<EditorFile[]>(INITIAL_STATES.files);
|
||||
openFiles = signal<EditorFile[]>(INITIAL_STATES.files);
|
||||
currentFile = signal<EditorFile>(INITIAL_STATES.currentFile);
|
||||
|
||||
// An instance of web worker used to run virtual TypeScript environment in the browser.
|
||||
// It allows to enrich CodeMirror UX for TypeScript files.
|
||||
private tsVfsWorker: Worker | null = null;
|
||||
// EventManager gives ability to communicate between tsVfsWorker and CodeMirror instance
|
||||
private readonly eventManager$ = new Subject<ActionMessage>();
|
||||
|
||||
private readonly nodeRuntimeSandbox = inject(NodeRuntimeSandbox);
|
||||
private readonly nodeRuntimeState = inject(NodeRuntimeState);
|
||||
private readonly embeddedTutorialManager = inject(EmbeddedTutorialManager);
|
||||
private readonly typingsLoader = inject(TypingsLoader);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly diagnosticsState = inject(DiagnosticsState);
|
||||
|
||||
private _editorView: EditorView | null = INITIAL_STATES._editorView;
|
||||
private readonly _editorStates = new Map<EditorFile['filename'], EditorState>();
|
||||
|
||||
private readonly contentChanged$ = new Subject<string>();
|
||||
private readonly contentChangeListener$ = this.contentChanged$.asObservable();
|
||||
private contentChangeListenerSubscription$: Subscription | undefined =
|
||||
INITIAL_STATES.contentChangeListenerSubscription$;
|
||||
|
||||
private tutorialChangeListener$: Subscription | undefined =
|
||||
INITIAL_STATES.tutorialChangeListener$;
|
||||
|
||||
private createdFileListener$: Subscription | undefined = INITIAL_STATES.createdFile$;
|
||||
|
||||
init(parentElement: HTMLElement): void {
|
||||
if (this._editorView) return;
|
||||
|
||||
if (!this.nodeRuntimeState.error()) {
|
||||
this.initTypescriptVfsWorker();
|
||||
this.saveLibrariesTypes();
|
||||
}
|
||||
|
||||
this._editorView = new EditorView({
|
||||
parent: parentElement,
|
||||
state: this.createEditorState(),
|
||||
dispatchTransactions: (transactions, view) => {
|
||||
view.update(transactions);
|
||||
|
||||
for (const transaction of transactions) {
|
||||
if (transaction.docChanged && this.currentFile().filename) {
|
||||
this.contentChanged$.next(transaction.state.doc.toString());
|
||||
this.changeFileContentsOnRealTime(transaction);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.listenToProjectChanges();
|
||||
|
||||
this.contentChangeListenerSubscription$ = this.contentChangeListener$
|
||||
.pipe(debounceTime(EDITOR_CONTENT_CHANGE_DELAY_MILLIES), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(async (fileContents) => {
|
||||
await this.changeFileContentsScheduled(fileContents);
|
||||
});
|
||||
|
||||
this.createdFileListener$ = this.nodeRuntimeSandbox.createdFile$.subscribe(
|
||||
async (createdFile) => {
|
||||
await this.addCreatedFileToCodeEditor(createdFile);
|
||||
},
|
||||
);
|
||||
|
||||
// Create TypeScript virtual filesystem when default files map is created
|
||||
// and files are set
|
||||
this.eventManager$
|
||||
.pipe(
|
||||
filter(
|
||||
(event) =>
|
||||
event.action === TsVfsWorkerActions.INIT_DEFAULT_FILE_SYSTEM_MAP &&
|
||||
this.files().length > 0,
|
||||
),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.createVfsEnv();
|
||||
});
|
||||
}
|
||||
|
||||
disable(): void {
|
||||
this._editorView?.destroy();
|
||||
this._editorView = null;
|
||||
|
||||
this._editorView = INITIAL_STATES._editorView;
|
||||
this.files.set(INITIAL_STATES.files);
|
||||
this.currentFile.set(INITIAL_STATES.currentFile);
|
||||
|
||||
this._editorStates.clear();
|
||||
|
||||
this.contentChangeListenerSubscription$?.unsubscribe();
|
||||
this.contentChangeListenerSubscription$ = INITIAL_STATES.contentChangeListenerSubscription$;
|
||||
|
||||
this.tutorialChangeListener$?.unsubscribe();
|
||||
this.tutorialChangeListener$ = INITIAL_STATES.tutorialChangeListener$;
|
||||
|
||||
this.createdFileListener$?.unsubscribe();
|
||||
this.createdFileListener$ = INITIAL_STATES.createdFile$;
|
||||
}
|
||||
|
||||
changeCurrentFile(fileName: string): void {
|
||||
if (!this._editorView) return;
|
||||
|
||||
const newFile = this.files().find((file) => file.filename === fileName);
|
||||
if (!newFile) throw new Error(`File '${fileName}' not found`);
|
||||
|
||||
this.currentFile.set(newFile);
|
||||
|
||||
const editorState = this._editorStates.get(newFile.filename) ?? this.createEditorState();
|
||||
|
||||
this._editorView.setState(editorState);
|
||||
}
|
||||
|
||||
private initTypescriptVfsWorker(): void {
|
||||
if (this.tsVfsWorker) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tsVfsWorker = new Worker(new URL('./workers/typescript-vfs.worker', import.meta.url), {
|
||||
type: 'module',
|
||||
});
|
||||
|
||||
this.tsVfsWorker.addEventListener('message', ({data}: MessageEvent<ActionMessage>) => {
|
||||
this.eventManager$.next(data);
|
||||
});
|
||||
}
|
||||
|
||||
private saveLibrariesTypes(): void {
|
||||
this.typingsLoader.typings$
|
||||
.pipe(
|
||||
filter((typings) => typings.length > 0),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((typings) => {
|
||||
this.sendRequestToTsVfs({
|
||||
action: TsVfsWorkerActions.DEFINE_TYPES_REQUEST,
|
||||
data: typings,
|
||||
});
|
||||
|
||||
// Reset current file to trigger diagnostics after preload @angular libraries.
|
||||
this._editorView?.setState(this._editorStates.get(this.currentFile().filename)!);
|
||||
});
|
||||
}
|
||||
|
||||
// Method is responsible for sending request to Typescript VFS worker.
|
||||
private sendRequestToTsVfs = <T>(request: ActionMessage<T>) => {
|
||||
if (!this.tsVfsWorker) return;
|
||||
|
||||
// Send message to tsVfsWorker only when current file is TypeScript file.
|
||||
if (!this.currentFile().filename.endsWith('.ts')) return;
|
||||
|
||||
this.tsVfsWorker.postMessage(request);
|
||||
// tslint:disable-next-line:semicolon
|
||||
};
|
||||
|
||||
private getVfsEnvFileSystemMap(): Map<string, string> {
|
||||
const fileSystemMap = new Map<string, string>();
|
||||
|
||||
for (const file of this.files().filter((file) => file.filename.endsWith('.ts'))) {
|
||||
fileSystemMap.set(`/${file.filename}`, file.content);
|
||||
}
|
||||
|
||||
return fileSystemMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create virtual environment for TypeScript files
|
||||
*/
|
||||
private createVfsEnv(): void {
|
||||
this.sendRequestToTsVfs<Map<string, string>>({
|
||||
action: TsVfsWorkerActions.CREATE_VFS_ENV_REQUEST,
|
||||
data: this.getVfsEnvFileSystemMap(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update virtual TypeScript file system with current code editor files
|
||||
*/
|
||||
private updateVfsEnv(): void {
|
||||
this.sendRequestToTsVfs<Map<string, string>>({
|
||||
action: TsVfsWorkerActions.UPDATE_VFS_ENV_REQUEST,
|
||||
data: this.getVfsEnvFileSystemMap(),
|
||||
});
|
||||
}
|
||||
|
||||
private listenToProjectChanges() {
|
||||
this.tutorialChangeListener$ = this.embeddedTutorialManager.tutorialChanged$
|
||||
.pipe(
|
||||
map(() => this.embeddedTutorialManager.tutorialFiles()),
|
||||
filter((tutorialFiles) => Object.keys(tutorialFiles).length > 0),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.changeProject();
|
||||
});
|
||||
}
|
||||
|
||||
private changeProject() {
|
||||
this.setProjectFiles();
|
||||
|
||||
this._editorStates.clear();
|
||||
|
||||
this.changeCurrentFile(this.currentFile().filename);
|
||||
|
||||
this.updateVfsEnv();
|
||||
}
|
||||
|
||||
private setProjectFiles(): void {
|
||||
const tutorialFiles = this.getTutorialFiles(this.embeddedTutorialManager.tutorialFiles());
|
||||
|
||||
const openFiles: EditorFile[] = [];
|
||||
|
||||
// iterate openFiles to keep files order
|
||||
for (const openFileName of this.embeddedTutorialManager.openFiles()) {
|
||||
const openFile = tutorialFiles.find(({filename}) => filename === openFileName);
|
||||
|
||||
if (openFile) {
|
||||
openFiles.push(openFile);
|
||||
}
|
||||
}
|
||||
|
||||
this.files.set(tutorialFiles);
|
||||
this.openFiles.set(openFiles);
|
||||
this.changeCurrentFile(openFiles[0].filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the code editor files when files are created
|
||||
*/
|
||||
private async addCreatedFileToCodeEditor(createdFile: string): Promise<void> {
|
||||
const fileContents = await this.nodeRuntimeSandbox.readFile(createdFile);
|
||||
|
||||
this.embeddedTutorialManager.tutorialFiles.update((files) => ({
|
||||
...files,
|
||||
[createdFile]: fileContents,
|
||||
}));
|
||||
|
||||
this.embeddedTutorialManager.openFiles.update((files) => [...files, createdFile]);
|
||||
|
||||
this.setProjectFiles();
|
||||
|
||||
this.updateVfsEnv();
|
||||
|
||||
this.saveLibrariesTypes();
|
||||
}
|
||||
|
||||
async createFile(filename: string) {
|
||||
// if file already exists, use its content
|
||||
const content = await this.nodeRuntimeSandbox.readFile(filename).catch((error) => {
|
||||
// empty content if file does not exist
|
||||
if (error.message.includes('ENOENT')) return '';
|
||||
else throw error;
|
||||
});
|
||||
|
||||
await this.nodeRuntimeSandbox.writeFile(filename, content);
|
||||
|
||||
this.embeddedTutorialManager.tutorialFiles.update((files) => ({
|
||||
...files,
|
||||
[filename]: content,
|
||||
}));
|
||||
|
||||
this.embeddedTutorialManager.openFiles.update((files) => [...files, filename]);
|
||||
|
||||
this.setProjectFiles();
|
||||
|
||||
this.updateVfsEnv();
|
||||
|
||||
this.saveLibrariesTypes();
|
||||
|
||||
this.changeCurrentFile(filename);
|
||||
}
|
||||
|
||||
async deleteFile(deletedFile: string): Promise<void> {
|
||||
await this.nodeRuntimeSandbox.deleteFile(deletedFile);
|
||||
|
||||
this.embeddedTutorialManager.tutorialFiles.update((files) => {
|
||||
delete files[deletedFile];
|
||||
return files;
|
||||
});
|
||||
|
||||
this.embeddedTutorialManager.openFiles.update((files) =>
|
||||
files.filter((file) => file !== deletedFile),
|
||||
);
|
||||
|
||||
this.setProjectFiles();
|
||||
|
||||
this.updateVfsEnv();
|
||||
|
||||
this.saveLibrariesTypes();
|
||||
}
|
||||
|
||||
private createEditorState(): EditorState {
|
||||
const newEditorState = EditorState.create({
|
||||
doc: this.currentFile().content,
|
||||
extensions: [
|
||||
...CODE_EDITOR_EXTENSIONS,
|
||||
|
||||
this.currentFile().language,
|
||||
|
||||
placeholderExtension('Type your code here...'),
|
||||
|
||||
...this.getLanguageExtensions(),
|
||||
],
|
||||
});
|
||||
|
||||
this._editorStates.set(this.currentFile().filename, newEditorState);
|
||||
|
||||
return newEditorState;
|
||||
}
|
||||
|
||||
private getLanguageExtensions() {
|
||||
if (this.currentFile().filename.endsWith('.ts')) {
|
||||
return [
|
||||
getAutocompleteExtension(this.eventManager$, this.currentFile, this.sendRequestToTsVfs),
|
||||
getDiagnosticsExtension(
|
||||
this.eventManager$,
|
||||
this.currentFile,
|
||||
this.sendRequestToTsVfs,
|
||||
this.diagnosticsState,
|
||||
),
|
||||
getTooltipExtension(this.eventManager$, this.currentFile, this.sendRequestToTsVfs),
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the new file contents to the sandbox filesystem
|
||||
*/
|
||||
private async changeFileContentsScheduled(newContent: string): Promise<void> {
|
||||
try {
|
||||
await this.nodeRuntimeSandbox.writeFile(this.currentFile().filename, newContent);
|
||||
} catch (err) {
|
||||
// Note: `writeFile` throws if the sandbox is not initialized yet, which can happen if
|
||||
// the user starts typing right after the page loads.
|
||||
// Here the error is ignored as it is expected.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change file contents on files signals and update the editor state
|
||||
*/
|
||||
private changeFileContentsOnRealTime(transaction: Transaction): void {
|
||||
this._editorStates.set(this.currentFile().filename, transaction.state);
|
||||
|
||||
const newContent = transaction.state.doc.toString();
|
||||
|
||||
this.currentFile.update((currentFile) => ({...currentFile, content: newContent}));
|
||||
|
||||
this.files.update((files) =>
|
||||
files.map((file) =>
|
||||
file.filename === this.currentFile().filename ? {...file, content: newContent} : file,
|
||||
),
|
||||
);
|
||||
|
||||
// send current file content to Ts Vfs worker to run diagnostics on current file state
|
||||
this.sendRequestToTsVfs<CodeChangeRequest>({
|
||||
action: TsVfsWorkerActions.CODE_CHANGED,
|
||||
data: {
|
||||
file: this.currentFile().filename,
|
||||
code: newContent,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private getTutorialFiles(files: FileAndContentRecord): EditorFile[] {
|
||||
const languagesExtensions = Object.keys(LANGUAGES);
|
||||
|
||||
const tutorialFiles = Object.entries(files)
|
||||
.filter(
|
||||
(fileAndContent): fileAndContent is [string, string] =>
|
||||
typeof fileAndContent[1] === 'string',
|
||||
)
|
||||
.map(([filename, content]) => {
|
||||
const extension = languagesExtensions.find((extension) => filename.endsWith(extension));
|
||||
const language = extension ? LANGUAGES[extension] : LANGUAGES['ts'];
|
||||
|
||||
return {
|
||||
filename,
|
||||
content,
|
||||
language,
|
||||
};
|
||||
});
|
||||
|
||||
return tutorialFiles;
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {EditorState, Extension} from '@codemirror/state';
|
||||
|
||||
import {
|
||||
lineNumbers,
|
||||
highlightActiveLineGutter,
|
||||
highlightSpecialChars,
|
||||
drawSelection,
|
||||
dropCursor,
|
||||
rectangularSelection,
|
||||
crosshairCursor,
|
||||
highlightActiveLine,
|
||||
keymap,
|
||||
EditorView,
|
||||
} from '@codemirror/view';
|
||||
export {EditorView} from '@codemirror/view';
|
||||
import {
|
||||
foldGutter,
|
||||
indentOnInput,
|
||||
syntaxHighlighting,
|
||||
defaultHighlightStyle,
|
||||
bracketMatching,
|
||||
foldKeymap,
|
||||
HighlightStyle,
|
||||
} from '@codemirror/language';
|
||||
import {history, defaultKeymap, historyKeymap, indentWithTab} from '@codemirror/commands';
|
||||
import {highlightSelectionMatches, searchKeymap} from '@codemirror/search';
|
||||
import {
|
||||
closeBrackets,
|
||||
autocompletion,
|
||||
closeBracketsKeymap,
|
||||
completionKeymap,
|
||||
startCompletion,
|
||||
} from '@codemirror/autocomplete';
|
||||
import {lintKeymap} from '@codemirror/lint';
|
||||
import {SYNTAX_STYLES} from './syntax-styles';
|
||||
import {CODE_EDITOR_THEME_STYLES} from './theme-styles';
|
||||
|
||||
export const CODE_EDITOR_EXTENSIONS: Extension[] = [
|
||||
lineNumbers(),
|
||||
highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
foldGutter(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
autocompletion(),
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
highlightActiveLine(),
|
||||
highlightSelectionMatches(),
|
||||
|
||||
syntaxHighlighting(defaultHighlightStyle, {fallback: true}),
|
||||
syntaxHighlighting(HighlightStyle.define(SYNTAX_STYLES)),
|
||||
EditorView.lineWrapping,
|
||||
|
||||
EditorView.theme(
|
||||
CODE_EDITOR_THEME_STYLES,
|
||||
// TODO: get from global theme, reconfigure on change: https://discuss.codemirror.net/t/dynamic-light-mode-dark-mode-how/4709
|
||||
{dark: true},
|
||||
),
|
||||
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
...lintKeymap,
|
||||
indentWithTab,
|
||||
{
|
||||
key: 'Ctrl-.',
|
||||
run: startCompletion,
|
||||
mac: 'Mod-.',
|
||||
},
|
||||
]),
|
||||
];
|
||||
@@ -1,27 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {html} from '@codemirror/lang-html';
|
||||
import {angular} from '@codemirror/lang-angular';
|
||||
import {css} from '@codemirror/lang-css';
|
||||
import {sass} from '@codemirror/lang-sass';
|
||||
import {javascript} from '@codemirror/lang-javascript';
|
||||
import {angularComponent} from '../utils/component-ts-syntax';
|
||||
import {LRLanguage, LanguageSupport} from '@codemirror/language';
|
||||
|
||||
export const LANGUAGES: Record<string, LanguageSupport | LRLanguage> = {
|
||||
'component.ts': angularComponent(),
|
||||
'main.ts': angularComponent(),
|
||||
html: angular({base: html()}),
|
||||
svg: html(),
|
||||
ts: javascript({typescript: true}),
|
||||
css: css(),
|
||||
sass: sass(),
|
||||
scss: sass(),
|
||||
json: javascript(),
|
||||
};
|
||||
@@ -1,246 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import type {TagStyle} from '@codemirror/language';
|
||||
import {tags} from '@lezer/highlight';
|
||||
|
||||
export const SYNTAX_STYLES: TagStyle[] = [
|
||||
/** A comment. */
|
||||
{tag: tags.comment, color: 'var(--code-comment)'},
|
||||
|
||||
/** A language keyword. */
|
||||
{tag: tags.keyword, color: 'var(--code-keyword)'},
|
||||
|
||||
/** A string literal */
|
||||
{tag: tags.string, color: 'var(--code-string)'},
|
||||
|
||||
/** A number literal. */
|
||||
{tag: tags.number, color: 'var(--code-number)'},
|
||||
|
||||
/** A tag name, subtag of typeName. */
|
||||
{tag: tags.tagName, color: 'var(--code-tags)'},
|
||||
|
||||
/** The name of a class. */
|
||||
{tag: tags.className, color: 'var(--code-component)'},
|
||||
|
||||
/** A line comment. */
|
||||
{tag: tags.lineComment, color: 'var(--code-line-comment)'},
|
||||
|
||||
/** A block comment. */
|
||||
{tag: tags.blockComment, color: 'var(--code-block-comment)'},
|
||||
|
||||
/** A documentation comment. */
|
||||
{tag: tags.docComment, color: 'var(--code-doc-comment)'},
|
||||
|
||||
/** Any kind of identifier. */
|
||||
{tag: tags.name, color: 'var(--code-name)'},
|
||||
|
||||
/** The name of a variable. */
|
||||
{tag: tags.variableName, color: 'var(--code-variable-name)'},
|
||||
|
||||
/** A type name */
|
||||
{tag: tags.typeName, color: 'var(--code-type-name)'},
|
||||
|
||||
/** A property or field name. */
|
||||
{tag: tags.propertyName, color: 'var(--code-property-name)'},
|
||||
|
||||
/** An attribute name, subtag of propertyName. */
|
||||
{tag: tags.attributeName, color: 'var(--code-attribute-name)'},
|
||||
|
||||
/** A label name. */
|
||||
{tag: tags.labelName, color: 'var(--code-label-name)'},
|
||||
|
||||
/** A namespace name. */
|
||||
{tag: tags.namespace, color: 'var(--code-namespace)'},
|
||||
|
||||
/** The name of a macro. */
|
||||
{tag: tags.macroName, color: 'var(--code-macro-name)'},
|
||||
|
||||
/** A literal value. */
|
||||
{tag: tags.literal, color: 'var(--code-literal)'},
|
||||
|
||||
/** A documentation string. */
|
||||
{tag: tags.docString, color: 'var(--code-doc-string)'},
|
||||
|
||||
/** A character literal (subtag of string). */
|
||||
{tag: tags.character, color: 'var(--code-character)'},
|
||||
|
||||
/** An attribute value (subtag of string). */
|
||||
{tag: tags.attributeValue, color: 'var(--code-attribute-value)'},
|
||||
|
||||
/** An integer number literal. */
|
||||
{tag: tags.integer, color: 'var(--code-integer)'},
|
||||
|
||||
/** A floating-point number literal. */
|
||||
{tag: tags.float, color: 'var(--code-float)'},
|
||||
|
||||
/** A boolean literal. */
|
||||
{tag: tags.bool, color: 'var(--code-bool)'},
|
||||
|
||||
/** Regular expression literal. */
|
||||
{tag: tags.regexp, color: 'var(--code-regexp)'},
|
||||
|
||||
/** An escape literal, for example a backslash escape in a string. */
|
||||
{tag: tags.escape, color: 'var(--code-escape)'},
|
||||
|
||||
/** A color literal . */
|
||||
{tag: tags.color, color: 'var(--code-color)'},
|
||||
|
||||
/** A URL literal. */
|
||||
{tag: tags.url, color: 'var(--code-url)'},
|
||||
|
||||
/** The keyword for the self or this object. */
|
||||
{tag: tags.self, color: 'var(--code-self)'},
|
||||
|
||||
/** The keyword for null. */
|
||||
{tag: tags.null, color: 'var(--code-null)'},
|
||||
|
||||
/** A keyword denoting some atomic value. */
|
||||
{tag: tags.atom, color: 'var(--code-atom)'},
|
||||
|
||||
/** A keyword that represents a unit. */
|
||||
{tag: tags.unit, color: 'var(--code-unit)'},
|
||||
|
||||
/** A modifier keyword. */
|
||||
{tag: tags.modifier, color: 'var(--code-modifier)'},
|
||||
|
||||
/** A keyword that acts as an operator. */
|
||||
{tag: tags.operatorKeyword, color: 'var(--code-operator-keyword)'},
|
||||
|
||||
/** A control-flow related keyword. */
|
||||
{tag: tags.controlKeyword, color: 'var(--code-control-keyword)'},
|
||||
|
||||
/** A keyword that defines something. */
|
||||
{tag: tags.definitionKeyword, color: 'var(--code-definition-keyword)'},
|
||||
|
||||
/** A keyword related to defining or interfacing with modules. */
|
||||
{tag: tags.moduleKeyword, color: 'var(--code-module-keyword)'},
|
||||
|
||||
/** An operator. */
|
||||
{tag: tags.operator, color: 'var(--code-operator)'},
|
||||
|
||||
/** An operator that dereferences something. */
|
||||
{tag: tags.derefOperator, color: 'var(--code-deref-operator)'},
|
||||
|
||||
/** Arithmetic-related operator. */
|
||||
{tag: tags.arithmeticOperator, color: 'var(--code-arithmetic-operator)'},
|
||||
|
||||
/** Logical operator. */
|
||||
{tag: tags.logicOperator, color: 'var(--code-logic-operator)'},
|
||||
|
||||
/** Bit operator. */
|
||||
{tag: tags.bitwiseOperator, color: 'var(--code-bitwise-operator)'},
|
||||
|
||||
/** Comparison operator. */
|
||||
{tag: tags.compareOperator, color: 'var(--code-compare-operator)'},
|
||||
|
||||
/** Operator that updates its operand. */
|
||||
{tag: tags.updateOperator, color: 'var(--code-update-operator)'},
|
||||
|
||||
/** Operator that defines something. */
|
||||
{tag: tags.definitionOperator, color: 'var(--code-definition-operator)'},
|
||||
|
||||
/** Type-related operator. */
|
||||
{tag: tags.typeOperator, color: 'var(--code-type-operator)'},
|
||||
|
||||
/** Control-flow operator. */
|
||||
{tag: tags.controlOperator, color: 'var(--code-control-operator)'},
|
||||
|
||||
/** Program or markup punctuation. */
|
||||
{tag: tags.punctuation, color: 'var(--code-punctuation)'},
|
||||
|
||||
/** Punctuation that separates things. */
|
||||
{tag: tags.separator, color: 'var(--code-separator)'},
|
||||
|
||||
/** Bracket-style punctuation. */
|
||||
{tag: tags.bracket, color: 'var(--code-bracket)'},
|
||||
|
||||
/** Angle brackets (usually `<` and `>` tokens). */
|
||||
{tag: tags.angleBracket, color: 'var(--code-angle-bracket)'},
|
||||
|
||||
/** Square brackets (usually `[` and `]` tokens). */
|
||||
{tag: tags.squareBracket, color: 'var(--code-square-bracket)'},
|
||||
|
||||
/** Parentheses (usually `(` and `)` tokens). Subtag of bracket. */
|
||||
{tag: tags.paren, color: 'var(--code-paren)'},
|
||||
|
||||
/** Braces (usually `{` and `}` tokens). Subtag of bracket. */
|
||||
{tag: tags.brace, color: 'var(--code-brace)'},
|
||||
|
||||
/** Content, for example plain text in XML or markup documents. */
|
||||
{tag: tags.content, color: 'var(--code-content)'},
|
||||
|
||||
/** Content that represents a heading. */
|
||||
{tag: tags.heading, color: 'var(--code-heading)'},
|
||||
|
||||
/** A level 1 heading. */
|
||||
{tag: tags.heading1, color: 'var(--code-heading1)'},
|
||||
|
||||
/** A level 2 heading. */
|
||||
{tag: tags.heading2, color: 'var(--code-heading2)'},
|
||||
|
||||
/** A level 3 heading. */
|
||||
{tag: tags.heading3, color: 'var(--code-heading3)'},
|
||||
|
||||
/** A level 4 heading. */
|
||||
{tag: tags.heading4, color: 'var(--code-heading4)'},
|
||||
|
||||
/** A level 5 heading. */
|
||||
{tag: tags.heading5, color: 'var(--code-heading5)'},
|
||||
|
||||
/** A level 6 heading. */
|
||||
{tag: tags.heading6, color: 'var(--code-heading6)'},
|
||||
|
||||
/** A prose separator (such as a horizontal rule). */
|
||||
{tag: tags.contentSeparator, color: 'var(--code-content-separator)'},
|
||||
|
||||
/** Content that represents a list. */
|
||||
{tag: tags.list, color: 'var(--code-list)'},
|
||||
|
||||
/** Content that represents a quote. */
|
||||
{tag: tags.quote, color: 'var(--code-quote)'},
|
||||
|
||||
/** Content that is emphasized. */
|
||||
{tag: tags.emphasis, color: 'var(--code-emphasis)'},
|
||||
|
||||
/** Content that is styled strong. */
|
||||
{tag: tags.strong, color: 'var(--code-strong)'},
|
||||
|
||||
/** Content that is part of a link. */
|
||||
{tag: tags.link, color: 'var(--code-link)'},
|
||||
|
||||
/** Content that is styled as code or monospace. */
|
||||
{tag: tags.monospace, color: 'var(--code-monospace)'},
|
||||
|
||||
/** Content that has a strike-through style. */
|
||||
{tag: tags.strikethrough, color: 'var(--code-strikethrough)'},
|
||||
|
||||
/** Inserted text in a change-tracking format. */
|
||||
{tag: tags.inserted, color: 'var(--code-inserted)'},
|
||||
|
||||
/** Deleted text. */
|
||||
{tag: tags.deleted, color: 'var(--code-deleted)'},
|
||||
|
||||
/** Changed text. */
|
||||
{tag: tags.changed, color: 'var(--code-changed)'},
|
||||
|
||||
/** An invalid or unsyntactic element. */
|
||||
{tag: tags.invalid, color: 'var(--code-invalid)'},
|
||||
|
||||
/** Metadata or meta-instruction. */
|
||||
{tag: tags.meta, color: 'var(--code-meta)'},
|
||||
|
||||
/** Metadata that applies to the entire document. */
|
||||
{tag: tags.documentMeta, color: 'var(--code-document-meta)'},
|
||||
|
||||
/** Metadata that annotates or adds attributes to a given syntactic element. */
|
||||
{tag: tags.annotation, color: 'var(--code-annotation)'},
|
||||
|
||||
/** Processing instruction or preprocessor directive. Subtag of meta. */
|
||||
{tag: tags.processingInstruction, color: 'var(--code-processing-instruction)'},
|
||||
];
|
||||
@@ -1,76 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
export const CODE_EDITOR_THEME_STYLES = {
|
||||
'&': {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
'background-color': 'var(--code-editor-background)',
|
||||
color: 'var(--code-editor-text-base-color)',
|
||||
},
|
||||
'.cm-gutters': {
|
||||
border: 'none',
|
||||
},
|
||||
'.cm-gutter': {
|
||||
'background-color': 'var(--code-editor-background)',
|
||||
color: 'var(--code-editor-code-base-color)',
|
||||
},
|
||||
'.cm-line.cm-activeLine': {
|
||||
'background-color': 'var(--code-editor-active-line-background)',
|
||||
},
|
||||
'.cm-activeLineGutter': {
|
||||
'background-color': 'var(--code-editor-active-line-background)',
|
||||
},
|
||||
'&.cm-focused .cm-selectionBackground': {
|
||||
'background-color': 'var(--code-editor-focused-selection-background) !important',
|
||||
},
|
||||
'.cm-selectionBackground': {
|
||||
'background-color': 'var(--code-editor-selection-background) !important',
|
||||
},
|
||||
'.cm-cursor': {
|
||||
'border-color': 'var(--code-editor-cursor-color)',
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
color: 'var(--code-editor-tooltip-color)',
|
||||
border: 'var(--code-editor-tooltip-border)',
|
||||
'border-radius': 'var(--code-editor-tooltip-border-radius)',
|
||||
background: 'var(--code-editor-tooltip-background)',
|
||||
'overflow-y': 'scroll',
|
||||
'max-height': '70%',
|
||||
'max-width': '100%',
|
||||
'margin-right': '10px',
|
||||
},
|
||||
'.cm-tooltip.cm-tooltip-autocomplete > ul': {
|
||||
background: 'var(--code-editor-autocomplete-background)',
|
||||
},
|
||||
'.cm-tooltip .keyword': {
|
||||
color: 'var(--code-module-keyword)',
|
||||
},
|
||||
'.cm-tooltip .aliasName': {
|
||||
color: 'var(--code-variable-name)',
|
||||
},
|
||||
'.cm-tooltip .localName': {
|
||||
color: 'var(--code-variable-name)',
|
||||
},
|
||||
'.cm-tooltip-autocomplete ul li[aria-selected]': {
|
||||
background: 'var(--code-editor-autocomplete-item-background)',
|
||||
color: 'var(--code-editor-autocomplete-item-color)',
|
||||
},
|
||||
'.cm-tooltip-lint': {
|
||||
background: 'var(--code-editor-lint-tooltip-background)',
|
||||
color: 'var(--code-editor-lint-tooltip-color)',
|
||||
},
|
||||
'.cm-panels': {
|
||||
background: 'var(--code-editor-panels-background)',
|
||||
color: 'var(--code-editor-panels-color)',
|
||||
},
|
||||
'.cm-foldPlaceholder': {
|
||||
background: 'var(--code-editor-fold-placeholder-background)',
|
||||
},
|
||||
};
|
||||
@@ -1,118 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Signal} from '@angular/core';
|
||||
import {
|
||||
Completion,
|
||||
CompletionContext,
|
||||
CompletionResult,
|
||||
autocompletion,
|
||||
closeCompletion,
|
||||
completeFromList,
|
||||
insertCompletionText,
|
||||
} from '@codemirror/autocomplete';
|
||||
import {EditorView} from '@codemirror/view';
|
||||
import {Subject, filter, take} from 'rxjs';
|
||||
|
||||
import {EditorFile} from '../code-mirror-editor.service';
|
||||
import {TsVfsWorkerActions} from '../workers/enums/actions';
|
||||
import {AutocompleteRequest} from '../workers/interfaces/autocomplete-request';
|
||||
import {AutocompleteItem, AutocompleteResponse} from '../workers/interfaces/autocomplete-response';
|
||||
import {ActionMessage} from '../workers/interfaces/message';
|
||||
import {TransactionSpec} from '@codemirror/state';
|
||||
|
||||
// Factory method for autocomplete extension.
|
||||
export const getAutocompleteExtension = (
|
||||
emitter: Subject<ActionMessage>,
|
||||
currentFile: Signal<EditorFile>,
|
||||
sendRequestToTsVfs: (request: ActionMessage<AutocompleteRequest>) => void,
|
||||
) => {
|
||||
return autocompletion({
|
||||
activateOnTyping: true,
|
||||
override: [
|
||||
async (context: CompletionContext): Promise<CompletionResult | null> => {
|
||||
try {
|
||||
const contextPositions = context.state.wordAt(context.pos);
|
||||
|
||||
sendRequestToTsVfs({
|
||||
action: TsVfsWorkerActions.AUTOCOMPLETE_REQUEST,
|
||||
data: {
|
||||
file: currentFile().filename,
|
||||
position: context.pos,
|
||||
from: contextPositions?.from,
|
||||
to: contextPositions?.to,
|
||||
content: context.state.doc.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
const completions = await new Promise<AutocompleteResponse | undefined>((resolve) => {
|
||||
emitter
|
||||
.pipe(
|
||||
filter((event) => event.action === TsVfsWorkerActions.AUTOCOMPLETE_RESPONSE),
|
||||
take(1),
|
||||
)
|
||||
.subscribe((message: ActionMessage<AutocompleteResponse>) => {
|
||||
resolve(message.data);
|
||||
});
|
||||
});
|
||||
|
||||
if (!completions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const completionSource = completeFromList(
|
||||
completions.map((completionItem) => {
|
||||
const suggestions: Completion = {
|
||||
type: completionItem.kind,
|
||||
label: completionItem.name,
|
||||
boost: 1 / Number(completionItem.sortText),
|
||||
detail: completionItem?.codeActions?.[0]?.description,
|
||||
apply: (view, completion, from, to) =>
|
||||
applyWithCodeAction(view, {...completion, ...completionItem}, from, to),
|
||||
};
|
||||
|
||||
return suggestions;
|
||||
}),
|
||||
);
|
||||
|
||||
return completionSource(context);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const applyWithCodeAction = (
|
||||
view: EditorView,
|
||||
completion: Completion & AutocompleteItem,
|
||||
from: number,
|
||||
to: number,
|
||||
) => {
|
||||
const transactionSpecs: TransactionSpec[] = [
|
||||
insertCompletionText(view.state, completion.label, from, to),
|
||||
];
|
||||
|
||||
if (completion.codeActions?.length) {
|
||||
const {span, newText} = completion.codeActions[0].changes[0].textChanges[0];
|
||||
|
||||
transactionSpecs.push(
|
||||
insertCompletionText(view.state, newText, span.start, span.start + span.length),
|
||||
);
|
||||
}
|
||||
|
||||
view.dispatch(
|
||||
...transactionSpecs,
|
||||
// avoid moving cursor to the autocompleted text
|
||||
{selection: view.state.selection},
|
||||
);
|
||||
|
||||
// Manually close the autocomplete picker after applying the completion
|
||||
closeCompletion(view);
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Diagnostic, linter} from '@codemirror/lint';
|
||||
import {TsVfsWorkerActions} from '../workers/enums/actions';
|
||||
import {Signal} from '@angular/core';
|
||||
import {EditorFile} from '../code-mirror-editor.service';
|
||||
import {ActionMessage} from '../workers/interfaces/message';
|
||||
import {DiagnosticsRequest} from '../workers/interfaces/diagnostics-request';
|
||||
import {Subject, filter, take} from 'rxjs';
|
||||
import {DiagnosticWithLocation, DiagnosticsState} from '../services/diagnostics-state.service';
|
||||
|
||||
// Factory method for diagnostics extension.
|
||||
export const getDiagnosticsExtension = (
|
||||
eventManager: Subject<ActionMessage>,
|
||||
currentFile: Signal<EditorFile>,
|
||||
sendRequestToTsVfs: (request: ActionMessage<DiagnosticsRequest>) => void,
|
||||
diagnosticsState: DiagnosticsState,
|
||||
) => {
|
||||
return linter(
|
||||
async (view): Promise<Diagnostic[]> => {
|
||||
sendRequestToTsVfs({
|
||||
action: TsVfsWorkerActions.DIAGNOSTICS_REQUEST,
|
||||
data: {
|
||||
file: currentFile().filename,
|
||||
},
|
||||
});
|
||||
|
||||
const diagnostics = await new Promise((resolve) => {
|
||||
eventManager
|
||||
.pipe(
|
||||
filter((event) => event.action === TsVfsWorkerActions.DIAGNOSTICS_RESPONSE),
|
||||
take(1),
|
||||
)
|
||||
.subscribe((response: ActionMessage<Diagnostic[]>) => {
|
||||
resolve(response.data);
|
||||
});
|
||||
});
|
||||
|
||||
const result = !!diagnostics ? (diagnostics as DiagnosticWithLocation[]) : [];
|
||||
|
||||
diagnosticsState.setDiagnostics(result);
|
||||
|
||||
return result;
|
||||
},
|
||||
{
|
||||
delay: 400,
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -1,143 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Signal} from '@angular/core';
|
||||
import {Tooltip, hoverTooltip} from '@codemirror/view';
|
||||
import {marked} from 'marked';
|
||||
import {Subject, filter, take} from 'rxjs';
|
||||
import type {JSDocTagInfo, SymbolDisplayPart} from 'typescript';
|
||||
|
||||
import {EditorFile} from '../code-mirror-editor.service';
|
||||
import {TsVfsWorkerActions} from '../workers/enums/actions';
|
||||
import {DisplayTooltipRequest} from '../workers/interfaces/display-tooltip-request';
|
||||
import {DisplayTooltipResponse} from '../workers/interfaces/display-tooltip-response';
|
||||
import {ActionMessage} from '../workers/interfaces/message';
|
||||
|
||||
export const getTooltipExtension = (
|
||||
emitter: Subject<ActionMessage<DisplayTooltipResponse>>,
|
||||
currentFile: Signal<EditorFile>,
|
||||
sendRequestToTsVfs: (request: ActionMessage<DisplayTooltipRequest>) => void,
|
||||
) => {
|
||||
return hoverTooltip(
|
||||
async (_, pos: number): Promise<Tooltip | null> => {
|
||||
sendRequestToTsVfs({
|
||||
action: TsVfsWorkerActions.DISPLAY_TOOLTIP_REQUEST,
|
||||
data: {
|
||||
file: currentFile().filename,
|
||||
position: pos,
|
||||
},
|
||||
});
|
||||
|
||||
const response = await new Promise<DisplayTooltipResponse>((resolve) => {
|
||||
emitter
|
||||
.pipe(
|
||||
filter((event) => event.action === TsVfsWorkerActions.DISPLAY_TOOLTIP_RESPONSE),
|
||||
take(1),
|
||||
)
|
||||
.subscribe((message) => {
|
||||
resolve(message.data!);
|
||||
});
|
||||
});
|
||||
|
||||
if (!response?.displayParts) return null;
|
||||
|
||||
const {displayParts, tags, documentation} = response;
|
||||
|
||||
return {
|
||||
pos,
|
||||
create() {
|
||||
const tooltip = document.createElement('div');
|
||||
|
||||
tooltip.appendChild(getHtmlFromDisplayParts(displayParts));
|
||||
|
||||
// use documentation if available as it's more informative than tags
|
||||
if (documentation?.[0]?.text) {
|
||||
tooltip.appendChild(getMarkedHtmlFromString(documentation[0]?.text));
|
||||
} else if (tags?.length) {
|
||||
tooltip.appendChild(getTagsHtml(tags));
|
||||
}
|
||||
|
||||
return {
|
||||
dom: tooltip,
|
||||
|
||||
// Note: force the tooltip to scroll to the top on mount and on position change
|
||||
// because depending on the position of the mouse and the size of the tooltip content,
|
||||
// the tooltip might render with its initial scroll position on the bottom
|
||||
mount: (_) => forceTooltipScrollTop(),
|
||||
positioned: (_) => forceTooltipScrollTop(),
|
||||
resize: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
hideOnChange: true,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
function forceTooltipScrollTop() {
|
||||
const activeTooltip = document.querySelector('.cm-tooltip');
|
||||
|
||||
// only scroll if the tooltip is scrollable
|
||||
if (activeTooltip && activeTooltip.scrollHeight > activeTooltip.clientHeight) {
|
||||
activeTooltip.scroll(0, -activeTooltip.scrollHeight);
|
||||
}
|
||||
}
|
||||
|
||||
function getMarkedHtmlFromString(content: string): HTMLDivElement {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = marked(content) as string;
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function getHtmlFromDisplayParts(displayParts: SymbolDisplayPart[]): HTMLDivElement {
|
||||
const wrapper = document.createElement('div');
|
||||
|
||||
let displayPartWrapper = document.createElement('div');
|
||||
|
||||
for (const part of displayParts) {
|
||||
const span = document.createElement('span');
|
||||
span.classList.add(part.kind);
|
||||
span.textContent = part.text;
|
||||
|
||||
// create new div to separate lines when a line break is found
|
||||
if (part.kind === 'lineBreak') {
|
||||
wrapper.appendChild(displayPartWrapper);
|
||||
|
||||
displayPartWrapper = document.createElement('div');
|
||||
} else {
|
||||
displayPartWrapper.appendChild(span);
|
||||
}
|
||||
}
|
||||
|
||||
wrapper.appendChild(displayPartWrapper);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function getTagsHtml(tags: JSDocTagInfo[]): HTMLDivElement {
|
||||
const tagsWrapper = document.createElement('div');
|
||||
|
||||
let contentString = '';
|
||||
|
||||
for (let tag of tags) {
|
||||
contentString += `\n@${tag.name}\n`;
|
||||
|
||||
if (tag.text) {
|
||||
for (const {text} of tag.text) {
|
||||
contentString += text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tagsWrapper.innerHTML = marked(contentString) as string;
|
||||
|
||||
return tagsWrapper;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user