Compare commits

...

26 Commits

Author SHA1 Message Date
Andrew Kushnir 0b95d141ab release: cut the v17.2.0 release 2024-02-14 10:55:44 -08:00
Kristiyan Kostadinov 78b48cc272 refactor(compiler-cli): do not emit signal unwrap calls in versions older than 17.2 (#54423)
In order to allow both signals and non-signals in two-way bindings, we have to pass the expression through `ɵunwrapWritableSignal`. The problem is that the language service uses a bundled compiler that is fairly new, but it may be compiling an older version of Angular that doesn't expose `ɵunwrapWritableSignal` (see https://github.com/angular/vscode-ng-language-service/issues/2001).

These changes add a `_angularCoreVersion` flag to the compiler which the language service can use to pass the parsed Angular version to the compiler which can then decide whether to emit the function.

PR Close #54423
2024-02-13 15:53:42 -08:00
Matthieu Riegler 1c536250b6 fix(http): Use string body to generate transfer cache key. (#54379)
This is particularly usefull for GraphQL queries where the string body might be the only discriminator.

Fixes #54377

PR Close #54379
2024-02-13 11:53:22 -08:00
Joey Perrott 674b6c36b0 fix(docs-infra): don't include prerender flag based on fast/full build in adev (#54400)
The serve command for architect does not support the `--prerender` flag

PR Close #54400
2024-02-13 11:52:40 -08:00
Lukas Matta 35925263bd docs: fix typo (#54403)
PR Close #54403
2024-02-13 11:51:56 -08:00
Kristiyan Kostadinov 501c159e05 build: update node (#54399)
Updates the repo to a newer version of Node to fix errors in the integration tests.

PR Close #54399
2024-02-12 15:36:19 -08:00
Kristiyan Kostadinov 277540c4fc build: reuse fake core in type checking tests (#54344) (#54398)
Currently we have two fake copies of `@angular/core` in the compiler tests which can be out of sync and cause inconsistent tests. These changes reuse a single copy instead.

PR Close #54344

PR Close #54398
2024-02-12 14:31:26 -08:00
Kristiyan Kostadinov 6d00115bf4 fix(core): show placeholder block on the server with immediate trigger (#54394)
Currently all triggers are set up to show the placeholder block on the server, except for `on immediate` which is basically a noop. These changes update `on immediate` to match the rest of the triggers.

Fixes #54385.

PR Close #54394
2024-02-12 11:02:41 -08:00
Kristiyan Kostadinov ef965ac87c test(language-service): add tests for model inputs (#54387)
Updates the language service tests to cover `model()` inputs.

PR Close #54387
2024-02-12 11:01:54 -08:00
Kristiyan Kostadinov 369e96c837 refactor(compiler-cli): split input and model tests (#54387)
Splits up the tests for `input()` and `model()` into separate files.

PR Close #54387
2024-02-12 11:01:54 -08:00
Kristiyan Kostadinov 2363e77002 refactor(compiler-cli): add diagnostic tests for models (#54387)
Sets up type checking diagnostic tests for model() inputs.

PR Close #54387
2024-02-12 11:01:54 -08:00
Kristiyan Kostadinov 1581988d2e test(core): add type tests for ɵunwrapWritableSignal (#54387)
Getting the typing for `ɵunwrapWritableSignal` just right was tricky so these changes add some tests to ensure that we don't regress.

Also reworks the type tester a bit to make it easier to find where to add new test files.

PR Close #54387
2024-02-12 11:01:54 -08:00
Kristiyan Kostadinov 0ecc2d12f1 refactor(core): reuse input signal node for models (#54387)
Reworks the model so that it reuses `INPUT_SIGNAL_NODE` instead of implementing its own.

PR Close #54387
2024-02-12 11:01:53 -08:00
Kristiyan Kostadinov 453e00c9b0 refactor(core): avoid wrapper around subscribe return value (#54387)
Reworks `ModelSignal.subscribe` so it doesn't have to wrap its value to look like a subscription.

PR Close #54387
2024-02-12 11:01:53 -08:00
arturovt 4d722a276f fix(zone.js): patch form-associated custom element callbacks (#50686)
This commit updates the implementation of the `customElements` patch and also
patches FACE callbacks (`formAssociatedCallback`, `formDisabledCallback`, `formResetCallback`
and `formStateRestoreCallback`). This now allows invoking those callbacks in the same zone
where the custom element has been defined.

PR Close #50686
2024-02-12 08:50:57 -08:00
Pawel Kozlowski 744cb1e561 fix(core): return the same children query results if there are no changes (#54392)
Assure that the same readonly array corresponding to the children query results
is returned for cases where a query is marked as dirty but there were no actual
changes to the content of the results array (this can happen if a view is added
and removed thus marking queries as dirty but not influencing final results).

Fixes #54376

PR Close #54392
2024-02-12 08:48:29 -08:00
Joey Perrott deb9ea07c5 refactor(docs-infra): create fast mode (#54364)
Creates a "fast mode" for building the adev site, currently only disabling prerender during fast build.
This is intended to be used for local development.

PR Close #54364
2024-02-09 10:59:08 -08:00
JoostK 62b87b4551 fix(core): do not crash for signal query that does not have any matches (#54353)
The newly introduced signal queries would error if no match exists, due to an
invalid read within the query internals.

This commit addresses the crash by allowing there to be no matches.

PR Close #54353
2024-02-09 14:59:51 +00:00
Hakeem 6c278a9069 docs(docs-infra): change deprecated modules' headers' style in the hover state (#54337)
PR Close #54337
2024-02-09 14:58:56 +00:00
Joey Perrott 5de985a565 fix(docs-infra): include manually defined api reference docs in adev (#54356)
Include the manual defined api reference docs in adev

PR Close #54356
2024-02-09 14:58:29 +00:00
Angular Robot 85c05514be build: update cross-repo angular dependencies (#54186)
See associated pull request for more information.

PR Close #54186
2024-02-09 14:57:46 +00:00
Paul Gschwendtner 7aa92fa8fe release: cut the v17.2.0-rc.1 release 2024-02-09 11:54:07 +01:00
Kristiyan Kostadinov 4b96f370ee fix(core): expose model signal subcribe for type checking purposes (#54357)
The `@internal` in the comment above `ModelSignal.subscribe` ended up marking the method as internal even though it wasn't meant to be.

PR Close #54357
2024-02-09 09:58:56 +00:00
Matthieu Riegler 9e2c1405d1 refactor(devtools): show tooltip for hydration icon on directive forest (#54326)
The MatTooltip was missing after migrating to standalone

PR Close #54326
2024-02-08 19:18:37 +00:00
Joey Perrott 9f05732656 refactor: migrate router to prettier formatting (#54318)
Migrate formatting to prettier for router from clang-format

PR Close #54318
2024-02-08 19:17:14 +00:00
Jessica Janiuk ed41bfcad7 release: cut the v17.2.0-rc.0 release 2024-02-08 10:38:52 -08:00
176 changed files with 17379 additions and 12049 deletions
+3
View File
@@ -177,6 +177,9 @@ 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
+4 -4
View File
@@ -21,16 +21,16 @@ jobs:
(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@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
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@f89362d698858f9674463e48fde514b097a15838
- 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}}'
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
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@f89362d698858f9674463e48fde514b097a15838
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@c83e99a12397014162531ca125c94549db55dd84
with:
github-token: '${{secrets.GITHUB_TOKEN}}'
workflow-artifact-name: 'adev-preview'
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: ./.github/actions/yarn-install
- uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
- uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
with:
bazelrc: ./.bazelrc.user
@@ -34,7 +34,7 @@ jobs:
# 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@f89362d698858f9674463e48fde514b097a15838
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@c83e99a12397014162531ca125c94549db55dd84
with:
workflow-artifact-name: 'aio'
pull-number: '${{github.event.pull_request.number}}'
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
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@f89362d698858f9674463e48fde514b097a15838
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@c83e99a12397014162531ca125c94549db55dd84
with:
github-token: '${{secrets.GITHUB_TOKEN}}'
workflow-artifact-name: 'aio'
@@ -16,6 +16,6 @@ jobs:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
persist-credentials: false
- uses: angular/dev-infra/github-actions/branch-manager@f89362d698858f9674463e48fde514b097a15838
- uses: angular/dev-infra/github-actions/branch-manager@c83e99a12397014162531ca125c94549db55dd84
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
- uses: ./.github/actions/yarn-install
- uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
- uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
with:
bazelrc: ./.bazelrc.user
+2 -2
View File
@@ -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@f89362d698858f9674463e48fde514b097a15838
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.
@@ -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@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- 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
+28 -28
View File
@@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
with:
cache-node-modules: true
node-module-directories: |
@@ -60,13 +60,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
with:
cache-node-modules: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Run unit tests
@@ -78,13 +78,13 @@ jobs:
runs-on: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
with:
cache-node-modules: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
- name: Install node modules
run: yarn install --frozen-lockfile --network-timeout 100000
- name: Run CI tests for framework
@@ -94,15 +94,15 @@ jobs:
runs-on: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
- name: Install node modules for aio
run: yarn install --cwd aio --frozen-lockfile
- name: Run AIO tests with upstream packages
@@ -115,11 +115,11 @@ jobs:
labels: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
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
@@ -130,15 +130,15 @@ jobs:
labels: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
- name: Install node modules
run: yarn install --cwd aio --frozen-lockfile
- name: Run AIO tests with local packages
@@ -153,15 +153,15 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
- name: Install node modules for aio
run: yarn install --cwd aio --frozen-lockfile
- name: Set the stable branch environment variable
@@ -197,13 +197,13 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
with:
cache-node-modules: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
- name: Install node modules
run: yarn install --frozen-lockfile
- run: echo "https://${{secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN}}:@github.com" > ${HOME}/.git_credentials
@@ -215,7 +215,7 @@ jobs:
labels: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
with:
cache-node-modules: true
node-module-directories: |
@@ -223,9 +223,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@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
- name: Install node modules
run: yarn install --frozen-lockfile
- run: |
@@ -264,7 +264,7 @@ jobs:
JOBS: 2
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
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.
@@ -272,9 +272,9 @@ jobs:
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
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:
+2 -2
View File
@@ -13,13 +13,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: angular/dev-infra/github-actions/commit-message-based-labels@f89362d698858f9674463e48fde514b097a15838
- uses: angular/dev-infra/github-actions/commit-message-based-labels@c83e99a12397014162531ca125c94549db55dd84
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@f89362d698858f9674463e48fde514b097a15838
- uses: angular/dev-infra/github-actions/post-approval-changes@c83e99a12397014162531ca125c94549db55dd84
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+1 -1
View File
@@ -14,6 +14,6 @@ jobs:
if: github.repository == 'angular/angular'
runs-on: ubuntu-latest
steps:
- uses: angular/dev-infra/github-actions/feature-request@f89362d698858f9674463e48fde514b097a15838
- uses: angular/dev-infra/github-actions/feature-request@c83e99a12397014162531ca125c94549db55dd84
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: angular/dev-infra/github-actions/google-internal-tests@f89362d698858f9674463e48fde514b097a15838
- uses: angular/dev-infra/github-actions/google-internal-tests@c83e99a12397014162531ca125c94549db55dd84
with:
run-tests-guide-url: http://go/angular-g3sync-start
github-token: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -9,6 +9,6 @@ jobs:
status:
runs-on: ubuntu-latest
steps:
- uses: angular/dev-infra/github-actions/unified-status-check@f89362d698858f9674463e48fde514b097a15838
- uses: angular/dev-infra/github-actions/unified-status-check@c83e99a12397014162531ca125c94549db55dd84
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+4 -4
View File
@@ -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@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
- 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@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
with:
cache-node-modules: true
node-module-directories: |
+1 -1
View File
@@ -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@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/create-pr-for-changes@c83e99a12397014162531ca125c94549db55dd84
with:
branch-prefix: update-cli-help
pr-title: 'docs: update Angular CLI help [${{github.ref_name}}]'
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
- 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@f89362d698858f9674463e48fde514b097a15838
uses: angular/dev-infra/github-actions/create-pr-for-changes@c83e99a12397014162531ca125c94549db55dd84
with:
branch-prefix: docs-update-events
pr-title: 'docs: update events'
+3 -1
View File
@@ -20,6 +20,7 @@ export const format: FormatConfig = {
'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}',
@@ -33,7 +34,7 @@ export const format: FormatConfig = {
},
'clang-format': {
'matchers': [
//'**/*.{js,ts}',
'**/*.{js,ts}',
// TODO: burn down format failures and remove aio and integration exceptions.
'!aio/**',
'!integration/**',
@@ -70,6 +71,7 @@ export const format: FormatConfig = {
'!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}',
],
+1 -1
View File
@@ -1 +1 @@
18.13.0
18.18.2
+1
View File
@@ -2,6 +2,7 @@
"printWidth": 100,
"tabWidth": 2,
"tabs": false,
"embeddedLanguageFormatting": "off",
"singleQuote": true,
"semicolon": true,
"quoteProps": "preserve",
+110 -112
View File
@@ -1,3 +1,51 @@
<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
@@ -21,50 +69,9 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.2.0-next.1"></a>
# 17.2.0-next.1 (2024-01-31)
###
| Commit | Type | Description |
| -- | -- | -- |
| [dfc6c8d0c7](https://github.com/angular/angular/commit/dfc6c8d0c76380ee7beb6d904261e40857b375ed) | fix | cta clickability issue in adev homepage. ([#52905](https://github.com/angular/angular/pull/52905)) |
### animations
| Commit | Type | Description |
| -- | -- | -- |
| [75aeae42b7](https://github.com/angular/angular/commit/75aeae42b7f512553262d515966f43d11e34d228) | fix | cleanup DOM elements when root view is removed with async animations ([#53033](https://github.com/angular/angular/pull/53033)) |
### common
| Commit | Type | Description |
| -- | -- | -- |
| [f5c520b836](https://github.com/angular/angular/commit/f5c520b836c4545c7043649f28b3a0369c168747) | feat | add placeholder to NgOptimizedImage ([#53783](https://github.com/angular/angular/pull/53783)) |
| [122213d37d](https://github.com/angular/angular/commit/122213d37d3e73fc0dcfd5a10a2c388dc573b6cf) | 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 |
| -- | -- | -- |
| [47e6e84101](https://github.com/angular/angular/commit/47e6e841016abfca0c1aa84051d82a04b3027617) | feat | Add a TSConfig option `useTemplatePipeline` ([#54057](https://github.com/angular/angular/pull/54057)) |
| [7b4d275f49](https://github.com/angular/angular/commit/7b4d275f494a64c38b61cea7045ba8b6e8447b78) | fix | Fix the template pipeline option ([#54148](https://github.com/angular/angular/pull/54148)) |
| [eddf5dae5e](https://github.com/angular/angular/commit/eddf5dae5eb9e1aa3ca4c276c2cb2b897b73a9e0) | fix | Update type check block to fix control flow source mappings ([#53980](https://github.com/angular/angular/pull/53980)) |
### 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)) |
| [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)) |
| [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)) |
| [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 |
| -- | -- | -- |
| [656bc282e3](https://github.com/angular/angular/commit/656bc282e345c5e37a9189a0a4daa631e02c31bf) | fix | add toString implementation to signals ([#54002](https://github.com/angular/angular/pull/54002)) |
| [037b79b72e](https://github.com/angular/angular/commit/037b79b72ea18f08b3a74f9ad541bbdca183b1aa) | fix | change defer block fixture default behavior to playthrough ([#54088](https://github.com/angular/angular/pull/54088)) |
### migrations
| Commit | Type | Description |
| -- | -- | -- |
| [28ad6fc4ad](https://github.com/angular/angular/commit/28ad6fc4ad518884bb5777fd20e9075d8969b27a) | 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.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)) |
@@ -91,15 +98,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.2.0-next.0"></a>
# 17.2.0-next.0 (2024-01-24)
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [66e940aebf](https://github.com/angular/angular/commit/66e940aebfd5a93944860a4e0dbd14e1072f80f2) | feat | scope selectors in @starting-style ([#53943](https://github.com/angular/angular/pull/53943)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.1.1"></a>
# 17.1.1 (2024-01-24)
### router
@@ -435,9 +433,9 @@ Live long and prosper 🖖🏻
[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,
@@ -451,30 +449,30 @@ Live long and prosper 🖖🏻
- 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]);
```
@@ -490,7 +488,7 @@ Live long and prosper 🖖🏻
`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';
...
@@ -513,13 +511,13 @@ Live long and prosper 🖖🏻
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
@@ -530,7 +528,7 @@ Live long and prosper 🖖🏻
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';
@@ -541,7 +539,7 @@ Live long and prosper 🖖🏻
- 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`
@@ -550,12 +548,12 @@ Live long and prosper 🖖🏻
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)) |
@@ -1169,7 +1167,7 @@ Live long and prosper 🖖🏻
[Blog post "Angular v16 is now available"](https://goo.gle/angular-v16).
## Breaking Changes
###
###
- Angular Compatibility Compiler (ngcc) has been removed and as a result Angular View Engine libraries will no longer work
- Deprecated `EventManager` method `addGlobalEventListener` has been removed as it is not used by Ivy.
### bazel
@@ -1192,14 +1190,14 @@ Live long and prosper 🖖🏻
The ideal fix would be to update the code to instead be compatible with
`MockPlatformLocation` instead.
- If the 'ngTemplateOutletContext' is different from the context, it will result in a compile-time error.
Before the change, the following template was compiling:
```typescript
interface MyContext {
$implicit: string;
}
@Component({
standalone: true,
imports: [NgTemplateOutlet],
@@ -1217,7 +1215,7 @@ Live long and prosper 🖖🏻
}
```
However, it does not compile now because the 'xxx' property does not exist in 'MyContext', resulting in the error: 'Type '{ $implicit: string; xxx: string; }' is not assignable to type 'MyContext'.'
The solution is either:
- add the 'xxx' property to 'MyContext' with the correct type or
- add '$any(...)' inside the template to make the error disappear. However, adding '$any(...)' does not correct the error but only preserves the previous behavior of the code.
@@ -1236,7 +1234,7 @@ Live long and prosper 🖖🏻
value differs from the previous call to `setInput`.
- `RendererType2.styles` no longer accepts a nested arrays.
- The `APP_ID` token value is no longer randomly generated. If you are bootstrapping multiple application on the same page you will need to set to provide the `APP_ID` yourself.
```ts
bootstrapApplication(ComponentA, {
providers: [
@@ -1247,21 +1245,21 @@ Live long and prosper 🖖🏻
```
- The `ReflectiveInjector` and related symbols were removed. Please update the code to avoid references to the `ReflectiveInjector` symbol. Use `Injector.create` as a replacement to create an injector instead.
- Node.js v14 support has been removed
Node.js v14 is planned to be End-of-Life on 2023-04-30. Angular will stop supporting Node.js v14 in Angular v16. Angular v16 will continue to officially support Node.js versions v16 and v18.
### platform-browser
- The deprecated `BrowserTransferStateModule` was removed, since it's no longer needed. The `TransferState` class can be injected without providing the module. The `BrowserTransferStateModule` was empty starting from v14 and you can just remove the reference to that module from your applications.
### platform-server
- Users that are using SSR with JIT mode will now need to add `import to @angular/compiler` before bootstrapping the application.
**NOTE:** this does not effect users using the Angular CLI.
- `renderApplication` method no longer accepts a root component as first argument. Instead, provide a bootstrapping function that returns a `Promise<ApplicationRef>`.
Before
```ts
const output: string = await renderApplication(RootComponent, options);
```
Now
```ts
const bootstrap = () => bootstrapApplication(RootComponent, appConfig);
@@ -1296,14 +1294,14 @@ Live long and prosper 🖖🏻
## Deprecations
### core
- `makeStateKey`, `StateKey` and `TransferState` exports have been moved from `@angular/platform-browser` to `@angular/core`. Please update the imports.
```diff
- import {makeStateKey, StateKey, TransferState} from '@angular/platform-browser';
+ import {makeStateKey, StateKey, TransferState} from '@angular/core';
```
- `EnvironmentInjector.runInContext` is now deprecated, with
`runInInjectionContext` functioning as a direct replacement:
```typescript
// Previous method version (deprecated):
envInjector.runInContext(fn);
@@ -1316,7 +1314,7 @@ Live long and prosper 🖖🏻
### platform-browser
- `BrowserModule.withServerTransition` has been deprecated. `APP_ID` should be used instead to set the application ID.
NB: Unless, you render multiple Angular applications on the same page, setting an application ID is not necessary.
Before:
```ts
imports: [
@@ -1324,7 +1322,7 @@ Live long and prosper 🖖🏻
...
]
```
After:
```ts
imports: [
@@ -1336,7 +1334,7 @@ Live long and prosper 🖖🏻
- `ApplicationConfig` has moved, please import `ApplicationConfig` from `@angular/core` instead.
### platform-server
- `PlatformConfig.baseUrl` and `PlatformConfig.useAbsoluteUrl` platform-server config options are deprecated as these were not used.
###
###
| Commit | Type | Description |
| -- | -- | -- |
| [48aa96ea13](https://github.com/angular/angular/commit/48aa96ea13ebfadf2f6b13516c7702dae740a7be) | refactor | remove Angular Compatibility Compiler (ngcc) ([#49101](https://github.com/angular/angular/pull/49101)) |
@@ -1645,11 +1643,11 @@ Alan Agius, Andrew Kushnir, Aristeidis Bampakos, Craig Spence, Doug Parker, Ivá
<a name="15.2.0"></a>
# 15.2.0 (2023-02-22)
## Deprecations
###
###
- Class and `InjectionToken` guards and resolvers are
deprecated. Instead, write guards as plain JavaScript functions and
inject dependencies with `inject` from `@angular/core`.
###
###
| Commit | Type | Description |
| -- | -- | -- |
| [926c35f4ac](https://github.com/angular/angular/commit/926c35f4ac70f5e4d142e545d6d056dd67aac97b) | docs | Deprecate class and InjectionToken and resolvers ([#47924](https://github.com/angular/angular/pull/47924)) |
@@ -1814,21 +1812,21 @@ Alan Agius, Alex Rickabaugh, Andrew Scott, Aristeidis Bampakos, Bob Watson, Jens
- CanLoad guards in the Router are deprecated. Use CanMatch
instead.
- router writable properties
The following strategies are meant to be configured by registering the
application strategy in DI via the `providers` in the root `NgModule` or
`bootstrapApplication`:
* `routeReuseStrategy`
* `titleStrategy`
* `urlHandlingStrategy`
The following options are meant to be configured using the options
available in `RouterModule.forRoot` or `provideRouter`.
* `onSameUrlNavigation`
* `paramsInheritanceStrategy`
* `urlUpdateStrategy`
* `canceledNavigationResolution`
The following options are available in `RouterModule.forRoot` but not
available in `provideRouter`:
* `malformedUriErrorHandler` - This was found to not be used anywhere
@@ -2023,23 +2021,23 @@ Andrew Kushnir
- Keyframes names are now prefixed with the component's "scope name".
For example, the following keyframes rule in a component definition,
whose "scope name" is host-my-cmp:
@keyframes foo { ... }
will become:
@keyframes host-my-cmp_foo { ... }
Any TypeScript/JavaScript code which relied on the names of keyframes rules
will no longer match.
The recommended solutions in this case are to either:
- change the component's view encapsulation to the `None` or `ShadowDom`
- define keyframes rules in global stylesheets (e.g styles.css)
- define keyframes rules programmatically in code.
### compiler-cli
- Invalid constructors for DI may now report compilation errors
When a class inherits its constructor from a base class, the compiler may now
report an error when that constructor cannot be used for DI purposes. This may
either be because the base class is missing an Angular decorator such as
@@ -2048,7 +2046,7 @@ Andrew Kushnir
These situations used to behave unexpectedly at runtime, where the class may be
constructed without any of its constructor parameters, so this is now reported
as an error during compilation.
Any new errors that may be reported because of this change can be resolved either
by decorating the base class from which the constructor is inherited, or by adding
an explicit constructor to the class for which the error is reported.
@@ -2082,16 +2080,16 @@ Andrew Kushnir
### core
- - The ability to pass an `NgModule` to the `providedIn` option for
`@Injectable` and `InjectionToken` is now deprecated.
`providedIn: NgModule` was intended to be a tree-shakable alternative to
NgModule providers. It does not have wide usage, and in most cases is used
incorrectly, in circumstances where `providedIn: 'root'` should be
preferred. If providers should truly be scoped to a specific NgModule, use
`NgModule.providers` instead.
- The ability to set `providedIn: 'any'` for an `@Injectable` or
`InjectionToken` is now deprecated.
`providedIn: 'any'` is an option with confusing semantics and is almost
never used apart from a handful of esoteric cases internal to the framework.
- The bit field signature of `Injector.get()` has been deprecated, in favor of the new options object.
@@ -2694,16 +2692,16 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
- Keyframes names are now prefixed with the component's "scope name".
For example, the following keyframes rule in a component definition,
whose "scope name" is host-my-cmp:
@keyframes foo { ... }
will become:
@keyframes host-my-cmp_foo { ... }
Any TypeScript/JavaScript code which relied on the names of keyframes rules
will no longer match.
The recommended solutions in this case are to either:
- change the component's view encapsulation to the `None` or `ShadowDom`
- define keyframes rules in global stylesheets (e.g styles.css)
@@ -2713,23 +2711,23 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
- Support for Node.js v12 has been removed as it will become EOL on 2022-04-30. Please use Node.js v14.15 or later.
- TypeScript versions older than 4.6 are no longer supported.
- Forms [email] input coercion
Forms [email] input value will be considered as true if it is defined with any value rather
than false and 'false'.
- Since Ivy, TestBed doesn't use AOT summaries. The `aotSummaries` fields in TestBed APIs were present, but unused. The fields were deprecated in previous major version and in v14 those fields are removed. The `aotSummaries` fields were completely unused, so you can just drop them from the TestBed APIs usage.
### forms
- Forms classes accept a generic.
Forms model classes now accept a generic type parameter. Untyped versions of these classes are available to opt-out of the new, stricter behavior.
- objects with a length key set to zero will no longer validate as empty.
This is technically a breaking change, since objects with a key `length` and value `0` will no longer validate as empty. This is a very minor change, and any reliance on this behavior is probably a bug anyway.
### http
- Queries including + will now actually query for + instead of space.
Most workarounds involving custom codecs will be unaffected.
Possible server-side workarounds will need to be undone.
- JSONP will throw an error when headers are set on a reques
JSONP does not support headers being set on requests. Before when
a request was sent to a JSONP backend that had headers set the headers
were ignored. The JSONP backend will now throw an error if it
@@ -2740,7 +2738,7 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
- This change may cause a breaking change in unit tests that are implicitly depending on a specific
number and sequence of change detections in order for their assertions to pass.
- This may break invalid calls to `TransferState` methods.
This tightens parameter types of `TransferState` usage, and is a minor breaking change which may reveal existing problematic calls.
### router
- The type of `Route.pathMatch` is now stricter. Places that use
@@ -2762,7 +2760,7 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
actually gets set to something completely different. It's set to the
current internal `UrlTree` of the Router at the time navigation
occurs.
With this change, there is no exact replacement for the old value of
`initialUrl` because it was never intended to be exposed.
`Router.url` is likely the best replacement for this.
@@ -2786,7 +2784,7 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
returned by other guards: only the first value is used.
### zone.js
- in TaskTrackingZoneSpec track a periodic task until it is cancelled
The breaking change is scoped only to the plugin
`zone.js/plugins/task-tracking`. If you used `TaskTrackingZoneSpec` and
checked the pending macroTasks e.g. using `(this.ngZone as any)._inner
@@ -2795,7 +2793,7 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
previously the `setInterval` macrotask was no longer tracked after its
callback was executed for the first time. Now it's tracked until
the task is explicitly cancelled, e.g with `clearInterval(id)`.
## Deprecations
### common
- The `ngModuleFactory` input of the `NgComponentOutlet` directive is deprecated in favor of a newly added `ngModule` input. The `ngModule` input accepts references to the NgModule class directly, without the need to resolve module factory first.
+11 -1
View File
@@ -64,7 +64,17 @@ load("@rules_nodejs//nodejs:repositories.bzl", "nodejs_register_toolchains")
nodejs_register_toolchains(
name = "nodejs",
node_version = "18.13.0",
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",
)
# Download npm dependencies.
+26 -1
View File
@@ -1,4 +1,5 @@
load("//:packages.bzl", "link_packages")
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")
@@ -96,12 +97,36 @@ copy_to_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",
-1
View File
@@ -26,7 +26,6 @@
"index": "src/index.html",
"browser": "src/main.ts",
"server": "src/main.server.ts",
"prerender": true,
"polyfills": ["src/polyfills.ts", "zone.js"],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
+3
View File
@@ -65,10 +65,13 @@ copy_to_directory(
"//packages/upgrade:upgrade_docs",
"//packages/upgrade/static:upgrade_static_docs",
"//packages/upgrade/static/testing:upgrade_static_testing_docs",
"//tools/manual_api_docs/blocks:blocks_docs",
"//tools/manual_api_docs/elements:elements_docs",
],
replace_prefixes = {
"adev/src/content": "",
"packages/**/": "api/",
"tools/**/": "api/",
},
)
+1 -1
View File
@@ -43,7 +43,7 @@ It also includes troubleshooting information and any recommended manual changes
For simple updates, the CLI command [`ng update`](cli/update) is all you need.
Without additional arguments, [`ng update`](cli/update) lists the updates that are available to you and provides recommended steps to update your application to the most current version.
[Angular Versioning and Releases](reference/releases#versioning "Angular Release Practices, Versioning") describes the level of change that you can expect based a release's version number.
[Angular Versioning and Releases](reference/releases#versioning "Angular Release Practices, Versioning") describes the level of change that you can expect based on a release's version number.
It also describes supported update paths.
## Resource summary
+1 -1
View File
@@ -58,7 +58,7 @@ It also includes troubleshooting information and any recommended manual changes
For simple updates, the CLI command [`ng update`](cli/update) is all you need.
Without additional arguments, [`ng update`](cli/update) lists the updates that are available to you and provides recommended steps to update your application to the most current version.
[Angular Versioning and Releases](guide/releases#versioning "Angular Release Practices, Versioning") describes the level of change that you can expect based a release's version number.
[Angular Versioning and Releases](guide/releases#versioning "Angular Release Practices, Versioning") describes the level of change that you can expect based on a release's version number.
It also describes supported update paths.
<a id="resources"></a>
+17 -17
View File
@@ -61,33 +61,33 @@
},
"private": true,
"dependencies": {
"@angular/animations": "17.2.0-next.0",
"@angular/cdk": "17.2.0-next.0",
"@angular/common": "17.2.0-next.0",
"@angular/compiler": "17.2.0-next.0",
"@angular/core": "17.2.0-next.0",
"@angular/elements": "17.2.0-next.0",
"@angular/forms": "17.2.0-next.0",
"@angular/material": "17.2.0-next.0",
"@angular/platform-browser": "17.2.0-next.0",
"@angular/platform-browser-dynamic": "17.2.0-next.0",
"@angular/router": "17.2.0-next.0",
"@angular/service-worker": "17.2.0-next.0",
"@angular/animations": "17.2.0-rc.1",
"@angular/cdk": "17.2.0-rc.0",
"@angular/common": "17.2.0-rc.1",
"@angular/compiler": "17.2.0-rc.1",
"@angular/core": "17.2.0-rc.1",
"@angular/elements": "17.2.0-rc.1",
"@angular/forms": "17.2.0-rc.1",
"@angular/material": "17.2.0-rc.0",
"@angular/platform-browser": "17.2.0-rc.1",
"@angular/platform-browser-dynamic": "17.2.0-rc.1",
"@angular/router": "17.2.0-rc.1",
"@angular/service-worker": "17.2.0-rc.1",
"rxjs": "~7.8.0",
"safevalues": "^0.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/architect-cli": "0.1702.0-next.0",
"@angular-devkit/build-angular": "17.2.0-next.0",
"@angular-devkit/architect-cli": "0.1702.0-rc.0",
"@angular-devkit/build-angular": "17.2.0-rc.0",
"@angular-eslint/builder": "16.0.3",
"@angular-eslint/eslint-plugin": "16.0.3",
"@angular-eslint/eslint-plugin-template": "16.0.3",
"@angular-eslint/template-parser": "^16.0.3",
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#07da06414a4ce04a8f9710f2bc9346c2c753a0d2",
"@angular/cli": "17.2.0-next.0",
"@angular/compiler-cli": "17.2.0-next.0",
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#7c4cf003cb4ac849986beaa243d7e85a893612f2",
"@angular/cli": "17.2.0-rc.0",
"@angular/compiler-cli": "17.2.0-rc.1",
"@bazel/bazelisk": "^1.7.5",
"@bazel/buildozer": "^6.0.0",
"@bazel/ibazel": "^0.16.2",
@@ -94,6 +94,10 @@ aio-api-list {
h2 {
margin-top: 16px;
margin-bottom: 16px;
& .deprecated-api-item {
text-decoration: line-through;
}
}
/* API CLASS LIST */
@@ -13,14 +13,14 @@
},
"private": true,
"dependencies": {
"@angular/animations": "17.1.1",
"@angular/common": "17.1.1",
"@angular/compiler": "17.1.1",
"@angular/core": "17.1.1",
"@angular/forms": "17.1.1",
"@angular/platform-browser": "17.1.1",
"@angular/platform-browser-dynamic": "17.1.1",
"@angular/router": "17.1.1",
"@angular/animations": "17.1.3",
"@angular/common": "17.1.3",
"@angular/compiler": "17.1.3",
"@angular/core": "17.1.3",
"@angular/forms": "17.1.3",
"@angular/platform-browser": "17.1.3",
"@angular/platform-browser-dynamic": "17.1.3",
"@angular/router": "17.1.3",
"angular": "^1.8.3",
"angular-in-memory-web-api": "~0.17.0",
"angular-route": "1.8.3",
@@ -29,9 +29,9 @@
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.1.1",
"@angular/cli": "17.1.1",
"@angular/compiler-cli": "17.1.1",
"@angular-devkit/build-angular": "17.1.3",
"@angular/cli": "17.1.3",
"@angular/compiler-cli": "17.1.3",
"@types/angular": "^1.8.4",
"@types/angular-route": "^1.7.2",
"@types/jasmine": "~5.1.0",
@@ -13,23 +13,23 @@
},
"private": true,
"dependencies": {
"@angular/animations": "17.1.1",
"@angular/common": "17.1.1",
"@angular/compiler": "17.1.1",
"@angular/core": "17.1.1",
"@angular/forms": "17.1.1",
"@angular/platform-browser": "17.1.1",
"@angular/platform-browser-dynamic": "17.1.1",
"@angular/router": "17.1.1",
"@angular/animations": "17.1.3",
"@angular/common": "17.1.3",
"@angular/compiler": "17.1.3",
"@angular/core": "17.1.3",
"@angular/forms": "17.1.3",
"@angular/platform-browser": "17.1.3",
"@angular/platform-browser-dynamic": "17.1.3",
"@angular/router": "17.1.3",
"angular-in-memory-web-api": "~0.17.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.1.1",
"@angular/cli": "17.1.1",
"@angular/compiler-cli": "17.1.1",
"@angular-devkit/build-angular": "17.1.3",
"@angular/cli": "17.1.3",
"@angular/compiler-cli": "17.1.3",
"@types/jasmine": "~5.1.0",
"@types/node": "^16.11.35",
"copyfiles": "^2.4.1",
@@ -13,24 +13,24 @@
},
"private": true,
"dependencies": {
"@angular/animations": "17.1.1",
"@angular/common": "17.1.1",
"@angular/compiler": "17.1.1",
"@angular/core": "17.1.1",
"@angular/elements": "17.1.1",
"@angular/forms": "17.1.1",
"@angular/platform-browser": "17.1.1",
"@angular/platform-browser-dynamic": "17.1.1",
"@angular/router": "17.1.1",
"@angular/animations": "17.1.3",
"@angular/common": "17.1.3",
"@angular/compiler": "17.1.3",
"@angular/core": "17.1.3",
"@angular/elements": "17.1.3",
"@angular/forms": "17.1.3",
"@angular/platform-browser": "17.1.3",
"@angular/platform-browser-dynamic": "17.1.3",
"@angular/router": "17.1.3",
"angular-in-memory-web-api": "~0.17.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.1.1",
"@angular/cli": "17.1.1",
"@angular/compiler-cli": "17.1.1",
"@angular-devkit/build-angular": "17.1.3",
"@angular/cli": "17.1.3",
"@angular/compiler-cli": "17.1.3",
"@types/jasmine": "~5.1.0",
"@types/node": "^16.11.35",
"jasmine-core": "~5.1.0",
@@ -15,24 +15,24 @@
},
"private": true,
"dependencies": {
"@angular/animations": "17.1.1",
"@angular/common": "17.1.1",
"@angular/compiler": "17.1.1",
"@angular/core": "17.1.1",
"@angular/forms": "17.1.1",
"@angular/localize": "17.1.1",
"@angular/platform-browser": "17.1.1",
"@angular/platform-browser-dynamic": "17.1.1",
"@angular/router": "17.1.1",
"@angular/animations": "17.1.3",
"@angular/common": "17.1.3",
"@angular/compiler": "17.1.3",
"@angular/core": "17.1.3",
"@angular/forms": "17.1.3",
"@angular/localize": "17.1.3",
"@angular/platform-browser": "17.1.3",
"@angular/platform-browser-dynamic": "17.1.3",
"@angular/router": "17.1.3",
"angular-in-memory-web-api": "~0.17.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.1.1",
"@angular/cli": "17.1.1",
"@angular/compiler-cli": "17.1.1",
"@angular-devkit/build-angular": "17.1.3",
"@angular/cli": "17.1.3",
"@angular/compiler-cli": "17.1.3",
"@types/jasmine": "~5.1.0",
"@types/node": "^16.11.35",
"jasmine-core": "~5.1.0",
@@ -13,24 +13,24 @@
},
"private": true,
"dependencies": {
"@angular/animations": "17.1.1",
"@angular/common": "17.1.1",
"@angular/compiler": "17.1.1",
"@angular/core": "17.1.1",
"@angular/forms": "17.1.1",
"@angular/platform-browser": "17.1.1",
"@angular/platform-browser-dynamic": "17.1.1",
"@angular/router": "17.1.1",
"@angular/service-worker": "17.1.1",
"@angular/animations": "17.1.3",
"@angular/common": "17.1.3",
"@angular/compiler": "17.1.3",
"@angular/core": "17.1.3",
"@angular/forms": "17.1.3",
"@angular/platform-browser": "17.1.3",
"@angular/platform-browser-dynamic": "17.1.3",
"@angular/router": "17.1.3",
"@angular/service-worker": "17.1.3",
"angular-in-memory-web-api": "~0.17.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.1.1",
"@angular/cli": "17.1.1",
"@angular/compiler-cli": "17.1.1",
"@angular-devkit/build-angular": "17.1.3",
"@angular/cli": "17.1.3",
"@angular/compiler-cli": "17.1.3",
"@types/jasmine": "~5.1.0",
"@types/node": "^16.11.35",
"jasmine-core": "~5.1.0",
@@ -25,15 +25,15 @@
},
"private": true,
"dependencies": {
"@angular/animations": "17.1.1",
"@angular/common": "17.1.1",
"@angular/compiler": "17.1.1",
"@angular/core": "17.1.1",
"@angular/forms": "17.1.1",
"@angular/platform-browser": "17.1.1",
"@angular/platform-browser-dynamic": "17.1.1",
"@angular/router": "17.1.1",
"@angular/upgrade": "17.1.1",
"@angular/animations": "17.1.3",
"@angular/common": "17.1.3",
"@angular/compiler": "17.1.3",
"@angular/core": "17.1.3",
"@angular/forms": "17.1.3",
"@angular/platform-browser": "17.1.3",
"@angular/platform-browser-dynamic": "17.1.3",
"@angular/router": "17.1.3",
"@angular/upgrade": "17.1.3",
"core-js": "^3.0.0",
"rxjs": "~7.8.0",
"systemjs": "~0.19.39",
@@ -42,7 +42,7 @@
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular/compiler-cli": "17.1.1",
"@angular/compiler-cli": "17.1.3",
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-node-resolve": "^13.2.0",
"@types/angular": "^1.8.4",
@@ -13,16 +13,16 @@
},
"private": true,
"dependencies": {
"@angular/animations": "17.1.1",
"@angular/common": "17.1.1",
"@angular/compiler": "17.1.1",
"@angular/core": "17.1.1",
"@angular/forms": "17.1.1",
"@angular/platform-browser": "17.1.1",
"@angular/platform-browser-dynamic": "17.1.1",
"@angular/platform-server": "17.1.1",
"@angular/router": "17.1.1",
"@angular/ssr": "17.1.1",
"@angular/animations": "17.1.3",
"@angular/common": "17.1.3",
"@angular/compiler": "17.1.3",
"@angular/core": "17.1.3",
"@angular/forms": "17.1.3",
"@angular/platform-browser": "17.1.3",
"@angular/platform-browser-dynamic": "17.1.3",
"@angular/platform-server": "17.1.3",
"@angular/router": "17.1.3",
"@angular/ssr": "17.1.3",
"angular-in-memory-web-api": "~0.17.0",
"express": "^4.18.1",
"rxjs": "~7.8.0",
@@ -30,9 +30,9 @@
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.1.1",
"@angular/cli": "17.1.1",
"@angular/compiler-cli": "17.1.1",
"@angular-devkit/build-angular": "17.1.3",
"@angular/cli": "17.1.3",
"@angular/compiler-cli": "17.1.3",
"@types/express": "^4.17.13",
"@types/jasmine": "~5.1.0",
"@types/node": "^16.11.35",
+17 -17
View File
@@ -19,20 +19,20 @@
"license": "MIT",
"repository": {},
"dependencies": {
"@angular/animations": "17.1.1",
"@angular/common": "17.1.1",
"@angular/compiler": "17.1.1",
"@angular/core": "17.1.1",
"@angular/elements": "17.1.1",
"@angular/forms": "17.1.1",
"@angular/localize": "17.1.1",
"@angular/platform-browser": "17.1.1",
"@angular/platform-browser-dynamic": "17.1.1",
"@angular/platform-server": "17.1.1",
"@angular/router": "17.1.1",
"@angular/service-worker": "17.1.1",
"@angular/ssr": "17.1.1",
"@angular/upgrade": "17.1.1",
"@angular/animations": "17.1.3",
"@angular/common": "17.1.3",
"@angular/compiler": "17.1.3",
"@angular/core": "17.1.3",
"@angular/elements": "17.1.3",
"@angular/forms": "17.1.3",
"@angular/localize": "17.1.3",
"@angular/platform-browser": "17.1.3",
"@angular/platform-browser-dynamic": "17.1.3",
"@angular/platform-server": "17.1.3",
"@angular/router": "17.1.3",
"@angular/service-worker": "17.1.3",
"@angular/ssr": "17.1.3",
"@angular/upgrade": "17.1.3",
"angular": "^1.8.3",
"angular-in-memory-web-api": "~0.17.0",
"angular-route": "1.8.3",
@@ -45,9 +45,9 @@
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.1.1",
"@angular/cli": "17.1.1",
"@angular/compiler-cli": "17.1.1",
"@angular-devkit/build-angular": "17.1.3",
"@angular/cli": "17.1.3",
"@angular/compiler-cli": "17.1.3",
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-node-resolve": "^13.2.0",
"@types/angular": "^1.8.4",
+105 -105
View File
@@ -10,23 +10,23 @@
"@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9"
"@angular-devkit/architect@0.1701.1":
version "0.1701.1"
resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1701.1.tgz#80e2bc94763e21c273ef6d9f905390f1d935df60"
integrity sha512-vT3ZRAIfNyIg0vJWT6umPbCKiKFCukNkxLe9kgOU0tinZKNr/LgWYaBZ92Rxxi6C3NrAnmAYjsih8x4zdyoRXw==
"@angular-devkit/architect@0.1701.3":
version "0.1701.3"
resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1701.3.tgz#974cc8680baa5b8440cfc8039bbc4c46008e80bf"
integrity sha512-K5rvhslbXNwx04cCLviEJCA27MwoJRMMzALFXySi9BqjZnZUOtZnOBuuCdrTPaRmFaYqGO4Im5GNzpbb/NB8zg==
dependencies:
"@angular-devkit/core" "17.1.1"
"@angular-devkit/core" "17.1.3"
rxjs "7.8.1"
"@angular-devkit/build-angular@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-17.1.1.tgz#26a65e3daf8d306e39634b3322ee68ff19d0118c"
integrity sha512-GchDM8H+RQNts731c+jnhDgOm0PnCS3YB12uVwRiGsaNsUMrqKnu3P0poh6AImDMPyXKnIvTWLDCMD8TDziR0A==
"@angular-devkit/build-angular@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-17.1.3.tgz#05634e6a81ade1eb4e12844e37cbf636bf949bc4"
integrity sha512-pusFVSWMnrm2GrF3+Fw19OhA2rNw4WkfTMUruhaKAjW5QIvZ3wHYf+pH//1Ud+tuhFBi9BH7UALP2vnJMu1ehw==
dependencies:
"@ampproject/remapping" "2.2.1"
"@angular-devkit/architect" "0.1701.1"
"@angular-devkit/build-webpack" "0.1701.1"
"@angular-devkit/core" "17.1.1"
"@angular-devkit/architect" "0.1701.3"
"@angular-devkit/build-webpack" "0.1701.3"
"@angular-devkit/core" "17.1.3"
"@babel/core" "7.23.7"
"@babel/generator" "7.23.6"
"@babel/helper-annotate-as-pure" "7.22.5"
@@ -37,7 +37,7 @@
"@babel/preset-env" "7.23.7"
"@babel/runtime" "7.23.7"
"@discoveryjs/json-ext" "0.5.7"
"@ngtools/webpack" "17.1.1"
"@ngtools/webpack" "17.1.3"
"@vitejs/plugin-basic-ssl" "1.0.2"
ansi-colors "4.1.3"
autoprefixer "10.4.16"
@@ -90,18 +90,18 @@
optionalDependencies:
esbuild "0.19.11"
"@angular-devkit/build-webpack@0.1701.1":
version "0.1701.1"
resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1701.1.tgz#ab510fef446d2a9a2e71d4b1a688000494b28e09"
integrity sha512-YgNl/6xLmI0XdUCu/H4Jyi34BhrANCDP4N2Pz+tGwnz2+Vl8oZGLPGtKVbh/LKSAmEHk/B6GQUekSBpKWrPJoA==
"@angular-devkit/build-webpack@0.1701.3":
version "0.1701.3"
resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1701.3.tgz#584275d98be9e76d5db3d870ff37c57175d04a45"
integrity sha512-fpZtJf6yvXM7mX1T83caeYpa0e3zPv7sgKmx0ZIJKGL8+DETgNcCCeCTgui7HMBcHGCD8yj72DZ8xMMBWwVBIA==
dependencies:
"@angular-devkit/architect" "0.1701.1"
"@angular-devkit/architect" "0.1701.3"
rxjs "7.8.1"
"@angular-devkit/core@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-17.1.1.tgz#c140e6fcd158cea87e15d0d09f3879fc23a52637"
integrity sha512-b1wd1caegc1p18nTrfPhfHQAZW1GnWWKGldq5MZ8C/nkgJbjjN8SKb1Vw7GONkOnH6KxWDAXS4i93/wdQcz4Bg==
"@angular-devkit/core@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-17.1.3.tgz#d3b2212f9fa753506b1ccab24ee34edcdcd02e63"
integrity sha512-iuVK4hyW3YhusxIi8zGBvvVA9pWtDT3H6LQbWdVk9D3jXCZBIrEMklvAiJErqficKnUurf6gtFOeA8Fop6GotA==
dependencies:
ajv "8.12.0"
ajv-formats "2.1.1"
@@ -110,33 +110,33 @@
rxjs "7.8.1"
source-map "0.7.4"
"@angular-devkit/schematics@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-17.1.1.tgz#caaaeff64519d691121a92c92dbfd53581ff0bc4"
integrity sha512-3AtEO7k0Znzg11o51ZqebkW8063QkZ7Y7BC96Oye+wSdpT3ow57P0w0UtOpUNesNKzj1iMuPWqqm4i+YqitjCw==
"@angular-devkit/schematics@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-17.1.3.tgz#9fc4c82f64af9375249910d26093e449a73f0166"
integrity sha512-zKoWG1hDfvi1vR9Hqoca9hWo9vDg8evmQvGcBW5jXR5ndZi5Oit/uDcGdA8WUKvBd1EG7WMqp0FgcDR9EA9WCw==
dependencies:
"@angular-devkit/core" "17.1.1"
"@angular-devkit/core" "17.1.3"
jsonc-parser "3.2.0"
magic-string "0.30.5"
ora "5.4.1"
rxjs "7.8.1"
"@angular/animations@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-17.1.1.tgz#45b311fd9bd1d4aee1a4bedb49a4164cc622cd51"
integrity sha512-85qm8b4fNKa5zx4YlvCvAb3lWGlRsEcClnpCuwSVP8pXG7n/cW8MsJOh8i/wOkQTr9Ac7vvAE+yFWMi2A/qlTg==
"@angular/animations@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-17.1.3.tgz#be67fc479cfc700d7c1270222003c6d2bc622194"
integrity sha512-AS9CHOjjKqkuAzlKEMJfAkZfkIdSoagB3D8HwvH+ZHo6GVJc9KbtLQn/okNijFK+Fg7QK/hYbQ3lJhjgk0GQDA==
dependencies:
tslib "^2.3.0"
"@angular/cli@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-17.1.1.tgz#3aadcee37b8fd5b4ecb6c7ae8057dfb26973cc73"
integrity sha512-JG/Vf+PScR3PC6u7B+jFF4s5eBByzCpOfCfRFw98nlCqDAOxYOig7wi2Sbp5fnvILQH8vbc/NG8MzdgONrG6Hg==
"@angular/cli@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-17.1.3.tgz#45af3e5041f3df14ff10dc48ee64960f575d5aac"
integrity sha512-ysPWDdqo2cwfeskKVAg8p4C8xuezWcIWyW/ACSjWw6yp4OZvyVd6cGZrc0POVZzAPtTOYJSgWOpF/DCHQFluSg==
dependencies:
"@angular-devkit/architect" "0.1701.1"
"@angular-devkit/core" "17.1.1"
"@angular-devkit/schematics" "17.1.1"
"@schematics/angular" "17.1.1"
"@angular-devkit/architect" "0.1701.3"
"@angular-devkit/core" "17.1.3"
"@angular-devkit/schematics" "17.1.3"
"@schematics/angular" "17.1.3"
"@yarnpkg/lockfile" "1.1.0"
ansi-colors "4.1.3"
ini "4.1.1"
@@ -152,17 +152,17 @@
symbol-observable "4.0.0"
yargs "17.7.2"
"@angular/common@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular/common/-/common-17.1.1.tgz#67f6469329130d7c43d36e1cb3e6220157391ea0"
integrity sha512-YMM2vImWJg7H3Yaej7ncGpFKT28V2Y6X9/rLpRdSKAiUbcbj7GeWtX/upfZGR9KmD08baYZw0YTNMR03Ubv/mg==
"@angular/common@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular/common/-/common-17.1.3.tgz#d8411db80510a99a23ad01a56264c3594625936a"
integrity sha512-AzLzoNSeRSNGBQk0K+iG0XdYG36SDeJqYqE8rfoiWuv1NDFLL05UJM2/fQfaMNg0oX5bHOlHUqHFj3sFR/NVpw==
dependencies:
tslib "^2.3.0"
"@angular/compiler-cli@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-17.1.1.tgz#554e4cd090f4d64294bedd03868d3853a6d1af45"
integrity sha512-d6Aev1P92q7wd5u3UcJifzNlU9svxaYI2Ts6MKoD4jY4/GPN/gPDqi20weDMujEgirrkcwGbsCXBRoEGkA5c9A==
"@angular/compiler-cli@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-17.1.3.tgz#f1cbb68eb089c7aa161bab315cdc1a0004fac768"
integrity sha512-bNDHXo3Twub0BZK9OmXly+0REs0RuR1SUXlTAeq+0XubCvnBDvpg9peL7UTTGS5YRo9sUTBnR6faSUA1F5objQ==
dependencies:
"@babel/core" "7.23.2"
"@jridgewell/sourcemap-codec" "^1.4.14"
@@ -173,92 +173,92 @@
tslib "^2.3.0"
yargs "^17.2.1"
"@angular/compiler@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-17.1.1.tgz#b3e182390eb4f54690c0b6dd60d02ac389a1c0b4"
integrity sha512-lEQ5YNMJQm2iO2EZbGkwL3SqnxtE2ENfymgbS023F6ACsnP3kKB2DMwOnIbGgQY4+8r4sJFiMAIjEkj5c9kttg==
"@angular/compiler@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-17.1.3.tgz#157e834eb4b0cb4f339bc8a0da8dcb438e92034a"
integrity sha512-k/s21gPPKStxVOLr6l4Y145OIxyBY7BhTPVOl/qEAgE+IcZ9vkiA8dYl8yjL884Kl1ZKPmFA3AofMJjWjZGNag==
dependencies:
tslib "^2.3.0"
"@angular/core@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular/core/-/core-17.1.1.tgz#9da19f0c832d69994c79255c56e81f40ae7ae68a"
integrity sha512-JtNYM9eHr8eUSrGPq/kn0+/F+TSZ7EBWxZhM1ZndOlGu1gA4fGhrDid4ZXIHIs07DbM4NZjMn+LhRyx02YDsSA==
"@angular/core@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular/core/-/core-17.1.3.tgz#12f4adb8d1037ba1572c7531997d82a82fbe8fc1"
integrity sha512-2lZ4DRHN8KJ/aQads+YXIcx5Ri9yyeFIlw69m5Pn7wAi/+Rakg7IsclgLaWs7aBtWwMHG7LnqFKxAVq7CjXKtA==
dependencies:
tslib "^2.3.0"
"@angular/elements@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular/elements/-/elements-17.1.1.tgz#1fbbe9291a5029949a6bc5862b3ca11b945d94a9"
integrity sha512-GrPUCSw3RpjX1XmUpXrRdf19YE27hFDqXNpc/t36gWrQ3ndkYfU+I+fhNXigSoBvDW9/Pk+EJXTKX1LJ2CeOdw==
"@angular/elements@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular/elements/-/elements-17.1.3.tgz#944cb425e8c724417e499ab730009ce32d5ed2f7"
integrity sha512-q2JCogiHFB7Ac3IpiQGIUNF7ZDM5t/fF7xlDFvTLto7HHCb0g8y4erzmUmV+QRKX0YKedo5QAkd8w9TFlJ4ecA==
dependencies:
tslib "^2.3.0"
"@angular/forms@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-17.1.1.tgz#1e07572b207f4e78dc8abe9435d00c8fc0f37343"
integrity sha512-rqHVzaJDV8+VbnfC6mDgzX6ooa0X0hmnd+XfuOZaEJ7MtyOmqQ8qas2PAKXU7nMIImYXfYc4O4XWbSc1pRy1Hw==
"@angular/forms@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-17.1.3.tgz#1e87b39e237a1d6f7cc2f78ad8d6b597b5ef08aa"
integrity sha512-aNa0jGLT5d+hnKVrSo8tk3TRo/NLNu1RxLNx8RhIczKAeCK3eD8SvTMy27iJtyXmNG2GWN7QPiDeGepd75nbxQ==
dependencies:
tslib "^2.3.0"
"@angular/localize@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-17.1.1.tgz#f64b1724bdf4f0ab33551ca7bddd5e92d8cba6b8"
integrity sha512-vZc+YhpE7eRWFzdZFOidSjZlTl6+8VH0x2m7WesaSgw2fu1IgOQffZFnaGveAQ8DXbMfgBb4exPNJFPYmNgoSQ==
"@angular/localize@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-17.1.3.tgz#626564dcd89a3f5d26cd31eb2fcdc848e01898ad"
integrity sha512-Jdz5FWcaQLRhEhUHJk0iyMOT1ts4HHcY9B20mBmAd7u3kqEoBc57N8hOX0vHjU4/rk1tGqIvzfhJJf6zqIWcZQ==
dependencies:
"@babel/core" "7.23.2"
"@types/babel__core" "7.20.2"
fast-glob "3.3.1"
yargs "^17.2.1"
"@angular/platform-browser-dynamic@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.1.1.tgz#8d389d9739319a1260df27aceb7f4fffdecd28b7"
integrity sha512-UKI8966nwo+p01EjmQdkepLIeVLpPZTSDZAM4va7CfMO6lbCN5xFecDd/sVbut8J6ySIsbJxyDkP+SHMQjE+xg==
"@angular/platform-browser-dynamic@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.1.3.tgz#7391d547a22b85e95de8ee1f16faf6b876f26ae1"
integrity sha512-0lFhcFJfDzCSSVe8l8OY+UgUiwUwcbxwpvLod3XWBpf1iEUlr5720FIMA3VJYwpW3Oj4Uey3nVm13EMtRqpqdA==
dependencies:
tslib "^2.3.0"
"@angular/platform-browser@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-17.1.1.tgz#758b289f9d4920217c3bf754135e1558cd3d1cc1"
integrity sha512-/80znuEkdDvsF6EX/fe03isQlLCUS9+ldCgB4n0ZL+qAkf2/lJlU3n97SyEN7rzb189U+K1fDe0fb1nDwbbcWQ==
"@angular/platform-browser@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-17.1.3.tgz#3883cf089f2e5bd24b99df1e7c2cdd3df3d97514"
integrity sha512-onPCvdk9f/6OhOo2zP6nfGKpzLma1QIxpFqD3jymbmIJTcVMOOQDMYW3eLtY+uSX8ribcJ7GQcbDGIM4rliTFg==
dependencies:
tslib "^2.3.0"
"@angular/platform-server@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-17.1.1.tgz#9e0feec18c10974e3eed80e9a4e8cf28be6490a7"
integrity sha512-zZbJL56m1n/btwVV0TNc/YPqXYZp9rcnDEvl9HHFNfoinkQrcxHy6MHQUjEyQ1IAIv9Y0f73pmSbJ86BcloIAw==
"@angular/platform-server@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-17.1.3.tgz#b30236072a8a6f97198eaf41a121759e62922477"
integrity sha512-EB+sk4oZuUPEbkGSM0jpHAKiZVH98IBlXaRYDcmRCRw1hjZwsxl0kAQNuInSGfwJlyjVF7WCrZ6eNCuxlxXiAQ==
dependencies:
tslib "^2.3.0"
xhr2 "^0.2.0"
"@angular/router@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular/router/-/router-17.1.1.tgz#752e5856360844512f78267bea5364c7b7dac0d7"
integrity sha512-pPIRX0v8agij2dRSU25iwj9qFy0S25cztsy7bGfZ+M510jwRCqu1JsitqXtQ85XSv/bdFqiNiFU0UbwVFl+QiQ==
"@angular/router@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular/router/-/router-17.1.3.tgz#30d12b43079f44fda2ab968cde41da3548450d9e"
integrity sha512-6HigdtFjm+76UU2hiLGLE2SpOecQhD6TnAVTocDuRitpN5m0dyiffBrqxarfNwoZuMdIiXyqClJR4JRo1rJjoQ==
dependencies:
tslib "^2.3.0"
"@angular/service-worker@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-17.1.1.tgz#1cc2823d9a5fce14197f008b3b226dcab4d24a58"
integrity sha512-/ggG10tY5LWsGvJYLGhE0gdL8r9fEmRrZ6i+UCdwG/+Ydyb5GBxLS7OGH6yy0R8WP19FDRmVoDVMFJMtc4JMAw==
"@angular/service-worker@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-17.1.3.tgz#1222de7a4752b6f8dc7bbb5dcc6015db13346b45"
integrity sha512-DSndSwNvNnETiqtFAt/jPUMBUUfAxot7IhEUp19KFbS4HhasSZ+QxkNECN9bT6SUWjeaN7wvPcvR4gdSkQs3Pg==
dependencies:
tslib "^2.3.0"
"@angular/ssr@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular/ssr/-/ssr-17.1.1.tgz#8199fc8ce5d321c45592f471b523813ea8d39885"
integrity sha512-HHWk827BBLkVNpwsp31GYO484vXdFGMUoegoHCVd3qZL7gXcaAdN+zreE9E1OnaIImZWpj99le3MAsH6ee69BQ==
"@angular/ssr@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular/ssr/-/ssr-17.1.3.tgz#4bab9172d31043baeef4fdafb8a7789567f1704e"
integrity sha512-zgghp3gdh+pv//lYWWynfZOE9jkndSmflojgRWBR4FdAtapdyrm1MaE6TIwC7qT+r/sW0bKypK2fKxwN8sXpHw==
dependencies:
critters "0.0.20"
tslib "^2.3.0"
"@angular/upgrade@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@angular/upgrade/-/upgrade-17.1.1.tgz#2c6b8b4f690782423a499b0ca1c4f9cbd319bf17"
integrity sha512-DfIzp/t3cZZGcCyNghpyzBQFDMKo8qmRADtmR3ToBujERX4cwDcEySa/XBnFDjedcDusQB8pABnLOiatNk3HzA==
"@angular/upgrade@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@angular/upgrade/-/upgrade-17.1.3.tgz#4cf0a05866dced04114cb40de9f9e75d6897c0e4"
integrity sha512-Iw6J9jOg4P+CohWCsbchPnmOiLzuHhs5y0RqdA4+JL0dtTK8FkGkgaI39za74MYAbqNm6Z+q2yzlwyoe3gNEuw==
dependencies:
tslib "^2.3.0"
@@ -1737,10 +1737,10 @@
dependencies:
call-bind "^1.0.2"
"@ngtools/webpack@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-17.1.1.tgz#271177de0a382fccb5ba887a110ada9da79dc653"
integrity sha512-uPWEpRuAUmMBZhYMpkEHNbeI8QAgeVM5lnWM+02lK75u1+sgYy32ed+RcRvEI+8hRQcsCQ8HtR4QubgJb4TzCQ==
"@ngtools/webpack@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-17.1.3.tgz#510c5f6f6bf9ae470b0e43d4560cfc4ffc3a5ed5"
integrity sha512-mszRSb7aMNKHnkh3Jrfo83KVOguX/cUamJJcGIYe9o7tnLGRIoMp4vP0fx6Og4J0/CGDRhSDG4IiJ29aOU7K8A==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -1937,13 +1937,13 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz#10491ccf4f63c814d4149e0316541476ea603602"
integrity sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ==
"@schematics/angular@17.1.1":
version "17.1.1"
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-17.1.1.tgz#5ac8f09a66e7dff86cbf664b9388a0dde4a97de3"
integrity sha512-1Wqefy1W9Y63g48Fp7BscL95V4U1seDGgZawH6DcJnytJVW89hazao7YREzLYfdoediuw7lU+OHJksWYe4VQww==
"@schematics/angular@17.1.3":
version "17.1.3"
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-17.1.3.tgz#3c3e0f67985ed500d9d3b66fdb63387cc775e9a9"
integrity sha512-hmeasOvzmniy6urtzUKhEqGO67iPuLX/dVtkF4nWp2NTtcEKlvcJobNDMc+CTlX4+ZMPVOvmhDMQqrlfekZ+NQ==
dependencies:
"@angular-devkit/core" "17.1.1"
"@angular-devkit/schematics" "17.1.1"
"@angular-devkit/core" "17.1.3"
"@angular-devkit/schematics" "17.1.3"
jsonc-parser "3.2.0"
"@sigstore/bundle@^2.1.0":
+779 -108
View File
File diff suppressed because it is too large Load Diff
@@ -32,6 +32,7 @@ import {isChildOf, parentCollapsed} from './directive-forest-utils';
import {IndexedNode} from './index-forest';
import {MatIcon} from '@angular/material/icon';
import {FilterComponent} from './filter/filter.component';
import {MatTooltip} from '@angular/material/tooltip';
@Component({
selector: 'ng-directive-forest',
@@ -45,6 +46,7 @@ import {FilterComponent} from './filter/filter.component';
CdkFixedSizeVirtualScroll,
CdkVirtualForOf,
MatIcon,
MatTooltip,
],
})
export class DirectiveForestComponent {
+3 -1
View File
@@ -1098,7 +1098,9 @@ export interface ModelSignal<T> extends WritableSignal<T> {
// (undocumented)
[ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]: T;
// (undocumented)
[SIGNAL]: ModelSignalNode<T>;
[SIGNAL]: InputSignalNode<T, T>;
// @deprecated (undocumented)
subscribe(callback: (value: T) => void): () => void;
}
// @public @deprecated
+12 -12
View File
@@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "17.2.0-next.1",
"version": "17.2.0",
"private": true,
"description": "Angular - a web framework for modern web apps",
"homepage": "https://github.com/angular/angular",
@@ -43,18 +43,18 @@
"devtools:build:chrome": "bazelisk build --config snapshot-build --//devtools/projects/shell-browser/src:flag_browser=chrome -- devtools/projects/shell-browser/src:prodapp",
"devtools:build:firefox": "bazelisk build --config snapshot-build --//devtools/projects/shell-browser/src:flag_browser=firefox -- devtools/projects/shell-browser/src:prodapp",
"devtools:test": "bazelisk test --config snapshot-build --//devtools/projects/shell-browser/src:flag_browser=chrome -- //devtools/...",
"docs": "yarn bazel run --config=aio_local_deps //adev:serve",
"docs:build": "yarn bazel build --config=aio_local_deps //adev:build",
"docs": "[[ -n $CI ]] && echo 'Cannot run this yarn script on CI' && exit 1 || yarn bazel run --config=aio_local_deps //adev:serve --fast_adev",
"docs:build": "[[ -n $CI ]] && echo 'Cannot run this yarn script on CI' && exit 1 || yarn bazel build --config=aio_local_deps //adev:build --fast_adev",
"benchmarks": "ts-node --esm scripts/benchmarks/index.mts"
},
"// 1": "dependencies are used locally and by bazel",
"dependencies": {
"@angular-devkit/build-angular": "17.2.0-next.0",
"@angular-devkit/core": "17.2.0-next.0",
"@angular-devkit/schematics": "17.2.0-next.0",
"@angular/cdk": "17.2.0-next.0",
"@angular/cli": "17.2.0-next.0",
"@angular/material": "17.2.0-next.0",
"@angular-devkit/build-angular": "17.2.0-rc.0",
"@angular-devkit/core": "17.2.0-rc.0",
"@angular-devkit/schematics": "17.2.0-rc.0",
"@angular/cdk": "17.2.0-rc.0",
"@angular/cli": "17.2.0-rc.0",
"@angular/material": "17.2.0-rc.0",
"@babel/cli": "7.23.9",
"@babel/core": "7.23.9",
"@babel/generator": "7.23.6",
@@ -71,7 +71,7 @@
"@rollup/plugin-babel": "^6.0.0",
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-node-resolve": "^13.0.4",
"@schematics/angular": "17.2.0-next.0",
"@schematics/angular": "17.2.0-rc.0",
"@types/angular": "^1.6.47",
"@types/babel__core": "7.20.5",
"@types/babel__generator": "7.6.8",
@@ -152,9 +152,9 @@
"@actions/core": "^1.10.0",
"@angular-devkit/architect-cli": "^0.1702.0-next",
"@angular/animations": "^17.2.0-next",
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#07da06414a4ce04a8f9710f2bc9346c2c753a0d2",
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#7c4cf003cb4ac849986beaa243d7e85a893612f2",
"@angular/docs": "https://github.com/angular/dev-infra-private-docs-builds.git#87eb92c0cc022678fa3fedbd5ac25b84f285a7fa",
"@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#eacabd9f66aad05822ccb5b0dc893ebe7eb62321",
"@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#c21f93acb618bcaeda52a8065e7b6c9242def182",
"@babel/helper-remap-async-to-generator": "^7.18.9",
"@babel/plugin-proposal-async-generator-functions": "^7.20.7",
"@bazel/bazelisk": "^1.7.5",
+2
View File
@@ -98,5 +98,7 @@ generate_api_manifest(
"//packages/upgrade:upgrade_docs_extraction",
"//packages/upgrade/static:upgrade_static_docs_extraction",
"//packages/upgrade/static/testing:upgrade_static_testing_docs_extraction",
"//tools/manual_api_docs/blocks",
"//tools/manual_api_docs/elements",
],
)
+3 -2
View File
@@ -197,13 +197,14 @@ function getFilteredHeaders(
function makeCacheKey(request: HttpRequest<any>): StateKey<TransferHttpResponse> {
// make the params encoded same as a url so it's easy to identify
const {params, method, responseType, url} = request;
const {params, method, responseType, url, body} = request;
const encodedParams = params
.keys()
.sort()
.map((k) => `${k}=${params.getAll(k)}`)
.join('&');
const key = method + '.' + responseType + '.' + url + '?' + encodedParams;
const strBody = typeof body === 'string' ? body : '';
const key = [method, responseType, url, strBody, encodedParams].join('|');
const hash = generateHash(key);
@@ -98,8 +98,8 @@ describe('TransferCache', () => {
it('should store HTTP calls in cache when application is not stable', () => {
makeRequestAndExpectOne('/test', 'foo');
const key = makeStateKey('432906284');
const transferState = TestBed.inject(TransferState);
const key = makeStateKey(Object.keys((transferState as any).store)[0]);
expect(transferState.get(key, null)).toEqual(jasmine.objectContaining({[BODY]: 'foo'}));
});
@@ -115,7 +115,7 @@ describe('TransferCache', () => {
const transferState = TestBed.inject(TransferState);
expect(JSON.parse(transferState.toJson()) as Record<string, unknown>).toEqual({
'3706062792': {
'2400571479': {
[BODY]: 'foo',
[HEADERS]: {},
[STATUS]: 200,
@@ -123,7 +123,7 @@ describe('TransferCache', () => {
[URL]: '/test-1',
[RESPONSE_TYPE]: 'json',
},
'3706062823': {
'2400572440': {
[BODY]: 'buzz',
[HEADERS]: {},
[STATUS]: 200,
@@ -74,6 +74,14 @@ export interface InternalOptions {
* @internal
*/
_enableBlockSyntax?: boolean;
/**
* Detected version of `@angular/core` in the workspace. Used by the
* compiler to adjust the output depending on the available symbols.
*
* @internal
*/
_angularCoreVersion?: string;
}
/**
@@ -266,6 +266,7 @@ export class NgCompiler {
readonly ignoreForEmit: Set<ts.SourceFile>;
readonly enableTemplateTypeChecker: boolean;
private readonly enableBlockSyntax: boolean;
private readonly angularCoreVersion: Version|null;
/**
* `NgCompiler` can be reused for multiple compilations (for resource-only changes), and each
@@ -330,7 +331,10 @@ export class NgCompiler {
) {
this.enableTemplateTypeChecker =
enableTemplateTypeChecker || (options['_enableTemplateTypeChecker'] ?? false);
// TODO(crisbeto): remove this flag and base `enableBlockSyntx` on the `angularCoreVersion`.
this.enableBlockSyntax = options['_enableBlockSyntax'] ?? true;
this.angularCoreVersion =
options['_angularCoreVersion'] == null ? null : new Version(options['_angularCoreVersion']);
this.constructionDiagnostics.push(
...this.adapter.constructionDiagnostics, ...verifyCompatibleTypeCheckOptions(this.options));
@@ -791,6 +795,14 @@ export class NgCompiler {
const useInlineTypeConstructors = this.programDriver.supportsInlineOperations;
// Only Angular versions greater than 17.2 have the necessary symbols to type check signals in
// two-way bindings. We also allow version 0.0.0 in case somebody is using Angular at head.
const allowSignalsInTwoWayBindings = this.angularCoreVersion === null ||
this.angularCoreVersion.major > 17 ||
this.angularCoreVersion.major === 17 && this.angularCoreVersion.minor >= 2 ||
(this.angularCoreVersion.major === 0 && this.angularCoreVersion.minor === 0 ||
this.angularCoreVersion.patch === 0);
// First select a type-checking configuration, based on whether full template type-checking is
// requested.
let typeCheckingConfig: TypeCheckingConfig;
@@ -829,6 +841,7 @@ export class NgCompiler {
suggestionsForSuboptimalTypeInference: this.enableTemplateTypeChecker && !strictTemplates,
controlFlowPreventingContentProjection:
this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning,
allowSignalsInTwoWayBindings,
};
} else {
typeCheckingConfig = {
@@ -859,6 +872,7 @@ export class NgCompiler {
suggestionsForSuboptimalTypeInference: false,
controlFlowPreventingContentProjection:
this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning,
allowSignalsInTwoWayBindings,
};
}
@@ -1441,3 +1455,17 @@ function versionMapFromProgram(
}
return versions;
}
/** Representation of a parsed persion string. */
class Version {
readonly major: number;
readonly minor: number;
readonly patch: number;
constructor(public full: string) {
[this.major, this.minor, this.patch] = full.split('.').map(part => {
const parsed = parseInt(part);
return isNaN(parsed) ? -1 : parsed;
});
}
}
@@ -138,8 +138,7 @@ export interface WritableSignal<T> extends Signal<T> {
asReadonly(): Signal<T>;
}
// Note: needs to be kept in sync with the copies in `render3/reactivity/signal.ts` and
// `ngtsc/typecheck/testing/index.ts` to ensure consistent tests.
// Note: needs to be kept in sync with the copies in `render3/reactivity/signal.ts`.
export function ɵunwrapWritableSignal<T>(value: T|{[WRITABLE_SIGNAL]: T}): T {
return null!;
}
@@ -200,7 +199,7 @@ export interface ModelOptions {
export interface ModelSignal<T> extends WritableSignal<T> {
[ɵINPUT_SIGNAL_BRAND_READ_TYPE]: T;
[ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]: T;
subscribe(callback: (value: T) => void): {unsubscribe: () => void};
subscribe(callback: (value: T) => void): () => void;
}
export interface ModelFunction {
@@ -217,7 +216,20 @@ export const viewChildren: any = null!;
export const contentChild: any = null!;
export const contentChildren: any = null!;
export interface OutputEmitter<T> {
emit(value: T): void;
subscribe(listener: (v: T) => void): void;
}
/** Initializer-based output() API. */
export function output<T>(_opts?: {alias?: string}): EventEmitter<T> {
export function output<T>(_opts?: {alias?: string}): OutputEmitter<T> {
return null!;
}
export interface CreateComputedOptions<T> {
equal?: (a: T, b: T) => boolean;
}
export function computed<T>(computation: () => T, options?: CreateComputedOptions<T>): Signal<T> {
return null!;
}
@@ -330,6 +330,11 @@ export interface TypeCheckingConfig {
* opportunities to improve their own developer experience.
*/
suggestionsForSuboptimalTypeInference: boolean;
/**
* Whether the type of two-way bindings should be widened to allow `WritableSignal`.
*/
allowSignalsInTwoWayBindings: boolean;
}
@@ -21,6 +21,9 @@ ts_library(
jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular"],
data = [
"//packages/compiler-cli/src/ngtsc/testing/fake_core:npm_package",
],
deps = [
":test_lib",
],
@@ -21,6 +21,9 @@ ts_library(
jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular"],
data = [
"//packages/compiler-cli/src/ngtsc/testing/fake_core:npm_package",
],
deps = [
":test_lib",
],
@@ -21,6 +21,9 @@ ts_library(
jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular"],
data = [
"//packages/compiler-cli/src/ngtsc/testing/fake_core:npm_package",
],
deps = [
":test_lib",
],
@@ -21,6 +21,9 @@ ts_library(
jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular"],
data = [
"//packages/compiler-cli/src/ngtsc/testing/fake_core:npm_package",
],
deps = [
":test_lib",
],
@@ -21,6 +21,9 @@ ts_library(
jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular"],
data = [
"//packages/compiler-cli/src/ngtsc/testing/fake_core:npm_package",
],
deps = [
":test_lib",
],
@@ -21,6 +21,9 @@ ts_library(
jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular"],
data = [
"//packages/compiler-cli/src/ngtsc/testing/fake_core:npm_package",
],
deps = [
":test_lib",
],
@@ -21,6 +21,9 @@ ts_library(
jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular"],
data = [
"//packages/compiler-cli/src/ngtsc/testing/fake_core:npm_package",
],
deps = [
":test_lib",
],
@@ -21,6 +21,9 @@ ts_library(
jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular"],
data = [
"//packages/compiler-cli/src/ngtsc/testing/fake_core:npm_package",
],
deps = [
":test_lib",
],
@@ -21,6 +21,9 @@ ts_library(
jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular"],
data = [
"//packages/compiler-cli/src/ngtsc/testing/fake_core:npm_package",
],
deps = [
":test_lib",
],
@@ -815,7 +815,7 @@ class TcbDirectiveInputsOp extends TcbOp {
}
// Two-way bindings accept `T | WritableSignal<T>` so we have to unwrap the value.
if (isTwoWayBinding) {
if (isTwoWayBinding && this.tcb.env.config.allowSignalsInTwoWayBindings) {
assignment = unwrapWritableSignal(assignment, this.tcb);
}
@@ -2499,7 +2499,7 @@ function tcbCallTypeCtor(
// For bound inputs, the property is assigned the binding expression.
let expr = widenBinding(input.expression, tcb);
if (input.isTwoWayBinding) {
if (input.isTwoWayBinding && tcb.env.config.allowSignalsInTwoWayBindings) {
expr = unwrapWritableSignal(expr, tcb);
}
@@ -35,6 +35,9 @@ ts_library(
jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular"],
data = [
"//packages/compiler-cli/src/ngtsc/testing/fake_core:npm_package",
],
deps = [
":test_lib",
],
@@ -0,0 +1,735 @@
/**
* @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.io/license
*/
import {runInEachFileSystem} from '../../file_system/testing';
import {generateDiagnoseJasmineSpecs, TestCase} from './test_case_helper';
runInEachFileSystem(() => {
describe('model inputs type-check diagnostics', () => {
const bindingCases: TestCase[] = [
{
id: 'static binding',
inputs: {show: {type: 'ModelSignal<boolean>', isSignal: true}},
outputs: {showChange: {type: 'ModelSignal<boolean>'}},
template: `<div dir show="works">`,
expected: [
`TestComponent.html(1, 10): Type 'string' is not assignable to type 'boolean'.`,
],
},
{
id: 'one-way property binding',
inputs: {show: {type: 'ModelSignal<boolean>', isSignal: true}},
outputs: {showChange: {type: 'ModelSignal<boolean>'}},
template: `<div dir [show]="prop">`,
component: 'prop = true;',
expected: [],
},
{
id: 'complex object one-way binding',
inputs: {show: {type: 'ModelSignal<{works: boolean}>', isSignal: true}},
outputs: {showChange: {type: 'ModelSignal<{works: boolean}>'}},
template: `<div dir [show]="{works: true}">`,
expected: [],
},
{
id: 'complex object one-way binding, unexpected extra fields',
inputs: {show: {type: 'ModelSignal<{works: boolean}>', isSignal: true}},
outputs: {showChange: {type: 'ModelSignal<boolean>'}},
template: `<div dir [show]="{works: true, extraField: true}">`,
expected: [
jasmine.stringContaining(
`Object literal may only specify known properties, and '"extraField"' does not exist in type '{ works: boolean; }'.`)
],
},
{
id: 'complex object input, missing fields',
inputs: {show: {type: 'ModelSignal<{works: boolean}>', isSignal: true}},
outputs: {showChange: {type: 'ModelSignal<boolean>'}},
template: `<div dir [show]="{}">`,
expected: [
`TestComponent.html(1, 11): Property 'works' is missing in type '{}' but required in type '{ works: boolean; }'.`
],
},
// mixing cases
{
id: 'mixing zone input and model, valid',
inputs: {
zoneProp: {type: 'string', isSignal: false},
signalProp: {type: 'ModelSignal<string>', isSignal: true},
},
outputs: {signalPropChange: {type: 'ModelSignal<string>'}},
template: `<div dir [zoneProp]="'works'" [signalProp]="'stringVal'">`,
expected: []
},
{
id: 'mixing zone input and model, invalid zone binding',
inputs: {
zoneProp: {type: 'string', isSignal: false},
signalProp: {type: 'ModelSignal<string>', isSignal: true},
},
outputs: {signalPropChange: {type: 'ModelSignal<string>'}},
template: `<div dir [zoneProp]="false" [signalProp]="'stringVal'">`,
expected: [`TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`]
},
{
id: 'mixing zone input and model, invalid signal binding',
inputs: {
zoneProp: {type: 'string', isSignal: false},
signalProp: {type: 'ModelSignal<string>', isSignal: true},
},
outputs: {signalPropChange: {type: 'ModelSignal<string>'}},
template: `<div dir [zoneProp]="'works'" [signalProp]="{}">`,
expected: [`TestComponent.html(1, 32): Type '{}' is not assignable to type 'string'.`]
},
{
id: 'mixing zone input and model, both invalid',
inputs: {
zoneProp: {type: 'string', isSignal: false},
signalProp: {type: 'ModelSignal<string>', isSignal: true},
},
outputs: {signalPropChange: {type: 'ModelSignal<string>'}},
template: `<div dir [zoneProp]="false" [signalProp]="{}">`,
expected: [
`TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`,
`TestComponent.html(1, 30): Type '{}' is not assignable to type 'string'.`
]
},
// restricted fields
{
id: 'disallows access to private model',
inputs: {
pattern: {type: 'ModelSignal<string>', isSignal: true, restrictionModifier: 'private'}
},
outputs: {patternChange: {type: 'ModelSignal<string>'}},
template: `<div dir [pattern]="'works'">`,
expected: [
`TestComponent.html(1, 11): Property 'pattern' is private and only accessible within class 'Dir'.`
],
},
{
id: 'disallows access to protected model',
inputs: {
pattern: {type: 'ModelSignal<string>', isSignal: true, restrictionModifier: 'protected'}
},
outputs: {patternChange: {type: 'ModelSignal<string>'}},
template: `<div dir [pattern]="'works'">`,
expected: [
`TestComponent.html(1, 11): Property 'pattern' is protected and only accessible within class 'Dir' and its subclasses.`
],
},
{
id: 'allows access to readonly model by default',
inputs: {
pattern: {type: 'ModelSignal<string>', isSignal: true, restrictionModifier: 'readonly'}
},
outputs: {patternChange: {type: 'ModelSignal<string>'}},
template: `<div dir [pattern]="'works'">`,
expected: [],
},
// restricted fields (but opt-out of check)
{
id: 'allow access to private input if modifiers are explicitly ignored',
inputs: {
pattern: {type: 'ModelSignal<string>', isSignal: true, restrictionModifier: 'private'}
},
outputs: {patternChange: {type: 'ModelSignal<string>'}},
template: `<div dir [pattern]="'works'">`,
expected: [],
options: {
honorAccessModifiersForInputBindings: false,
},
},
{
id: 'allow access to protected model if modifiers are explicitly ignored',
inputs: {
pattern: {type: 'ModelSignal<string>', isSignal: true, restrictionModifier: 'protected'}
},
outputs: {patternChange: {type: 'ModelSignal<string>'}},
template: `<div dir [pattern]="'works'">`,
expected: [],
options: {
honorAccessModifiersForInputBindings: false,
},
},
{
id: 'allow access to readonly input if modifiers are explicitly ignored',
inputs: {
pattern: {type: 'ModelSignal<string>', isSignal: true, restrictionModifier: 'readonly'}
},
outputs: {patternChange: {type: 'ModelSignal<string>'}},
template: `<div dir [pattern]="'works'">`,
expected: [],
options: {
honorAccessModifiersForInputBindings: false,
},
},
{
id: 'allow access to private model if modifiers are explicitly ignored, but error if not assignable',
inputs: {
pattern: {type: 'ModelSignal<string>', isSignal: true, restrictionModifier: 'private'}
},
outputs: {patternChange: {type: 'ModelSignal<string>'}},
template: `<div dir [pattern]="false">`,
expected: [
`TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`,
],
options: {
honorAccessModifiersForInputBindings: false,
},
},
// coercion is not supported / respected
{
id: 'coercion members are not respected',
inputs: {pattern: {type: 'ModelSignal<string>', isSignal: true}},
outputs: {patternChange: {type: 'ModelSignal<string>'}},
extraDirectiveMembers: [
'static ngAcceptInputType_pattern: string|boolean',
],
template: `<div dir [pattern]="false">`,
expected: [
`TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`,
],
},
// with generics (type constructor tests)
{
id: 'generic inference and one-way binding to directive, all model inputs',
inputs: {
gen: {
type: 'ModelSignal<T>',
isSignal: true,
},
other: {
type: 'ModelSignal<T>',
isSignal: true,
}
},
outputs: {
genChange: {type: 'ModelSignal<T>'},
otherChange: {type: 'ModelSignal<T>'},
},
directiveGenerics: '<T>',
template: `<div dir [gen]="false" [other]="'invalid'">`,
expected: [
`TestComponent.html(1, 25): Type 'string' is not assignable to type 'boolean'.`,
],
},
{
id: 'generic inference and one-way binding to directive, mix of zone input and model',
inputs: {
gen: {
type: 'ModelSignal<T>',
isSignal: true,
},
other: {
type: 'T',
isSignal: false,
}
},
outputs: {genChange: {type: 'ModelSignal<T>'}},
directiveGenerics: '<T>',
template: `<div dir [gen]="false" [other]="'invalid'">`,
expected: [
`TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`,
],
},
{
id: 'generic inference and one-way binding to directive (with `extends boolean`), all model inputs',
inputs: {
gen: {
type: 'ModelSignal<T>',
isSignal: true,
},
other: {
type: 'ModelSignal<T>',
isSignal: true,
}
},
outputs: {genChange: {type: 'ModelSignal<T>'}, otherChange: {type: 'ModelSignal<T>'}},
directiveGenerics: '<T extends boolean>',
template: `<div dir [gen]="false" [other]="'invalid'">`,
expected: [
`TestComponent.html(1, 25): Type '"invalid"' is not assignable to type 'false'.`,
],
},
{
id: 'generic inference and one-way binding to directive (with `extends boolean`), mix of zone inputs and model',
inputs: {
gen: {
type: 'ModelSignal<T>',
isSignal: true,
},
other: {
type: 'T',
isSignal: false,
}
},
outputs: {genChange: {type: 'ModelSignal<T>'}},
directiveGenerics: '<T extends boolean>',
template: `<div dir [gen]="false" [other]="'invalid'">`,
expected: [
`TestComponent.html(1, 25): Type 'string' is not assignable to type 'boolean'.`,
],
},
{
id: 'generic multi-inference and one-way bindings to directive, all model inputs',
inputs: {
gen: {
type: 'ModelSignal<T>',
isSignal: true,
},
other: {
type: 'ModelSignal<U>',
isSignal: true,
}
},
outputs: {
genChange: {type: 'ModelSignal<T>'},
otherChange: {type: 'ModelSignal<U>'},
},
extraDirectiveMembers: [
'tester: {t: T, u: U} = null!',
],
directiveGenerics: '<T, U>',
template: `
<div dir [gen]="false" [other]="'text'"
#ref="dir" (click)="ref.tester = {t: 1, u: 0}">`,
expected: [
`TestComponent.html(3, 61): Type 'number' is not assignable to type 'boolean'.`,
`TestComponent.html(3, 67): Type 'number' is not assignable to type 'string'.`,
],
},
{
id: 'generic multi-inference and one-way bindings to directive, mix of zone inputs and models',
inputs: {
gen: {
type: 'ModelSignal<T>',
isSignal: true,
},
other: {
type: 'U',
isSignal: false,
}
},
outputs: {genChange: {type: 'ModelSignal<T>'}},
extraDirectiveMembers: [
'tester: {t: T, u: U} = null!',
],
directiveGenerics: '<T, U>',
template: `
<div dir [gen]="false" [other]="'text'"
#ref="dir" (click)="ref.tester = {t: 1, u: 0}">`,
expected: [
`TestComponent.html(3, 61): Type 'number' is not assignable to type 'boolean'.`,
`TestComponent.html(3, 67): Type 'number' is not assignable to type 'string'.`,
],
},
{
id: 'generic multi-inference and one-way bindings to directive, more complicated generic inference',
inputs: {
gen: {
type: 'ModelSignal<T>',
isSignal: true,
},
other: {
type: 'ModelSignal<{u: U}>',
isSignal: true,
}
},
outputs: {
genChange: {type: 'ModelSignal<T>'},
otherChange: {type: 'ModelSignal<{u: U}>'},
},
extraDirectiveMembers: [
'tester: {t: T, u: U} = null!',
],
directiveGenerics: '<T, U>',
template: `
<div dir [gen]="false" [other]="{u: null}"
#ref="dir" (click)="ref.tester = {t: 1, u: 0}">`,
expected: [
`TestComponent.html(3, 57): Type 'number' is not assignable to type 'boolean'.`,
`TestComponent.html(3, 63): Type 'number' is not assignable to type 'null'.`,
],
},
{
id: 'inline constructor generic inference, one-way binding',
inputs: {
bla: {
type: 'ModelSignal<T>',
isSignal: true,
},
},
outputs: {blaChange: {type: 'ModelSignal<T>'}},
extraFileContent: `
class SomeNonExportedClass {}
`,
extraDirectiveMembers: [
`tester: {t: T} = null!`,
],
directiveGenerics: '<T extends SomeNonExportedClass>',
template: `<div dir [bla]="prop" #ref="dir" (click)="ref.tester = {t: 0}">`,
component: `prop: HTMLElement = null!`,
expected: [
// This verifies that the `ref.tester.ts` is correctly inferred to be `HTMLElement`.
`TestComponent.html(1, 60): Type 'number' is not assignable to type 'HTMLElement'.`,
],
},
{
id: 'generic inference and two-way binding to directive, all model inputs',
inputs: {
gen: {
type: 'ModelSignal<T>',
isSignal: true,
},
other: {
type: 'ModelSignal<T>',
isSignal: true,
}
},
outputs: {
genChange: {type: 'ModelSignal<T>'},
otherChange: {type: 'ModelSignal<T>'},
},
directiveGenerics: '<T>',
component: `
genVal!: boolean;
otherVal!: string;
`,
template: `<div dir [(gen)]="genVal" [(other)]="otherVal">`,
expected: [
`TestComponent.html(1, 29): Type 'string' is not assignable to type 'boolean'.`,
],
},
{
id: 'generic inference and two-way binding to directive, mix of zone input and model',
inputs: {
gen: {
type: 'ModelSignal<T>',
isSignal: true,
},
other: {
type: 'T',
isSignal: false,
}
},
outputs: {
genChange: {type: 'ModelSignal<T>'},
otherChange: {type: 'EventEmitter<T>'},
},
directiveGenerics: '<T>',
template: `<div dir [(gen)]="genVal" [(other)]="otherVal">`,
component: `
genVal!: boolean;
otherVal!: string;
`,
expected: [
`TestComponent.html(1, 12): Type 'boolean' is not assignable to type 'string'.`,
],
},
{
id: 'generic inference and two-way binding to directive (with `extends boolean`), all model inputs',
inputs: {
gen: {
type: 'ModelSignal<T>',
isSignal: true,
},
other: {
type: 'ModelSignal<T>',
isSignal: true,
}
},
outputs: {genChange: {type: 'ModelSignal<T>'}, otherChange: {type: 'ModelSignal<T>'}},
directiveGenerics: '<T extends boolean>',
template: `<div dir [(gen)]="genVal" [(other)]="otherVal">`,
component: `
genVal!: boolean;
otherVal!: string;
`,
expected: [
`TestComponent.html(1, 29): Type 'string' is not assignable to type 'boolean'.`,
],
},
{
id: 'generic inference and two-way binding to directive (with `extends boolean`), mix of zone inputs and model',
inputs: {
gen: {
type: 'ModelSignal<T>',
isSignal: true,
},
other: {
type: 'T',
isSignal: false,
}
},
outputs: {
genChange: {type: 'ModelSignal<T>'},
otherChange: {type: 'EventEmitter<T>'},
},
directiveGenerics: '<T extends boolean>',
template: `<div dir [(gen)]="genVal" [(other)]="otherVal">`,
component: `
genVal!: boolean;
otherVal!: string;
`,
expected: [
`TestComponent.html(1, 29): Type 'string' is not assignable to type 'boolean'.`,
],
},
{
id: 'generic multi-inference and two-way bindings to directive, all model inputs',
inputs: {
gen: {
type: 'ModelSignal<T>',
isSignal: true,
},
other: {
type: 'ModelSignal<U>',
isSignal: true,
}
},
outputs: {
genChange: {type: 'ModelSignal<T>'},
otherChange: {type: 'ModelSignal<U>'},
},
extraDirectiveMembers: [
'tester: {t: T, u: U} = null!',
],
directiveGenerics: '<T, U>',
template: `
<div dir [(gen)]="genVal" [(other)]="otherVal"
#ref="dir" (click)="ref.tester = {t: 1, u: 0}">`,
component: `
genVal!: boolean;
otherVal!: string;
`,
expected: [
`TestComponent.html(3, 61): Type 'number' is not assignable to type 'boolean'.`,
`TestComponent.html(3, 67): Type 'number' is not assignable to type 'string'.`,
],
},
{
id: 'generic multi-inference and two-way bindings to directive, mix of zone inputs and models',
inputs: {
gen: {
type: 'ModelSignal<T>',
isSignal: true,
},
other: {
type: 'U',
isSignal: false,
}
},
outputs: {
genChange: {type: 'ModelSignal<T>'},
otherChange: {type: 'EventEmitter<U>'},
},
extraDirectiveMembers: [
'tester: {t: T, u: U} = null!',
],
directiveGenerics: '<T, U>',
template: `
<div dir [(gen)]="genVal" [(other)]="otherVal"
#ref="dir" (click)="ref.tester = {t: 1, u: 0}">`,
component: `
genVal!: boolean;
otherVal!: string;
`,
expected: [
`TestComponent.html(3, 61): Type 'number' is not assignable to type 'boolean'.`,
`TestComponent.html(3, 67): Type 'number' is not assignable to type 'string'.`,
],
},
{
id: 'generic multi-inference and two-way bindings to directive, more complicated generic inference',
inputs: {
gen: {
type: 'ModelSignal<T>',
isSignal: true,
},
other: {
type: 'ModelSignal<{u: U}>',
isSignal: true,
}
},
outputs: {
genChange: {type: 'ModelSignal<T>'},
otherChange: {type: 'ModelSignal<{u: U}>'},
},
extraDirectiveMembers: [
'tester: {t: T, u: U} = null!',
],
directiveGenerics: '<T, U>',
template: `
<div dir [(gen)]="genVal" [(other)]="otherVal"
#ref="dir" (click)="ref.tester = {t: 1, u: 0}">`,
component: `
genVal!: boolean;
otherVal!: {u: null};
`,
expected: [
`TestComponent.html(3, 57): Type 'number' is not assignable to type 'boolean'.`,
`TestComponent.html(3, 63): Type 'number' is not assignable to type 'null'.`,
],
},
{
id: 'inline constructor generic inference, two-way binding',
inputs: {
bla: {
type: 'ModelSignal<T>',
isSignal: true,
},
},
outputs: {blaChange: {type: 'ModelSignal<T>'}},
extraFileContent: `
class SomeNonExportedClass {}
`,
extraDirectiveMembers: [
`tester: {t: T} = null!`,
],
directiveGenerics: '<T extends SomeNonExportedClass>',
template: `<div dir [(bla)]="prop" #ref="dir" (click)="ref.tester = {t: 0}">`,
component: `prop: HTMLElement = null!`,
expected: [
// This verifies that the `ref.tester.ts` is correctly inferred to be `HTMLElement`.
`TestComponent.html(1, 62): Type 'number' is not assignable to type 'HTMLElement'.`,
],
},
{
id: 'one-way output binding to a model',
inputs: {evt: {type: 'ModelSignal<string>', isSignal: true}},
outputs: {evtChange: {type: 'ModelSignal<string>'}},
template: `<div dir (evtChange)="$event.bla">`,
expected: [
`TestComponent.html(1, 30): Property 'bla' does not exist on type 'string'.`,
],
},
{
id: 'one-way output to a model with a void type',
inputs: {evt: {type: 'ModelSignal<string>', isSignal: true}},
outputs: {evtChange: {type: 'ModelSignal<void>'}},
template: `<div dir (evtChange)="$event.x">`,
expected: [
`TestComponent.html(1, 30): Property 'x' does not exist on type 'void'.`,
],
},
{
id: 'two-way binding to primitive, invalid',
inputs: {value: {type: 'ModelSignal<string>', isSignal: true}},
outputs: {valueChange: {type: 'ModelSignal<string>'}},
template: `<div dir [(value)]="bla">`,
component: `bla = true;`,
expected: [
`TestComponent.html(1, 12): Type 'boolean' is not assignable to type 'string'.`,
],
},
{
id: 'two-way binding to primitive, valid',
inputs: {value: {type: 'ModelSignal<string>', isSignal: true}},
outputs: {valueChange: {type: 'ModelSignal<string>'}},
template: `<div dir [(value)]="bla">`,
component: `bla: string = ''`,
expected: [],
},
// mixing cases
{
id: 'mixing decorator-based and model-based event bindings',
inputs: {evt1: {type: 'ModelSignal<string>', isSignal: true}},
outputs: {
evt1Change: {type: 'ModelSignal<string>'},
evt2: {type: 'EventEmitter<string>'},
},
template: `<div dir (evt1Change)="x1 = $event" (evt2)="x2 = $event">`,
component: `
x1: never = null!;
x2: never = null!;
`,
expected: [
`TestComponent.html(1, 24): Type 'string' is not assignable to type 'never'.`,
`TestComponent.html(1, 45): Type 'string' is not assignable to type 'never'.`,
]
},
// restricted fields
{
id: 'allows access to private output',
inputs: {
evt: {
type: 'ModelSignal<string>',
isSignal: true,
restrictionModifier: 'private',
}
},
outputs: {evtChange: {type: 'ModelSignal<string>', restrictionModifier: 'private'}},
template: `<div dir (evtChange)="true">`,
expected: [],
},
{
id: 'allows access to protected output',
inputs: {
evt: {
type: 'ModelSignal<string>',
isSignal: true,
restrictionModifier: 'protected',
}
},
outputs: {evt: {type: 'ModelSignal<string>', restrictionModifier: 'protected'}},
template: `<div dir (evtChange)="true">`,
expected: [],
},
{
id: 'WritableSignal binding, valid',
inputs: {value: {type: 'ModelSignal<boolean>', isSignal: true}},
outputs: {valueChange: {type: 'ModelSignal<boolean>'}},
template: `<div dir [(value)]="val">`,
component: `val!: WritableSignal<boolean>;`,
expected: [],
},
{
id: 'WritableSignal binding, invalid',
inputs: {value: {type: 'ModelSignal<boolean>', isSignal: true}},
outputs: {valueChange: {type: 'ModelSignal<boolean>'}},
template: `<div dir [(value)]="val">`,
component: `val!: WritableSignal<string>;`,
expected: [
`TestComponent.html(1, 12): Type 'string' is not assignable to type 'boolean'.`,
],
},
{
id: 'non-writable signal binding',
inputs: {value: {type: 'ModelSignal<boolean>', isSignal: true}},
outputs: {valueChange: {type: 'ModelSignal<boolean>'}},
template: `<div dir [(value)]="val">`,
component: `val!: InputSignal<boolean>;`,
expected: [
`TestComponent.html(1, 10): Type 'InputSignal<boolean>' is not assignable to type 'boolean'.`,
],
},
{
id: 'getter function binding, valid',
inputs: {value: {type: 'ModelSignal<(v: string) => number>', isSignal: true}},
outputs: {valueChange: {type: 'ModelSignal<(v: string) => number>'}},
template: `<div dir [(value)]="val">`,
component: `val!: (v: string) => number;`,
expected: [],
},
{
id: 'getter function binding, invalid',
inputs: {value: {type: 'ModelSignal<(v: number) => number>', isSignal: true}},
outputs: {valueChange: {type: 'ModelSignal<(v: number) => number>'}},
template: `<div dir [(value)]="val">`,
component: `val!: (v: string) => number;`,
expected: [
jasmine.stringContaining(
`TestComponent.html(1, 12): Type '(v: string) => number' is not assignable to type '(v: number) => number`),
],
},
];
generateDiagnoseJasmineSpecs(bindingCases);
});
});
@@ -66,7 +66,14 @@ export function typeCheckDiagnose(c: TestCase) {
name => `${outputs[name].restrictionModifier ?? ''} ${name}: ${outputs[name].type}`);
const testComponent = `
import {InputSignal, EventEmitter, OutputEmitter, InputSignalWithTransform} from '@angular/core';
import {
InputSignal,
EventEmitter,
OutputEmitter,
InputSignalWithTransform,
ModelSignal,
WritableSignal,
} from '@angular/core';
${c.extraFileContent ?? ''}
@@ -673,8 +673,10 @@ describe('type check blocks', () => {
isGeneric: true,
}];
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('const _ctor1: <T extends string = any>(init: Pick<i0.TwoWay<T>, "input">) => i0.TwoWay<T> = null!');
expect(block).toContain('var _t1 = _ctor1({ "input": (i1.ɵunwrapWritableSignal(((this).value))) });');
expect(block).toContain(
'const _ctor1: <T extends string = any>(init: Pick<i0.TwoWay<T>, "input">) => i0.TwoWay<T> = null!');
expect(block).toContain(
'var _t1 = _ctor1({ "input": (i1.ɵunwrapWritableSignal(((this).value))) });');
expect(block).toContain('_t1.input = i1.ɵunwrapWritableSignal((((this).value)));');
});
@@ -697,7 +699,8 @@ describe('type check blocks', () => {
}];
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('var _t1 = null! as i0.TwoWay;');
expect(block).toContain('_t1.input[i1.ɵINPUT_SIGNAL_BRAND_WRITE_TYPE] = i1.ɵunwrapWritableSignal((((this).value)));');
expect(block).toContain(
'_t1.input[i1.ɵINPUT_SIGNAL_BRAND_WRITE_TYPE] = i1.ɵunwrapWritableSignal((((this).value)));');
});
it('should handle a two-way binding to an input with a transform', () => {
@@ -871,6 +874,7 @@ describe('type check blocks', () => {
useInlineTypeConstructors: true,
suggestionsForSuboptimalTypeInference: false,
controlFlowPreventingContentProjection: 'warning',
allowSignalsInTwoWayBindings: true,
};
describe('config.applyTemplateContextGuards', () => {
@@ -1214,6 +1218,37 @@ describe('type check blocks', () => {
'_t1.fieldA = (((this).foo)); ');
});
});
describe('config.allowSignalsInTwoWayBindings', () => {
it('should not unwrap signals in two-way binding expressions', () => {
const TEMPLATE = `<div twoWay [(input)]="value"></div>`;
const DIRECTIVES: TestDeclaration[] = [{
type: 'directive',
name: 'TwoWay',
selector: '[twoWay]',
inputs: {input: 'input'},
outputs: {inputChange: 'inputChange'},
}];
const block =
tcb(TEMPLATE, DIRECTIVES, {...BASE_CONFIG, allowSignalsInTwoWayBindings: false});
expect(block).not.toContain('ɵunwrapWritableSignal');
});
it('should not unwrap signals in two-way bindings to generic directives', () => {
const TEMPLATE = `<div twoWay [(input)]="value"></div>`;
const DIRECTIVES: TestDeclaration[] = [{
type: 'directive',
name: 'TwoWay',
selector: '[twoWay]',
inputs: {input: 'input'},
outputs: {inputChange: 'inputChange'},
isGeneric: true,
}];
const block =
tcb(TEMPLATE, DIRECTIVES, {...BASE_CONFIG, allowSignalsInTwoWayBindings: false});
expect(block).not.toContain('ɵunwrapWritableSignal');
});
});
});
it('should use `any` type for type constructors with bound generic params ' +
@@ -7,6 +7,8 @@
*/
import {BindingPipe, CssSelector, ParseSourceFile, ParseSourceSpan, parseTemplate, ParseTemplateOptions, R3TargetBinder, SchemaMetadata, SelectorMatcher, TmplAstElement} from '@angular/compiler';
import {readFileSync} from 'fs';
import path from 'path';
import ts from 'typescript';
import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError, LogicalFileSystem} from '../../file_system';
@@ -18,7 +20,7 @@ import {NOOP_PERF_RECORDER} from '../../perf';
import {TsCreateProgramDriver} from '../../program_driver';
import {ClassDeclaration, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
import {ComponentScopeKind, ComponentScopeReader, LocalModuleScope, ScopeData, TypeCheckScopeRegistry} from '../../scope';
import {makeProgram} from '../../testing';
import {makeProgram, resolveFromRunfiles} from '../../testing';
import {getRootDirs} from '../../util/src/typescript';
import {OptimizeFor, ProgramTypeCheckAdapter, TemplateDiagnostic, TemplateTypeChecker, TypeCheckContext} from '../api';
import {TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckingConfig} from '../api/api';
@@ -90,133 +92,12 @@ export function typescriptLibDts(): TestFile {
}
export function angularCoreDts(): TestFile {
const directory =
resolveFromRunfiles('angular/packages/compiler-cli/src/ngtsc/testing/fake_core/npm_package');
return {
name: absoluteFrom('/node_modules/@angular/core/index.d.ts'),
contents: `
export declare class TemplateRef<C> {
readonly elementRef: unknown;
createEmbeddedView(context: C): unknown;
}
export declare class EventEmitter<T> {
subscribe(next?: (value: T) => void, error?: (error: any) => void, complete?: () => void): unknown;
subscribe(observerOrNext?: any, error?: any, complete?: any): unknown;
}
export declare type NgIterable<T> = Array<T> | Iterable<T>;
/**
* -------
* Signals
* ------
*/
export declare const SIGNAL: unique symbol;
export declare type Signal<T> = (() => T) & {
[SIGNAL]: unknown;
};
export declare function signal<T>(initialValue: T): WritableSignal<T>;
export declare function computed<T>(computation: () => T): Signal<T>;
const WRITABLE_SIGNAL = /* @__PURE__ */ Symbol('WRITABLE_SIGNAL');
export interface WritableSignal<T> extends Signal<T> {
[WRITABLE_SIGNAL]: T;
set(value: T): void;
update(updateFn: (value: T) => T): void;
asReadonly(): Signal<T>;
}
/**
* -------
* Signal inputs
* ------
*/
export interface InputOptions<ReadT, WriteT> {
alias?: string;
transform?: (v: WriteT) => ReadT;
}
export type InputOptionsWithoutTransform<ReadT> =
InputOptions<ReadT, ReadT>&{transform?: undefined};
export type InputOptionsWithTransform<ReadT, WriteT> =
Required<Pick<InputOptions<ReadT, WriteT>, 'transform'>>&InputOptions<ReadT, WriteT>;
const ɵINPUT_SIGNAL_BRAND_READ_TYPE: unique symbol;
export const ɵINPUT_SIGNAL_BRAND_WRITE_TYPE: unique symbol;
export interface InputSignalWithTransform<ReadT, WriteT> extends Signal<ReadT> {
[ɵINPUT_SIGNAL_BRAND_READ_TYPE]: ReadT;
[ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]: WriteT;
}
export interface InputSignal<ReadT> extends InputSignalWithTransform<ReadT, ReadT> {}
export function inputFunction<ReadT>(): InputSignal<ReadT|undefined>;
export function inputFunction<ReadT>(
initialValue: ReadT, opts?: InputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;
export function inputFunction<ReadT, WriteT>(
initialValue: ReadT,
opts: InputOptionsWithTransform<ReadT, WriteT>): InputSignalWithTransform<ReadT, WriteT>;
export function inputFunction<ReadT, WriteT>(
_initialValue?: ReadT,
_opts?: InputOptions<ReadT, WriteT>): InputSignalWithTransform<ReadT|undefined, WriteT> {
return null!;
}
export function inputRequiredFunction<ReadT>(opts?: InputOptionsWithoutTransform<ReadT>):
InputSignal<ReadT>;
export function inputRequiredFunction<ReadT, WriteT>(
opts: InputOptionsWithTransform<ReadT, WriteT>): InputSignalWithTransform<ReadT, WriteT>;
export function inputRequiredFunction<ReadT, WriteT>(_opts?: InputSignalWithTransform<ReadT, WriteT>):
InputSignalWithTransform<ReadT, WriteT> {
return null!;
}
export type InputFunction = typeof inputFunction&{required: typeof inputRequiredFunction};
export const input: InputFunction = (() => {
(inputFunction as any).required = inputRequiredFunction;
return inputFunction as InputFunction;
})();
export type Signal<T> = (() => T);
// Note: needs to be kept in sync with the copies in render3/reactivity/signal.ts and
// fake_core/index.ts to ensure consistent tests.
export function ɵunwrapWritableSignal<T>(value: T|{[WRITABLE_SIGNAL]: T}): T {
return null!;
}
export interface ModelOptions {
alias?: string;
}
export interface ModelSignal<T> extends WritableSignal<T> {
[ɵINPUT_SIGNAL_BRAND_READ_TYPE]: T;
[ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]: T;
subscribe(callback: (value: T) => void): {unsubscribe: () => void};
}
export interface ModelFunction {
<T>(): ModelSignal<T|undefined>;
<T>(initialValue: T, opts?: ModelOptions): ModelSignal<T>;
required<T>(opts?: ModelOptions): ModelSignal<T>;
}
export const model: ModelFunction = null!;
export type ɵUnwrapInputSignalWriteType<Field> =
Field extends InputSignalWithTransform<unknown, infer WriteT>? WriteT : never;
export type ɵUnwrapDirectiveSignalInputs<Dir, Fields extends keyof Dir> = {
[P in Fields]: ɵUnwrapInputSignalWriteType<Dir[P]>
};
export interface OutputEmitter<T> {
emit(value: T): void;
subscribe(listener: (v: T) => void): void;
}
`
contents: readFileSync(path.join(directory, 'index.d.ts'), 'utf8'),
};
}
@@ -333,6 +214,7 @@ export const ALL_ENABLED_CONFIG: Readonly<TypeCheckingConfig> = {
useInlineTypeConstructors: true,
suggestionsForSuboptimalTypeInference: false,
controlFlowPreventingContentProjection: 'warning',
allowSignalsInTwoWayBindings: true,
};
// Remove 'ref' from TypeCheckableDirectiveMeta and add a 'selector' instead.
@@ -444,6 +326,7 @@ export function tcb(
enableTemplateTypeChecker: false,
useInlineTypeConstructors: true,
suggestionsForSuboptimalTypeInference: false,
allowSignalsInTwoWayBindings: true,
...config
};
options = options || {
@@ -24,9 +24,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class TestDir {
a: import("@angular/core").EventEmitter<unknown>;
b: import("@angular/core").EventEmitter<string>;
c: import("@angular/core").EventEmitter<void>;
a: import("@angular/core").OutputEmitter<unknown>;
b: import("@angular/core").OutputEmitter<string>;
c: import("@angular/core").OutputEmitter<void>;
static ɵfac: i0.ɵɵFactoryDeclaration<TestDir, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<TestDir, never, never, {}, { "a": "a"; "b": "b"; "c": "cPublic"; }, never, never, true, never>;
}
@@ -58,9 +58,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class TestComp {
a: import("@angular/core").EventEmitter<unknown>;
b: import("@angular/core").EventEmitter<string>;
c: import("@angular/core").EventEmitter<void>;
a: import("@angular/core").OutputEmitter<unknown>;
b: import("@angular/core").OutputEmitter<string>;
c: import("@angular/core").OutputEmitter<void>;
static ɵfac: i0.ɵɵFactoryDeclaration<TestComp, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestComp, "ng-component", never, {}, { "a": "a"; "b": "b"; "c": "cPublic"; }, never, never, true, never>;
}
@@ -102,9 +102,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
import { EventEmitter } from '@angular/core';
import * as i0 from "@angular/core";
export declare class TestDir {
click1: EventEmitter<unknown>;
click2: EventEmitter<boolean>;
_bla: EventEmitter<void>;
click1: import("@angular/core").OutputEmitter<unknown>;
click2: import("@angular/core").OutputEmitter<boolean>;
_bla: import("@angular/core").OutputEmitter<void>;
clickDecorator1: EventEmitter<unknown>;
clickDecorator2: EventEmitter<boolean>;
_blaDecorator: EventEmitter<void>;
@@ -0,0 +1,313 @@
/**
* @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.io/license
*/
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '../../src/ngtsc/testing';
import {NgtscTestEnvironment} from './env';
const testFiles = loadStandardTestFiles();
runInEachFileSystem(() => {
describe('initializer-based input() API', () => {
let env!: NgtscTestEnvironment;
beforeEach(() => {
env = NgtscTestEnvironment.setup(testFiles);
env.tsconfig({strictTemplates: true});
});
it('should handle a basic, primitive valued input', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
@Directive()
export class TestDir {
data = input('test');
}
`);
env.driveMain();
const js = env.getContents('test.js');
expect(js).toContain('inputs: { data: [i0.ɵɵInputFlags.SignalBased, "data"] }');
});
it('should fail if @Input is applied on signal input member', () => {
env.write('test.ts', `
import {Directive, Input, input} from '@angular/core';
@Directive()
export class TestDir {
@Input() data = input('test');
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics).toEqual([jasmine.objectContaining({
messageText: `Using @Input with a signal input is not allowed.`,
})]);
});
it('should fail if signal input is also declared in `inputs` decorator field.', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
@Directive({
inputs: ['data'],
})
export class TestDir {
data = input('test');
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics).toEqual([
jasmine.objectContaining({
messageText: 'Input "data" is also declared as non-signal in @Directive.',
}),
]);
});
it('should fail if signal input declares a non-statically analyzable alias', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
const ALIAS = 'bla';
@Directive({
inputs: ['data'],
})
export class TestDir {
data = input('test', {alias: ALIAS});
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics).toEqual([
jasmine.objectContaining({
messageText: 'Alias needs to be a string that is statically analyzable.',
}),
]);
});
it('should fail if signal input declares a non-statically analyzable options', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
const OPTIONS = {};
@Directive({
inputs: ['data'],
})
export class TestDir {
data = input('test', OPTIONS);
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics).toEqual([
jasmine.objectContaining({
messageText: 'Argument needs to be an object literal that is statically analyzable.',
}),
]);
});
it('should fail if signal input is declared on static member', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
@Directive()
export class TestDir {
static data = input('test');
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics).toEqual([
jasmine.objectContaining({
messageText: 'Input "data" is incorrectly declared as static member of "TestDir".',
}),
]);
});
it('should handle an alias configured, primitive valued input', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
@Directive()
export class TestDir {
data = input('test', {
alias: 'publicName',
});
}
`);
env.driveMain();
const js = env.getContents('test.js');
expect(js).toContain('inputs: { data: [i0.ɵɵInputFlags.SignalBased, "publicName", "data"] }');
});
it('should error if a required input declares an initial value', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
@Directive()
export class TestDir {
data = input.required({
initialValue: 'bla',
});
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics[0].messageText).toEqual(jasmine.objectContaining({
messageText: 'No overload matches this call.',
}));
});
it('should handle a transform and required input', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
@Directive()
export class TestDir {
data = input.required({
transform: (v: string|number) => 'works',
});
}
`);
env.driveMain();
expect(env.getContents('test.js'))
.toContain(`inputs: { data: [i0.ɵɵInputFlags.SignalBased, "data"] }`);
expect(env.getContents('test.d.ts')).toContain('"required": true; "isSignal": true;');
expect(env.getContents('test.d.ts'))
.withContext(
'Expected no coercion member as input signal captures the write type of the transform')
.not.toContain('ngAcceptInputType');
});
it('should handle a non-primitive initial value', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
@Directive()
export class TestDir {
data = input(/default pattern/);
}
`);
env.driveMain();
expect(env.getContents('test.js'))
.toContain(`inputs: { data: [i0.ɵɵInputFlags.SignalBased, "data"] }`);
});
// TODO(crisbeto): we may not want to support this combination. Will discuss with the team.
it('should report mixed two-way binding with a signal input', () => {
env.write('test.ts', `
import {Component, Directive, input, Output, EventEmitter} from '@angular/core';
@Directive({standalone: true, selector: '[dir]'})
export class TestDir {
value = input('hello');
@Output() valueChange = new EventEmitter<string>();
}
@Component({
standalone: true,
template: \`<div dir [(value)]="value"></div>\`,
imports: [TestDir],
})
export class TestComp {
value = 1;
}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(`Type 'number' is not assignable to type 'string'.`);
});
describe('type checking', () => {
it('should work', () => {
env.write('test.ts', `
import {Component, Directive, input} from '@angular/core';
@Directive({
selector: '[directiveName]',
standalone: true,
})
export class TestDir {
data = input(1);
}
@Component({
standalone: true,
template: \`<div directiveName [data]="false"></div>\`,
imports: [TestDir],
})
export class TestComp {
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics.length).toBe(1);
expect(diagnostics[0].messageText)
.toBe(`Type 'boolean' is not assignable to type 'number'.`);
});
it('should work with transforms', () => {
env.write('test.ts', `
import {Component, Directive, input} from '@angular/core';
@Directive({
selector: '[directiveName]',
standalone: true,
})
export class TestDir {
data = input.required({
transform: (v: string|number) => 'works',
});
}
@Component({
standalone: true,
template: \`<div directiveName [data]="false"></div>\`,
imports: [TestDir],
})
export class TestComp {
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics.length).toBe(1);
expect(diagnostics[0].messageText)
.toBe(`Type 'boolean' is not assignable to type 'string | number'.`);
});
it('should report unset required inputs', () => {
env.write('test.ts', `
import {Component, Directive, input} from '@angular/core';
@Directive({
selector: '[directiveName]',
standalone: true,
})
export class TestDir {
data = input.required<boolean>();
}
@Component({
standalone: true,
template: \`<div directiveName></div>\`,
imports: [TestDir],
})
export class TestComp {
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics.length).toBe(1);
expect(diagnostics[0].messageText)
.toBe(`Required input 'data' from directive TestDir must be specified.`);
});
});
});
});
@@ -14,304 +14,7 @@ import {NgtscTestEnvironment} from './env';
const testFiles = loadStandardTestFiles();
runInEachFileSystem(() => {
describe('input()', () => {
let env!: NgtscTestEnvironment;
beforeEach(() => {
env = NgtscTestEnvironment.setup(testFiles);
env.tsconfig({strictTemplates: true});
});
it('should handle a basic, primitive valued input', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
@Directive()
export class TestDir {
data = input('test');
}
`);
env.driveMain();
const js = env.getContents('test.js');
expect(js).toContain('inputs: { data: [i0.ɵɵInputFlags.SignalBased, "data"] }');
});
it('should fail if @Input is applied on signal input member', () => {
env.write('test.ts', `
import {Directive, Input, input} from '@angular/core';
@Directive()
export class TestDir {
@Input() data = input('test');
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics).toEqual([jasmine.objectContaining({
messageText: `Using @Input with a signal input is not allowed.`,
})]);
});
it('should fail if signal input is also declared in `inputs` decorator field.', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
@Directive({
inputs: ['data'],
})
export class TestDir {
data = input('test');
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics).toEqual([
jasmine.objectContaining({
messageText: 'Input "data" is also declared as non-signal in @Directive.',
}),
]);
});
it('should fail if signal input declares a non-statically analyzable alias', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
const ALIAS = 'bla';
@Directive({
inputs: ['data'],
})
export class TestDir {
data = input('test', {alias: ALIAS});
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics).toEqual([
jasmine.objectContaining({
messageText: 'Alias needs to be a string that is statically analyzable.',
}),
]);
});
it('should fail if signal input declares a non-statically analyzable options', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
const OPTIONS = {};
@Directive({
inputs: ['data'],
})
export class TestDir {
data = input('test', OPTIONS);
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics).toEqual([
jasmine.objectContaining({
messageText: 'Argument needs to be an object literal that is statically analyzable.',
}),
]);
});
it('should fail if signal input is declared on static member', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
@Directive()
export class TestDir {
static data = input('test');
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics).toEqual([
jasmine.objectContaining({
messageText: 'Input "data" is incorrectly declared as static member of "TestDir".',
}),
]);
});
it('should handle an alias configured, primitive valued input', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
@Directive()
export class TestDir {
data = input('test', {
alias: 'publicName',
});
}
`);
env.driveMain();
const js = env.getContents('test.js');
expect(js).toContain('inputs: { data: [i0.ɵɵInputFlags.SignalBased, "publicName", "data"] }');
});
it('should error if a required input declares an initial value', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
@Directive()
export class TestDir {
data = input.required({
initialValue: 'bla',
});
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics[0].messageText).toEqual(jasmine.objectContaining({
messageText: 'No overload matches this call.',
}));
});
it('should handle a transform and required input', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
@Directive()
export class TestDir {
data = input.required({
transform: (v: string|number) => 'works',
});
}
`);
env.driveMain();
expect(env.getContents('test.js'))
.toContain(`inputs: { data: [i0.ɵɵInputFlags.SignalBased, "data"] }`);
expect(env.getContents('test.d.ts')).toContain('"required": true; "isSignal": true;');
expect(env.getContents('test.d.ts'))
.withContext(
'Expected no coercion member as input signal captures the write type of the transform')
.not.toContain('ngAcceptInputType');
});
it('should handle a non-primitive initial value', () => {
env.write('test.ts', `
import {Directive, input} from '@angular/core';
@Directive()
export class TestDir {
data = input(/default pattern/);
}
`);
env.driveMain();
expect(env.getContents('test.js'))
.toContain(`inputs: { data: [i0.ɵɵInputFlags.SignalBased, "data"] }`);
});
// TODO(crisbeto): we may not want to support this combination. Will discuss with the team.
it('should report mixed two-way binding with a signal input', () => {
env.write('test.ts', `
import {Component, Directive, input, Output, EventEmitter} from '@angular/core';
@Directive({standalone: true, selector: '[dir]'})
export class TestDir {
value = input('hello');
@Output() valueChange = new EventEmitter<string>();
}
@Component({
standalone: true,
template: \`<div dir [(value)]="value"></div>\`,
imports: [TestDir],
})
export class TestComp {
value = 1;
}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(`Type 'number' is not assignable to type 'string'.`);
});
describe('type checking', () => {
it('should work', () => {
env.write('test.ts', `
import {Component, Directive, input} from '@angular/core';
@Directive({
selector: '[directiveName]',
standalone: true,
})
export class TestDir {
data = input(1);
}
@Component({
standalone: true,
template: \`<div directiveName [data]="false"></div>\`,
imports: [TestDir],
})
export class TestComp {
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics.length).toBe(1);
expect(diagnostics[0].messageText)
.toBe(`Type 'boolean' is not assignable to type 'number'.`);
});
it('should work with transforms', () => {
env.write('test.ts', `
import {Component, Directive, input} from '@angular/core';
@Directive({
selector: '[directiveName]',
standalone: true,
})
export class TestDir {
data = input.required({
transform: (v: string|number) => 'works',
});
}
@Component({
standalone: true,
template: \`<div directiveName [data]="false"></div>\`,
imports: [TestDir],
})
export class TestComp {
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics.length).toBe(1);
expect(diagnostics[0].messageText)
.toBe(`Type 'boolean' is not assignable to type 'string | number'.`);
});
it('should report unset required inputs', () => {
env.write('test.ts', `
import {Component, Directive, input} from '@angular/core';
@Directive({
selector: '[directiveName]',
standalone: true,
})
export class TestDir {
data = input.required<boolean>();
}
@Component({
standalone: true,
template: \`<div directiveName></div>\`,
imports: [TestDir],
})
export class TestComp {
}
`);
const diagnostics = env.driveDiagnostics();
expect(diagnostics.length).toBe(1);
expect(diagnostics[0].messageText)
.toBe(`Required input 'data' from directive TestDir must be specified.`);
});
});
});
describe('model()', () => {
describe('initializer-based model() API', () => {
let env!: NgtscTestEnvironment;
beforeEach(() => {
@@ -828,5 +531,63 @@ runInEachFileSystem(() => {
const diags = env.driveDiagnostics();
expect(diags).toEqual([]);
});
it('should not widen the type of two-way bindings on Angular versions less than 17.2', () => {
env.tsconfig({_angularCoreVersion: '16.50.60', strictTemplates: true});
env.write('test.ts', `
import {Component, Directive, Input, Output, EventEmitter, signal} from '@angular/core';
@Directive({
selector: '[dir]',
standalone: true,
})
export class TestDir {
@Input() value = 0;
@Output() valueChange = new EventEmitter<number>();
}
@Component({
standalone: true,
template: \`<div dir [(value)]="value"></div>\`,
imports: [TestDir],
})
export class TestComp {
value = signal(1);
}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText)
.toBe(`Type 'WritableSignal<number>' is not assignable to type 'number'.`);
});
it('should widen the type of two-way bindings on Angular version 0.0.0', () => {
env.tsconfig({_angularCoreVersion: '0.0.0-PLACEHOLDER', strictTemplates: true});
env.write('test.ts', `
import {Component, Directive, Input, Output, EventEmitter, signal} from '@angular/core';
@Directive({
selector: '[dir]',
standalone: true,
})
export class TestDir {
@Input() value = 0;
@Output() valueChange = new EventEmitter<number>();
}
@Component({
standalone: true,
template: \`<div dir [(value)]="value"></div>\`,
imports: [TestDir],
})
export class TestComp {
value = signal(1);
}
`);
const diags = env.driveDiagnostics();
expect(diags).toEqual([]);
});
});
});
@@ -14,8 +14,7 @@ export const REQUIRED_UNSET_VALUE = /* @__PURE__ */ Symbol('InputSignalNode#UNSE
* Reactive node type for an input signal. An input signal extends a signal.
* There are special properties to enable transforms and required inputs.
*/
export interface InputSignalNode<ReadT, WriteT> extends
SignalNode<ReadT|typeof REQUIRED_UNSET_VALUE> {
export interface InputSignalNode<ReadT, WriteT> extends SignalNode<ReadT> {
/**
* User-configured transform that will run whenever a new value is applied
* to the input signal node.
+2 -1
View File
@@ -6,8 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {REQUIRED_UNSET_VALUE} from '../input/input_signal_node';
import {createModelSignal, ModelOptions, ModelSignal} from './model_signal';
import {REQUIRED_UNSET_VALUE} from './model_signal_node';
export function modelFunction<T>(initialValue?: T): ModelSignal<T|undefined> {
return createModelSignal(initialValue);
@@ -12,8 +12,7 @@ import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {Signal} from '../../render3/reactivity/api';
import {WritableSignal} from '../../render3/reactivity/signal';
import {ɵINPUT_SIGNAL_BRAND_READ_TYPE, ɵINPUT_SIGNAL_BRAND_WRITE_TYPE} from '../input/input_signal';
import {MODEL_SIGNAL_NODE, ModelSignalNode, REQUIRED_UNSET_VALUE} from './model_signal_node';
import {INPUT_SIGNAL_NODE, InputSignalNode, REQUIRED_UNSET_VALUE} from '../input/input_signal_node';
/**
* @developerPreview
@@ -37,16 +36,13 @@ export interface ModelOptions {
* @developerPreview
*/
export interface ModelSignal<T> extends WritableSignal<T> {
[SIGNAL]: ModelSignalNode<T>;
[SIGNAL]: InputSignalNode<T, T>;
[ɵINPUT_SIGNAL_BRAND_READ_TYPE]: T;
[ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]: T;
// TODO(crisbeto): mark this as @internal
/**
* Subscribes to changes in the model's value. Used by listener instructions at runtime.
* @deprecated Do not use, will be removed.
*/
subscribe(callback: (value: T) => void): {unsubscribe: () => void};
// TODO(crisbeto): either make this a public API or mark as internal pending discussion.
/** @deprecated Do not use, will be removed. */
subscribe(callback: (value: T) => void): () => void;
}
/**
@@ -58,7 +54,7 @@ export interface ModelSignal<T> extends WritableSignal<T> {
*/
export function createModelSignal<T>(initialValue: T): ModelSignal<T> {
const subscriptions: ((value: T) => void)[] = [];
const node: ModelSignalNode<T> = Object.create(MODEL_SIGNAL_NODE);
const node: InputSignalNode<T, T> = Object.create(INPUT_SIGNAL_NODE);
node.value = initialValue;
@@ -92,14 +88,11 @@ export function createModelSignal<T>(initialValue: T): ModelSignal<T> {
getter.subscribe = (callback: (value: T) => void) => {
subscriptions.push(callback);
// TODO(crisbeto): figure out if we can get rid of the object literal.
return {
unsubscribe: () => {
const index = subscriptions.indexOf(callback);
return () => {
const index = subscriptions.indexOf(callback);
if (index > -1) {
subscriptions.splice(index, 1);
}
if (index > -1) {
subscriptions.splice(index, 1);
}
};
};
@@ -1,33 +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.io/license
*/
import {SIGNAL_NODE, SignalNode, signalSetFn} from '@angular/core/primitives/signals';
export const REQUIRED_UNSET_VALUE = /* @__PURE__ */ Symbol('ModelSignalNode#UNSET');
/**
* Reactive node type for a model signal. Model signals extend
* signals by adding the ability to track subscriptions and to be required.
*/
export interface ModelSignalNode<T> extends SignalNode<T> {
/** Used by the runtime to write a value to the signal input. */
applyValueToInputSignal: (node: ModelSignalNode<T>, value: T) => void;
}
// Note: Using an IIFE here to ensure that the spread assignment is not considered
// a side-effect, ending up preserving `COMPUTED_NODE` and `REACTIVE_NODE`.
// TODO: remove when https://github.com/evanw/esbuild/issues/3392 is resolved.
export const MODEL_SIGNAL_NODE: ModelSignalNode<unknown> = /* @__PURE__ */ (() => {
return {
...SIGNAL_NODE,
// TODO(crisbeto): figure out how to avoid this.
// Maybe set an input flag that the value is writeable?
applyValueToInputSignal: <T>(node: ModelSignalNode<T>, value: T) => signalSetFn(node, value)
};
})();
+4 -3
View File
@@ -258,12 +258,13 @@ export function ɵɵdeferOnImmediate() {
const lView = getLView();
const tNode = getCurrentTNode()!;
const tView = lView[TVIEW];
const injector = lView[INJECTOR]!;
const tDetails = getTDeferBlockDetails(tView, tNode);
// Render placeholder block only if loading template is not present
// to avoid content flickering, since it would be immediately replaced
// Render placeholder block only if loading template is not present and we're on
// the client to avoid content flickering, since it would be immediately replaced
// by the loading block.
if (tDetails.loadingTmplIndex === null) {
if (!shouldTriggerDeferBlock(injector) || tDetails.loadingTmplIndex === null) {
renderPlaceholder(lView, tNode);
}
triggerDeferBlock(lView, tNode);
@@ -207,10 +207,16 @@ export function listenerInternal(
directiveInstance.constructor.name}'.`);
}
const subscription = (output as SubscribableOutput<unknown>).subscribe(listenerFn);
const subscriptionOrCallback =
(output as SubscribableOutput<unknown>).subscribe(listenerFn);
const idx = lCleanup.length;
lCleanup.push(listenerFn, subscription);
tCleanup && tCleanup.push(eventName, tNode.index, idx, -(idx + 1));
lCleanup.push(listenerFn, subscriptionOrCallback);
if (tCleanup) {
// The cleanup function expects negative indexes to be
// of type Subscription while positive are cleanup functions.
const cleanupIdx = typeof subscriptionOrCallback === 'function' ? idx + 1 : -(idx + 1);
tCleanup.push(eventName, tNode.index, idx, cleanupIdx);
}
}
}
}
@@ -277,8 +283,8 @@ function wrapListener(
}
/** Describes a subscribable output field value. */
export interface SubscribableOutput<T> {
subscribe(listener: (v: T) => void): {unsubscribe: () => void;};
interface SubscribableOutput<T> {
subscribe(listener: (v: T) => void): {unsubscribe: () => void;}|(() => void);
}
/**
@@ -287,7 +293,7 @@ export interface SubscribableOutput<T> {
* For example, an `EventEmitter, a `Subject`, an `Observable` or an
* `OutputEmitter`.
*/
export function isOutputSubscribable(value: unknown): value is SubscribableOutput<unknown> {
function isOutputSubscribable(value: unknown): value is SubscribableOutput<unknown> {
return value != null &&
typeof (value as Partial<SubscribableOutput<unknown>>).subscribe === 'function';
}
@@ -442,7 +442,7 @@ function processCleanups(tView: TView, lView: LView): void {
const targetIdx = tCleanup[i + 3];
ngDevMode && assertNumber(targetIdx, 'cleanup target must be a number');
if (targetIdx >= 0) {
// unregister
// Destroy anything whose teardown is a function call (e.g. QueryList, ModelSignal).
lCleanup[targetIdx]();
} else {
// Subscription
+3 -3
View File
@@ -354,9 +354,9 @@ function materializeViewResults<T>(
const lQuery = lView[QUERIES]!.queries![queryIndex];
if (lQuery.matches === null) {
const tViewData = tView.data;
const tQueryMatches = tQuery.matches!;
const result: T|null[] = [];
for (let i = 0; i < tQueryMatches.length; i += 2) {
const tQueryMatches = tQuery.matches;
const result: Array<T|null> = [];
for (let i = 0; tQueryMatches !== null && i < tQueryMatches.length; i += 2) {
const matchedNodeIdx = tQueryMatches[i];
if (matchedNodeIdx < 0) {
// we at the <ng-template> marker which might have results in views created based on this
+17 -1
View File
@@ -24,6 +24,11 @@ interface QuerySignalNode<T> extends ComputedNode<T|ReadonlyArray<T>> {
_queryIndex?: number;
_queryList?: QueryList<T>;
_dirtyCounter: WritableSignal<number>;
/**
* Stores the last seen, flattened results for a query. This is to avoid marking the signal result
* computed as dirty when there was view manipulation that didn't impact final results.
*/
_flatValue?: T|ReadonlyArray<T>;
}
/**
@@ -58,6 +63,7 @@ function createQuerySignalFn<V>(firstOnly: boolean, required: boolean) {
});
node = signalFn[SIGNAL] as QuerySignalNode<V>;
node._dirtyCounter = signal(0);
node._flatValue = undefined;
if (ngDevMode) {
signalFn.toString = () => `[Query Signal]`;
@@ -111,5 +117,15 @@ function refreshSignalQuery<V>(node: QuerySignalNode<V>, firstOnly: boolean): V|
queryList.reset(results, unwrapElementRef);
return firstOnly ? queryList.first : queryList.toArray();
if (firstOnly) {
return queryList.first;
} else {
// TODO: remove access to the private _changesDetected field by abstracting / removing usage of
// QueryList in the signal-based queries (perf follow-up)
const resultChanged = (queryList as any as {_changesDetected: boolean})._changesDetected;
if (resultChanged || node._flatValue === undefined) {
return node._flatValue = queryList.toArray();
}
return node._flatValue;
}
}
@@ -43,8 +43,7 @@ export interface WritableSignal<T> extends Signal<T> {
* @codeGenApi
*/
export function ɵunwrapWritableSignal<T>(value: T|{[WRITABLE_SIGNAL]: T}): T {
// Note: needs to be kept in sync with the copies in `fake_core/index.ts` and
// `ngtsc/typecheck/testing/index.ts` to ensure consistent tests.
// Note: needs to be kept in sync with the copy in `fake_core/index.ts`.
// Note: the function uses `WRITABLE_SIGNAL` as a brand instead of `WritableSignal<T>`,
// because the latter incorrectly unwraps non-signal getter functions.
return null!;
@@ -564,4 +564,29 @@ describe('model inputs', () => {
}),
]);
});
it('should not throw for mixed model and output subscriptions', () => {
@Directive({selector: '[dir]', standalone: true})
class Dir {
model = model(0);
@Output() output = new EventEmitter();
model2 = model(0);
@Output() output2 = new EventEmitter();
}
@Component({
template: `
<div dir (model)="noop()" (output)="noop()" (model2)="noop()" (output2)="noop()"></div>
`,
standalone: true,
imports: [Dir],
})
class App {
noop() {}
}
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(() => fixture.destroy()).not.toThrow();
});
});
@@ -183,6 +183,23 @@ describe('queries as signals', () => {
expect(result2.length).toBe(1);
expect(result2).toBe(result1);
});
it('should be empty when no query matches exist', () => {
@Component({
standalone: true,
template: ``,
})
class AppComponent {
result = viewChild('unknown');
results = viewChildren('unknown');
}
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
expect(fixture.componentInstance.result()).toBeUndefined();
expect(fixture.componentInstance.results().length).toBe(0);
});
});
describe('content queries', () => {
@@ -319,5 +336,109 @@ describe('queries as signals', () => {
expect(queryDir.results().length).toBe(2);
});
it('should be empty when no query matches exist', () => {
@Directive({
selector: '[declare]',
standalone: true,
})
class DeclareQuery {
result = contentChild('unknown');
results = contentChildren('unknown');
}
@Component({
standalone: true,
imports: [DeclareQuery],
template: `<div declare></div>`,
})
class AppComponent {
}
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const queryDir =
fixture.debugElement.query(By.directive(DeclareQuery)).injector.get(DeclareQuery);
expect(queryDir.result()).toBeUndefined();
expect(queryDir.results().length).toBe(0);
});
});
describe('reactivity and performance', () => {
it('should not dirty a children query when a list of matches does not change - a view with matches',
() => {
let recomputeCount = 0;
@Component({
standalone: true,
template: `
<div #el></div>
@if (show) {
<div #el></div>
}
`,
})
class AppComponent {
divEls = viewChildren<ElementRef<HTMLDivElement>>('el');
foundElCount = computed(() => {
recomputeCount++;
return this.divEls().length;
});
show = false;
}
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
expect(fixture.componentInstance.foundElCount()).toBe(1);
expect(recomputeCount).toBe(1);
// trigger view manipulation that should dirty queries but not change the results
fixture.componentInstance.show = true;
fixture.detectChanges();
fixture.componentInstance.show = false;
fixture.detectChanges();
expect(fixture.componentInstance.foundElCount()).toBe(1);
expect(recomputeCount).toBe(1);
});
it('should not dirty a children query when a list of matches does not change - a view with another container',
() => {
let recomputeCount = 0;
@Component({
standalone: true,
template: `
<div #el></div>
@if (show) {
<!-- an empty if to create a container -->
@if (true) {}
}
`,
})
class AppComponent {
divEls = viewChildren<ElementRef<HTMLDivElement>>('el');
foundElCount = computed(() => {
recomputeCount++;
return this.divEls().length;
});
show = false;
}
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
expect(fixture.componentInstance.foundElCount()).toBe(1);
expect(recomputeCount).toBe(1);
// trigger view manipulation that should dirty queries but not change the results
fixture.componentInstance.show = true;
fixture.detectChanges();
fixture.componentInstance.show = false;
fixture.detectChanges();
expect(fixture.componentInstance.foundElCount()).toBe(1);
expect(recomputeCount).toBe(1);
});
});
});
+1
View File
@@ -7,6 +7,7 @@ ts_library(
"signal_input_signature_test.ts",
"signal_model_signature_test.ts",
"signal_queries_signature_test.ts",
"unwrap_writable_signal_signature_test.ts",
],
deps = ["//packages/core"],
)
@@ -15,7 +15,7 @@ import {ModelSignal} from '@angular/core/src/authoring/model/model_signal';
* in `ModelSignal`, but it's marked as internal which makes it unavailable in tests.
*/
type SubscribableSignal<T> = ModelSignal<T>&{
subscribe(callback: (value: T) => void): {unsubscribe: () => void};
subscribe(callback: (value: T) => void): () => void;
};
describe('model signal', () => {
@@ -68,7 +68,7 @@ describe('model signal', () => {
signal.set(1);
expect(values).toEqual([1]);
subscription.unsubscribe();
subscription();
signal.set(2);
expect(values).toEqual([1]);
});
@@ -103,11 +103,11 @@ describe('model signal', () => {
signal2.set(1);
expect(emitCount).toBe(2);
subscription1.unsubscribe();
subscription1();
signal2.set(2);
expect(emitCount).toBe(3);
subscription2.unsubscribe();
subscription2();
signal2.set(3);
expect(emitCount).toBe(3);
});
+13 -7
View File
@@ -11,6 +11,17 @@ import path from 'path';
import ts from 'typescript';
import url from 'url';
/** Currently-configured tests. */
const TESTS = new Map<string, (value: string) => string>([
[
'signal_input_signature_test',
v => v.includes(',') ? `InputSignalWithTransform<${v}>` : `InputSignal<${v}>`
],
['signal_queries_signature_test', v => `Signal<${v}>`],
['signal_model_signature_test', v => `ModelSignal<${v}>`],
['unwrap_writable_signal_signature_test', v => v]
]);
const containingDir = path.dirname(url.fileURLToPath(import.meta.url));
/**
@@ -21,8 +32,7 @@ function isTestClass(classDeclaration: ts.ClassDeclaration): boolean {
return classDeclaration.name !== undefined && classDeclaration.name.text.endsWith('Test');
}
function testFile(
testFileName: string, getType: (v: string) => string, inferWriteType: boolean): boolean {
function testFile(testFileName: string, getType: (v: string) => string): boolean {
const fileContent = fs.readFileSync(path.join(containingDir, `${testFileName}.d.ts`), 'utf8');
const sourceFile = ts.createSourceFile('test.ts', fileContent, ts.ScriptTarget.ESNext, true);
const testClazz = sourceFile.statements.find(
@@ -63,11 +73,7 @@ function testFile(
async function main() {
let failing = false;
failing ||= testFile(
'signal_input_signature_test',
v => v.includes(',') ? `InputSignalWithTransform<${v}>` : `InputSignal<${v}>`, true);
failing ||= testFile('signal_queries_signature_test', v => `Signal<${v}>`, false);
failing ||= testFile('signal_model_signature_test', v => `ModelSignal<${v}>`, false);
TESTS.forEach((callback, filename) => failing ||= testFile(filename, callback));
if (failing) {
throw new Error('Failing assertions');
@@ -0,0 +1,44 @@
/**
* @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.io/license
*/
/**
* @fileoverview
* This file contains various signal `model()` patterns and ensures
* the resulting types match our expectations (via comments asserting the `.d.ts`).
*/
import {input, model, signal, ɵunwrapWritableSignal as unwrapWritableSignal} from '@angular/core';
// clang-format off
// import preserved to simplify `.d.ts` emit and simplify the `type_tester` logic.
// tslint:disable-next-line no-duplicate-imports
import {InputSignal, WritableSignal} from '@angular/core';
// clang-format on
export class SignalModelSignatureTest {
/** string | undefined */
optionalModel = unwrapWritableSignal(model<string>());
/** string */
requiredModel = unwrapWritableSignal(model.required<string>());
/** string | number */
writableSignal = unwrapWritableSignal(signal<string|number>(0));
/** InputSignal<string | undefined> */
optionalReadonlySignal = unwrapWritableSignal(input<string>());
/** InputSignal<string> */
requiredReadonlySignal = unwrapWritableSignal(input.required<string>());
/** number */
primitiveValue = unwrapWritableSignal(123);
/** (value: string | null | undefined) => number */
getterFunction =
unwrapWritableSignal((value: string|null|undefined) => value ? parseInt(value) : 0);
}
@@ -1562,9 +1562,6 @@
{
"name": "init_model_signal"
},
{
"name": "init_model_signal_node"
},
{
"name": "init_module"
},
+5
View File
@@ -31,6 +31,11 @@ export interface PluginConfig {
* versions of Angular that do not support blocks (pre-v17) used with the language service.
*/
enableBlockSyntax?: false;
/**
* Version of `@angular/core` that was detected in the user's workspace.
*/
angularCoreVersion?: string;
}
export type GetTcbResponse = {
@@ -523,6 +523,8 @@ function parseNgCompilerOptions(
options['_enableBlockSyntax'] = false;
}
options['_angularCoreVersion'] = config.angularCoreVersion;
return options;
}
@@ -388,6 +388,74 @@ describe('completions', () => {
});
});
describe('model inputs', () => {
const directiveWithModel = {
'Dir': `
@Directive({
selector: '[dir]',
})
export class Dir {
twoWayValue = model<string>();
}
`
};
it('should return completions for both properties and events', () => {
const {templateFile} = setup(`<button dir ></button>`, ``, directiveWithModel);
templateFile.moveCursorToText(`<button dir ¦>`);
const completions = templateFile.getCompletionsAtPosition();
expectContain(completions, DisplayInfoKind.PROPERTY, ['[twoWayValue]']);
expectContain(completions, DisplayInfoKind.PROPERTY, ['[(twoWayValue)]']);
expectContain(completions, DisplayInfoKind.EVENT, ['(twoWayValueChange)']);
});
it('should return property access completions in the property side of the binding', () => {
const {templateFile} = setup(`<input dir [twoWayValue]="'foo'.">`, '', directiveWithModel);
templateFile.moveCursorToText(`dir [twoWayValue]="'foo'.¦">`);
const completions = templateFile.getCompletionsAtPosition();
expectContain(
completions, ts.ScriptElementKind.memberFunctionElement,
[`charAt`, 'toLowerCase', /* etc. */]);
});
it('should return property access completions in the event side of the binding', () => {
const {templateFile} =
setup(`<input dir (twoWayValueChange)="'foo'.">`, '', directiveWithModel);
templateFile.moveCursorToText(`dir (twoWayValueChange)="'foo'.¦">`);
const completions = templateFile.getCompletionsAtPosition();
expectContain(
completions, ts.ScriptElementKind.memberFunctionElement,
[`charAt`, 'toLowerCase', /* etc. */]);
});
it('should return property access completions in a two-way binding', () => {
const {templateFile} = setup(`<input dir [(twoWayValue)]="'foo'.">`, '', directiveWithModel);
templateFile.moveCursorToText(`dir [(twoWayValue)]="'foo'.¦">`);
const completions = templateFile.getCompletionsAtPosition();
expectContain(
completions, ts.ScriptElementKind.memberFunctionElement,
[`charAt`, 'toLowerCase', /* etc. */]);
});
it('should return completions of string literals, number literals, `true`, ' +
'`false`, `null` and `undefined`',
() => {
const {templateFile} =
setup(`<input dir (twoWayValueChange)="$event.">`, '', directiveWithModel);
templateFile.moveCursorToText('dir (twoWayValueChange)="$event.¦">');
const completions = templateFile.getCompletionsAtPosition();
expectContain(
completions, ts.ScriptElementKind.memberFunctionElement,
[`charAt`, 'toLowerCase', /* etc. */]);
});
});
describe('for blocks', () => {
const completionPrefixes = ['@', '@i'];
@@ -1680,7 +1748,15 @@ function setup(
const env = LanguageServiceTestEnv.setup();
const project = env.addProject('test', {
'test.ts': `
import {Component, input, output, Directive, NgModule, Pipe, TemplateRef} from '@angular/core';
import {Component,
input,
output,
model,
Directive,
NgModule,
Pipe,
TemplateRef,
} from '@angular/core';
${functionDeclarations}
@@ -208,6 +208,103 @@ describe('definitions', () => {
assertFileNames([def3, def2, def], ['dir3.ts', 'dir2.ts', 'dir.ts']);
});
it('gets definitions for all model inputs when attribute matches more than one in a static attribute',
() => {
initMockFileSystem('Native');
const files = {
'app.ts': `
import {Component, NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
@Component({templateUrl: 'app.html'})
export class AppCmp {}
`,
'app.html': '<div dir inputA="abc"></div>',
'dir.ts': `
import {Directive, model} from '@angular/core';
@Directive({selector: '[dir]'})
export class MyDir {
inputA = model('');
}`,
'dir2.ts': `
import {Directive, model} from '@angular/core';
@Directive({selector: '[dir]'})
export class MyDir2 {
inputA = model('');
}`
};
const env = LanguageServiceTestEnv.setup();
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
const template = project.openFile('app.html');
template.moveCursorToText('inpu¦tA="abc"');
const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length))
.toBe('inputA');
expect(definitions.length).toBe(2);
const [def, def2] = definitions;
expect(def.textSpan).toContain('inputA');
expect(def2.textSpan).toContain('inputA');
// TODO(atscott): investigate why the text span includes more than just 'inputA'
// assertTextSpans([def, def2], ['inputA']);
assertFileNames([def, def2], ['dir2.ts', 'dir.ts']);
});
it('gets definitions for all model inputs when attribute matches more than one in a two-way binding',
() => {
initMockFileSystem('Native');
const files = {
'app.ts': `
import {Component, NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
@Component({templateUrl: 'app.html'})
export class AppCmp {
value = 'abc';
}
`,
'app.html': '<div dir [(inputA)]="value"></div>',
'dir.ts': `
import {Directive, model} from '@angular/core';
@Directive({selector: '[dir]'})
export class MyDir {
inputA = model('');
}`,
'dir2.ts': `
import {Directive, model} from '@angular/core';
@Directive({selector: '[dir]'})
export class MyDir2 {
inputA = model('');
}`
};
const env = LanguageServiceTestEnv.setup();
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
const template = project.openFile('app.html');
template.moveCursorToText('[(inpu¦tA)]="value"');
const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length))
.toBe('inputA');
expect(definitions.length).toBe(4);
const [def, def2, def3, def4] = definitions;
// We have four definitons to the `inputA` member, because each `model()`
// instance implicitly creates both an input and an output.
expect(def.textSpan).toContain('inputA');
expect(def2.textSpan).toContain('inputA');
expect(def3.textSpan).toContain('inputA');
expect(def4.textSpan).toContain('inputA');
assertFileNames([def, def2, def3, def4], ['dir2.ts', 'dir.ts', 'dir2.ts', 'dir.ts']);
});
it('should go to the pre-compiled style sheet', () => {
initMockFileSystem('Native');
const files = {
@@ -83,7 +83,7 @@ describe('language service adapter', () => {
});
it('should always disable block syntax if enableBlockSyntax is false', () => {
const {project, tsLS, configFileFs} = setup();
const {project, tsLS} = setup();
const ngLS = new LanguageService(project, tsLS, {
enableBlockSyntax: false,
});
@@ -92,6 +92,17 @@ describe('language service adapter', () => {
'_enableBlockSyntax': false,
}));
});
it('should pass the @angular/core version along to the compiler', () => {
const {project, tsLS} = setup();
const ngLS = new LanguageService(project, tsLS, {
angularCoreVersion: '17.2.11-rc.8',
});
expect(ngLS.getCompilerOptions()).toEqual(jasmine.objectContaining({
'_angularCoreVersion': '17.2.11-rc.8',
}));
});
});
describe('compiler options diagnostics', () => {
@@ -14,7 +14,7 @@ import {createModuleAndProjectWithDeclarations, LanguageServiceTestEnv, Project}
function quickInfoSkeleton(): {[fileName: string]: string} {
return {
'app.ts': `
import {Component, Directive, EventEmitter, Input, NgModule, Output, Pipe, PipeTransform} from '@angular/core';
import {Component, Directive, EventEmitter, Input, NgModule, Output, Pipe, PipeTransform, model} from '@angular/core';
import {CommonModule} from '@angular/common';
export interface Address {
@@ -71,6 +71,14 @@ function quickInfoSkeleton(): {[fileName: string]: string} {
@Output() modelChange!: EventEmitter<string>;
}
@Directive({
selector: '[signal-model]',
exportAs: 'signalModel',
})
export class SignalModel {
signalModel = model<string>();
}
@Directive({selector: 'button[custom-button][compound]'})
export class CompoundCustomButtonDirective {
@Input() config?: {color?: string};
@@ -82,6 +90,7 @@ function quickInfoSkeleton(): {[fileName: string]: string} {
CompoundCustomButtonDirective,
StringModel,
TestComponent,
SignalModel,
],
imports: [
CommonModule,
@@ -237,6 +246,15 @@ describe('quick info', () => {
expectedDisplayString: '(property) StringModel.model: string'
});
});
it('should work for signal-based two-way binding providers', () => {
expectQuickInfo({
templateOverride: `<test-comp signal-model [(signa¦lModel)]="title"></test-comp>`,
expectedSpanText: 'signalModel',
expectedDisplayString:
'(property) SignalModel.signalModel: ModelSignal<string | undefined>'
});
});
});
describe('outputs', () => {
@@ -1285,6 +1285,35 @@ describe('find references and rename locations', () => {
assertTextSpans(refs, ['model', 'modelChange']);
});
it('should get references to model() input binding', () => {
const files = {
'dir.ts': `
import {Directive, model} from '@angular/core';
@Directive({selector: '[signal-model]'})
export class SignalModel {
signalModel = model<string>();
}`,
'app.ts': `
import {Component} from '@angular/core';
@Component({template: '<div signal-model [(signalModel)]="title"></div>'})
export class AppCmp {
title = 'title';
}`
};
env = LanguageServiceTestEnv.setup();
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
const file = project.openFile('app.ts');
file.moveCursorToText('[(signal¦Model)]');
const refs = getReferencesAtPosition(file)!;
expect(refs.length).toBe(2);
assertFileNames(refs, ['dir.ts', 'app.ts']);
assertTextSpans(refs, ['signalModel']);
});
describe('directives', () => {
describe('when cursor is on the directive class', () => {
let file: OpenBuffer;
@@ -1513,7 +1542,7 @@ describe('find references and rename locations', () => {
import {Component} from '@angular/core';
@Component({
template: '@if (x; as aliasX) { {{aliasX}} {{aliasX + "second"}} }',
template: '@if (x; as aliasX) { {{aliasX}} {{aliasX + "second"}} }',
standalone: true
})
export class AppCmp {
@@ -106,7 +106,70 @@ describe('type definitions', () => {
project, {templateOverride: `<my-dir (name¦Changes)="doSmth()" />`});
expect(definitions!.length).toEqual(1);
assertTextSpans(definitions, ['EventEmitter']);
assertTextSpans(definitions, ['OutputEmitter']);
assertFileNames(definitions, ['index.d.ts']);
});
});
describe('model inputs', () => {
const files = {
'app.ts': `
import {Component, Directive, model} from '@angular/core';
@Directive({
selector: 'my-dir',
standalone: true
})
export class MyDir {
twoWayValue = model<string>();
}
@Component({
templateUrl: 'app.html',
standalone: true,
imports: [MyDir],
})
export class AppCmp {
noop() {}
value = 'hello';
}
`,
'app.html': `Will be overridden`,
};
it('should return the definition for the property side of a two-way binding', () => {
initMockFileSystem('Native');
env = LanguageServiceTestEnv.setup();
const project = env.addProject('test', files);
const definitions = getTypeDefinitionsAndAssertBoundSpan(
project, {templateOverride: `<my-dir [twoWa¦yValue]="value" />`});
expect(definitions.length).toBe(1);
assertTextSpans(definitions, ['ModelSignal']);
assertFileNames(definitions, ['index.d.ts']);
});
it('should return the definition for the event side of a two-way binding', () => {
initMockFileSystem('Native');
env = LanguageServiceTestEnv.setup();
const project = env.addProject('test', files);
const definitions = getTypeDefinitionsAndAssertBoundSpan(
project, {templateOverride: `<my-dir (twoWayV¦alueChange)="noop()" />`});
expect(definitions.length).toBe(1);
assertTextSpans(definitions, ['ModelSignal']);
assertFileNames(definitions, ['index.d.ts']);
});
it('should return the definition of a two-way binding', () => {
initMockFileSystem('Native');
env = LanguageServiceTestEnv.setup();
const project = env.addProject('test', files);
const definitions = getTypeDefinitionsAndAssertBoundSpan(
project, {templateOverride: `<my-dir [(twoWa¦yValue)]="value" />`});
expect(definitions.length).toBe(1);
assertTextSpans(definitions, ['ModelSignal']);
assertFileNames(definitions, ['index.d.ts']);
});
});
+57 -28
View File
@@ -16,9 +16,8 @@ import {navigationCancelingError} from './navigation_canceling_error';
import {Params, PRIMARY_OUTLET} from './shared';
import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
export class NoMatch {
public segmentGroup: UrlSegmentGroup|null;
public segmentGroup: UrlSegmentGroup | null;
constructor(segmentGroup?: UrlSegmentGroup) {
this.segmentGroup = segmentGroup || null;
@@ -40,23 +39,30 @@ export function absoluteRedirect(newTree: UrlTree): Observable<any> {
}
export function namedOutletsRedirect(redirectTo: string): Observable<any> {
return throwError(new RuntimeError(
return throwError(
new RuntimeError(
RuntimeErrorCode.NAMED_OUTLET_REDIRECT,
(typeof ngDevMode === 'undefined' || ngDevMode) &&
`Only absolute redirects can have named outlets. redirectTo: '${redirectTo}'`));
`Only absolute redirects can have named outlets. redirectTo: '${redirectTo}'`,
),
);
}
export function canLoadFails(route: Route): Observable<LoadedRouterConfig> {
return throwError(navigationCancelingError(
return throwError(
navigationCancelingError(
(typeof ngDevMode === 'undefined' || ngDevMode) &&
`Cannot load children because the guard of the route "path: '${
route.path}'" returned false`,
NavigationCancellationCode.GuardRejected));
`Cannot load children because the guard of the route "path: '${route.path}'" returned false`,
NavigationCancellationCode.GuardRejected,
),
);
}
export class ApplyRedirects {
constructor(private urlSerializer: UrlSerializer, private urlTree: UrlTree) {}
constructor(
private urlSerializer: UrlSerializer,
private urlTree: UrlTree,
) {}
lineralizeSegments(route: Route, urlTree: UrlTree): Observable<UrlSegment[]> {
let res: UrlSegment[] = [];
@@ -76,9 +82,16 @@ export class ApplyRedirects {
}
applyRedirectCommands(
segments: UrlSegment[], redirectTo: string, posParams: {[k: string]: UrlSegment}): UrlTree {
segments: UrlSegment[],
redirectTo: string,
posParams: {[k: string]: UrlSegment},
): UrlTree {
const newTree = this.applyRedirectCreateUrlTree(
redirectTo, this.urlSerializer.parse(redirectTo), segments, posParams);
redirectTo,
this.urlSerializer.parse(redirectTo),
segments,
posParams,
);
if (redirectTo.startsWith('/')) {
throw new AbsoluteRedirect(newTree);
}
@@ -86,12 +99,17 @@ export class ApplyRedirects {
}
applyRedirectCreateUrlTree(
redirectTo: string, urlTree: UrlTree, segments: UrlSegment[],
posParams: {[k: string]: UrlSegment}): UrlTree {
redirectTo: string,
urlTree: UrlTree,
segments: UrlSegment[],
posParams: {[k: string]: UrlSegment},
): UrlTree {
const newRoot = this.createSegmentGroup(redirectTo, urlTree.root, segments, posParams);
return new UrlTree(
newRoot, this.createQueryParams(urlTree.queryParams, this.urlTree.queryParams),
urlTree.fragment);
newRoot,
this.createQueryParams(urlTree.queryParams, this.urlTree.queryParams),
urlTree.fragment,
);
}
createQueryParams(redirectToParams: Params, actualParams: Params): Params {
@@ -109,8 +127,11 @@ export class ApplyRedirects {
}
createSegmentGroup(
redirectTo: string, group: UrlSegmentGroup, segments: UrlSegment[],
posParams: {[k: string]: UrlSegment}): UrlSegmentGroup {
redirectTo: string,
group: UrlSegmentGroup,
segments: UrlSegment[],
posParams: {[k: string]: UrlSegment},
): UrlSegmentGroup {
const updatedSegments = this.createSegments(redirectTo, group.segments, segments, posParams);
let children: {[n: string]: UrlSegmentGroup} = {};
@@ -122,22 +143,30 @@ export class ApplyRedirects {
}
createSegments(
redirectTo: string, redirectToSegments: UrlSegment[], actualSegments: UrlSegment[],
posParams: {[k: string]: UrlSegment}): UrlSegment[] {
return redirectToSegments.map(
s => s.path.startsWith(':') ? this.findPosParam(redirectTo, s, posParams) :
this.findOrReturn(s, actualSegments));
redirectTo: string,
redirectToSegments: UrlSegment[],
actualSegments: UrlSegment[],
posParams: {[k: string]: UrlSegment},
): UrlSegment[] {
return redirectToSegments.map((s) =>
s.path.startsWith(':')
? this.findPosParam(redirectTo, s, posParams)
: this.findOrReturn(s, actualSegments),
);
}
findPosParam(
redirectTo: string, redirectToUrlSegment: UrlSegment,
posParams: {[k: string]: UrlSegment}): UrlSegment {
redirectTo: string,
redirectToUrlSegment: UrlSegment,
posParams: {[k: string]: UrlSegment},
): UrlSegment {
const pos = posParams[redirectToUrlSegment.path.substring(1)];
if (!pos)
throw new RuntimeError(
RuntimeErrorCode.MISSING_REDIRECT,
(typeof ngDevMode === 'undefined' || ngDevMode) &&
`Cannot redirect to '${redirectTo}'. Cannot find '${redirectToUrlSegment.path}'.`);
RuntimeErrorCode.MISSING_REDIRECT,
(typeof ngDevMode === 'undefined' || ngDevMode) &&
`Cannot redirect to '${redirectTo}'. Cannot find '${redirectToUrlSegment.path}'.`,
);
return pos;
}
@@ -24,7 +24,6 @@ import {RouterOutlet} from '../directives/router_outlet';
imports: [RouterOutlet],
standalone: true,
})
export class ɵEmptyOutletComponent {
}
export class ɵEmptyOutletComponent {}
export {ɵEmptyOutletComponent as EmptyOutletComponent};
+30 -12
View File
@@ -9,19 +9,28 @@
import {BehaviorSubject} from 'rxjs';
import {DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
import {
ActivatedRoute,
ActivatedRouteSnapshot,
RouterState,
RouterStateSnapshot,
} from './router_state';
import {TreeNode} from './utils/tree';
export function createRouterState(
routeReuseStrategy: RouteReuseStrategy, curr: RouterStateSnapshot,
prevState: RouterState): RouterState {
routeReuseStrategy: RouteReuseStrategy,
curr: RouterStateSnapshot,
prevState: RouterState,
): RouterState {
const root = createNode(routeReuseStrategy, curr._root, prevState ? prevState._root : undefined);
return new RouterState(root, curr);
}
function createNode(
routeReuseStrategy: RouteReuseStrategy, curr: TreeNode<ActivatedRouteSnapshot>,
prevState?: TreeNode<ActivatedRoute>): TreeNode<ActivatedRoute> {
routeReuseStrategy: RouteReuseStrategy,
curr: TreeNode<ActivatedRouteSnapshot>,
prevState?: TreeNode<ActivatedRoute>,
): TreeNode<ActivatedRoute> {
// reuse an activated route that is currently displayed on the screen
if (prevState && routeReuseStrategy.shouldReuseRoute(curr.value, prevState.value.snapshot)) {
const value = prevState.value;
@@ -35,21 +44,23 @@ function createNode(
if (detachedRouteHandle !== null) {
const tree = (detachedRouteHandle as DetachedRouteHandleInternal).route;
tree.value._futureSnapshot = curr.value;
tree.children = curr.children.map(c => createNode(routeReuseStrategy, c));
tree.children = curr.children.map((c) => createNode(routeReuseStrategy, c));
return tree;
}
}
const value = createActivatedRoute(curr.value);
const children = curr.children.map(c => createNode(routeReuseStrategy, c));
const children = curr.children.map((c) => createNode(routeReuseStrategy, c));
return new TreeNode<ActivatedRoute>(value, children);
}
}
function createOrReuseChildren(
routeReuseStrategy: RouteReuseStrategy, curr: TreeNode<ActivatedRouteSnapshot>,
prevState: TreeNode<ActivatedRoute>) {
return curr.children.map(child => {
routeReuseStrategy: RouteReuseStrategy,
curr: TreeNode<ActivatedRouteSnapshot>,
prevState: TreeNode<ActivatedRoute>,
) {
return curr.children.map((child) => {
for (const p of prevState.children) {
if (routeReuseStrategy.shouldReuseRoute(child.value, p.value.snapshot)) {
return createNode(routeReuseStrategy, child, p);
@@ -61,6 +72,13 @@ function createOrReuseChildren(
function createActivatedRoute(c: ActivatedRouteSnapshot) {
return new ActivatedRoute(
new BehaviorSubject(c.url), new BehaviorSubject(c.params), new BehaviorSubject(c.queryParams),
new BehaviorSubject(c.fragment), new BehaviorSubject(c.data), c.outlet, c.component, c);
new BehaviorSubject(c.url),
new BehaviorSubject(c.params),
new BehaviorSubject(c.queryParams),
new BehaviorSubject(c.fragment),
new BehaviorSubject(c.data),
c.outlet,
c.component,
c,
);
}
+94 -48
View File
@@ -14,7 +14,6 @@ import {Params, PRIMARY_OUTLET} from './shared';
import {createRoot, squashSegmentGroup, UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree';
import {last, shallowEqual} from './utils/collection';
/**
* Creates a `UrlTree` relative to an `ActivatedRouteSnapshot`.
*
@@ -67,17 +66,21 @@ import {last, shallowEqual} from './utils/collection';
* ```
*/
export function createUrlTreeFromSnapshot(
relativeTo: ActivatedRouteSnapshot, commands: any[], queryParams: Params|null = null,
fragment: string|null = null): UrlTree {
relativeTo: ActivatedRouteSnapshot,
commands: any[],
queryParams: Params | null = null,
fragment: string | null = null,
): UrlTree {
const relativeToUrlSegmentGroup = createSegmentGroupFromRoute(relativeTo);
return createUrlTreeFromSegmentGroup(relativeToUrlSegmentGroup, commands, queryParams, fragment);
}
export function createSegmentGroupFromRoute(route: ActivatedRouteSnapshot): UrlSegmentGroup {
let targetGroup: UrlSegmentGroup|undefined;
let targetGroup: UrlSegmentGroup | undefined;
function createSegmentGroupFromRouteRecursive(currentRoute: ActivatedRouteSnapshot):
UrlSegmentGroup {
function createSegmentGroupFromRouteRecursive(
currentRoute: ActivatedRouteSnapshot,
): UrlSegmentGroup {
const childOutlets: {[outlet: string]: UrlSegmentGroup} = {};
for (const childSnapshot of currentRoute.children) {
const root = createSegmentGroupFromRouteRecursive(childSnapshot);
@@ -96,8 +99,11 @@ export function createSegmentGroupFromRoute(route: ActivatedRouteSnapshot): UrlS
}
export function createUrlTreeFromSegmentGroup(
relativeTo: UrlSegmentGroup, commands: any[], queryParams: Params|null,
fragment: string|null): UrlTree {
relativeTo: UrlSegmentGroup,
commands: any[],
queryParams: Params | null,
fragment: string | null,
): UrlTree {
let root = relativeTo;
while (root.parent) {
root = root.parent;
@@ -116,9 +122,9 @@ export function createUrlTreeFromSegmentGroup(
}
const position = findStartingPositionForTargetGroup(nav, root, relativeTo);
const newSegmentGroup = position.processChildren ?
updateSegmentGroupChildren(position.segmentGroup, position.index, nav.commands) :
updateSegmentGroup(position.segmentGroup, position.index, nav.commands);
const newSegmentGroup = position.processChildren
? updateSegmentGroupChildren(position.segmentGroup, position.index, nav.commands)
: updateSegmentGroup(position.segmentGroup, position.index, nav.commands);
return tree(root, position.segmentGroup, newSegmentGroup, queryParams, fragment);
}
@@ -135,8 +141,12 @@ function isCommandWithOutlets(command: any): command is {outlets: {[key: string]
}
function tree(
oldRoot: UrlSegmentGroup, oldSegmentGroup: UrlSegmentGroup, newSegmentGroup: UrlSegmentGroup,
queryParams: Params|null, fragment: string|null): UrlTree {
oldRoot: UrlSegmentGroup,
oldSegmentGroup: UrlSegmentGroup,
newSegmentGroup: UrlSegmentGroup,
queryParams: Params | null,
fragment: string | null,
): UrlTree {
let qp: any = {};
if (queryParams) {
Object.entries(queryParams).forEach(([name, value]) => {
@@ -163,8 +173,10 @@ function tree(
* value.
*/
function replaceSegment(
current: UrlSegmentGroup, oldSegment: UrlSegmentGroup,
newSegment: UrlSegmentGroup): UrlSegmentGroup {
current: UrlSegmentGroup,
oldSegment: UrlSegmentGroup,
newSegment: UrlSegmentGroup,
): UrlSegmentGroup {
const children: {[key: string]: UrlSegmentGroup} = {};
Object.entries(current.children).forEach(([outletName, c]) => {
if (c === oldSegment) {
@@ -178,20 +190,25 @@ function replaceSegment(
class Navigation {
constructor(
public isAbsolute: boolean, public numberOfDoubleDots: number, public commands: any[]) {
public isAbsolute: boolean,
public numberOfDoubleDots: number,
public commands: any[],
) {
if (isAbsolute && commands.length > 0 && isMatrixParams(commands[0])) {
throw new RuntimeError(
RuntimeErrorCode.ROOT_SEGMENT_MATRIX_PARAMS,
(typeof ngDevMode === 'undefined' || ngDevMode) &&
'Root segment cannot have matrix parameters');
RuntimeErrorCode.ROOT_SEGMENT_MATRIX_PARAMS,
(typeof ngDevMode === 'undefined' || ngDevMode) &&
'Root segment cannot have matrix parameters',
);
}
const cmdWithOutlet = commands.find(isCommandWithOutlets);
if (cmdWithOutlet && cmdWithOutlet !== last(commands)) {
throw new RuntimeError(
RuntimeErrorCode.MISPLACED_OUTLETS_COMMAND,
(typeof ngDevMode === 'undefined' || ngDevMode) &&
'{outlets:{}} has to be the last command');
RuntimeErrorCode.MISPLACED_OUTLETS_COMMAND,
(typeof ngDevMode === 'undefined' || ngDevMode) &&
'{outlets:{}} has to be the last command',
);
}
}
@@ -202,7 +219,7 @@ class Navigation {
/** Transforms commands to a normalized `Navigation` */
function computeNavigation(commands: any[]): Navigation {
if ((typeof commands[0] === 'string') && commands.length === 1 && commands[0] === '/') {
if (typeof commands[0] === 'string' && commands.length === 1 && commands[0] === '/') {
return new Navigation(true, 0, commands);
}
@@ -232,9 +249,11 @@ function computeNavigation(commands: any[]): Navigation {
cmd.split('/').forEach((urlPart, partIndex) => {
if (partIndex == 0 && urlPart === '.') {
// skip './a'
} else if (partIndex == 0 && urlPart === '') { // '/a'
} else if (partIndex == 0 && urlPart === '') {
// '/a'
isAbsolute = true;
} else if (urlPart === '..') { // '../a'
} else if (urlPart === '..') {
// '../a'
numberOfDoubleDots++;
} else if (urlPart != '') {
res.push(urlPart);
@@ -252,12 +271,17 @@ function computeNavigation(commands: any[]): Navigation {
class Position {
constructor(
public segmentGroup: UrlSegmentGroup, public processChildren: boolean, public index: number) {
}
public segmentGroup: UrlSegmentGroup,
public processChildren: boolean,
public index: number,
) {}
}
function findStartingPositionForTargetGroup(
nav: Navigation, root: UrlSegmentGroup, target: UrlSegmentGroup): Position {
nav: Navigation,
root: UrlSegmentGroup,
target: UrlSegmentGroup,
): Position {
if (nav.isAbsolute) {
return new Position(root, true, 0);
}
@@ -279,7 +303,10 @@ function findStartingPositionForTargetGroup(
}
function createPositionApplyingDoubleDots(
group: UrlSegmentGroup, index: number, numberOfDoubleDots: number): Position {
group: UrlSegmentGroup,
index: number,
numberOfDoubleDots: number,
): Position {
let g = group;
let ci = index;
let dd = numberOfDoubleDots;
@@ -288,15 +315,16 @@ function createPositionApplyingDoubleDots(
g = g.parent!;
if (!g) {
throw new RuntimeError(
RuntimeErrorCode.INVALID_DOUBLE_DOTS,
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Invalid number of \'../\'');
RuntimeErrorCode.INVALID_DOUBLE_DOTS,
(typeof ngDevMode === 'undefined' || ngDevMode) && "Invalid number of '../'",
);
}
ci = g.segments.length;
}
return new Position(g, false, ci - dd);
}
function getOutlets(commands: unknown[]): {[k: string]: unknown[]|string} {
function getOutlets(commands: unknown[]): {[k: string]: unknown[] | string} {
if (isCommandWithOutlets(commands[0])) {
return commands[0].outlets;
}
@@ -305,7 +333,10 @@ function getOutlets(commands: unknown[]): {[k: string]: unknown[]|string} {
}
function updateSegmentGroup(
segmentGroup: UrlSegmentGroup|undefined, startIndex: number, commands: any[]): UrlSegmentGroup {
segmentGroup: UrlSegmentGroup | undefined,
startIndex: number,
commands: any[],
): UrlSegmentGroup {
segmentGroup ??= new UrlSegmentGroup([], {});
if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) {
return updateSegmentGroupChildren(segmentGroup, startIndex, commands);
@@ -315,8 +346,10 @@ function updateSegmentGroup(
const slicedCommands = commands.slice(m.commandIndex);
if (m.match && m.pathIndex < segmentGroup.segments.length) {
const g = new UrlSegmentGroup(segmentGroup.segments.slice(0, m.pathIndex), {});
g.children[PRIMARY_OUTLET] =
new UrlSegmentGroup(segmentGroup.segments.slice(m.pathIndex), segmentGroup.children);
g.children[PRIMARY_OUTLET] = new UrlSegmentGroup(
segmentGroup.segments.slice(m.pathIndex),
segmentGroup.children,
);
return updateSegmentGroupChildren(g, 0, slicedCommands);
} else if (m.match && slicedCommands.length === 0) {
return new UrlSegmentGroup(segmentGroup.segments, {});
@@ -330,7 +363,10 @@ function updateSegmentGroup(
}
function updateSegmentGroupChildren(
segmentGroup: UrlSegmentGroup, startIndex: number, commands: any[]): UrlSegmentGroup {
segmentGroup: UrlSegmentGroup,
startIndex: number,
commands: any[],
): UrlSegmentGroup {
if (commands.length === 0) {
return new UrlSegmentGroup(segmentGroup.segments, {});
} else {
@@ -357,11 +393,17 @@ function updateSegmentGroupChildren(
// `UrlSegmentGroup` that is created from an "unsquashed"/expanded `ActivatedRoute` tree.
// This code effectively "squashes" empty path primary routes when they have no siblings on
// the same level of the tree.
if (Object.keys(outlets).some(o => o !== PRIMARY_OUTLET) &&
segmentGroup.children[PRIMARY_OUTLET] && segmentGroup.numberOfChildren === 1 &&
segmentGroup.children[PRIMARY_OUTLET].segments.length === 0) {
const childrenOfEmptyChild =
updateSegmentGroupChildren(segmentGroup.children[PRIMARY_OUTLET], startIndex, commands);
if (
Object.keys(outlets).some((o) => o !== PRIMARY_OUTLET) &&
segmentGroup.children[PRIMARY_OUTLET] &&
segmentGroup.numberOfChildren === 1 &&
segmentGroup.children[PRIMARY_OUTLET].segments.length === 0
) {
const childrenOfEmptyChild = updateSegmentGroupChildren(
segmentGroup.children[PRIMARY_OUTLET],
startIndex,
commands,
);
return new UrlSegmentGroup(segmentGroup.segments, childrenOfEmptyChild.children);
}
@@ -400,11 +442,11 @@ function prefixedWith(segmentGroup: UrlSegmentGroup, startIndex: number, command
}
const curr = `${command}`;
const next =
currentCommandIndex < commands.length - 1 ? commands[currentCommandIndex + 1] : null;
currentCommandIndex < commands.length - 1 ? commands[currentCommandIndex + 1] : null;
if (currentPathIndex > 0 && curr === undefined) break;
if (curr && next && (typeof next === 'object') && next.outlets === undefined) {
if (curr && next && typeof next === 'object' && next.outlets === undefined) {
if (!compare(curr, next, path)) return noMatch;
currentCommandIndex += 2;
} else {
@@ -418,7 +460,10 @@ function prefixedWith(segmentGroup: UrlSegmentGroup, startIndex: number, command
}
function createNewSegmentGroup(
segmentGroup: UrlSegmentGroup, startIndex: number, commands: any[]): UrlSegmentGroup {
segmentGroup: UrlSegmentGroup,
startIndex: number,
commands: any[],
): UrlSegmentGroup {
const paths = segmentGroup.segments.slice(0, startIndex);
let i = 0;
@@ -438,7 +483,7 @@ function createNewSegmentGroup(
}
const curr = isCommandWithOutlets(command) ? command.outlets[PRIMARY_OUTLET] : `${command}`;
const next = (i < commands.length - 1) ? commands[i + 1] : null;
const next = i < commands.length - 1 ? commands[i + 1] : null;
if (curr && next && isMatrixParams(next)) {
paths.push(new UrlSegment(curr, stringify(next)));
i += 2;
@@ -450,8 +495,9 @@ function createNewSegmentGroup(
return new UrlSegmentGroup(paths, {});
}
function createNewSegmentChildren(outlets: {[name: string]: unknown[]|string}):
{[outlet: string]: UrlSegmentGroup} {
function createNewSegmentChildren(outlets: {[name: string]: unknown[] | string}): {
[outlet: string]: UrlSegmentGroup;
} {
const children: {[outlet: string]: UrlSegmentGroup} = {};
Object.entries(outlets).forEach(([outlet, commands]) => {
if (typeof commands === 'string') {
@@ -466,7 +512,7 @@ function createNewSegmentChildren(outlets: {[name: string]: unknown[]|string}):
function stringify(params: {[key: string]: any}): {[key: string]: string} {
const res: {[key: string]: string} = {};
Object.entries(params).forEach(([k, v]) => res[k] = `${v}`);
Object.entries(params).forEach(([k, v]) => (res[k] = `${v}`));
return res;
}
+67 -37
View File
@@ -7,7 +7,20 @@
*/
import {LocationStrategy} from '@angular/common';
import {Attribute, booleanAttribute, Directive, ElementRef, HostBinding, HostListener, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges, ɵɵsanitizeUrlOrResourceUrl} from '@angular/core';
import {
Attribute,
booleanAttribute,
Directive,
ElementRef,
HostBinding,
HostListener,
Input,
OnChanges,
OnDestroy,
Renderer2,
SimpleChanges,
ɵɵsanitizeUrlOrResourceUrl,
} from '@angular/core';
import {Subject, Subscription} from 'rxjs';
import {Event, NavigationEnd} from '../events';
@@ -17,7 +30,6 @@ import {ActivatedRoute} from '../router_state';
import {Params} from '../shared';
import {UrlTree} from '../url_tree';
/**
* @description
*
@@ -124,7 +136,7 @@ export class RouterLink implements OnChanges, OnDestroy {
* Represents an `href` attribute value applied to a host element,
* when a host element is `<a>`. For other tags, the value is `null`.
*/
href: string|null = null;
href: string | null = null;
/**
* Represents the `target` attribute on a host element.
@@ -138,7 +150,7 @@ export class RouterLink implements OnChanges, OnDestroy {
* @see {@link UrlCreationOptions#queryParams}
* @see {@link Router#createUrlTree}
*/
@Input() queryParams?: Params|null;
@Input() queryParams?: Params | null;
/**
* Passed to {@link Router#createUrlTree} as part of the
* `UrlCreationOptions`.
@@ -152,7 +164,7 @@ export class RouterLink implements OnChanges, OnDestroy {
* @see {@link UrlCreationOptions#queryParamsHandling}
* @see {@link Router#createUrlTree}
*/
@Input() queryParamsHandling?: QueryParamsHandling|null;
@Input() queryParamsHandling?: QueryParamsHandling | null;
/**
* Passed to {@link Router#navigateByUrl} as part of the
* `NavigationBehaviorOptions`.
@@ -176,9 +188,9 @@ export class RouterLink implements OnChanges, OnDestroy {
* @see {@link UrlCreationOptions#relativeTo}
* @see {@link Router#createUrlTree}
*/
@Input() relativeTo?: ActivatedRoute|null;
@Input() relativeTo?: ActivatedRoute | null;
private commands: any[]|null = null;
private commands: any[] | null = null;
/** Whether a host element is an `<a>` tag. */
private isAnchorElement: boolean;
@@ -189,10 +201,13 @@ export class RouterLink implements OnChanges, OnDestroy {
onChanges = new Subject<RouterLink>();
constructor(
private router: Router, private route: ActivatedRoute,
@Attribute('tabindex') private readonly tabIndexAttribute: string|null|undefined,
private readonly renderer: Renderer2, private readonly el: ElementRef,
private locationStrategy?: LocationStrategy) {
private router: Router,
private route: ActivatedRoute,
@Attribute('tabindex') private readonly tabIndexAttribute: string | null | undefined,
private readonly renderer: Renderer2,
private readonly el: ElementRef,
private locationStrategy?: LocationStrategy,
) {
const tagName = el.nativeElement.tagName?.toLowerCase();
this.isAnchorElement = tagName === 'a' || tagName === 'area';
@@ -235,7 +250,7 @@ export class RouterLink implements OnChanges, OnDestroy {
* Modifies the tab index if there was not a tabindex attribute on the element during
* instantiation.
*/
private setTabIndexIfNotOnNativeEl(newTabIndex: string|null) {
private setTabIndexIfNotOnNativeEl(newTabIndex: string | null) {
if (this.tabIndexAttribute != null /* both `null` and `undefined` */ || this.isAnchorElement) {
return;
}
@@ -260,7 +275,7 @@ export class RouterLink implements OnChanges, OnDestroy {
* @see {@link Router#createUrlTree}
*/
@Input()
set routerLink(commands: any[]|string|null|undefined) {
set routerLink(commands: any[] | string | null | undefined) {
if (commands != null) {
this.commands = Array.isArray(commands) ? commands : [commands];
this.setTabIndexIfNotOnNativeEl('0');
@@ -271,13 +286,22 @@ export class RouterLink implements OnChanges, OnDestroy {
}
/** @nodoc */
@HostListener(
'click',
['$event.button', '$event.ctrlKey', '$event.shiftKey', '$event.altKey', '$event.metaKey'])
onClick(button: number, ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean):
boolean {
@HostListener('click', [
'$event.button',
'$event.ctrlKey',
'$event.shiftKey',
'$event.altKey',
'$event.metaKey',
])
onClick(
button: number,
ctrlKey: boolean,
shiftKey: boolean,
altKey: boolean,
metaKey: boolean,
): boolean {
const urlTree = this.urlTree;
if (urlTree === null) {
return true;
}
@@ -313,27 +337,33 @@ export class RouterLink implements OnChanges, OnDestroy {
private updateHref(): void {
const urlTree = this.urlTree;
this.href = urlTree !== null && this.locationStrategy ?
this.locationStrategy?.prepareExternalUrl(this.router.serializeUrl(urlTree)) :
null;
this.href =
urlTree !== null && this.locationStrategy
? this.locationStrategy?.prepareExternalUrl(this.router.serializeUrl(urlTree))
: null;
const sanitizedValue = this.href === null ?
null :
// This class represents a directive that can be added to both `<a>` elements,
// as well as other elements. As a result, we can't define security context at
// compile time. So the security context is deferred to runtime.
// The `ɵɵsanitizeUrlOrResourceUrl` selects the necessary sanitizer function
// based on the tag and property names. The logic mimics the one from
// `packages/compiler/src/schema/dom_security_schema.ts`, which is used at compile time.
//
// Note: we should investigate whether we can switch to using `@HostBinding('attr.href')`
// instead of applying a value via a renderer, after a final merge of the
// `RouterLinkWithHref` directive.
ɵɵsanitizeUrlOrResourceUrl(this.href, this.el.nativeElement.tagName.toLowerCase(), 'href');
const sanitizedValue =
this.href === null
? null
: // This class represents a directive that can be added to both `<a>` elements,
// as well as other elements. As a result, we can't define security context at
// compile time. So the security context is deferred to runtime.
// The `ɵɵsanitizeUrlOrResourceUrl` selects the necessary sanitizer function
// based on the tag and property names. The logic mimics the one from
// `packages/compiler/src/schema/dom_security_schema.ts`, which is used at compile time.
//
// Note: we should investigate whether we can switch to using `@HostBinding('attr.href')`
// instead of applying a value via a renderer, after a final merge of the
// `RouterLinkWithHref` directive.
ɵɵsanitizeUrlOrResourceUrl(
this.href,
this.el.nativeElement.tagName.toLowerCase(),
'href',
);
this.applyAttributeValue('href', sanitizedValue);
}
private applyAttributeValue(attrName: string, attrValue: string|null) {
private applyAttributeValue(attrName: string, attrValue: string | null) {
const renderer = this.renderer;
const nativeElement = this.el.nativeElement;
if (attrValue !== null) {
@@ -343,7 +373,7 @@ export class RouterLink implements OnChanges, OnDestroy {
}
}
get urlTree(): UrlTree|null {
get urlTree(): UrlTree | null {
if (this.commands === null) {
return null;
}
@@ -6,7 +6,22 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AfterContentInit, ChangeDetectorRef, ContentChildren, Directive, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Optional, Output, QueryList, Renderer2, SimpleChanges} from '@angular/core';
import {
AfterContentInit,
ChangeDetectorRef,
ContentChildren,
Directive,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnDestroy,
Optional,
Output,
QueryList,
Renderer2,
SimpleChanges,
} from '@angular/core';
import {from, of, Subscription} from 'rxjs';
import {mergeAll} from 'rxjs/operators';
@@ -16,7 +31,6 @@ import {IsActiveMatchOptions} from '../url_tree';
import {RouterLink} from './router_link';
/**
*
* @description
@@ -110,8 +124,7 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
*
* @see {@link Router#isActive}
*/
@Input() routerLinkActiveOptions: {exact: boolean}|IsActiveMatchOptions = {exact: false};
@Input() routerLinkActiveOptions: {exact: boolean} | IsActiveMatchOptions = {exact: false};
/**
* Aria-current attribute to apply when the router link is active.
@@ -120,7 +133,7 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
*
* @see {@link https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current}
*/
@Input() ariaCurrentWhenActive?: 'page'|'step'|'location'|'date'|'time'|true|false;
@Input() ariaCurrentWhenActive?: 'page' | 'step' | 'location' | 'date' | 'time' | true | false;
/**
*
@@ -141,8 +154,12 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
@Output() readonly isActiveChange: EventEmitter<boolean> = new EventEmitter();
constructor(
private router: Router, private element: ElementRef, private renderer: Renderer2,
private readonly cdr: ChangeDetectorRef, @Optional() private link?: RouterLink) {
private router: Router,
private element: ElementRef,
private renderer: Renderer2,
private readonly cdr: ChangeDetectorRef,
@Optional() private link?: RouterLink,
) {
this.routerEventsSubscription = router.events.subscribe((s: Event) => {
if (s instanceof NavigationEnd) {
this.update();
@@ -153,28 +170,32 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
/** @nodoc */
ngAfterContentInit(): void {
// `of(null)` is used to force subscribe body to execute once immediately (like `startWith`).
of(this.links.changes, of(null)).pipe(mergeAll()).subscribe(_ => {
this.update();
this.subscribeToEachLinkOnChanges();
});
of(this.links.changes, of(null))
.pipe(mergeAll())
.subscribe((_) => {
this.update();
this.subscribeToEachLinkOnChanges();
});
}
private subscribeToEachLinkOnChanges() {
this.linkInputChangesSubscription?.unsubscribe();
const allLinkChanges = [...this.links.toArray(), this.link]
.filter((link): link is RouterLink => !!link)
.map(link => link.onChanges);
this.linkInputChangesSubscription = from(allLinkChanges).pipe(mergeAll()).subscribe(link => {
if (this._isActive !== this.isLinkActive(this.router)(link)) {
this.update();
}
});
.filter((link): link is RouterLink => !!link)
.map((link) => link.onChanges);
this.linkInputChangesSubscription = from(allLinkChanges)
.pipe(mergeAll())
.subscribe((link) => {
if (this._isActive !== this.isLinkActive(this.router)(link)) {
this.update();
}
});
}
@Input()
set routerLinkActive(data: string[]|string) {
set routerLinkActive(data: string[] | string) {
const classes = Array.isArray(data) ? data : data.split(' ');
this.classes = classes.filter(c => !!c);
this.classes = classes.filter((c) => !!c);
}
/** @nodoc */
@@ -203,7 +224,10 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
});
if (hasActiveLinks && this.ariaCurrentWhenActive !== undefined) {
this.renderer.setAttribute(
this.element.nativeElement, 'aria-current', this.ariaCurrentWhenActive.toString());
this.element.nativeElement,
'aria-current',
this.ariaCurrentWhenActive.toString(),
);
} else {
this.renderer.removeAttribute(this.element.nativeElement, 'aria-current');
}
@@ -215,11 +239,12 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
}
private isLinkActive(router: Router): (link: RouterLink) => boolean {
const options: boolean|IsActiveMatchOptions =
isActiveMatchOptions(this.routerLinkActiveOptions) ?
this.routerLinkActiveOptions :
// While the types should disallow `undefined` here, it's possible without strict inputs
(this.routerLinkActiveOptions.exact || false);
const options: boolean | IsActiveMatchOptions = isActiveMatchOptions(
this.routerLinkActiveOptions,
)
? this.routerLinkActiveOptions
: // While the types should disallow `undefined` here, it's possible without strict inputs
this.routerLinkActiveOptions.exact || false;
return (link: RouterLink) => {
const urlTree = link.urlTree;
return urlTree ? router.isActive(urlTree, options) : false;
@@ -228,14 +253,15 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
private hasActiveLinks(): boolean {
const isActiveCheckFn = this.isLinkActive(this.router);
return this.link && isActiveCheckFn(this.link) || this.links.some(isActiveCheckFn);
return (this.link && isActiveCheckFn(this.link)) || this.links.some(isActiveCheckFn);
}
}
/**
* Use instead of `'paths' in options` to be compatible with property renaming
*/
function isActiveMatchOptions(options: {exact: boolean}|
IsActiveMatchOptions): options is IsActiveMatchOptions {
function isActiveMatchOptions(
options: {exact: boolean} | IsActiveMatchOptions,
): options is IsActiveMatchOptions {
return !!(options as IsActiveMatchOptions).paths;
}
+88 -57
View File
@@ -6,7 +6,25 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ChangeDetectorRef, ComponentRef, Directive, EnvironmentInjector, EventEmitter, inject, Injectable, InjectionToken, Injector, Input, OnDestroy, OnInit, Output, reflectComponentType, SimpleChanges, ViewContainerRef, ɵRuntimeError as RuntimeError,} from '@angular/core';
import {
ChangeDetectorRef,
ComponentRef,
Directive,
EnvironmentInjector,
EventEmitter,
inject,
Injectable,
InjectionToken,
Injector,
Input,
OnDestroy,
OnInit,
Output,
reflectComponentType,
SimpleChanges,
ViewContainerRef,
ɵRuntimeError as RuntimeError,
} from '@angular/core';
import {combineLatest, of, Subscription} from 'rxjs';
import {switchMap} from 'rxjs/operators';
@@ -16,7 +34,6 @@ import {ChildrenOutletContexts} from '../router_outlet_context';
import {ActivatedRoute} from '../router_state';
import {PRIMARY_OUTLET} from '../shared';
/**
* An interface that defines the contract for developing a component outlet for the `Router`.
*
@@ -39,7 +56,7 @@ export interface RouterOutletContract {
isActivated: boolean;
/** The instance of the activated component or `null` if the outlet is not activated. */
component: Object|null;
component: Object | null;
/**
* The `Data` of the `ActivatedRoute` snapshot.
@@ -49,12 +66,15 @@ export interface RouterOutletContract {
/**
* The `ActivatedRoute` for the outlet or `null` if the outlet is not activated.
*/
activatedRoute: ActivatedRoute|null;
activatedRoute: ActivatedRoute | null;
/**
* Called by the `Router` when the outlet should activate (create a component).
*/
activateWith(activatedRoute: ActivatedRoute, environmentInjector: EnvironmentInjector|null): void;
activateWith(
activatedRoute: ActivatedRoute,
environmentInjector: EnvironmentInjector | null,
): void;
/**
* A request to destroy the currently activated component.
@@ -167,12 +187,12 @@ export interface RouterOutletContract {
standalone: true,
})
export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
private activated: ComponentRef<any>|null = null;
private activated: ComponentRef<any> | null = null;
/** @internal */
get activatedComponentRef(): ComponentRef<any>|null {
get activatedComponentRef(): ComponentRef<any> | null {
return this.activated;
}
private _activatedRoute: ActivatedRoute|null = null;
private _activatedRoute: ActivatedRoute | null = null;
/**
* The name of the outlet
*
@@ -270,16 +290,18 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
get component(): Object {
if (!this.activated)
throw new RuntimeError(
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated');
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated',
);
return this.activated.instance;
}
get activatedRoute(): ActivatedRoute {
if (!this.activated)
throw new RuntimeError(
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated');
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated',
);
return this._activatedRoute as ActivatedRoute;
}
@@ -296,8 +318,9 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
detach(): ComponentRef<any> {
if (!this.activated)
throw new RuntimeError(
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated');
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated',
);
this.location.detach();
const cmp = this.activated;
this.activated = null;
@@ -327,12 +350,13 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
}
}
activateWith(activatedRoute: ActivatedRoute, environmentInjector?: EnvironmentInjector|null) {
activateWith(activatedRoute: ActivatedRoute, environmentInjector?: EnvironmentInjector | null) {
if (this.isActivated) {
throw new RuntimeError(
RuntimeErrorCode.OUTLET_ALREADY_ACTIVATED,
(typeof ngDevMode === 'undefined' || ngDevMode) &&
'Cannot activate an already activated outlet');
RuntimeErrorCode.OUTLET_ALREADY_ACTIVATED,
(typeof ngDevMode === 'undefined' || ngDevMode) &&
'Cannot activate an already activated outlet',
);
}
this._activatedRoute = activatedRoute;
const location = this.location;
@@ -344,7 +368,7 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
this.activated = location.createComponent(component, {
index: location.length,
injector,
environmentInjector: environmentInjector ?? this.environmentInjector
environmentInjector: environmentInjector ?? this.environmentInjector,
});
// Calling `markForCheck` to make sure we will run the change detection when the
// `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component.
@@ -356,8 +380,10 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
class OutletInjector implements Injector {
constructor(
private route: ActivatedRoute, private childContexts: ChildrenOutletContexts,
private parent: Injector) {}
private route: ActivatedRoute,
private childContexts: ChildrenOutletContexts,
private parent: Injector,
) {}
get(token: any, notFoundValue?: any): any {
if (token === ActivatedRoute) {
@@ -390,7 +416,7 @@ export const INPUT_BINDER = new InjectionToken<RoutedComponentInputBinder>('');
*/
@Injectable()
export class RoutedComponentInputBinder {
private outletDataSubscriptions = new Map<RouterOutlet, Subscription>;
private outletDataSubscriptions = new Map<RouterOutlet, Subscription>();
bindActivatedRouteToOutletComponent(outlet: RouterOutlet) {
this.unsubscribeFromRouteData(outlet);
@@ -404,43 +430,48 @@ export class RoutedComponentInputBinder {
private subscribeToRouteData(outlet: RouterOutlet) {
const {activatedRoute} = outlet;
const dataSubscription =
combineLatest([
activatedRoute.queryParams,
activatedRoute.params,
activatedRoute.data,
])
.pipe(switchMap(([queryParams, params, data], index) => {
data = {...queryParams, ...params, ...data};
// Get the first result from the data subscription synchronously so it's available to
// the component as soon as possible (and doesn't require a second change detection).
if (index === 0) {
return of(data);
}
// Promise.resolve is used to avoid synchronously writing the wrong data when
// two of the Observables in the `combineLatest` stream emit one after
// another.
return Promise.resolve(data);
}))
.subscribe(data => {
// Outlet may have been deactivated or changed names to be associated with a different
// route
if (!outlet.isActivated || !outlet.activatedComponentRef ||
outlet.activatedRoute !== activatedRoute || activatedRoute.component === null) {
this.unsubscribeFromRouteData(outlet);
return;
}
const dataSubscription = combineLatest([
activatedRoute.queryParams,
activatedRoute.params,
activatedRoute.data,
])
.pipe(
switchMap(([queryParams, params, data], index) => {
data = {...queryParams, ...params, ...data};
// Get the first result from the data subscription synchronously so it's available to
// the component as soon as possible (and doesn't require a second change detection).
if (index === 0) {
return of(data);
}
// Promise.resolve is used to avoid synchronously writing the wrong data when
// two of the Observables in the `combineLatest` stream emit one after
// another.
return Promise.resolve(data);
}),
)
.subscribe((data) => {
// Outlet may have been deactivated or changed names to be associated with a different
// route
if (
!outlet.isActivated ||
!outlet.activatedComponentRef ||
outlet.activatedRoute !== activatedRoute ||
activatedRoute.component === null
) {
this.unsubscribeFromRouteData(outlet);
return;
}
const mirror = reflectComponentType(activatedRoute.component);
if (!mirror) {
this.unsubscribeFromRouteData(outlet);
return;
}
const mirror = reflectComponentType(activatedRoute.component);
if (!mirror) {
this.unsubscribeFromRouteData(outlet);
return;
}
for (const {templateName} of mirror.inputs) {
outlet.activatedComponentRef.setInput(templateName, data[templateName]);
}
});
for (const {templateName} of mirror.inputs) {
outlet.activatedComponentRef.setInput(templateName, data[templateName]);
}
});
this.outletDataSubscriptions.set(outlet, dataSubscription);
}
+180 -163
View File
@@ -19,7 +19,7 @@ import {UrlTree} from './url_tree';
*
* @publicApi
*/
export type NavigationTrigger = 'imperative'|'popstate'|'hashchange';
export type NavigationTrigger = 'imperative' | 'popstate' | 'hashchange';
export const IMPERATIVE_NAVIGATION = 'imperative';
/**
@@ -73,10 +73,11 @@ export enum EventType {
*/
export class RouterEvent {
constructor(
/** A unique ID that the router assigns to every router navigation. */
public id: number,
/** The URL that is the destination for this navigation. */
public url: string) {}
/** A unique ID that the router assigns to every router navigation. */
public id: number,
/** The URL that is the destination for this navigation. */
public url: string,
) {}
}
/**
@@ -114,17 +115,18 @@ export class NavigationStart extends RouterEvent {
* remembered state, such as scroll position.
*
*/
restoredState?: {[k: string]: any, navigationId: number}|null;
restoredState?: {[k: string]: any; navigationId: number} | null;
constructor(
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
navigationTrigger: NavigationTrigger = 'imperative',
/** @docsNotRequired */
restoredState: {[k: string]: any, navigationId: number}|null = null) {
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
navigationTrigger: NavigationTrigger = 'imperative',
/** @docsNotRequired */
restoredState: {[k: string]: any; navigationId: number} | null = null,
) {
super(id, url);
this.navigationTrigger = navigationTrigger;
this.restoredState = restoredState;
@@ -149,19 +151,19 @@ export class NavigationEnd extends RouterEvent {
readonly type = EventType.NavigationEnd;
constructor(
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
public urlAfterRedirects: string) {
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
public urlAfterRedirects: string,
) {
super(id, url);
}
/** @docsNotRequired */
override toString(): string {
return `NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${
this.urlAfterRedirects}')`;
return `NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}')`;
}
}
@@ -225,21 +227,22 @@ export class NavigationCancel extends RouterEvent {
readonly type = EventType.NavigationCancel;
constructor(
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/**
* A description of why the navigation was cancelled. For debug purposes only. Use `code`
* instead for a stable cancellation reason that can be used in production.
*/
public reason: string,
/**
* A code to indicate why the navigation was canceled. This cancellation code is stable for
* the reason and can be relied on whereas the `reason` string could change and should not be
* used in production.
*/
readonly code?: NavigationCancellationCode) {
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/**
* A description of why the navigation was cancelled. For debug purposes only. Use `code`
* instead for a stable cancellation reason that can be used in production.
*/
public reason: string,
/**
* A code to indicate why the navigation was canceled. This cancellation code is stable for
* the reason and can be relied on whereas the `reason` string could change and should not be
* used in production.
*/
readonly code?: NavigationCancellationCode,
) {
super(id, url);
}
@@ -261,21 +264,22 @@ export class NavigationSkipped extends RouterEvent {
readonly type = EventType.NavigationSkipped;
constructor(
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/**
* A description of why the navigation was skipped. For debug purposes only. Use `code`
* instead for a stable skipped reason that can be used in production.
*/
public reason: string,
/**
* A code to indicate why the navigation was skipped. This code is stable for
* the reason and can be relied on whereas the `reason` string could change and should not be
* used in production.
*/
readonly code?: NavigationSkippedCode) {
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/**
* A description of why the navigation was skipped. For debug purposes only. Use `code`
* instead for a stable skipped reason that can be used in production.
*/
public reason: string,
/**
* A code to indicate why the navigation was skipped. This code is stable for
* the reason and can be relied on whereas the `reason` string could change and should not be
* used in production.
*/
readonly code?: NavigationSkippedCode,
) {
super(id, url);
}
}
@@ -293,19 +297,20 @@ export class NavigationError extends RouterEvent {
readonly type = EventType.NavigationError;
constructor(
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
public error: any,
/**
* The target of the navigation when the error occurred.
*
* Note that this can be `undefined` because an error could have occurred before the
* `RouterStateSnapshot` was created for the navigation.
*/
readonly target?: RouterStateSnapshot) {
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
public error: any,
/**
* The target of the navigation when the error occurred.
*
* Note that this can be `undefined` because an error could have occurred before the
* `RouterStateSnapshot` was created for the navigation.
*/
readonly target?: RouterStateSnapshot,
) {
super(id, url);
}
@@ -324,21 +329,21 @@ export class RoutesRecognized extends RouterEvent {
readonly type = EventType.RoutesRecognized;
constructor(
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
public urlAfterRedirects: string,
/** @docsNotRequired */
public state: RouterStateSnapshot) {
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
public urlAfterRedirects: string,
/** @docsNotRequired */
public state: RouterStateSnapshot,
) {
super(id, url);
}
/** @docsNotRequired */
override toString(): string {
return `RoutesRecognized(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${
this.urlAfterRedirects}', state: ${this.state})`;
return `RoutesRecognized(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`;
}
}
@@ -353,20 +358,20 @@ export class GuardsCheckStart extends RouterEvent {
readonly type = EventType.GuardsCheckStart;
constructor(
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
public urlAfterRedirects: string,
/** @docsNotRequired */
public state: RouterStateSnapshot) {
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
public urlAfterRedirects: string,
/** @docsNotRequired */
public state: RouterStateSnapshot,
) {
super(id, url);
}
override toString(): string {
return `GuardsCheckStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${
this.urlAfterRedirects}', state: ${this.state})`;
return `GuardsCheckStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`;
}
}
@@ -381,22 +386,22 @@ export class GuardsCheckEnd extends RouterEvent {
readonly type = EventType.GuardsCheckEnd;
constructor(
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
public urlAfterRedirects: string,
/** @docsNotRequired */
public state: RouterStateSnapshot,
/** @docsNotRequired */
public shouldActivate: boolean) {
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
public urlAfterRedirects: string,
/** @docsNotRequired */
public state: RouterStateSnapshot,
/** @docsNotRequired */
public shouldActivate: boolean,
) {
super(id, url);
}
override toString(): string {
return `GuardsCheckEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${
this.urlAfterRedirects}', state: ${this.state}, shouldActivate: ${this.shouldActivate})`;
return `GuardsCheckEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state}, shouldActivate: ${this.shouldActivate})`;
}
}
@@ -414,20 +419,20 @@ export class ResolveStart extends RouterEvent {
readonly type = EventType.ResolveStart;
constructor(
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
public urlAfterRedirects: string,
/** @docsNotRequired */
public state: RouterStateSnapshot) {
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
public urlAfterRedirects: string,
/** @docsNotRequired */
public state: RouterStateSnapshot,
) {
super(id, url);
}
override toString(): string {
return `ResolveStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${
this.urlAfterRedirects}', state: ${this.state})`;
return `ResolveStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`;
}
}
@@ -441,20 +446,20 @@ export class ResolveEnd extends RouterEvent {
readonly type = EventType.ResolveEnd;
constructor(
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
public urlAfterRedirects: string,
/** @docsNotRequired */
public state: RouterStateSnapshot) {
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
public urlAfterRedirects: string,
/** @docsNotRequired */
public state: RouterStateSnapshot,
) {
super(id, url);
}
override toString(): string {
return `ResolveEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${
this.urlAfterRedirects}', state: ${this.state})`;
return `ResolveEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`;
}
}
@@ -469,8 +474,9 @@ export class RouteConfigLoadStart {
readonly type = EventType.RouteConfigLoadStart;
constructor(
/** @docsNotRequired */
public route: Route) {}
/** @docsNotRequired */
public route: Route,
) {}
toString(): string {
return `RouteConfigLoadStart(path: ${this.route.path})`;
}
@@ -487,8 +493,9 @@ export class RouteConfigLoadEnd {
readonly type = EventType.RouteConfigLoadEnd;
constructor(
/** @docsNotRequired */
public route: Route) {}
/** @docsNotRequired */
public route: Route,
) {}
toString(): string {
return `RouteConfigLoadEnd(path: ${this.route.path})`;
}
@@ -506,10 +513,11 @@ export class ChildActivationStart {
readonly type = EventType.ChildActivationStart;
constructor(
/** @docsNotRequired */
public snapshot: ActivatedRouteSnapshot) {}
/** @docsNotRequired */
public snapshot: ActivatedRouteSnapshot,
) {}
toString(): string {
const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || '';
const path = (this.snapshot.routeConfig && this.snapshot.routeConfig.path) || '';
return `ChildActivationStart(path: '${path}')`;
}
}
@@ -525,10 +533,11 @@ export class ChildActivationEnd {
readonly type = EventType.ChildActivationEnd;
constructor(
/** @docsNotRequired */
public snapshot: ActivatedRouteSnapshot) {}
/** @docsNotRequired */
public snapshot: ActivatedRouteSnapshot,
) {}
toString(): string {
const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || '';
const path = (this.snapshot.routeConfig && this.snapshot.routeConfig.path) || '';
return `ChildActivationEnd(path: '${path}')`;
}
}
@@ -545,10 +554,11 @@ export class ActivationStart {
readonly type = EventType.ActivationStart;
constructor(
/** @docsNotRequired */
public snapshot: ActivatedRouteSnapshot) {}
/** @docsNotRequired */
public snapshot: ActivatedRouteSnapshot,
) {}
toString(): string {
const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || '';
const path = (this.snapshot.routeConfig && this.snapshot.routeConfig.path) || '';
return `ActivationStart(path: '${path}')`;
}
}
@@ -565,10 +575,11 @@ export class ActivationEnd {
readonly type = EventType.ActivationEnd;
constructor(
/** @docsNotRequired */
public snapshot: ActivatedRouteSnapshot) {}
/** @docsNotRequired */
public snapshot: ActivatedRouteSnapshot,
) {}
toString(): string {
const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || '';
const path = (this.snapshot.routeConfig && this.snapshot.routeConfig.path) || '';
return `ActivationEnd(path: '${path}')`;
}
}
@@ -582,14 +593,15 @@ export class Scroll {
readonly type = EventType.Scroll;
constructor(
/** @docsNotRequired */
readonly routerEvent: NavigationEnd|NavigationSkipped,
/** @docsNotRequired */
readonly routerEvent: NavigationEnd | NavigationSkipped,
/** @docsNotRequired */
readonly position: [number, number]|null,
/** @docsNotRequired */
readonly position: [number, number] | null,
/** @docsNotRequired */
readonly anchor: string|null) {}
/** @docsNotRequired */
readonly anchor: string | null,
) {}
toString(): string {
const pos = this.position ? `${this.position[0]}, ${this.position[1]}` : null;
@@ -601,7 +613,7 @@ export class BeforeActivateRoutes {}
export class RedirectRequest {
constructor(readonly url: UrlTree) {}
}
export type PrivateRouterEvents = BeforeActivateRoutes|RedirectRequest;
export type PrivateRouterEvents = BeforeActivateRoutes | RedirectRequest;
/**
* Router events that allow you to track the lifecycle of the router.
@@ -636,10 +648,24 @@ export type PrivateRouterEvents = BeforeActivateRoutes|RedirectRequest;
*
* @publicApi
*/
export type Event = NavigationStart|NavigationEnd|NavigationCancel|NavigationError|RoutesRecognized|
GuardsCheckStart|GuardsCheckEnd|RouteConfigLoadStart|RouteConfigLoadEnd|ChildActivationStart|
ChildActivationEnd|ActivationStart|ActivationEnd|Scroll|ResolveStart|ResolveEnd|
NavigationSkipped;
export type Event =
| NavigationStart
| NavigationEnd
| NavigationCancel
| NavigationError
| RoutesRecognized
| GuardsCheckStart
| GuardsCheckEnd
| RouteConfigLoadStart
| RouteConfigLoadEnd
| ChildActivationStart
| ChildActivationEnd
| ActivationStart
| ActivationEnd
| Scroll
| ResolveStart
| ResolveEnd
| NavigationSkipped;
export function stringifyEvent(routerEvent: Event): string {
switch (routerEvent.type) {
@@ -652,42 +678,33 @@ export function stringifyEvent(routerEvent: Event): string {
case EventType.ChildActivationStart:
return `ChildActivationStart(path: '${routerEvent.snapshot.routeConfig?.path || ''}')`;
case EventType.GuardsCheckEnd:
return `GuardsCheckEnd(id: ${routerEvent.id}, url: '${
routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${
routerEvent.state}, shouldActivate: ${routerEvent.shouldActivate})`;
return `GuardsCheckEnd(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${routerEvent.state}, shouldActivate: ${routerEvent.shouldActivate})`;
case EventType.GuardsCheckStart:
return `GuardsCheckStart(id: ${routerEvent.id}, url: '${
routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${
routerEvent.state})`;
return `GuardsCheckStart(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`;
case EventType.NavigationCancel:
return `NavigationCancel(id: ${routerEvent.id}, url: '${routerEvent.url}')`;
case EventType.NavigationSkipped:
return `NavigationSkipped(id: ${routerEvent.id}, url: '${routerEvent.url}')`;
case EventType.NavigationEnd:
return `NavigationEnd(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${
routerEvent.urlAfterRedirects}')`;
return `NavigationEnd(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}')`;
case EventType.NavigationError:
return `NavigationError(id: ${routerEvent.id}, url: '${routerEvent.url}', error: ${
routerEvent.error})`;
return `NavigationError(id: ${routerEvent.id}, url: '${routerEvent.url}', error: ${routerEvent.error})`;
case EventType.NavigationStart:
return `NavigationStart(id: ${routerEvent.id}, url: '${routerEvent.url}')`;
case EventType.ResolveEnd:
return `ResolveEnd(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${
routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`;
return `ResolveEnd(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`;
case EventType.ResolveStart:
return `ResolveStart(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${
routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`;
return `ResolveStart(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`;
case EventType.RouteConfigLoadEnd:
return `RouteConfigLoadEnd(path: ${routerEvent.route.path})`;
case EventType.RouteConfigLoadStart:
return `RouteConfigLoadStart(path: ${routerEvent.route.path})`;
case EventType.RoutesRecognized:
return `RoutesRecognized(id: ${routerEvent.id}, url: '${
routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${
routerEvent.state})`;
return `RoutesRecognized(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`;
case EventType.Scroll:
const pos =
routerEvent.position ? `${routerEvent.position[0]}, ${routerEvent.position[1]}` : null;
const pos = routerEvent.position
? `${routerEvent.position[0]}, ${routerEvent.position[1]}`
: null;
return `Scroll(anchor: '${routerEvent.anchor}', position: '${pos}')`;
}
}
+110 -10
View File
@@ -6,31 +6,131 @@
* found in the LICENSE file at https://angular.io/license
*/
export {createUrlTreeFromSnapshot} from './create_url_tree';
export {RouterLink, RouterLinkWithHref} from './directives/router_link';
export {RouterLinkActive} from './directives/router_link_active';
export {RouterOutlet, RouterOutletContract} from './directives/router_outlet';
export {ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, Event, EventType, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationCancellationCode as NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationSkippedCode, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, RoutesRecognized, Scroll} from './events';
export {CanActivateChildFn, CanActivateFn, CanDeactivateFn, CanLoadFn, CanMatchFn, Data, DefaultExport, LoadChildren, LoadChildrenCallback, NavigationBehaviorOptions, OnSameUrlNavigation, QueryParamsHandling, ResolveData, ResolveFn, Route, Routes, RunGuardsAndResolvers, UrlMatcher, UrlMatchResult} from './models';
export {
ActivationEnd,
ActivationStart,
ChildActivationEnd,
ChildActivationStart,
Event,
EventType,
GuardsCheckEnd,
GuardsCheckStart,
NavigationCancel,
NavigationCancellationCode as NavigationCancellationCode,
NavigationEnd,
NavigationError,
NavigationSkipped,
NavigationSkippedCode,
NavigationStart,
ResolveEnd,
ResolveStart,
RouteConfigLoadEnd,
RouteConfigLoadStart,
RouterEvent,
RoutesRecognized,
Scroll,
} from './events';
export {
CanActivateChildFn,
CanActivateFn,
CanDeactivateFn,
CanLoadFn,
CanMatchFn,
Data,
DefaultExport,
LoadChildren,
LoadChildrenCallback,
NavigationBehaviorOptions,
OnSameUrlNavigation,
QueryParamsHandling,
ResolveData,
ResolveFn,
Route,
Routes,
RunGuardsAndResolvers,
UrlMatcher,
UrlMatchResult,
} from './models';
export {ViewTransitionInfo, ViewTransitionsFeatureOptions} from './utils/view_transition';
export * from './models_deprecated';
export {Navigation, NavigationExtras, UrlCreationOptions} from './navigation_transition';
export {DefaultTitleStrategy, TitleStrategy} from './page_title_strategy';
export {DebugTracingFeature, DisabledInitialNavigationFeature, withViewTransitions, ViewTransitionsFeature, EnabledBlockingInitialNavigationFeature, InitialNavigationFeature, InMemoryScrollingFeature, NavigationErrorHandlerFeature, PreloadingFeature, provideRouter, provideRoutes, RouterConfigurationFeature, RouterFeature, RouterFeatures, RouterHashLocationFeature, withComponentInputBinding, withDebugTracing, withDisabledInitialNavigation, withEnabledBlockingInitialNavigation, withHashLocation, withInMemoryScrolling, withNavigationErrorHandler, withPreloading, withRouterConfig} from './provide_router';
export {BaseRouteReuseStrategy, DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
export {
DebugTracingFeature,
DisabledInitialNavigationFeature,
withViewTransitions,
ViewTransitionsFeature,
EnabledBlockingInitialNavigationFeature,
InitialNavigationFeature,
InMemoryScrollingFeature,
NavigationErrorHandlerFeature,
PreloadingFeature,
provideRouter,
provideRoutes,
RouterConfigurationFeature,
RouterFeature,
RouterFeatures,
RouterHashLocationFeature,
withComponentInputBinding,
withDebugTracing,
withDisabledInitialNavigation,
withEnabledBlockingInitialNavigation,
withHashLocation,
withInMemoryScrolling,
withNavigationErrorHandler,
withPreloading,
withRouterConfig,
} from './provide_router';
export {
BaseRouteReuseStrategy,
DetachedRouteHandle,
RouteReuseStrategy,
} from './route_reuse_strategy';
export {Router} from './router';
export {ExtraOptions, InitialNavigation, InMemoryScrollingOptions, ROUTER_CONFIGURATION, RouterConfigOptions} from './router_config';
export {
ExtraOptions,
InitialNavigation,
InMemoryScrollingOptions,
ROUTER_CONFIGURATION,
RouterConfigOptions,
} from './router_config';
export {ROUTES} from './router_config_loader';
export {ROUTER_INITIALIZER, RouterModule} from './router_module';
export {ChildrenOutletContexts, OutletContext} from './router_outlet_context';
export {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader';
export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
export {
NoPreloading,
PreloadAllModules,
PreloadingStrategy,
RouterPreloader,
} from './router_preloader';
export {
ActivatedRoute,
ActivatedRouteSnapshot,
RouterState,
RouterStateSnapshot,
} from './router_state';
export {convertToParamMap, defaultUrlMatcher, ParamMap, Params, PRIMARY_OUTLET} from './shared';
export {UrlHandlingStrategy} from './url_handling_strategy';
export {DefaultUrlSerializer, IsActiveMatchOptions, UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
export {mapToCanActivate, mapToCanActivateChild, mapToCanDeactivate, mapToCanMatch, mapToResolve} from './utils/functional_guards';
export {
DefaultUrlSerializer,
IsActiveMatchOptions,
UrlSegment,
UrlSegmentGroup,
UrlSerializer,
UrlTree,
} from './url_tree';
export {
mapToCanActivate,
mapToCanActivateChild,
mapToCanDeactivate,
mapToCanMatch,
mapToResolve,
} from './utils/functional_guards';
export {VERSION} from './version';
export * from './private_export';

Some files were not shown because too many files have changed in this diff Show More