mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9e022dae2 | |||
| 48d899f423 | |||
| 19b6564cb2 | |||
| f24246ef25 | |||
| 65d2ca9ed3 | |||
| a2db87ef81 | |||
| 94ed3c3630 | |||
| a64a5755e8 | |||
| 20add2738c | |||
| e27a319261 | |||
| fbbe5d55b1 | |||
| 507ed58152 | |||
| 31bbbe9665 | |||
| 83abe5f606 | |||
| 26b3a8f206 | |||
| edae26e117 | |||
| 8579ff8352 | |||
| 2bffacbe5e | |||
| f24a8c60ad | |||
| 036ee75684 | |||
| 33a826b196 | |||
| 114658cf8c | |||
| d30ea1b89c | |||
| aff6d3d135 | |||
| 1c31c0d141 | |||
| 3f0116607d | |||
| 080fb08b76 | |||
| d3dde17693 | |||
| 9ae9d9adde | |||
| c615bdcde9 | |||
| 79c337448e | |||
| c67c551fb8 | |||
| 1149686da5 | |||
| 5c3be24712 | |||
| 444671ac5b | |||
| d1233ebfa8 | |||
| 1c328ac261 | |||
| cd31bfcc3e | |||
| 27174165a6 | |||
| bf89792f8b | |||
| 49e3ec0dcf | |||
| c2de5f68b3 | |||
| dd70675893 | |||
| 1dd94476b3 | |||
| c288a037b5 | |||
| b81ea4f672 | |||
| 1b3b05bf72 | |||
| cfad089cc3 | |||
| 95d227e5ef | |||
| ddfaf0cd46 | |||
| b09f4a5c79 | |||
| 6c0c012e08 | |||
| a16dfb1b36 | |||
| e549bdec8c | |||
| 16ca676db0 | |||
| 187758ccca | |||
| 330c24aed9 | |||
| ada75b605e | |||
| b06ed3cbf8 | |||
| e012cf0acf | |||
| 8e9463bffb | |||
| 9b4727587a | |||
| ee2f07fcff | |||
| f76fd48b4a | |||
| 5de4b0cb70 | |||
| fca21320e6 | |||
| c4126c7b9a | |||
| 176475f0fc | |||
| 7d6636087d | |||
| ce3a9a24d7 | |||
| ef17371ab6 | |||
| 6473214384 | |||
| f23946abe9 | |||
| fa6d9c22ac | |||
| d5f467e601 | |||
| b5f28006b3 | |||
| aac3e8acf6 | |||
| 10fb8722fe | |||
| 93c1918ab1 | |||
| ebfbaeba54 | |||
| 9c5a6a6442 | |||
| 67366d2f93 | |||
| 225e602176 | |||
| 02a768e1d2 | |||
| 35d20cb4b4 | |||
| 352021e1d2 | |||
| 285abbb38f | |||
| 3fd6551cae | |||
| ea8662ab3e | |||
| 1d28a42815 | |||
| 21740846b5 | |||
| 4fde15438f | |||
| 7a1bf0610e | |||
| 195d0b1ff5 | |||
| 053593ef3e | |||
| 347c3d5761 | |||
| 8449c071da | |||
| f88ed8e470 | |||
| 11ea0a0f7d |
@@ -0,0 +1,21 @@
|
||||
# all commits that change formatting to prettier
|
||||
fd544159e300340a25a48e3f40c58795991a4c17
|
||||
31fdf0fbea6b89c8d3d141b2ef8e79c2737287cb
|
||||
0d78a92431178be73342aeddda4b62d9e49138ba
|
||||
f307e95459898b9027d5e13357a809d8952e9017
|
||||
b1dffa4abe8321a47d79b2ea29ee32a81acfe031
|
||||
ca517d7f2c38775969a65fde1d88f41ad5f27931
|
||||
8f69c83b848ad0dc3c7ffce2541fc726693dedcf
|
||||
a5b5b7d5ef84b9852d2115dd7a764f4ab3299379
|
||||
894c1c5f100a62866cc7ed3e23d025c8e505c60d
|
||||
ca39926de2ccaceb11de47a35e5bf9899147a877
|
||||
b857aafcb9bdb5f1400a4cfedd56657e71ebe8cd
|
||||
c4b880a025823efadb9d76195c6261f2992c7654
|
||||
4c2b0d94e22f022969b86b95af516b76322840e0
|
||||
0460a9dfafe3a7ebbff1b87e52048fb7416bba43
|
||||
ec03e462f3d4ec90d1267474cc344563c949c2cf
|
||||
bbbe477f479f20722f0fea7ccc46095aad5d4253
|
||||
cc34e5fb072e7903e01463953a19d18503a39347
|
||||
f9781f9804f1e7c84df356d9aeb267730a7fc499
|
||||
dc62446ef7f2898a20d3be1196ecaf2a92a1077d
|
||||
711cb416260ca11ff158fc39b372efa2cf022d36
|
||||
@@ -1,7 +1,7 @@
|
||||
## PR Checklist
|
||||
Please check if your PR fulfills the following requirements:
|
||||
|
||||
- [ ] The commit message follows our guidelines: https://github.com/angular/angular/blob/main/CONTRIBUTING.md#commit
|
||||
- [ ] The commit message follows our guidelines: https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md
|
||||
- [ ] Tests for the changes have been added (for bug fixes / features)
|
||||
- [ ] Docs have been added / updated (for bug fixes / features)
|
||||
|
||||
|
||||
@@ -29830,7 +29830,7 @@ function legacyRestEndpointMethods(octokit) {
|
||||
legacyRestEndpointMethods.VERSION = VERSION9;
|
||||
|
||||
//
|
||||
var VERSION10 = "21.1.0";
|
||||
var VERSION10 = "21.1.1";
|
||||
|
||||
//
|
||||
var Octokit2 = Octokit.plugin(requestLog, legacyRestEndpointMethods, paginateRest).defaults(
|
||||
|
||||
@@ -4,7 +4,7 @@ description: 'Installs the dependencies using Yarn'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4
|
||||
- uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4
|
||||
with:
|
||||
path: |
|
||||
./node_modules/
|
||||
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
- name: Run CI tests for framework
|
||||
run: yarn tsx ./scripts/build/build-packages-dist.mts
|
||||
- name: Archive build artifacts
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||
with:
|
||||
name: pr-artifacts-${{ github.event.number }}
|
||||
path: dist/packages-dist/
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Run analysis'
|
||||
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
|
||||
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
# Upload the results as artifacts.
|
||||
- name: 'Upload artifact'
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
@@ -47,6 +47,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: 'Upload to code-scanning'
|
||||
uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
|
||||
uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ Fixes #<issue number>
|
||||
# =============================
|
||||
#
|
||||
# The full specification of the Angular Commit Message Format can be found at
|
||||
# https://github.com/angular/angular/blob/main/CONTRIBUTING.md#commit
|
||||
# https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md
|
||||
#
|
||||
# The following is an excerpt of the specification with the most commonly needed info.
|
||||
#
|
||||
|
||||
@@ -7,6 +7,8 @@ export const commitMessage: CommitMessageConfig = {
|
||||
maxLineLength: Infinity,
|
||||
minBodyLength: 20,
|
||||
minBodyLengthTypeExcludes: ['docs'],
|
||||
// If you update this, also update the docs.
|
||||
// https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md#scope
|
||||
scopes: [
|
||||
'animations',
|
||||
'bazel',
|
||||
|
||||
@@ -144,7 +144,6 @@ groups:
|
||||
- >
|
||||
contains_any_globs(files.exclude('packages/core/primitives/*'), [
|
||||
'contributing-docs/public-api-surface.md',
|
||||
'goldens/circular-deps/packages.json',
|
||||
'integration/**/{*,.*}',
|
||||
'modules/**/{*,.*}',
|
||||
'packages/animations/**/{*,.*}',
|
||||
@@ -430,28 +429,6 @@ groups:
|
||||
required: 1 # Require that 1 person approve
|
||||
reviewed_for: required
|
||||
|
||||
# ================================================
|
||||
# Circular dependencies
|
||||
# ================================================
|
||||
circular-dependencies:
|
||||
<<: *defaults
|
||||
conditions:
|
||||
- *no-groups-above-this-pending
|
||||
- *no-groups-above-this-rejected
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
'goldens/circular-deps/packages.json'
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- alxhub
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- ~jelbourn
|
||||
- thePunderWoman
|
||||
- pkozlowski-opensource
|
||||
- kirjs
|
||||
|
||||
####################################################################################
|
||||
# Special Cases
|
||||
####################################################################################
|
||||
|
||||
Vendored
+8
-5
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"[javascript]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
// Exclude third party modules and build artifacts from the editor watchers/searches.
|
||||
"files.watcherExclude": {
|
||||
@@ -11,14 +11,17 @@
|
||||
"**/.git/subtree-cache/**": true,
|
||||
"**/node_modules/**": true,
|
||||
"**/bazel-out/**": true,
|
||||
"**/dist/**": true,
|
||||
"**/dist/**": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/bower_components": true,
|
||||
"**/bazel-out": true,
|
||||
"**/dist": true,
|
||||
".history": true,
|
||||
".history": true
|
||||
},
|
||||
"git.ignoreLimitWarning": true,
|
||||
}
|
||||
"gitlens.advanced.blame.customArguments": [
|
||||
"--ignore-revs-file .git-blame-ignore-revs"
|
||||
]
|
||||
}
|
||||
|
||||
+71
-101
@@ -1,3 +1,74 @@
|
||||
<a name="19.2.1"></a>
|
||||
# 19.2.1 (2025-03-05)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c2de5f68b3](https://github.com/angular/angular/commit/c2de5f68b3d3cd8a0c43b3c4325a1a1db874e132) | fix | clean up `onUrlChange` listener when root scope is destroyed ([#60004](https://github.com/angular/angular/pull/60004)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1dd94476b3](https://github.com/angular/angular/commit/1dd94476b35d08e753766b1d0c5d8af5faa017a9) | fix | ensure template IDs are not reused if a source file changes ([#60152](https://github.com/angular/angular/pull/60152)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1b3b05bf72](https://github.com/angular/angular/commit/1b3b05bf7294963aa50f83b726db620b250b1a5c) | fix | cache ComponentRef inputs and outputs ([#60156](https://github.com/angular/angular/pull/60156)) |
|
||||
| [330c24aed9](https://github.com/angular/angular/commit/330c24aed92e4b916fb7ac6a91fb17c3ae728869) | fix | prevent invoking replay listeners on disconnected nodes ([#60103](https://github.com/angular/angular/pull/60103)) |
|
||||
| [cfad089cc3](https://github.com/angular/angular/commit/cfad089cc359cddf08d462662d2a6bf3acab9b22) | fix | prevents event replay from being called on comment nodes ([#60130](https://github.com/angular/angular/pull/60130)) |
|
||||
### language-service
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3f0116607d](https://github.com/angular/angular/commit/3f0116607dc3ad7e31cb4d895a56094f77c82f5d) | fix | Forward the tags for quick info from the type definition ([#59524](https://github.com/angular/angular/pull/59524)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="19.2.0"></a>
|
||||
# 19.2.0 (2025-02-26)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3e39da593a](https://github.com/angular/angular/commit/3e39da593a0a0c047a2a03b8d5fcabf9dbace40f) | feat | introduce experimental `httpResource` ([#59876](https://github.com/angular/angular/pull/59876)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5b20bab96d](https://github.com/angular/angular/commit/5b20bab96d20fe89b5cc4b4af28edbaae2604da1) | feat | Add Skip Hydration diagnostic. ([#59576](https://github.com/angular/angular/pull/59576)) |
|
||||
| [fe8a68329b](https://github.com/angular/angular/commit/fe8a68329b50363f914a728579392f3fc68670a6) | feat | support untagged template literals in expressions ([#59230](https://github.com/angular/angular/pull/59230)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [ed705a856a](https://github.com/angular/angular/commit/ed705a856a164f91d33e2173b4f371329d07c472) | feat | detect missing structural directive imports ([#59443](https://github.com/angular/angular/pull/59443)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [2588985f43](https://github.com/angular/angular/commit/2588985f433b20a6a5a8d239347291f5d6fb2451) | feat | pass signal node to throwInvalidWriteToSignalErrorFn ([#59600](https://github.com/angular/angular/pull/59600)) |
|
||||
| [168516462a](https://github.com/angular/angular/commit/168516462a9673b158fcaa38b8ce17bf684a8ac9) | feat | support default value in `resource()` ([#59655](https://github.com/angular/angular/pull/59655)) |
|
||||
| [bc2ad7bfd3](https://github.com/angular/angular/commit/bc2ad7bfd37a61992b550943de5da0eab2eec98b) | feat | support streaming resources ([#59573](https://github.com/angular/angular/pull/59573)) |
|
||||
| [146ab9a76e](https://github.com/angular/angular/commit/146ab9a76e6b4d8db7d08d34e2571ba5207f8756) | feat | support TypeScript 5.8 ([#59830](https://github.com/angular/angular/pull/59830)) |
|
||||
| [6c92d65349](https://github.com/angular/angular/commit/6c92d653493404a5f13aa59cde390bcbed973fb6) | fix | add `hasValue` narrowing to `ResourceRef` ([#59708](https://github.com/angular/angular/pull/59708)) |
|
||||
| [96e602ebe9](https://github.com/angular/angular/commit/96e602ebe9cdf7355befad22c11f9f91e0436e01) | fix | cancel in-progress request when same value is assigned ([#59280](https://github.com/angular/angular/pull/59280)) |
|
||||
| [6789c7ef94](https://github.com/angular/angular/commit/6789c7ef947952551d7598fe37a3d86093b75720) | fix | Defer afterRender until after first CD ([#59455](https://github.com/angular/angular/pull/59455)) ([#59551](https://github.com/angular/angular/pull/59551)) |
|
||||
| [c87e581dd9](https://github.com/angular/angular/commit/c87e581dd9e240c88cea50f222942873bdccd01d) | fix | Don't run effects in check no changes pass ([#59455](https://github.com/angular/angular/pull/59455)) ([#59551](https://github.com/angular/angular/pull/59551)) |
|
||||
| [127fc0dc84](https://github.com/angular/angular/commit/127fc0dc847a4e8b62be36cdd980a067c4da974f) | fix | fix `resource()`'s `previous.state` ([#59708](https://github.com/angular/angular/pull/59708)) |
|
||||
| [b592b1b051](https://github.com/angular/angular/commit/b592b1b0516786c52c7d0638c4e7545b0de8a545) | fix | fix race condition in resource() ([#59851](https://github.com/angular/angular/pull/59851)) |
|
||||
| [a299e02e91](https://github.com/angular/angular/commit/a299e02e9141cdc4d74185deb58308fa010bb36e) | fix | preserve tracing snapshot until tick finishes ([#59796](https://github.com/angular/angular/pull/59796)) |
|
||||
### forms
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [fa0c3e3210](https://github.com/angular/angular/commit/fa0c3e3210885a36e5c9e9eb76e821032f5cd215) | feat | support type set in form validators ([#45793](https://github.com/angular/angular/pull/45793)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1cd3a7db83](https://github.com/angular/angular/commit/1cd3a7db83e1d05a31d23324676420b614cdabe2) | feat | add migration to convert templates to use self-closing tags ([#57342](https://github.com/angular/angular/pull/57342)) |
|
||||
### platform-browser
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e6cb411e43](https://github.com/angular/angular/commit/e6cb411e4393a4b1f5852d3d7c5b9622504399b1) | fix | automatically disable animations on the server ([#59762](https://github.com/angular/angular/pull/59762)) |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [fc5d187da5](https://github.com/angular/angular/commit/fc5d187da5e8895d60caa35b7b59e234998eddf0) | fix | decouple server from animations module ([#59762](https://github.com/angular/angular/pull/59762)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="19.1.7"></a>
|
||||
# 19.1.7 (2025-02-19)
|
||||
### common
|
||||
@@ -15,30 +86,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="19.2.0-next.3"></a>
|
||||
# 19.2.0-next.3 (2025-02-13)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [9e847fc60d](https://github.com/angular/angular/commit/9e847fc60d4eef47e665e789ccd2d4f0b4bb94ea) | fix | handle tracking expressions requiring temporary variables ([#58520](https://github.com/angular/angular/pull/58520)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5cd26a9420](https://github.com/angular/angular/commit/5cd26a94206dfe8aabdf0dd15bfc09e7a8c606da) | fix | handle deferred blocks with shared dependencies correctly ([#59926](https://github.com/angular/angular/pull/59926)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [6789c7ef94](https://github.com/angular/angular/commit/6789c7ef947952551d7598fe37a3d86093b75720) | fix | Defer afterRender until after first CD ([#59455](https://github.com/angular/angular/pull/59455)) ([#59551](https://github.com/angular/angular/pull/59551)) |
|
||||
| [c87e581dd9](https://github.com/angular/angular/commit/c87e581dd9e240c88cea50f222942873bdccd01d) | fix | Don't run effects in check no changes pass ([#59455](https://github.com/angular/angular/pull/59455)) ([#59551](https://github.com/angular/angular/pull/59551)) |
|
||||
| [b0266bda4a](https://github.com/angular/angular/commit/b0266bda4ad4efd19710fd0363a50984f48269dc) | fix | invalidate HMR component if replacement throws an error ([#59854](https://github.com/angular/angular/pull/59854)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [aa285c548c](https://github.com/angular/angular/commit/aa285c548c164917ebb5760802484843c6830daf) | fix | account for let declarations in control flow migration ([#59861](https://github.com/angular/angular/pull/59861)) |
|
||||
| [1119f85ca9](https://github.com/angular/angular/commit/1119f85ca935f43641f53ee8bc50efa47ad09717) | fix | count used dependencies inside existing control flow ([#59861](https://github.com/angular/angular/pull/59861)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="19.1.6"></a>
|
||||
# 19.1.6 (2025-02-12)
|
||||
### compiler
|
||||
@@ -61,27 +108,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="19.2.0-next.2"></a>
|
||||
# 19.2.0-next.2 (2025-02-06)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [a97136052f9](https://github.com/angular/angular/commit/a97136052f9cfdb696a3b335ed7a1b9fb994d408) | fix | gracefully fall back if const enum cannot be passed through ([#59815](https://github.com/angular/angular/pull/59815)) |
|
||||
| [bae94b82fda](https://github.com/angular/angular/commit/bae94b82fda1669a6aafad975a3a0eb6f2743039) | fix | handle const enums used inside HMR data ([#59815](https://github.com/angular/angular/pull/59815)) |
|
||||
| [03bcd30e348](https://github.com/angular/angular/commit/03bcd30e3487443983d450a5dfea2d68d551bbd3) | fix | handle enum members without initializers in partial evaluator ([#59815](https://github.com/angular/angular/pull/59815)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [146ab9a76e6](https://github.com/angular/angular/commit/146ab9a76e6b4d8db7d08d34e2571ba5207f8756) | feat | support TypeScript 5.8 ([#59830](https://github.com/angular/angular/pull/59830)) |
|
||||
| [b592b1b0516](https://github.com/angular/angular/commit/b592b1b0516786c52c7d0638c4e7545b0de8a545) | fix | fix race condition in resource() ([#59851](https://github.com/angular/angular/pull/59851)) |
|
||||
| [a299e02e914](https://github.com/angular/angular/commit/a299e02e9141cdc4d74185deb58308fa010bb36e) | fix | preserve tracing snapshot until tick finishes ([#59796](https://github.com/angular/angular/pull/59796)) |
|
||||
### forms
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [fa0c3e32108](https://github.com/angular/angular/commit/fa0c3e3210885a36e5c9e9eb76e821032f5cd215) | feat | support type set in form validators ([#45793](https://github.com/angular/angular/pull/45793)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="19.1.5"></a>
|
||||
# 19.1.5 (2025-02-06)
|
||||
### compiler-cli
|
||||
@@ -93,45 +119,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="19.2.0-next.1"></a>
|
||||
# 19.2.0-next.1 (2025-01-29)
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [168516462a9](https://github.com/angular/angular/commit/168516462a9673b158fcaa38b8ce17bf684a8ac9) | feat | support default value in `resource()` ([#59655](https://github.com/angular/angular/pull/59655)) |
|
||||
| [6c92d653493](https://github.com/angular/angular/commit/6c92d653493404a5f13aa59cde390bcbed973fb6) | fix | add `hasValue` narrowing to `ResourceRef` ([#59708](https://github.com/angular/angular/pull/59708)) |
|
||||
| [96e602ebe9c](https://github.com/angular/angular/commit/96e602ebe9cdf7355befad22c11f9f91e0436e01) | fix | cancel in-progress request when same value is assigned ([#59280](https://github.com/angular/angular/pull/59280)) |
|
||||
| [cf9054248d1](https://github.com/angular/angular/commit/cf9054248d1b8b5658c5f8b582cea71535f081c2) | fix | check whether application is destroyed before printing hydration stats ([#59716](https://github.com/angular/angular/pull/59716)) |
|
||||
| [82876242e55](https://github.com/angular/angular/commit/82876242e557abbced793cff06c4d68c4721e6d2) | fix | ensure type is preserved during HMR ([#59700](https://github.com/angular/angular/pull/59700)) |
|
||||
| [127fc0dc847](https://github.com/angular/angular/commit/127fc0dc847a4e8b62be36cdd980a067c4da974f) | fix | fix `resource()`'s `previous.state` ([#59708](https://github.com/angular/angular/pull/59708)) |
|
||||
| [f862ace1819](https://github.com/angular/angular/commit/f862ace18191d7fda3b12fc3c6486c035a6b431d) | fix | fixes test timer-based test flakiness in CI ([#59674](https://github.com/angular/angular/pull/59674)) |
|
||||
### elements
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0c1ecb453c8](https://github.com/angular/angular/commit/0c1ecb453c8b01447fb5a9f38fdfe9936a60abe5) | fix | not setting initial value on signal-based input ([#59773](https://github.com/angular/angular/pull/59773)) |
|
||||
### platform-browser
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e6cb411e439](https://github.com/angular/angular/commit/e6cb411e4393a4b1f5852d3d7c5b9622504399b1) | fix | automatically disable animations on the server ([#59762](https://github.com/angular/angular/pull/59762)) |
|
||||
| [6b09716754b](https://github.com/angular/angular/commit/6b09716754b979c98489a2710eda977e51fe92d0) | fix | prepend `baseHref` to `sourceMappingURL` in CSS content ([#59730](https://github.com/angular/angular/pull/59730)) |
|
||||
| [fd7ee47bf70](https://github.com/angular/angular/commit/fd7ee47bf702029e481ad40362684f0a01ef2ec0) | fix | Update pseudoevent created by createMouseSpecialEvent to populate `_originalEvent` property ([#59690](https://github.com/angular/angular/pull/59690)) |
|
||||
| [65cf061feb4](https://github.com/angular/angular/commit/65cf061feb402e1532cd06d0fda5d2b7749ac421) | fix | Update pseudoevent created by createMouseSpecialEvent to populate `_originalEvent` property ([#59690](https://github.com/angular/angular/pull/59690)) |
|
||||
| [af9d74710c4](https://github.com/angular/angular/commit/af9d74710c460b0aaeaf3226533e9e544025523c) | fix | Update pseudoevent created by createMouseSpecialEvent to populate `_originalEvent` property ([#59690](https://github.com/angular/angular/pull/59690)) |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [fc5d187da5e](https://github.com/angular/angular/commit/fc5d187da5e8895d60caa35b7b59e234998eddf0) | fix | decouple server from animations module ([#59762](https://github.com/angular/angular/pull/59762)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c7b6e1107c6](https://github.com/angular/angular/commit/c7b6e1107c6fcc7ed1dbb7c7b8698b09bfa8f1cc) | fix | prevent error handling when injector is destroyed ([#59457](https://github.com/angular/angular/pull/59457)) |
|
||||
### service-worker
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e2987a1d4ab](https://github.com/angular/angular/commit/e2987a1d4ab09b1abf1f42597c40f2055db116e1) | fix | add missing `rxjs` peer dependency ([#59747](https://github.com/angular/angular/pull/59747)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="19.1.4"></a>
|
||||
# 19.1.4 (2025-01-29)
|
||||
### core
|
||||
@@ -162,23 +149,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="19.2.0-next.0"></a>
|
||||
# 19.2.0-next.0 (2025-01-22)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [fe8a68329b](https://github.com/angular/angular/commit/fe8a68329b50363f914a728579392f3fc68670a6) | feat | support untagged template literals in expressions ([#59230](https://github.com/angular/angular/pull/59230)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [ed705a856a](https://github.com/angular/angular/commit/ed705a856a164f91d33e2173b4f371329d07c472) | feat | detect missing structural directive imports ([#59443](https://github.com/angular/angular/pull/59443)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [bc2ad7bfd3](https://github.com/angular/angular/commit/bc2ad7bfd37a61992b550943de5da0eab2eec98b) | feat | support streaming resources ([#59573](https://github.com/angular/angular/pull/59573)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="19.1.3"></a>
|
||||
# 19.1.3 (2025-01-22)
|
||||
### compiler
|
||||
|
||||
+6
-155
@@ -103,7 +103,7 @@ Before you submit your Pull Request (PR) consider the following guidelines:
|
||||
|
||||
8. Run the full Angular test suite, as described in the [developer documentation][dev-doc], and ensure that all tests pass.
|
||||
|
||||
9. Commit your changes using a descriptive commit message that follows our [commit message conventions](#commit).
|
||||
9. Commit your changes using a descriptive commit message that follows our [commit message conventions][commit-message-guidelines].
|
||||
Adherence to these conventions is necessary because release notes are automatically generated from these messages.
|
||||
|
||||
```shell
|
||||
@@ -145,7 +145,7 @@ That's it! Thank you for your contribution!
|
||||
|
||||
##### Updating the commit message
|
||||
|
||||
A reviewer might often suggest changes to a commit message (for example, to add more context for a change or adhere to our [commit message guidelines](#commit)).
|
||||
A reviewer might often suggest changes to a commit message (for example, to add more context for a change or adhere to our [commit message guidelines][commit-message-guidelines]).
|
||||
In order to update the commit message of the last commit on your branch:
|
||||
|
||||
1. Check out your branch:
|
||||
@@ -210,163 +210,14 @@ To ensure consistency throughout the source code, keep these rules in mind as yo
|
||||
An automated formatter is available, see [DEVELOPER.md](contributing-docs/building-and-testing-angular.md#formatting-your-source-code).
|
||||
|
||||
|
||||
## <a name="commit"></a> Commit Message Format
|
||||
|
||||
*This specification is inspired by and supersedes the [AngularJS commit message format][commit-message-format].*
|
||||
|
||||
We have very precise rules over how our Git commit messages must be formatted.
|
||||
This format leads to **easier to read commit history**.
|
||||
|
||||
Each commit message consists of a **header**, a **body**, and a **footer**.
|
||||
|
||||
|
||||
```
|
||||
<header>
|
||||
<BLANK LINE>
|
||||
<body>
|
||||
<BLANK LINE>
|
||||
<footer>
|
||||
```
|
||||
|
||||
The `header` is mandatory and must conform to the [Commit Message Header](#commit-header) format.
|
||||
|
||||
The `body` is mandatory for all commits except for those of type "docs".
|
||||
When the body is present it must be at least 20 characters long and must conform to the [Commit Message Body](#commit-body) format.
|
||||
|
||||
The `footer` is optional. The [Commit Message Footer](#commit-footer) format describes what the footer is used for and the structure it must have.
|
||||
|
||||
|
||||
#### <a name="commit-header"></a>Commit Message Header
|
||||
## <a name="commit"></a> Commit Message Guidelines
|
||||
|
||||
We have very precise rules over how our Git commit messages must be formatted:
|
||||
```
|
||||
<type>(<scope>): <short summary>
|
||||
│ │ │
|
||||
│ │ └─⫸ Summary in present tense. Not capitalized. No period at the end.
|
||||
│ │
|
||||
│ └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core|
|
||||
│ elements|forms|http|language-service|localize|platform-browser|
|
||||
│ platform-browser-dynamic|platform-server|router|service-worker|
|
||||
│ upgrade|zone.js|packaging|changelog|docs-infra|migrations|
|
||||
│ devtools
|
||||
│
|
||||
└─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test
|
||||
```
|
||||
|
||||
The `<type>` and `<summary>` fields are mandatory, the `(<scope>)` field is optional.
|
||||
|
||||
|
||||
##### Type
|
||||
|
||||
Must be one of the following:
|
||||
|
||||
* **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
|
||||
* **ci**: Changes to our CI configuration files and scripts (examples: Github Actions, SauceLabs)
|
||||
* **docs**: Documentation only changes
|
||||
* **feat**: A new feature
|
||||
* **fix**: A bug fix
|
||||
* **perf**: A code change that improves performance
|
||||
* **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||
* **test**: Adding missing tests or correcting existing tests
|
||||
|
||||
|
||||
##### Scope
|
||||
The scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages).
|
||||
|
||||
The following is the list of supported scopes:
|
||||
|
||||
* `animations`
|
||||
* `bazel`
|
||||
* `benchpress`
|
||||
* `common`
|
||||
* `compiler`
|
||||
* `compiler-cli`
|
||||
* `core`
|
||||
* `elements`
|
||||
* `forms`
|
||||
* `http`
|
||||
* `language-service`
|
||||
* `localize`
|
||||
* `platform-browser`
|
||||
* `platform-browser-dynamic`
|
||||
* `platform-server`
|
||||
* `router`
|
||||
* `service-worker`
|
||||
* `upgrade`
|
||||
* `zone.js`
|
||||
|
||||
There are currently a few exceptions to the "use package name" rule:
|
||||
|
||||
* `packaging`: used for changes that change the npm package layout in all of our packages, e.g. public path changes, package.json changes done to all packages, d.ts file/format changes, changes to bundles, etc.
|
||||
|
||||
* `changelog`: used for updating the release notes in CHANGELOG.md
|
||||
|
||||
* `dev-infra`: used for dev-infra related changes within the directories /scripts and /tools
|
||||
|
||||
* `docs-infra`: used for docs-app (angular.dev) related changes within the /adev directory of the repo
|
||||
|
||||
* `migrations`: used for changes to the `ng update` migrations.
|
||||
|
||||
* `devtools`: used for changes in the [browser extension](./devtools/README.md).
|
||||
|
||||
* none/empty string: useful for `test` and `refactor` changes that are done across all packages (e.g. `test: add missing unit tests`) and for docs changes that are not related to a specific package (e.g. `docs: fix typo in tutorial`).
|
||||
|
||||
|
||||
##### Summary
|
||||
|
||||
Use the summary field to provide a succinct description of the change:
|
||||
|
||||
* use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
* don't capitalize the first letter
|
||||
* no dot (.) at the end
|
||||
|
||||
|
||||
#### <a name="commit-body"></a>Commit Message Body
|
||||
|
||||
Just as in the summary, use the imperative, present tense: "fix" not "fixed" nor "fixes".
|
||||
|
||||
Explain the motivation for the change in the commit message body. This commit message should explain _why_ you are making the change.
|
||||
You can include a comparison of the previous behavior with the new behavior in order to illustrate the impact of the change.
|
||||
|
||||
|
||||
#### <a name="commit-footer"></a>Commit Message Footer
|
||||
|
||||
The footer can contain information about breaking changes and deprecations and is also the place to reference GitHub issues, Jira tickets, and other PRs that this commit closes or is related to.
|
||||
For example:
|
||||
|
||||
```
|
||||
BREAKING CHANGE: <breaking change summary>
|
||||
<BLANK LINE>
|
||||
<breaking change description + migration instructions>
|
||||
<BLANK LINE>
|
||||
<BLANK LINE>
|
||||
Fixes #<issue number>
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
DEPRECATED: <what is deprecated>
|
||||
<BLANK LINE>
|
||||
<deprecation description + recommended update path>
|
||||
<BLANK LINE>
|
||||
<BLANK LINE>
|
||||
Closes #<pr number>
|
||||
```
|
||||
|
||||
Breaking Change section should start with the phrase `BREAKING CHANGE: ` followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
|
||||
|
||||
Similarly, a Deprecation section should start with `DEPRECATED: ` followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path.
|
||||
|
||||
|
||||
### Revert commits
|
||||
|
||||
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit.
|
||||
|
||||
The content of the commit message body should contain:
|
||||
|
||||
- information about the SHA of the commit being reverted in the following format: `This reverts commit <SHA>`,
|
||||
- a clear description of the reason for reverting the commit message.
|
||||
|
||||
See [Commit Message Guidelines][commit-message-guidelines] for details.
|
||||
|
||||
## <a name="cla"></a> Signing the CLA
|
||||
|
||||
@@ -390,9 +241,9 @@ The following documents can help you sort out issues with GitHub accounts and mu
|
||||
|
||||
|
||||
[coc]: https://github.com/angular/code-of-conduct/blob/main/CODE_OF_CONDUCT.md
|
||||
[commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#
|
||||
[corporate-cla]: https://cla.developers.google.com/about/google-corporate
|
||||
[dev-doc]: ./contributing-docs/building-and-testing-angular.md
|
||||
[commit-message-guidelines]: ./contributing-docs/building-and-testing-angular.md
|
||||
[github]: https://github.com/angular/angular
|
||||
[discord]: https://discord.gg/angular
|
||||
[individual-cla]: https://cla.developers.google.com/about/google-individual
|
||||
|
||||
@@ -143,10 +143,10 @@ cldr_xml_data_repository(
|
||||
# sass rules
|
||||
http_archive(
|
||||
name = "io_bazel_rules_sass",
|
||||
sha256 = "54bca211ea0a4de2c740a6e24b9d11225942a95452799b6a64dba36e082b7249",
|
||||
strip_prefix = "rules_sass-c01e8848f30f8e4672babcbe41c3ac3551f3a800",
|
||||
sha256 = "e8b863f6be5609c6ed15664d12512ee36d93e8bcb2c1a331f85d9e8758a82ee7",
|
||||
strip_prefix = "rules_sass-3b3667fd5861b06a03bea1f1946b55ac7100d4ea",
|
||||
urls = [
|
||||
"https://github.com/bazelbuild/rules_sass/archive/c01e8848f30f8e4672babcbe41c3ac3551f3a800.zip",
|
||||
"https://github.com/bazelbuild/rules_sass/archive/3b3667fd5861b06a03bea1f1946b55ac7100d4ea.zip",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
@@ -23,6 +23,7 @@ ng_module(
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
"//packages/router",
|
||||
"@npm//@angular/material",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -7,8 +7,15 @@
|
||||
<li class="docs-faceted-list-item">
|
||||
@if (item.path) {
|
||||
@if (item.isExternal) {
|
||||
<a [href]="item.path" target="_blank">
|
||||
<span [class.docs-external-link]="item.isExternal">{{ item.label }}</span>
|
||||
<a
|
||||
[href]="item.path" target="_blank"
|
||||
[matTooltip]="item.label"
|
||||
[matTooltipDisabled]="item.label.length < 27"
|
||||
matTooltipPosition="after"
|
||||
[attr.aria-label]="item.label"
|
||||
[matTooltipClass]="'API-tooltip'"
|
||||
>
|
||||
<span [class.docs-external-link]="item.isExternal" class="docs-faceted-list-item-text">{{ item.label }}</span>
|
||||
@if (item.children && item.level! > 1 && !item.isExpanded) {
|
||||
<docs-icon>chevron_right</docs-icon>
|
||||
}
|
||||
@@ -25,8 +32,13 @@
|
||||
}"
|
||||
routerLinkActive="docs-faceted-list-item-active"
|
||||
(click)="emitClickOnLink()"
|
||||
[matTooltip]="item.label"
|
||||
[matTooltipDisabled]="item.label.length < 27"
|
||||
matTooltipPosition="after"
|
||||
[attr.aria-label]="item.label"
|
||||
[matTooltipClass]="'API-tooltip'"
|
||||
>
|
||||
<span>{{item.label}}</span>
|
||||
<span class="docs-faceted-list-item-text">{{item.label}}</span>
|
||||
@if (item.children && !item.isExpanded) {
|
||||
<docs-icon>chevron_right</docs-icon>
|
||||
}
|
||||
@@ -35,8 +47,14 @@
|
||||
} @else {
|
||||
<!-- Nav Section Header -->
|
||||
@if (item.level !== collapsableLevel() && item.level !== expandableLevel()) {
|
||||
<div class="docs-secondary-nav-header">
|
||||
<span>{{item.label}}</span>
|
||||
<div class="docs-secondary-nav-header"
|
||||
[matTooltip]="item.label"
|
||||
[matTooltipDisabled]="item.label.length < 27"
|
||||
matTooltipPosition="after"
|
||||
[attr.aria-label]="item.label"
|
||||
[matTooltipClass]="'API-tooltip'"
|
||||
>
|
||||
<span class="docs-faceted-list-item-text">{{item.label}}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -54,13 +72,17 @@
|
||||
[class.docs-expanded-button]="item.children && item.level == collapsableLevel()"
|
||||
[class.docs-not-expanded-button]="item.children && item.level === expandableLevel()"
|
||||
[class.docs-nav-item-has-icon]="
|
||||
item.children && item.level === expandableLevel() && !item.isExpanded
|
||||
"
|
||||
item.children && item.level === expandableLevel() && !item.isExpanded"
|
||||
[matTooltip]="item.label"
|
||||
[matTooltipDisabled]="item.label.length < 27"
|
||||
matTooltipPosition="after"
|
||||
[attr.aria-label]="item.label"
|
||||
[matTooltipClass]="'API-tooltip'"
|
||||
>
|
||||
@if (item.children && item.level === collapsableLevel()) {
|
||||
<docs-icon>arrow_back</docs-icon>
|
||||
}
|
||||
<span>{{ item.label }}</span>
|
||||
<span class="docs-faceted-list-item-text">{{ item.label }}</span>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,12 @@
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.docs-faceted-list-item-text {
|
||||
max-width: calc(100% - 1rem);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.docs-nav-item-has-icon {
|
||||
&::after {
|
||||
// FIXME: for some reason this disappears when transformed
|
||||
@@ -115,7 +121,7 @@ a,
|
||||
|
||||
// Add padding-bottom to last item in the list
|
||||
.docs-navigation-list {
|
||||
width: 100%;
|
||||
width: 15rem;
|
||||
|
||||
li:last-of-type {
|
||||
ul:last-of-type {
|
||||
@@ -144,3 +150,12 @@ a,
|
||||
margin-inline-end: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .API-tooltip{
|
||||
.mat-mdc-tooltip-surface{
|
||||
max-width: unset;
|
||||
width:auto;
|
||||
word-break: keep-all;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,18 @@ import {RouterLink, RouterLinkActive} from '@angular/router';
|
||||
import {IconComponent} from '../icon/icon.component';
|
||||
import {IsActiveNavigationItem} from '../../pipes/is-active-navigation-item.pipe';
|
||||
import {NgTemplateOutlet} from '@angular/common';
|
||||
import {MatTooltipModule} from '@angular/material/tooltip';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-navigation-list',
|
||||
imports: [RouterLink, RouterLinkActive, IconComponent, IsActiveNavigationItem, NgTemplateOutlet],
|
||||
imports: [
|
||||
RouterLink,
|
||||
RouterLinkActive,
|
||||
IconComponent,
|
||||
IsActiveNavigationItem,
|
||||
NgTemplateOutlet,
|
||||
MatTooltipModule,
|
||||
],
|
||||
templateUrl: './navigation-list.component.html',
|
||||
styleUrls: ['./navigation-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -2,6 +2,8 @@ dialog {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding-block-end: 3rem;
|
||||
margin: 0 auto;
|
||||
top: 15vh;
|
||||
|
||||
&::backdrop {
|
||||
backdrop-filter: blur(5px);
|
||||
|
||||
@@ -121,7 +121,7 @@ describe('DocViewer', () => {
|
||||
expect(checkIcon.nativeElement.innerHTML).toBe('check');
|
||||
});
|
||||
|
||||
it('should display example viewer in multi file mode when user clicks expand', async () => {
|
||||
it('should display example viewer in multi file mode when provided example is multi file snippet', async () => {
|
||||
const fixture = TestBed.createComponent(DocViewer);
|
||||
fixture.componentRef.setInput(
|
||||
'docContent',
|
||||
@@ -131,10 +131,6 @@ describe('DocViewer', () => {
|
||||
await fixture.whenStable();
|
||||
|
||||
const exampleViewer = fixture.debugElement.query(By.directive(ExampleViewer));
|
||||
const expandButton = fixture.debugElement.query(
|
||||
By.css('button[aria-label="Expand code example"]'),
|
||||
);
|
||||
expandButton.nativeElement.click();
|
||||
|
||||
expect(exampleViewer).not.toBeNull();
|
||||
expect(exampleViewer.componentInstance.view()).toBe(CodeExampleViewMode.MULTI_FILE);
|
||||
|
||||
@@ -196,13 +196,13 @@ export class DocViewer implements OnChanges {
|
||||
const exampleRef = this.viewContainer.createComponent(ExampleViewer);
|
||||
|
||||
this.countOfExamples++;
|
||||
exampleRef.instance.metadata = {
|
||||
exampleRef.setInput('metadata', {
|
||||
title: title ?? firstCodeSnippetTitle,
|
||||
path,
|
||||
files: snippets,
|
||||
preview,
|
||||
id: this.countOfExamples,
|
||||
};
|
||||
});
|
||||
|
||||
exampleRef.instance.githubUrl = `${GITHUB_CONTENT_URL}/${snippets[0].name}`;
|
||||
exampleRef.instance.stackblitzUrl = `${ASSETS_EXAMPLES_PATH}/${snippets[0].name}.html`;
|
||||
@@ -293,9 +293,6 @@ export class DocViewer implements OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger change detection after setting inputs.
|
||||
componentRef.changeDetectorRef.detectChanges();
|
||||
|
||||
// Attach a view to the ApplicationRef for change detection
|
||||
// purposes and for hydration serialization to pick it up
|
||||
// during SSG.
|
||||
|
||||
+42
-9
@@ -100,26 +100,59 @@ describe('ExampleViewer', () => {
|
||||
expect(component.tabs()![2].name).toBe('another-example.ts');
|
||||
});
|
||||
|
||||
it('should expandable be false when none of the example files have defined visibleLinesRange ', waitForAsync(async () => {
|
||||
it('should expand button not appear when there is no hidden line', waitForAsync(async () => {
|
||||
component.metadata = getMetadata();
|
||||
await component.renderExample();
|
||||
expect(component.expandable()).toBeFalse();
|
||||
const button = fixture.debugElement.query(By.css('button[aria-label="Expand code example"]'));
|
||||
expect(button).toBeNull();
|
||||
}));
|
||||
|
||||
it('should expandable be true when at least one example file has defined visibleLinesRange ', waitForAsync(async () => {
|
||||
it('should have line with hidden line class when expand button is present', waitForAsync(async () => {
|
||||
const expectedCodeSnippetContent =
|
||||
'typescript code<br/>' + '<div class="line">hidden line</div>';
|
||||
|
||||
component.metadata = getMetadata({
|
||||
files: [
|
||||
{name: 'example.ts', content: 'typescript file'},
|
||||
{
|
||||
name: 'example.html',
|
||||
content: 'html file',
|
||||
visibleLinesRange: '[1, 2]',
|
||||
name: 'example.ts',
|
||||
content: `<pre><code>${expectedCodeSnippetContent}</code></pre>`,
|
||||
visibleLinesRange: '[1]',
|
||||
},
|
||||
{name: 'another-example.ts', content: 'css file'},
|
||||
],
|
||||
});
|
||||
|
||||
await component.renderExample();
|
||||
expect(component.expandable()).toBeTrue();
|
||||
fixture.detectChanges();
|
||||
|
||||
const hiddenLine = fixture.debugElement.query(By.css('div[class="line hidden"]'));
|
||||
expect(hiddenLine).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should have no more line with hidden line class when expand button is clicked', waitForAsync(async () => {
|
||||
const expectedCodeSnippetContent =
|
||||
'typescript code<br/>' + '<div class="line">hidden line</div>';
|
||||
|
||||
component.metadata = getMetadata({
|
||||
files: [
|
||||
{
|
||||
name: 'example.ts',
|
||||
content: `<pre><code>${expectedCodeSnippetContent}</code></pre>`,
|
||||
visibleLinesRange: '[1]',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await component.renderExample();
|
||||
fixture.detectChanges();
|
||||
|
||||
const expandButton = fixture.debugElement.query(
|
||||
By.css('button[aria-label="Expand code example"]'),
|
||||
);
|
||||
expandButton.nativeElement.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
const hiddenLine = fixture.debugElement.query(By.css('div[class="line hidden"]'));
|
||||
expect(hiddenLine).toBeNull();
|
||||
}));
|
||||
|
||||
it('should set exampleComponent when metadata contains path and preview is true', waitForAsync(async () => {
|
||||
|
||||
@@ -7,18 +7,21 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
afterNextRender,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DestroyRef,
|
||||
Input,
|
||||
Type,
|
||||
computed,
|
||||
inject,
|
||||
ChangeDetectorRef,
|
||||
ViewChild,
|
||||
signal,
|
||||
Component,
|
||||
computed,
|
||||
DestroyRef,
|
||||
ElementRef,
|
||||
forwardRef,
|
||||
inject,
|
||||
Injector,
|
||||
input,
|
||||
Input,
|
||||
signal,
|
||||
Type,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import {CommonModule, DOCUMENT} from '@angular/common';
|
||||
import {MatTabGroup, MatTabsModule} from '@angular/material/tabs';
|
||||
@@ -47,10 +50,7 @@ export const HIDDEN_CLASS_NAME = 'hidden';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ExampleViewer {
|
||||
// TODO: replace by signal-based input when it'll be available
|
||||
@Input({required: true}) set metadata(value: ExampleMetadata) {
|
||||
this.exampleMetadata.set(value);
|
||||
}
|
||||
exampleMetadata = input.required<ExampleMetadata>({alias: 'metadata'});
|
||||
|
||||
@Input() githubUrl: string | null = null;
|
||||
@Input() stackblitzUrl: string | null = null;
|
||||
@@ -60,6 +60,7 @@ export class ExampleViewer {
|
||||
private readonly clipboard = inject(Clipboard);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly injector = inject(Injector);
|
||||
private readonly elementRef = inject(ElementRef<HTMLElement>);
|
||||
private readonly exampleViewerContentLoader = inject(EXAMPLE_VIEWER_CONTENT_LOADER);
|
||||
|
||||
@@ -74,8 +75,8 @@ export class ExampleViewer {
|
||||
CodeExampleViewMode = CodeExampleViewMode;
|
||||
exampleComponent?: Type<unknown>;
|
||||
|
||||
expandable = signal<boolean>(false);
|
||||
expanded = signal<boolean>(false);
|
||||
exampleMetadata = signal<ExampleMetadata | null>(null);
|
||||
snippetCode = signal<Snippet | undefined>(undefined);
|
||||
tabs = computed(() =>
|
||||
this.exampleMetadata()?.files.map((file) => ({
|
||||
@@ -89,9 +90,6 @@ export class ExampleViewer {
|
||||
? CodeExampleViewMode.SNIPPET
|
||||
: CodeExampleViewMode.MULTI_FILE,
|
||||
);
|
||||
expandable = computed(() =>
|
||||
this.exampleMetadata()?.files.some((file) => !!file.visibleLinesRange),
|
||||
);
|
||||
|
||||
async renderExample(): Promise<void> {
|
||||
// Lazy load live example component
|
||||
@@ -103,18 +101,27 @@ export class ExampleViewer {
|
||||
|
||||
this.snippetCode.set(this.exampleMetadata()?.files[0]);
|
||||
|
||||
this.changeDetector.detectChanges();
|
||||
afterNextRender(
|
||||
() => {
|
||||
// Several function below query the DOM directly, we need to wait until the DOM is rendered.
|
||||
this.setCodeLinesVisibility();
|
||||
|
||||
this.setCodeLinesVisibility();
|
||||
this.elementRef.nativeElement.setAttribute(
|
||||
'id',
|
||||
`example-${this.exampleMetadata()?.id.toString()!}`,
|
||||
);
|
||||
|
||||
this.elementRef.nativeElement.setAttribute(
|
||||
'id',
|
||||
`example-${this.exampleMetadata()?.id.toString()!}`,
|
||||
this.matTabGroup?.realignInkBar();
|
||||
|
||||
this.listenToMatTabIndexChange();
|
||||
|
||||
const lines = this.getHiddenCodeLines();
|
||||
const lineNumbers = this.getHiddenCodeLineNumbers();
|
||||
|
||||
this.expandable.set(lines.length > 0 || lineNumbers.length > 0);
|
||||
},
|
||||
{injector: this.injector},
|
||||
);
|
||||
|
||||
this.matTabGroup?.realignInkBar();
|
||||
|
||||
this.listenToMatTabIndexChange();
|
||||
}
|
||||
|
||||
toggleExampleVisibility(): void {
|
||||
@@ -157,21 +164,9 @@ export class ExampleViewer {
|
||||
}
|
||||
|
||||
private handleExpandedStateForCodeBlock(): void {
|
||||
const lines = <HTMLDivElement[]>(
|
||||
Array.from(
|
||||
this.elementRef.nativeElement.querySelectorAll(
|
||||
`.${CODE_LINE_CLASS_NAME}.${HIDDEN_CLASS_NAME}`,
|
||||
),
|
||||
)
|
||||
);
|
||||
const lines = this.getHiddenCodeLines();
|
||||
|
||||
const lineNumbers = <HTMLSpanElement[]>(
|
||||
Array.from(
|
||||
this.elementRef.nativeElement.querySelectorAll(
|
||||
`.${CODE_LINE_NUMBER_CLASS_NAME}.${HIDDEN_CLASS_NAME}`,
|
||||
),
|
||||
)
|
||||
);
|
||||
const lineNumbers = this.getHiddenCodeLineNumbers();
|
||||
|
||||
const gapLines = <HTMLDivElement[]>(
|
||||
Array.from(
|
||||
@@ -239,4 +234,24 @@ export class ExampleViewer {
|
||||
element.parentNode?.insertBefore(separator, element);
|
||||
}
|
||||
}
|
||||
|
||||
private getHiddenCodeLines(): HTMLDivElement[] {
|
||||
return <HTMLDivElement[]>(
|
||||
Array.from(
|
||||
this.elementRef.nativeElement.querySelectorAll(
|
||||
`.${CODE_LINE_CLASS_NAME}.${HIDDEN_CLASS_NAME}`,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private getHiddenCodeLineNumbers(): HTMLSpanElement[] {
|
||||
return <HTMLSpanElement[]>(
|
||||
Array.from(
|
||||
this.elementRef.nativeElement.querySelectorAll(
|
||||
`.${CODE_LINE_NUMBER_CLASS_NAME}.${HIDDEN_CLASS_NAME}`,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import {Renderer, Tokens} from 'marked';
|
||||
import {codeToHtml} from '../shiki/shiki';
|
||||
import {SECTION_HEADING, SECTION_SUB_HEADING} from '../styling/css-classes';
|
||||
|
||||
/**
|
||||
* Custom renderer for marked that will be used to transform markdown files to HTML
|
||||
@@ -55,21 +56,39 @@ export const renderer: Partial<Renderer> = {
|
||||
<div class="docs-table docs-scroll-track-transparent">
|
||||
<table>
|
||||
<thead>
|
||||
${this.tablerow({
|
||||
text: header.map((cell) => this.tablecell(cell)).join(''),
|
||||
})}
|
||||
${this.tablerow({text: header.map((cell) => this.tablecell(cell)).join('')})}
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows
|
||||
.map((row) =>
|
||||
this.tablerow({
|
||||
text: row.map((cell) => this.tablecell(cell)).join(''),
|
||||
}),
|
||||
)
|
||||
.map((row) => this.tablerow({text: row.map((cell) => this.tablecell(cell)).join('')}))
|
||||
.join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
heading(this: Renderer, {text, depth, tokens}: Tokens.Heading) {
|
||||
const id = text
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-')
|
||||
.replace(/[^a-z0-9-]/g, '');
|
||||
|
||||
// Since we have a code transformer `addApiLinksToHtml` which adds anchors
|
||||
// to code blocks of known symbols, we add an additional `data-skip-anchor`
|
||||
// attribute that prevents the transformation. This is needed since nested
|
||||
// anchor tags are illegal and break the HTML.
|
||||
const textRenderer = new Renderer();
|
||||
textRenderer.codespan = ({text}) => `<code data-skip-anchor>${text}</code>`;
|
||||
const parsedText = this.parser.parseInline(tokens, textRenderer);
|
||||
|
||||
// The template matches templates/section-heading.tsx
|
||||
return `
|
||||
<h${depth} id="${id}" class="${SECTION_HEADING} ${SECTION_SUB_HEADING}">
|
||||
<a href="#${id}" aria-label="Link to ${text} section" tabIndex="-1">
|
||||
${parsedText}
|
||||
</a>
|
||||
</h${depth}>
|
||||
`;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -27,3 +27,4 @@ export const HEADER_ENTRY_LABEL = 'docs-api-item-label';
|
||||
|
||||
export const SECTION_CONTAINER = 'docs-reference-section';
|
||||
export const SECTION_HEADING = 'docs-reference-section-heading';
|
||||
export const SECTION_SUB_HEADING = 'docs-reference-section-heading--sub';
|
||||
|
||||
@@ -38,8 +38,6 @@ export function getCurrentSymbol(): string | undefined {
|
||||
return currentSymbol;
|
||||
}
|
||||
|
||||
export function logUnknownSymbol(link: string, symbol: string): void {
|
||||
console.warn(
|
||||
`WARNING: {@link ${link}} is invalid, ${symbol} or ${currentSymbol}.${symbol} is unknown in this context`,
|
||||
);
|
||||
export function unknownSymbolMessage(link: string, symbol: string): string {
|
||||
return `WARNING: {@link ${link}} is invalid, ${symbol} or ${currentSymbol}.${symbol} is unknown in this context`;
|
||||
}
|
||||
|
||||
@@ -25,9 +25,7 @@ export function HeaderApi(props: {entry: DocEntryRenderable; showFullDescription
|
||||
// When ADEV is not deployed on the main branch branch anymore,
|
||||
// We should update it to point to the tag of the released version which ADEV runs on.
|
||||
|
||||
const sourceUrl = isDocEntryWithSourceInfo(entry)
|
||||
? `https://github.com/angular/angular/blob/main${entry.source.filePath}#L${entry.source.startLine}-L${entry.source.endLine}`
|
||||
: null;
|
||||
const sourceUrl = sourceUrlForEntry(entry);
|
||||
|
||||
return (
|
||||
<header className={HEADER_CLASS_NAME}>
|
||||
@@ -40,9 +38,7 @@ export function HeaderApi(props: {entry: DocEntryRenderable; showFullDescription
|
||||
{getEntryTypeDisplayName(entry.entryType)}
|
||||
</div>
|
||||
{entry.isDeprecated && (
|
||||
<div className={`${HEADER_ENTRY_LABEL} type-deprecated full`}>
|
||||
Deprecated
|
||||
</div>
|
||||
<div className={`${HEADER_ENTRY_LABEL} type-deprecated full`}>Deprecated</div>
|
||||
)}
|
||||
{entry.isDeveloperPreview && (
|
||||
<div className={`${HEADER_ENTRY_LABEL} type-developer_preview full`}>
|
||||
@@ -95,3 +91,16 @@ function getEntryTypeDisplayName(entryType: EntryType | string): string {
|
||||
}
|
||||
return entryType;
|
||||
}
|
||||
|
||||
function sourceUrlForEntry(entry: DocEntryRenderable): string | null {
|
||||
if (!isDocEntryWithSourceInfo(entry)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (entry.source.filePath.includes('node_modules')) {
|
||||
// We don't know the source path in external repos link the CLI
|
||||
return null;
|
||||
} else {
|
||||
return `https://github.com/angular/angular/blob/main${entry.source.filePath}#L${entry.source.startLine}-L${entry.source.endLine}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,13 @@ describe('markdown to html', () => {
|
||||
const symbols = new Map<string, string>([
|
||||
['AfterRenderPhase', 'core'],
|
||||
['afterRender', 'core'],
|
||||
['EmbeddedViewRef', 'core'],
|
||||
['ChangeDetectionStrategy', 'core'],
|
||||
['ChangeDetectorRef', 'core'],
|
||||
['withNoHttpTransferCache', 'platform-browser'],
|
||||
['withHttpTransferCacheOptions', 'platform-browser'],
|
||||
['withI18nSupport', 'platform-browser'],
|
||||
['withEventReplay', 'platform-browser'],
|
||||
]);
|
||||
setSymbols(symbols);
|
||||
for (const entry of entryJson.entries) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {getRenderable} from '../processing';
|
||||
import {DocEntryRenderable} from '../entities/renderables';
|
||||
import {initHighlighter} from '../shiki/shiki';
|
||||
import {configureMarkedGlobally} from '../marked/configuration';
|
||||
import {setSymbols} from '../symbol-context';
|
||||
|
||||
// Note: The tests will probably break if the schema of the api extraction changes.
|
||||
// All entries in the fake-entries are extracted from Angular's api.
|
||||
@@ -28,6 +29,19 @@ describe('renderable', () => {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const entryJson = JSON.parse(entryContent) as any;
|
||||
const symbols = new Map<string, string>([
|
||||
['AfterRenderPhase', 'core'],
|
||||
['afterRender', 'core'],
|
||||
['EmbeddedViewRef', 'core'],
|
||||
['ChangeDetectionStrategy', 'core'],
|
||||
['ChangeDetectorRef', 'core'],
|
||||
['withNoHttpTransferCache', 'platform-browser'],
|
||||
['withHttpTransferCacheOptions', 'platform-browser'],
|
||||
['withI18nSupport', 'platform-browser'],
|
||||
['withEventReplay', 'platform-browser'],
|
||||
]);
|
||||
setSymbols(symbols);
|
||||
|
||||
for (const entry of entryJson.entries) {
|
||||
const renderableJson = getRenderable(entry, '@angular/fakeentry') as DocEntryRenderable;
|
||||
entries.set(entry['name'], renderableJson);
|
||||
|
||||
@@ -66,6 +66,14 @@ describe('jsdoc transforms', () => {
|
||||
name: 'see',
|
||||
comment: '{@link https://angular.dev}',
|
||||
},
|
||||
{
|
||||
name: 'see',
|
||||
comment: '{@link /cli/build ng build}',
|
||||
},
|
||||
{
|
||||
name: 'see',
|
||||
comment: '{@link /ecosystem/rxjs-interop/output-interop Output Interop}',
|
||||
},
|
||||
],
|
||||
moduleName: 'test',
|
||||
});
|
||||
@@ -117,5 +125,30 @@ describe('jsdoc transforms', () => {
|
||||
label: 'angular.dev',
|
||||
url: 'https://angular.dev',
|
||||
});
|
||||
|
||||
expect(entry.additionalLinks[10]).toEqual({
|
||||
label: 'ng build',
|
||||
url: '/cli/build',
|
||||
});
|
||||
|
||||
expect(entry.additionalLinks[11]).toEqual({
|
||||
label: 'Output Interop',
|
||||
url: '/ecosystem/rxjs-interop/output-interop',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw on invalid relatie @link', () => {
|
||||
const entryFn = () =>
|
||||
addHtmlAdditionalLinks({
|
||||
jsdocTags: [
|
||||
{
|
||||
name: 'see',
|
||||
comment: '{@link cli/build ng build}',
|
||||
},
|
||||
],
|
||||
moduleName: 'test',
|
||||
});
|
||||
|
||||
expect(entryFn).toThrowError(/Forbidden relative link: cli\/build ng build/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -476,10 +476,11 @@ function appendPrefixAndSuffix(entry: DocEntry, codeTocData: CodeTableOfContents
|
||||
*/
|
||||
export function addApiLinksToHtml(htmlString: string): string {
|
||||
const result = htmlString.replace(
|
||||
// This regex looks for span/code blocks not wrapped by an anchor block.
|
||||
// This regex looks for span/code blocks not wrapped by an anchor block
|
||||
// or the tag doesn't contain `data-skip-anchor` attribute.
|
||||
// Their content are then replaced with a link if the symbol is known
|
||||
// The captured content ==> vvvvvvvv
|
||||
/(?<!<a[^>]*>)(<(?:(?:span)|(?:code))[^>]*>\s*)([^<]*?)(\s*<\/(?:span|code)>)/g,
|
||||
// The captured content ==> vvvvvvvv
|
||||
/(?<!<a[^>]*>)(<(?:(?:span)|(?:code))(?!\sdata-skip-anchor)[^>]*>\s*)([^<]*?)(\s*<\/(?:span|code)>)/g,
|
||||
(type: string, span1: string, potentialSymbolName: string, span2: string) => {
|
||||
let [symbol, subSymbol] = potentialSymbolName.split(/(?:#|\.)/) as [string, string?];
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
|
||||
import {getLinkToModule} from './url-transforms';
|
||||
import {addApiLinksToHtml} from './code-transforms';
|
||||
import {getCurrentSymbol, getModuleName, logUnknownSymbol} from '../symbol-context';
|
||||
import {getCurrentSymbol, getModuleName, unknownSymbolMessage} from '../symbol-context';
|
||||
|
||||
export const JS_DOC_REMARKS_TAG = 'remarks';
|
||||
export const JS_DOC_USAGE_NOTES_TAG = 'usageNotes';
|
||||
@@ -58,16 +58,10 @@ export function addHtmlDescription<T extends HasDescription & HasModuleName>(
|
||||
|
||||
const description = !!entry.description ? entry.description : jsDocDescription;
|
||||
const shortTextMatch = description.match(firstParagraphRule);
|
||||
const htmlDescription = getHtmlForJsDocText(description, entry).trim();
|
||||
const shortHtmlDescription = getHtmlForJsDocText(
|
||||
shortTextMatch ? shortTextMatch[0] : '',
|
||||
entry,
|
||||
).trim();
|
||||
return {
|
||||
...entry,
|
||||
htmlDescription,
|
||||
shortHtmlDescription,
|
||||
};
|
||||
const htmlDescription = getHtmlForJsDocText(description).trim();
|
||||
const shortHtmlDescription = getHtmlForJsDocText(shortTextMatch ? shortTextMatch[0] : '').trim();
|
||||
|
||||
return {...entry, htmlDescription, shortHtmlDescription};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,7 +75,7 @@ export function addHtmlJsDocTagComments<T extends HasJsDocTags & HasModuleName>(
|
||||
...entry,
|
||||
jsdocTags: entry.jsdocTags.map((tag) => ({
|
||||
...tag,
|
||||
htmlComment: getHtmlForJsDocText(tag.comment, entry),
|
||||
htmlComment: getHtmlForJsDocText(tag.comment),
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -100,20 +94,16 @@ export function addHtmlUsageNotes<T extends HasJsDocTags>(entry: T): T & HasHtml
|
||||
const usageNotesTag = entry.jsdocTags.find(
|
||||
({name}) => name === JS_DOC_USAGE_NOTES_TAG || name === JS_DOC_REMARKS_TAG,
|
||||
);
|
||||
const htmlUsageNotes = usageNotesTag
|
||||
? (marked.parse(wrapExampleHtmlElementsWithCode(usageNotesTag.comment)) as string)
|
||||
: '';
|
||||
|
||||
const transformedHtml = addApiLinksToHtml(htmlUsageNotes);
|
||||
const htmlUsageNotes = usageNotesTag ? getHtmlForJsDocText(usageNotesTag.comment) : '';
|
||||
|
||||
return {
|
||||
...entry,
|
||||
htmlUsageNotes: transformedHtml,
|
||||
htmlUsageNotes,
|
||||
};
|
||||
}
|
||||
|
||||
/** Given a markdown JsDoc text, gets the rendered HTML. */
|
||||
function getHtmlForJsDocText<T extends HasModuleName>(text: string, entry: T): string {
|
||||
function getHtmlForJsDocText(text: string): string {
|
||||
const parsed = marked.parse(convertLinks(wrapExampleHtmlElementsWithCode(text))) as string;
|
||||
return addApiLinksToHtml(parsed);
|
||||
}
|
||||
@@ -126,7 +116,7 @@ export function setEntryFlags<T extends HasJsDocTags & HasModuleName>(
|
||||
...entry,
|
||||
isDeprecated: isDeprecatedEntry(entry),
|
||||
deprecationMessage: deprecationMessage
|
||||
? getHtmlForJsDocText(deprecationMessage, entry)
|
||||
? getHtmlForJsDocText(deprecationMessage)
|
||||
: deprecationMessage,
|
||||
isDeveloperPreview: isDeveloperPreview(entry),
|
||||
isExperimental: isExperimental(entry),
|
||||
@@ -195,10 +185,16 @@ function parseAtLink(link: string): {label: string; url: string} {
|
||||
let [rawSymbol, description] = link.split(/\s(.+)/);
|
||||
if (rawSymbol.startsWith('#')) {
|
||||
rawSymbol = rawSymbol.substring(1);
|
||||
} else if (rawSymbol.startsWith('http://') || rawSymbol.startsWith('https://')) {
|
||||
} else if (rawSymbol.includes('/')) {
|
||||
if (!rawSymbol.startsWith('/') && !rawSymbol.startsWith('http')) {
|
||||
throw Error(
|
||||
`Forbidden relative link: ${link}. Links should be absolute and start with a slash`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
url: rawSymbol,
|
||||
label: rawSymbol.split('/').pop()!,
|
||||
label: description ?? rawSymbol.split('/').pop()!,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -214,11 +210,9 @@ function parseAtLink(link: string): {label: string; url: string} {
|
||||
moduleName = getModuleName(`${currentSymbol}.${symbol}`);
|
||||
|
||||
if (!moduleName || !currentSymbol) {
|
||||
// TODO: remove the links that generate this error
|
||||
// TODO: throw an error when there are no more warning generated
|
||||
logUnknownSymbol(link, symbol);
|
||||
return {label, url: '#'};
|
||||
throw unknownSymbolMessage(link, symbol);
|
||||
}
|
||||
|
||||
subSymbol = symbol;
|
||||
symbol = currentSymbol;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": "dist/example-app",
|
||||
"index": "src/index.html",
|
||||
@@ -66,7 +66,7 @@
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "example-app:build:production"
|
||||
@@ -78,13 +78,13 @@
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"builder": "@angular/build:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "example-app:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"builder": "@angular/build:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": ["zone.js", "zone.js/testing"],
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"zone.js": "~0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.0.0",
|
||||
"@angular/build": "^19.0.0",
|
||||
"@angular/cli": "^19.0.0",
|
||||
"@angular/compiler-cli": "^19.0.0",
|
||||
"@types/jasmine": "~3.10.0",
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": "dist/first-app",
|
||||
"index": "src/index.html",
|
||||
@@ -76,7 +76,7 @@
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "first-app:build:production"
|
||||
@@ -88,7 +88,7 @@
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"builder": "@angular/build:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "first-app:build"
|
||||
}
|
||||
|
||||
+3821
-9261
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "NG_BUILD_PARALLEL_TS=0 ng serve",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development"
|
||||
},
|
||||
@@ -15,12 +15,12 @@
|
||||
"@angular/platform-browser": "^19.0.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.0"
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.0.0",
|
||||
"@angular/build": "^19.0.0",
|
||||
"@angular/cli": "^19.0.0",
|
||||
"@angular/compiler-cli": "^19.0.0",
|
||||
"typescript": "~5.6.3"
|
||||
"typescript": "~5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Injectable, inject, signal} from '@angular/core';
|
||||
import {Injectable, inject, linkedSignal, signal} from '@angular/core';
|
||||
import {NavigationItem} from '../interfaces/index';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
@@ -19,11 +19,13 @@ export class NavigationState {
|
||||
private readonly _activeNavigationItem = signal<NavigationItem | null>(null);
|
||||
private readonly _expandedItems = signal<NavigationItem[]>([]);
|
||||
private readonly _isMobileNavVisible = signal<boolean>(false);
|
||||
private readonly _level = linkedSignal(() => this._expandedItems().length);
|
||||
|
||||
primaryActiveRouteItem = signal<string | null>(null);
|
||||
activeNavigationItem = this._activeNavigationItem.asReadonly();
|
||||
expandedItems = this._expandedItems.asReadonly();
|
||||
isMobileNavVisible = this._isMobileNavVisible.asReadonly();
|
||||
level = this._level.asReadonly();
|
||||
|
||||
async toggleItem(item: NavigationItem): Promise<void> {
|
||||
if (!item.children) {
|
||||
@@ -59,7 +61,7 @@ export class NavigationState {
|
||||
return;
|
||||
}
|
||||
// Returns item when parent node was already expanded
|
||||
const parentItem = this._expandedItems().find(
|
||||
const parentItem = this.actualExpandedItems().find(
|
||||
(expandedItem) =>
|
||||
item.parent?.label === expandedItem.label && item.parent?.path === expandedItem.path,
|
||||
);
|
||||
@@ -100,13 +102,25 @@ export class NavigationState {
|
||||
private expand(item: NavigationItem): void {
|
||||
// Add item to the expanded items list
|
||||
this._expandedItems.update((expandedItems) => {
|
||||
return [...(expandedItems ?? []), {...item, isExpanded: true}];
|
||||
return [...(this.actualExpandedItems() ?? []), {...item, isExpanded: true}];
|
||||
});
|
||||
|
||||
// No need to update the level here, this is handled by linkedSignal already
|
||||
}
|
||||
|
||||
private collapse(item: NavigationItem): void {
|
||||
item.isExpanded = false;
|
||||
this._expandedItems.update((expandedItems) => expandedItems.slice(0, -1));
|
||||
|
||||
// We won't remove the item, just update the level,
|
||||
// this allows animation on the items to hide them without destroying them
|
||||
this._level.set(this.actualExpandedItems().length - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* return the actual navigation items, that is to say the one that match the current level
|
||||
*/
|
||||
private actualExpandedItems() {
|
||||
return this.expandedItems().slice(0, this.level());
|
||||
}
|
||||
|
||||
private async navigateToFirstPageOfTheCategory(path: string): Promise<boolean> {
|
||||
|
||||
@@ -66,7 +66,11 @@
|
||||
.docs-reference-section-heading {
|
||||
padding-block-start: 3rem;
|
||||
|
||||
a {
|
||||
&--sub {
|
||||
padding-block-start: 1rem;
|
||||
}
|
||||
|
||||
& > a {
|
||||
@include anchor.docs-anchor();
|
||||
color: inherit;
|
||||
}
|
||||
@@ -98,7 +102,7 @@
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
&.docs-highlighted-card {
|
||||
box-shadow: 10px 4px 40px 0 rgba(0, 0, 0, 0.01);
|
||||
|
||||
&::before {
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
}
|
||||
|
||||
p > a,
|
||||
p > em > a,
|
||||
td > a,
|
||||
div > a:not(.docs-card),
|
||||
code > a,
|
||||
@@ -93,6 +94,7 @@
|
||||
}
|
||||
|
||||
p > a,
|
||||
p > em > a,
|
||||
.docs-list a,
|
||||
.docs-card a {
|
||||
margin-block: 0;
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
fill: var(--page-background) !important;
|
||||
}
|
||||
|
||||
.nodeLabel:not(.node:has(polygon) .nodeLabel) {
|
||||
.nodeLabel:not(.node:has(polygon) .nodeLabel):not(.eventNode .nodeLabel) {
|
||||
fill: var(--primary-contrast) !important;
|
||||
color: var(--primary-contrast) !important;
|
||||
}
|
||||
@@ -71,7 +71,6 @@
|
||||
}
|
||||
|
||||
.nodeLabel p {
|
||||
color: var(--page-background) !important;
|
||||
font-weight: 800 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {AppComponent} from './app.component';
|
||||
import {provideRouter} from '@angular/router';
|
||||
import {provideRouter, withComponentInputBinding} from '@angular/router';
|
||||
import {routes} from './routes';
|
||||
import {Search, WINDOW} from '@angular/docs';
|
||||
import {CURRENT_MAJOR_VERSION} from './core/providers/current-version';
|
||||
@@ -21,7 +21,7 @@ describe('AppComponent', () => {
|
||||
it('should create the app', () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideRouter(routes),
|
||||
provideRouter(routes, withComponentInputBinding()),
|
||||
{
|
||||
provide: WINDOW,
|
||||
useValue: fakeWindow,
|
||||
|
||||
@@ -13,11 +13,11 @@ import {
|
||||
inject,
|
||||
OnInit,
|
||||
PLATFORM_ID,
|
||||
input,
|
||||
signal,
|
||||
WritableSignal,
|
||||
} from '@angular/core';
|
||||
import {NavigationEnd, NavigationSkipped, Router, RouterOutlet} from '@angular/router';
|
||||
import {filter, map, skip} from 'rxjs/operators';
|
||||
import {filter, map} from 'rxjs/operators';
|
||||
import {
|
||||
CookiePopup,
|
||||
getActivatedRouteSnapshotFromRouter,
|
||||
@@ -56,13 +56,12 @@ export class AppComponent implements OnInit {
|
||||
private readonly router = inject(Router);
|
||||
private readonly headerService = inject(HeaderService);
|
||||
|
||||
currentUrl = signal('');
|
||||
displayFooter = signal(false);
|
||||
displaySecondaryNav = signal(false);
|
||||
displaySearchDialog: WritableSignal<boolean> = inject(IS_SEARCH_DIALOG_OPEN);
|
||||
|
||||
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
||||
|
||||
displaySecondaryNav = signal(false);
|
||||
displayFooter = signal(false);
|
||||
displaySearchDialog = inject(IS_SEARCH_DIALOG_OPEN);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.closeSearchDialogOnNavigationSkipped();
|
||||
this.router.events
|
||||
@@ -71,35 +70,24 @@ export class AppComponent implements OnInit {
|
||||
map((event) => event.urlAfterRedirects),
|
||||
)
|
||||
.subscribe((url) => {
|
||||
this.currentUrl.set(url);
|
||||
this.setComponentsVisibility();
|
||||
this.displaySearchDialog.set(false);
|
||||
// We can't use an input binded to the route here
|
||||
// because AppComponent itself is not a routed component
|
||||
// so we access it via the snapshot
|
||||
const activatedRoute = getActivatedRouteSnapshotFromRouter(this.router);
|
||||
this.displayFooter.set(!activatedRoute.data['hideFooter']);
|
||||
this.displaySecondaryNav.set(activatedRoute.data['displaySecondaryNav']);
|
||||
|
||||
this.displaySearchDialog.set(false);
|
||||
this.updateCanonicalLink(url);
|
||||
});
|
||||
}
|
||||
|
||||
focusFirstHeading(): void {
|
||||
if (!this.isBrowser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const h1 = this.document.querySelector<HTMLHeadingElement>('h1:not(docs-top-level-banner h1)');
|
||||
h1?.focus();
|
||||
}
|
||||
|
||||
private updateCanonicalLink(absoluteUrl: string) {
|
||||
this.headerService.setCanonical(absoluteUrl);
|
||||
}
|
||||
|
||||
private setComponentsVisibility(): void {
|
||||
const activatedRoute = getActivatedRouteSnapshotFromRouter(this.router as any);
|
||||
|
||||
this.displaySecondaryNav.set(activatedRoute.data['displaySecondaryNav']);
|
||||
this.displayFooter.set(!activatedRoute.data['hideFooter']);
|
||||
}
|
||||
|
||||
private setSearchDialogVisibilityOnKeyPress(event: KeyboardEvent): void {
|
||||
protected setSearchDialogVisibilityOnKeyPress(event: KeyboardEvent): void {
|
||||
if (event.key === SEARCH_TRIGGER_KEY && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
this.displaySearchDialog.update((display) => !display);
|
||||
@@ -111,6 +99,10 @@ export class AppComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
private updateCanonicalLink(absoluteUrl: string) {
|
||||
this.headerService.setCanonical(absoluteUrl);
|
||||
}
|
||||
|
||||
private closeSearchDialogOnNavigationSkipped(): void {
|
||||
this.router.events.pipe(filter((event) => event instanceof NavigationSkipped)).subscribe(() => {
|
||||
this.displaySearchDialog.set(false);
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
windowProvider,
|
||||
} from '@angular/docs';
|
||||
import {provideClientHydration} from '@angular/platform-browser';
|
||||
import {provideAnimationsAsync} from '@angular/platform-browser/animations/async';
|
||||
import {
|
||||
RouteReuseStrategy,
|
||||
Router,
|
||||
@@ -75,7 +74,6 @@ export const appConfig: ApplicationConfig = {
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
provideClientHydration(),
|
||||
provideHttpClient(withFetch()),
|
||||
provideAnimationsAsync(),
|
||||
provideEnvironmentInitializer(() => inject(AppScroller)),
|
||||
provideEnvironmentInitializer(() => inject(AnalyticsService)),
|
||||
{
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="docs-license">
|
||||
Super-powered by Google ©2010-2024. Code licensed under an
|
||||
Super-powered by Google ©2010-2025. Code licensed under an
|
||||
<a routerLink="/license" title="License text">MIT-style License</a>
|
||||
. Documentation licensed under
|
||||
<a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<!-- Third, fourth and next levels of navigation -->
|
||||
@for (item of navigationItemsSlides(); track item; let level = $index) {
|
||||
<docs-navigation-list
|
||||
[@leaveAnimation]
|
||||
[collapsableLevel]="level + maxVisibleLevelsOnSecondaryNav()"
|
||||
[expandableLevel]="level + maxVisibleLevelsOnSecondaryNav() + 1"
|
||||
[navigationItems]="[item]"
|
||||
|
||||
@@ -34,7 +34,6 @@ import {SUB_NAVIGATION_DATA} from '../../../sub-navigation-data';
|
||||
import {PagePrefix} from '../../enums/pages';
|
||||
import {ActivatedRouteSnapshot, NavigationEnd, Router, RouterStateSnapshot} from '@angular/router';
|
||||
import {isPlatformBrowser} from '@angular/common';
|
||||
import {trigger, transition, style, animate} from '@angular/animations';
|
||||
import {PRIMARY_NAV_ID, SECONDARY_NAV_ID} from '../../constants/element-ids';
|
||||
|
||||
export const ANIMATION_DURATION = 500;
|
||||
@@ -45,17 +44,6 @@ export const ANIMATION_DURATION = 500;
|
||||
templateUrl: './secondary-navigation.component.html',
|
||||
styleUrls: ['./secondary-navigation.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [
|
||||
trigger('leaveAnimation', [
|
||||
transition(':leave', [
|
||||
style({transform: 'translateX(0%)'}),
|
||||
animate(
|
||||
`${ANIMATION_DURATION}ms ${ANIMATION_DURATION}ms ease-out`,
|
||||
style({transform: 'translateX(100%)'}),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class SecondaryNavigation implements OnInit {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
@@ -69,10 +57,11 @@ export class SecondaryNavigation implements OnInit {
|
||||
this.primaryActiveRouteItem() === PagePrefix.REFERENCE ? 1 : 2,
|
||||
);
|
||||
readonly navigationItemsSlides = this.navigationState.expandedItems;
|
||||
|
||||
navigationItems: NavigationItem[] | undefined;
|
||||
|
||||
translateX = computed(() => {
|
||||
const level = this.navigationState.expandedItems()?.length ?? 0;
|
||||
const level = this.navigationState.level();
|
||||
return `translateX(${-level * 100}%)`;
|
||||
});
|
||||
transition = signal('0ms');
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import {DOCUMENT} from '@angular/common';
|
||||
import {ReferenceScrollHandler} from '../services/reference-scroll-handler.service';
|
||||
import {API_SECTION_CLASS_NAME} from '../constants/api-reference-prerender.constants';
|
||||
|
||||
const HIGHLIGHTED_CARD_CLASS = 'highlighted';
|
||||
const HIGHLIGHTED_CARD_CLASS = 'docs-highlighted-card';
|
||||
|
||||
@Component({
|
||||
selector: 'adev-reference-page',
|
||||
|
||||
@@ -238,10 +238,6 @@ export const routes: Route[] = [
|
||||
path: PagePrefix.DOCS,
|
||||
redirectTo: DefaultPage.DOCS,
|
||||
},
|
||||
{
|
||||
path: PagePrefix.TUTORIALS,
|
||||
redirectTo: DefaultPage.TUTORIALS,
|
||||
},
|
||||
{
|
||||
path: PagePrefix.REFERENCE,
|
||||
redirectTo: DefaultPage.REFERENCE,
|
||||
|
||||
@@ -859,6 +859,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
path: 'ecosystem/web-workers',
|
||||
contentPath: 'ecosystem/web-workers',
|
||||
},
|
||||
{
|
||||
label: 'Custom build pipeline',
|
||||
path: 'ecosystem/custom-build-pipeline',
|
||||
contentPath: 'ecosystem/custom-build-pipeline',
|
||||
},
|
||||
{
|
||||
label: 'Angular Fire',
|
||||
path: 'https://github.com/angular/angularfire#readme',
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Custom build pipeline
|
||||
|
||||
When building an Angular app we strongly recommend you to use the Angular CLI to leverage its structure-dependent update functionality and build system abstraction. This way your projects benefit from the latest security, performance, and API improvements and transparent build improvements.
|
||||
|
||||
This page explores the **rare use cases** when you need a custom build pipeline that does not use the Angular CLI. All listed tools below are open source build plugins that are maintained by members of the Angular community. To learn more about their support model and maintenance status look at their documentation and GitHub repository URLs.
|
||||
|
||||
## When should you use a custom build pipeline?
|
||||
|
||||
There are some niche use cases when you may want to maintain a custom build pipeline. For example:
|
||||
|
||||
* You have an existing app using a different toolchain and you’d like to add Angular to it
|
||||
* You’re strongly coupled to [module federation](https://module-federation.io/) and unable to adopt bundler-agnostic [native federation](https://www.npmjs.com/package/@angular-architects/native-federation)
|
||||
* You’d like to create an short-lived experiment using your favorite build tool
|
||||
|
||||
## What are the options?
|
||||
|
||||
Currently, there are two well supported community tools that enable you to create a custom build pipeline with a [Vite plugin](https://www.npmjs.com/package/@analogjs/vite-plugin-angular) and [Rspack plugin](https://www.npmjs.com/package/@ng-rspack/build). Both of them use underlying abstractions that power the Angular CLI. They allow you to create a flexible build pipeline and require manual maintenance and no automated update experience.
|
||||
|
||||
### Rspack
|
||||
|
||||
Rspack is a Rust-based bundler that aims to provide compatibility with the webpack plugin ecosystem.
|
||||
|
||||
If your project is tightly coupled to the webpack ecosystem, heavily relying on a custom webpack configuration you can leverage Rspack to improve your build times.
|
||||
|
||||
You can find more about Angular Rspack on the project’s [documentation website](https://angular-rspack.dev/guide/migration/from-webpack).
|
||||
|
||||
### Vite
|
||||
|
||||
Vite is a frontend build tool that aims to provide a faster and leaner development experience for modern web projects. Vite is also extensible through its plugin system that allows ecosystems to build integrations with Vite, such as Vitest for unit and browser testing, Storybook for authoring components in isolation, and more. The Angular CLI also uses Vite as its development server.
|
||||
|
||||
The [AnalogJS Vite plugin for Angular](https://www.npmjs.com/package/@analogjs/vite-plugin-angular) enables the adoption of Angular with a project or framework that uses or is built on top of Vite. This can consist of developing and building an Angular project with Vite directly, or adding Angular to an existing project or pipeline. One example is integrating Angular UI components into a documentation website using [Astro and Starlight](https://analogjs.org/docs/packages/astro-angular/overview).
|
||||
|
||||
You can learn more about AnalogJS and how to use the plugin through its [documentation page](https://analogjs.org/docs/packages/vite-plugin-angular/overview).
|
||||
@@ -90,7 +90,7 @@ describe('BannerComponent (with beforeEach)', () => {
|
||||
// #enddocregion v4-test-3
|
||||
|
||||
// #docregion v4-test-4
|
||||
it('should find the <p> with fixture.debugElement.nativeElement)', () => {
|
||||
it('should find the <p> with fixture.debugElement.nativeElement', () => {
|
||||
// #docregion debugElement-nativeElement
|
||||
const bannerDe: DebugElement = fixture.debugElement;
|
||||
const bannerEl: HTMLElement = bannerDe.nativeElement;
|
||||
|
||||
@@ -132,7 +132,7 @@ TypeScript property name as a key, rather than the alias.
|
||||
### ngOnDestroy
|
||||
|
||||
The `ngOnDestroy` method runs once just before a component is destroyed. Angular destroys a
|
||||
component when it is no longer shown on the page, such as being hidden by `NgIf` or upon navigating
|
||||
component when it is no longer shown on the page, such as being hidden by `@if` or upon navigating
|
||||
to another page.
|
||||
|
||||
#### DestroyRef
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Security
|
||||
|
||||
This topic describes Angular's built-in protections against common web-application vulnerabilities and attacks such as cross-site scripting attacks.
|
||||
This topic describes Angular's built-in protections against common web application vulnerabilities and attacks such as cross-site scripting attacks.
|
||||
It doesn't cover application-level security, such as authentication and authorization.
|
||||
|
||||
For more information about the attacks and mitigations described below, see the [Open Web Application Security Project (OWASP) Guide](https://www.owasp.org/index.php/Category:OWASP_Guide_Project).
|
||||
@@ -9,7 +9,7 @@ For more information about the attacks and mitigations described below, see the
|
||||
|
||||
<docs-callout title="Reporting vulnerabilities">
|
||||
|
||||
Angular is part of Google [Open Source Software Vulnerability Reward Program](https://bughunters.google.com/about/rules/6521337925468160/google-open-source-software-vulnerability-reward-program-rules). [For vulnerabilities in Angular, please submit your report at https://bughunters.google.com](https://bughunters.google.com/report).
|
||||
Angular is part of Google [Open Source Software Vulnerability Reward Program](https://bughunters.google.com/about/rules/6521337925468160/google-open-source-software-vulnerability-reward-program-rules). For vulnerabilities in Angular, please submit your report at [https://bughunters.google.com](https://bughunters.google.com/report).
|
||||
|
||||
For more information about how Google handles security issues, see [Google's security philosophy](https://www.google.com/about/appsecurity).
|
||||
|
||||
@@ -53,8 +53,8 @@ For this reason, it is strongly encouraged to take advantage of these features.
|
||||
|
||||
*Sanitization* is the inspection of an untrusted value, turning it into a value that's safe to insert into the DOM.
|
||||
In many cases, sanitization doesn't change a value at all.
|
||||
Sanitization depends on context:
|
||||
A value that's harmless in CSS is potentially dangerous in a URL.
|
||||
Sanitization depends on a context.
|
||||
For example, a value that's harmless in CSS is potentially dangerous in a URL.
|
||||
|
||||
Angular defines the following security contexts:
|
||||
|
||||
@@ -95,7 +95,7 @@ Avoid directly interacting with the DOM and instead use Angular templates where
|
||||
|
||||
For cases where this is unavoidable, use the built-in Angular sanitization functions.
|
||||
Sanitize untrusted values with the [DomSanitizer.sanitize](api/platform-browser/DomSanitizer#sanitize) method and the appropriate `SecurityContext`.
|
||||
That function also accepts values that were marked as trusted using the `bypassSecurityTrust` … functions, and does not sanitize them, as [described below](#trusting-safe-values).
|
||||
That function also accepts values that were marked as trusted using the `bypassSecurityTrust` functions, and does not sanitize them, as [described below](#trusting-safe-values).
|
||||
|
||||
### Trusting safe values
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ Angular's compiler produces a [dynamic import](https://developer.mozilla.org/en-
|
||||
|
||||
This is the primary block that defines the section of content that is lazily loaded. It is not rendered initially– deferred content loads and renders once the specified [trigger](/guide/defer#triggers) occurs or the `when` condition is met.
|
||||
|
||||
By default, a @defer block is triggered when the browser state becomes [idle](/guide/defer#on-idle).
|
||||
By default, a @defer block is triggered when the browser state becomes [idle](/guide/defer#idle).
|
||||
|
||||
```angular-html
|
||||
@defer {
|
||||
|
||||
@@ -68,59 +68,51 @@ export class AppComponent {
|
||||
|
||||
```angular-ts
|
||||
// './counter/counter.component.ts';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Component, model } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-counter',
|
||||
template: `
|
||||
<button (click)="updateCount(-1)">-</button>
|
||||
<span>{{ count }}</span>
|
||||
<span>{{ count() }}</span>
|
||||
<button (click)="updateCount(+1)">+</button>
|
||||
`,
|
||||
})
|
||||
export class CounterComponent {
|
||||
@Input() count: number;
|
||||
@Output() countChange = new EventEmitter<number>();
|
||||
count = model<number>(0);
|
||||
|
||||
updateCount(amount: number): void {
|
||||
this.count += amount;
|
||||
this.countChange.emit(this.count);
|
||||
this.count.update(currentCount => currentCount + amount);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Enabling two-way binding between components
|
||||
|
||||
If we break down the example above to its core , each two-way binding for components requires the following:
|
||||
If we break down the example above to its core, each two-way binding for components requires the following:
|
||||
|
||||
The child component must contain:
|
||||
|
||||
1. An `@Input()` property
|
||||
1. A corresponding `@Output()` event emitter that has the exact same name as the input property plus "Change" at the end. The emitter must also emit the same type as the input property.
|
||||
1. A method that emits to the event emitter with the updated value of the `@Input()`.
|
||||
The child component must contain a `model` property.
|
||||
|
||||
Here is a simplified example:
|
||||
|
||||
```angular-ts
|
||||
// './counter/counter.component.ts';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Component, model } from '@angular/core';
|
||||
|
||||
@Component({ // Omitted for brevity })
|
||||
export class CounterComponent {
|
||||
@Input() count: number;
|
||||
@Output() countChange = new EventEmitter<number>();
|
||||
count = model<number>(0);
|
||||
|
||||
updateCount(amount: number): void {
|
||||
this.count += amount;
|
||||
this.countChange.emit(this.count);
|
||||
this.count.update(currentCount => currentCount + amount);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The parent component must:
|
||||
|
||||
1. Wrap the `@Input()` property name in the two-way binding syntax.
|
||||
1. Specify the corresponding property to which the updated value is assigned
|
||||
1. Wrap the `model` property name in the two-way binding syntax.
|
||||
1. Assign a property or a signal to the `model` property.
|
||||
|
||||
Here is a simplified example:
|
||||
|
||||
|
||||
@@ -32,11 +32,29 @@ The following guide will walk you through setting up a local Angular project.
|
||||
|
||||
Open a terminal (if you're using [Visual Studio Code](https://code.visualstudio.com/), you can open an [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal)) and run the following command:
|
||||
|
||||
<docs-code language="shell">
|
||||
<docs-code-multifile>
|
||||
<docs-code
|
||||
header="npm"
|
||||
>
|
||||
npm install -g @angular/cli
|
||||
</docs-code>
|
||||
<docs-code
|
||||
header="pnpm"
|
||||
>
|
||||
pnpm install -g @angular/cli
|
||||
</docs-code>
|
||||
<docs-code
|
||||
header="yarn"
|
||||
>
|
||||
yarn global add @angular/cli
|
||||
</docs-code>
|
||||
<docs-code
|
||||
header="bun"
|
||||
>
|
||||
bun install -g @angular/cli
|
||||
</docs-code>
|
||||
|
||||
npm install -g @angular/cli
|
||||
|
||||
</docs-code>
|
||||
</docs-code-multifile>
|
||||
|
||||
If you are having issues running this command in Windows or Unix, check out the [CLI docs](/tools/cli/setup-local#install-the-angular-cli) for more info.
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ ng generate @angular/core:output-migration --path src/app/sub-folder
|
||||
## Exceptions
|
||||
|
||||
In some cases, the migration will not touch the code.
|
||||
One of these excpetions is the case where the event is used with a `pipe()` method.
|
||||
One of these exceptions is the case where the event is used with a `pipe()` method.
|
||||
The following code won't be migrated:
|
||||
|
||||
```typescript
|
||||
@@ -93,4 +93,4 @@ export class MyDialogComponent {
|
||||
this.close.pipe();
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@@ -49,18 +49,18 @@ export class MyComponent {
|
||||
import {Component, input} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: `Name: {{name() ?? ''}}`
|
||||
template: `Name: {{name() ?? ''}}`
|
||||
})
|
||||
export class MyComponent {
|
||||
readonly name = input<string>();
|
||||
readonly name = input<string>();
|
||||
|
||||
someMethod(): number {
|
||||
const name = this.name();
|
||||
if (name) {
|
||||
return name.length;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
someMethod(): number {
|
||||
const name = this.name();
|
||||
if (name) {
|
||||
return name.length;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
</docs-code>
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ HELPFUL: Approximate dates are offered as general guidance and are subject to ch
|
||||
|:--------|:-------------------|
|
||||
| v19.1 | Week of 2025-01-13 |
|
||||
| v19.2 | Week of 2025-02-24 |
|
||||
| v20.0 | Week of 2025-05-19 |
|
||||
| v20.0 | Week of 2025-05-26 |
|
||||
|
||||
### Support window
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ This table covers [Angular versions under active support](reference/releases#act
|
||||
|
||||
| Angular | Node.js | TypeScript | RxJS |
|
||||
| ------------------ | ------------------------------------ | -------------- | ------------------ |
|
||||
| 19.2.x | ^18.19.1 \|\| ^20.11.1 \|\| ^22.0.0 | >=5.5.0 <5.9.0 | ^6.5.3 \|\| ^7.4.0 |
|
||||
| 19.1.x | ^18.19.1 \|\| ^20.11.1 \|\| ^22.0.0 | >=5.5.0 <5.8.0 | ^6.5.3 \|\| ^7.4.0 |
|
||||
| 19.0.x | ^18.19.1 \|\| ^20.11.1 \|\| ^22.0.0 | >=5.5.0 <5.7.0 | ^6.5.3 \|\| ^7.4.0 |
|
||||
| 18.1.x \|\| 18.2.x | ^18.19.1 \|\| ^20.11.1 \|\| ^22.0.0 | >=5.4.0 <5.6.0 | ^6.5.3 \|\| ^7.4.0 |
|
||||
|
||||
@@ -37,11 +37,29 @@ See [Angular's version compatibility](reference/versions) guide for more informa
|
||||
|
||||
To install the Angular CLI, open a terminal window and run the following command:
|
||||
|
||||
<docs-code language="shell">
|
||||
<docs-code-multifile>
|
||||
<docs-code
|
||||
header="npm"
|
||||
>
|
||||
npm install -g @angular/cli
|
||||
</docs-code>
|
||||
<docs-code
|
||||
header="pnpm"
|
||||
>
|
||||
pnpm install -g @angular/cli
|
||||
</docs-code>
|
||||
<docs-code
|
||||
header="yarn"
|
||||
>
|
||||
yarn global add @angular/cli
|
||||
</docs-code>
|
||||
<docs-code
|
||||
header="bun"
|
||||
>
|
||||
bun install -g @angular/cli
|
||||
</docs-code>
|
||||
|
||||
npm install -g @angular/cli
|
||||
|
||||
</docs-code>
|
||||
</docs-code-multifile>
|
||||
|
||||
### Powershell execution policy
|
||||
|
||||
@@ -58,14 +76,32 @@ Carefully read the message displayed after executing the command and follow the
|
||||
|
||||
### Unix permissions
|
||||
|
||||
On some Unix-like setups, global npm scripts may be owned by the root user, so to the above command may fail with a permission error.
|
||||
On some Unix-like setups, global scripts may be owned by the root user, so to the above command may fail with a permission error.
|
||||
Run with `sudo` to execute the command as the root user and enter your password when prompted:
|
||||
|
||||
<docs-code language="sh">
|
||||
<docs-code-multifile>
|
||||
<docs-code
|
||||
header="npm"
|
||||
>
|
||||
sudo npm install -g @angular/cli
|
||||
</docs-code>
|
||||
<docs-code
|
||||
header="pnpm"
|
||||
>
|
||||
sudo pnpm install -g @angular/cli
|
||||
</docs-code>
|
||||
<docs-code
|
||||
header="yarn"
|
||||
>
|
||||
sudo yarn global add @angular/cli
|
||||
</docs-code>
|
||||
<docs-code
|
||||
header="bun"
|
||||
>
|
||||
sudo bun install -g @angular/cli
|
||||
</docs-code>
|
||||
|
||||
sudo npm install -g @angular/cli
|
||||
|
||||
</docs-code>
|
||||
</docs-code-multifile>
|
||||
|
||||
Make sure you understand the implications of running commands as root.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "NG_BUILD_PARALLEL_TS=0 ng serve",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development"
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "NG_BUILD_PARALLEL_TS=0 ng serve",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development"
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "NG_BUILD_PARALLEL_TS=0 ng serve",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development"
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "NG_BUILD_PARALLEL_TS=0 ng serve",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development"
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "NG_BUILD_PARALLEL_TS=0 ng serve",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# Commit Message Format
|
||||
|
||||
We have very precise rules over how our Git commit messages must be formatted.
|
||||
This format leads to **easier to read commit history** and makes it analyzable for changelog generation.
|
||||
|
||||
Each commit message consists of a **header**, a **body**, and a **footer**.
|
||||
|
||||
|
||||
```
|
||||
<header>
|
||||
<BLANK LINE>
|
||||
<body>
|
||||
<BLANK LINE>
|
||||
<footer>
|
||||
```
|
||||
|
||||
The `header` is mandatory and must conform to the [Commit Message Header](#commit-header) format.
|
||||
|
||||
The `body` is mandatory for all commits except for those of type "docs".
|
||||
When the body is present it must be at least 20 characters long and must conform to the [Commit Message Body](#commit-body) format.
|
||||
|
||||
The `footer` is optional. The [Commit Message Footer](#commit-footer) format describes what the footer is used for and the structure it must have.
|
||||
|
||||
|
||||
## <a name="commit-header"></a>Commit Message Header
|
||||
|
||||
```
|
||||
<type>(<scope>): <short summary>
|
||||
│ │ │
|
||||
│ │ └─⫸ Summary in present tense. Not capitalized. No period at the end.
|
||||
│ │
|
||||
│ └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core|
|
||||
│ elements|forms|http|language-service|localize|platform-browser|
|
||||
│ platform-browser-dynamic|platform-server|router|service-worker|
|
||||
│ upgrade|zone.js|packaging|changelog|docs-infra|migrations|
|
||||
│ devtools
|
||||
│
|
||||
└─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test
|
||||
```
|
||||
|
||||
The `<type>` and `<summary>` fields are mandatory, the `(<scope>)` field is optional.
|
||||
|
||||
|
||||
### Type
|
||||
|
||||
Must be one of the following:
|
||||
|
||||
| Type | Description |
|
||||
|--------------|-----------------------------------------------------------------------------------------------------|
|
||||
| **build** | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) |
|
||||
| **ci** | Changes to our CI configuration files and scripts (examples: Github Actions, SauceLabs) |
|
||||
| **docs** | Documentation only changes |
|
||||
| **feat** | A new feature |
|
||||
| **fix** | A bug fix |
|
||||
| **perf** | A code change that improves performance |
|
||||
| **refactor** | A code change that neither fixes a bug nor adds a feature |
|
||||
| **test** | Adding missing tests or correcting existing tests |
|
||||
|
||||
|
||||
### <a name="scope"></a> Scope
|
||||
The scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages).
|
||||
|
||||
The following is the list of supported scopes:
|
||||
|
||||
* `animations`
|
||||
* `bazel`
|
||||
* `benchpress`
|
||||
* `changelog`
|
||||
* `common`
|
||||
* `compiler`
|
||||
* `compiler-cli`
|
||||
* `core`
|
||||
* `dev-infra`
|
||||
* `devtools`
|
||||
* `docs-infra`
|
||||
* `elements`
|
||||
* `forms`
|
||||
* `http`
|
||||
* `language-service`
|
||||
* `localize`
|
||||
* `migrations`
|
||||
* `packaging`
|
||||
* `platform-browser`
|
||||
* `platform-browser-dynamic`
|
||||
* `platform-server`
|
||||
* `router`
|
||||
* `service-worker`
|
||||
* `upgrade`
|
||||
* `zone.js`
|
||||
|
||||
There are currently a few exceptions to the "use package name" rule:
|
||||
|
||||
* `packaging`: used for changes that change the npm package layout in all of our packages, e.g. public path changes, package.json changes done to all packages, d.ts file/format changes, changes to bundles, etc.
|
||||
|
||||
* `changelog`: used for updating the release notes in CHANGELOG.md
|
||||
|
||||
* `dev-infra`: used for dev-infra related changes within the directories /scripts and /tools
|
||||
|
||||
* `docs-infra`: used for docs-app (angular.dev) related changes within the /adev directory of the repo
|
||||
|
||||
* `migrations`: used for changes to the `ng update` migrations.
|
||||
|
||||
* `devtools`: used for changes in the [browser extension](../devtools/README.md).
|
||||
|
||||
* none/empty string: useful for `test` and `refactor` changes that are done across all packages (e.g. `test: add missing unit tests`) and for docs changes that are not related to a specific package (e.g. `docs: fix typo in tutorial`).
|
||||
|
||||
|
||||
### Summary
|
||||
|
||||
Use the summary field to provide a succinct description of the change:
|
||||
|
||||
* use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
* don't capitalize the first letter
|
||||
* no dot (.) at the end
|
||||
|
||||
|
||||
## <a name="commit-body"></a>Commit Message Body
|
||||
|
||||
Just as in the summary, use the imperative, present tense: "fix" not "fixed" nor "fixes".
|
||||
|
||||
Explain the motivation for the change in the commit message body. This commit message should explain _why_ you are making the change.
|
||||
You can include a comparison of the previous behavior with the new behavior in order to illustrate the impact of the change.
|
||||
|
||||
|
||||
## <a name="commit-footer"></a>Commit Message Footer
|
||||
|
||||
The footer can contain information about breaking changes and deprecations and is also the place to reference GitHub issues and other PRs that this commit closes or is related to.
|
||||
For example:
|
||||
|
||||
```
|
||||
BREAKING CHANGE: <breaking change summary>
|
||||
<BLANK LINE>
|
||||
<breaking change description + migration instructions>
|
||||
<BLANK LINE>
|
||||
<BLANK LINE>
|
||||
Fixes #<issue number>
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
DEPRECATED: <what is deprecated>
|
||||
<BLANK LINE>
|
||||
<deprecation description + recommended update path>
|
||||
<BLANK LINE>
|
||||
<BLANK LINE>
|
||||
Closes #<pr number>
|
||||
```
|
||||
|
||||
Breaking Change section should start with the phrase `BREAKING CHANGE: ` followed by a *brief* summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
|
||||
|
||||
Similarly, a Deprecation section should start with `DEPRECATED: ` followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path.
|
||||
|
||||
## Revert commits
|
||||
|
||||
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit.
|
||||
|
||||
The content of the commit message body should contain:
|
||||
|
||||
- information about the SHA of the commit being reverted in the following format: `This reverts commit <SHA>`,
|
||||
- a clear description of the reason for reverting the commit message.
|
||||
|
||||
|
||||
|
||||
|
||||
[angularjs-commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#
|
||||
@@ -44,13 +44,13 @@ describe('Viewing component metadata', () => {
|
||||
});
|
||||
|
||||
it('should display correct set of inputs', () => {
|
||||
cy.contains('.cy-inputs', '@Inputs');
|
||||
cy.contains('.cy-inputs', 'Inputs');
|
||||
cy.contains('.cy-inputs mat-tree-node:first span:first', 'inputOne');
|
||||
cy.contains('.cy-inputs mat-tree-node:last span:first', 'inputTwo');
|
||||
});
|
||||
|
||||
it('should display correct set of outputs', () => {
|
||||
cy.contains('.cy-outputs', '@Outputs');
|
||||
cy.contains('.cy-outputs', 'Outputs');
|
||||
cy.contains('.cy-outputs mat-tree-node:first span:first', 'outputOne');
|
||||
cy.contains('.cy-outputs mat-tree-node:last span:first', 'outputTwo');
|
||||
});
|
||||
|
||||
@@ -569,13 +569,13 @@ export const updateState = (updatedStateData: UpdatedStateData): void => {
|
||||
if (updatedStateData.directiveId.directive !== undefined) {
|
||||
const directive = node.directives[updatedStateData.directiveId.directive].instance;
|
||||
mutateComponentOrDirective(updatedStateData, directive);
|
||||
ng.applyChanges(ng.getOwningComponent(directive)!);
|
||||
ng.applyChanges?.(ng.getOwningComponent(directive)!);
|
||||
return;
|
||||
}
|
||||
if (node.component) {
|
||||
const comp = node.component.instance;
|
||||
mutateComponentOrDirective(updatedStateData, comp);
|
||||
ng.applyChanges(comp);
|
||||
ng.applyChanges?.(comp);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ function createOverlay(color: RgbColor): {overlay: HTMLElement; overlayContent:
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'ng-devtools-overlay';
|
||||
overlay.style.backgroundColor = toCSSColor(...color, 0.35);
|
||||
overlay.style.position = 'fixed';
|
||||
overlay.style.position = 'absolute';
|
||||
overlay.style.zIndex = '2147483647';
|
||||
overlay.style.pointerEvents = 'none';
|
||||
overlay.style.display = 'flex';
|
||||
@@ -196,8 +196,8 @@ function showOverlay(
|
||||
const {width, height, top, left} = dimensions;
|
||||
overlay.style.width = ~~width + 'px';
|
||||
overlay.style.height = ~~height + 'px';
|
||||
overlay.style.top = ~~top + 'px';
|
||||
overlay.style.left = ~~left + 'px';
|
||||
overlay.style.top = ~~top + window.scrollY + 'px';
|
||||
overlay.style.left = ~~left + window.scrollX + 'px';
|
||||
|
||||
positionOverlayContent(overlayContent, dimensions, labelPosition);
|
||||
overlayContent.replaceChildren();
|
||||
|
||||
+2
-2
@@ -57,14 +57,14 @@ export class PropertyViewBodyComponent {
|
||||
>(() => {
|
||||
return [
|
||||
{
|
||||
title: '@Inputs',
|
||||
title: 'Inputs',
|
||||
hidden: this.directiveInputControls().dataSource.data.length === 0,
|
||||
controls: this.directiveInputControls(),
|
||||
documentation: 'https://angular.dev/api/core/input',
|
||||
class: 'cy-inputs',
|
||||
},
|
||||
{
|
||||
title: '@Outputs',
|
||||
title: 'Outputs',
|
||||
hidden: this.directiveOutputControls().dataSource.data.length === 0,
|
||||
controls: this.directiveOutputControls(),
|
||||
documentation: 'https://angular.dev/api/core/output',
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"short_name": "Angular DevTools",
|
||||
"name": "Angular DevTools",
|
||||
"description": "Angular DevTools extends Chrome DevTools adding Angular specific debugging and profiling capabilities.",
|
||||
"version": "1.0.23",
|
||||
"version_name": "1.0.23",
|
||||
"version": "1.0.24",
|
||||
"version_name": "1.0.24",
|
||||
"minimum_chrome_version": "102",
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self'; object-src 'self'"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"short_name": "Angular DevTools",
|
||||
"name": "Angular DevTools",
|
||||
"description": "Angular DevTools extends Firefox DevTools adding Angular specific debugging and profiling capabilities.",
|
||||
"version": "1.0.23",
|
||||
"version": "1.0.24",
|
||||
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
|
||||
"icons": {
|
||||
"16": "assets/icon16.png",
|
||||
|
||||
@@ -10,16 +10,3 @@ yarn public-api:check
|
||||
yarn public-api:update
|
||||
```
|
||||
|
||||
### *`packages-circular-deps.json`*
|
||||
|
||||
This golden file contains a list of all circular dependencies in the project. As part of the
|
||||
lint CI job we compare the current circular dependencies against this golden to ensure that
|
||||
we don't add more cycles. If cycles have been fixed, this file is also updated so that we can
|
||||
slowly burn down the number of cycles in the project.
|
||||
|
||||
To check or update the golden, run the following commands:
|
||||
|
||||
```bash
|
||||
yarn ts-circular-deps:check
|
||||
yarn ts-circular-deps:approve
|
||||
```
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
## API Report File for "@angular/core_primitives_di"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
|
||||
// @public
|
||||
export function getCurrentInjector(): Injector | undefined | null;
|
||||
|
||||
// @public
|
||||
export interface InjectionToken<T> extends Type<T> {
|
||||
// (undocumented)
|
||||
ɵprov: ɵɵInjectableDeclaration<T>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface Injector {
|
||||
// (undocumented)
|
||||
retrieve?<T>(token: InjectionToken<T>, options?: unknown): T | NotFound;
|
||||
}
|
||||
|
||||
// @public
|
||||
export function isNotFound(e: unknown): e is NotFound;
|
||||
|
||||
// @public
|
||||
export const NOT_FOUND: unique symbol;
|
||||
|
||||
// @public
|
||||
export type NotFound = typeof NOT_FOUND | NotFoundError;
|
||||
|
||||
// @public
|
||||
export class NotFoundError extends Error {
|
||||
constructor(message: string);
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export function setCurrentInjector(injector: Injector | null | undefined): Injector | undefined | null;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
||||
```
|
||||
@@ -20,7 +20,7 @@
|
||||
"@angular/upgrade": "file:../../dist/packages-dist/upgrade",
|
||||
"@types/jasmine": "file:../../node_modules/@types/jasmine",
|
||||
"rxjs": "file:../../node_modules/rxjs",
|
||||
"typescript": "5.8.1-rc",
|
||||
"typescript": "5.8.2",
|
||||
"zone.js": "0.14.10"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -11,19 +11,19 @@
|
||||
"@jridgewell/trace-mapping" "^0.3.24"
|
||||
|
||||
"@angular/animations@file:../../dist/packages-dist/animations":
|
||||
version "19.2.0-next.2"
|
||||
version "20.0.0-next.0"
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/common@file:../../dist/packages-dist/common":
|
||||
version "19.2.0-next.2"
|
||||
version "20.0.0-next.0"
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/compiler-cli@file:../../dist/packages-dist/compiler-cli":
|
||||
version "19.2.0-next.2"
|
||||
version "20.0.0-next.0"
|
||||
dependencies:
|
||||
"@babel/core" "7.26.0"
|
||||
"@babel/core" "7.26.9"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
chokidar "^4.0.0"
|
||||
convert-source-map "^1.5.1"
|
||||
@@ -33,65 +33,65 @@
|
||||
yargs "^17.2.1"
|
||||
|
||||
"@angular/compiler@file:../../dist/packages-dist/compiler":
|
||||
version "19.2.0-next.2"
|
||||
version "20.0.0-next.0"
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/core@file:../../dist/packages-dist/core":
|
||||
version "19.2.0-next.2"
|
||||
version "20.0.0-next.0"
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/elements@file:../../dist/packages-dist/elements":
|
||||
version "19.2.0-next.2"
|
||||
version "20.0.0-next.0"
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/forms@file:../../dist/packages-dist/forms":
|
||||
version "19.2.0-next.2"
|
||||
version "20.0.0-next.0"
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/localize@file:../../dist/packages-dist/localize":
|
||||
version "19.2.0-next.2"
|
||||
version "20.0.0-next.0"
|
||||
dependencies:
|
||||
"@babel/core" "7.26.0"
|
||||
"@babel/core" "7.26.9"
|
||||
"@types/babel__core" "7.20.5"
|
||||
fast-glob "3.3.3"
|
||||
yargs "^17.2.1"
|
||||
|
||||
"@angular/platform-browser-dynamic@file:../../dist/packages-dist/platform-browser-dynamic":
|
||||
version "19.2.0-next.2"
|
||||
version "20.0.0-next.0"
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/platform-browser@file:../../dist/packages-dist/platform-browser":
|
||||
version "19.2.0-next.2"
|
||||
version "20.0.0-next.0"
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/platform-server@file:../../dist/packages-dist/platform-server":
|
||||
version "19.2.0-next.2"
|
||||
version "20.0.0-next.0"
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
xhr2 "^0.2.0"
|
||||
|
||||
"@angular/router@file:../../dist/packages-dist/router":
|
||||
version "19.2.0-next.2"
|
||||
version "20.0.0-next.0"
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/service-worker@file:../../dist/packages-dist/service-worker":
|
||||
version "19.2.0-next.2"
|
||||
version "20.0.0-next.0"
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/upgrade@file:../../dist/packages-dist/upgrade":
|
||||
version "19.2.0-next.2"
|
||||
version "20.0.0-next.0"
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0":
|
||||
"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.2":
|
||||
version "7.26.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85"
|
||||
integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==
|
||||
@@ -100,33 +100,33 @@
|
||||
js-tokens "^4.0.0"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
"@babel/compat-data@^7.25.9":
|
||||
version "7.26.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.2.tgz#278b6b13664557de95b8f35b90d96785850bb56e"
|
||||
integrity sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==
|
||||
"@babel/compat-data@^7.26.5":
|
||||
version "7.26.8"
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.8.tgz#821c1d35641c355284d4a870b8a4a7b0c141e367"
|
||||
integrity sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==
|
||||
|
||||
"@babel/core@7.26.0":
|
||||
version "7.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40"
|
||||
integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==
|
||||
"@babel/core@7.26.9":
|
||||
version "7.26.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.9.tgz#71838542a4b1e49dfed353d7acbc6eb89f4a76f2"
|
||||
integrity sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==
|
||||
dependencies:
|
||||
"@ampproject/remapping" "^2.2.0"
|
||||
"@babel/code-frame" "^7.26.0"
|
||||
"@babel/generator" "^7.26.0"
|
||||
"@babel/helper-compilation-targets" "^7.25.9"
|
||||
"@babel/code-frame" "^7.26.2"
|
||||
"@babel/generator" "^7.26.9"
|
||||
"@babel/helper-compilation-targets" "^7.26.5"
|
||||
"@babel/helper-module-transforms" "^7.26.0"
|
||||
"@babel/helpers" "^7.26.0"
|
||||
"@babel/parser" "^7.26.0"
|
||||
"@babel/template" "^7.25.9"
|
||||
"@babel/traverse" "^7.25.9"
|
||||
"@babel/types" "^7.26.0"
|
||||
"@babel/helpers" "^7.26.9"
|
||||
"@babel/parser" "^7.26.9"
|
||||
"@babel/template" "^7.26.9"
|
||||
"@babel/traverse" "^7.26.9"
|
||||
"@babel/types" "^7.26.9"
|
||||
convert-source-map "^2.0.0"
|
||||
debug "^4.1.0"
|
||||
gensync "^1.0.0-beta.2"
|
||||
json5 "^2.2.3"
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/generator@^7.25.9", "@babel/generator@^7.26.0":
|
||||
"@babel/generator@^7.25.9":
|
||||
version "7.26.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.2.tgz#87b75813bec87916210e5e01939a4c823d6bb74f"
|
||||
integrity sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==
|
||||
@@ -137,12 +137,23 @@
|
||||
"@jridgewell/trace-mapping" "^0.3.25"
|
||||
jsesc "^3.0.2"
|
||||
|
||||
"@babel/helper-compilation-targets@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz#55af025ce365be3cdc0c1c1e56c6af617ce88875"
|
||||
integrity sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==
|
||||
"@babel/generator@^7.26.9":
|
||||
version "7.26.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.9.tgz#75a9482ad3d0cc7188a537aa4910bc59db67cbca"
|
||||
integrity sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==
|
||||
dependencies:
|
||||
"@babel/compat-data" "^7.25.9"
|
||||
"@babel/parser" "^7.26.9"
|
||||
"@babel/types" "^7.26.9"
|
||||
"@jridgewell/gen-mapping" "^0.3.5"
|
||||
"@jridgewell/trace-mapping" "^0.3.25"
|
||||
jsesc "^3.0.2"
|
||||
|
||||
"@babel/helper-compilation-targets@^7.26.5":
|
||||
version "7.26.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz#75d92bb8d8d51301c0d49e52a65c9a7fe94514d8"
|
||||
integrity sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==
|
||||
dependencies:
|
||||
"@babel/compat-data" "^7.26.5"
|
||||
"@babel/helper-validator-option" "^7.25.9"
|
||||
browserslist "^4.24.0"
|
||||
lru-cache "^5.1.1"
|
||||
@@ -190,13 +201,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72"
|
||||
integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==
|
||||
|
||||
"@babel/helpers@^7.26.0":
|
||||
version "7.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.0.tgz#30e621f1eba5aa45fe6f4868d2e9154d884119a4"
|
||||
integrity sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==
|
||||
"@babel/helpers@^7.26.9":
|
||||
version "7.26.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.9.tgz#28f3fb45252fc88ef2dc547c8a911c255fc9fef6"
|
||||
integrity sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==
|
||||
dependencies:
|
||||
"@babel/template" "^7.25.9"
|
||||
"@babel/types" "^7.26.0"
|
||||
"@babel/template" "^7.26.9"
|
||||
"@babel/types" "^7.26.9"
|
||||
|
||||
"@babel/parser@^7.1.0", "@babel/parser@^7.20.7":
|
||||
version "7.25.6"
|
||||
@@ -205,13 +216,20 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.25.6"
|
||||
|
||||
"@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.2":
|
||||
"@babel/parser@^7.25.9", "@babel/parser@^7.26.2":
|
||||
version "7.26.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.2.tgz#fd7b6f487cfea09889557ef5d4eeb9ff9a5abd11"
|
||||
integrity sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==
|
||||
dependencies:
|
||||
"@babel/types" "^7.26.0"
|
||||
|
||||
"@babel/parser@^7.26.9":
|
||||
version "7.26.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.9.tgz#d9e78bee6dc80f9efd8f2349dcfbbcdace280fd5"
|
||||
integrity sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==
|
||||
dependencies:
|
||||
"@babel/types" "^7.26.9"
|
||||
|
||||
"@babel/template@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016"
|
||||
@@ -221,6 +239,15 @@
|
||||
"@babel/parser" "^7.25.9"
|
||||
"@babel/types" "^7.25.9"
|
||||
|
||||
"@babel/template@^7.26.9":
|
||||
version "7.26.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.26.9.tgz#4577ad3ddf43d194528cff4e1fa6b232fa609bb2"
|
||||
integrity sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.26.2"
|
||||
"@babel/parser" "^7.26.9"
|
||||
"@babel/types" "^7.26.9"
|
||||
|
||||
"@babel/traverse@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84"
|
||||
@@ -234,6 +261,19 @@
|
||||
debug "^4.3.1"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/traverse@^7.26.9":
|
||||
version "7.26.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.9.tgz#4398f2394ba66d05d988b2ad13c219a2c857461a"
|
||||
integrity sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.26.2"
|
||||
"@babel/generator" "^7.26.9"
|
||||
"@babel/parser" "^7.26.9"
|
||||
"@babel/template" "^7.26.9"
|
||||
"@babel/types" "^7.26.9"
|
||||
debug "^4.3.1"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.25.6":
|
||||
version "7.25.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.6.tgz#893942ddb858f32ae7a004ec9d3a76b3463ef8e6"
|
||||
@@ -251,6 +291,14 @@
|
||||
"@babel/helper-string-parser" "^7.25.9"
|
||||
"@babel/helper-validator-identifier" "^7.25.9"
|
||||
|
||||
"@babel/types@^7.26.9":
|
||||
version "7.26.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.9.tgz#08b43dec79ee8e682c2ac631c010bdcac54a21ce"
|
||||
integrity sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.25.9"
|
||||
"@babel/helper-validator-identifier" "^7.25.9"
|
||||
|
||||
"@jridgewell/gen-mapping@^0.3.5":
|
||||
version "0.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36"
|
||||
@@ -338,7 +386,7 @@
|
||||
"@babel/types" "^7.20.7"
|
||||
|
||||
"@types/jasmine@file:../../node_modules/@types/jasmine":
|
||||
version "5.1.5"
|
||||
version "5.1.7"
|
||||
|
||||
ansi-regex@^5.0.1:
|
||||
version "5.0.1"
|
||||
@@ -596,7 +644,7 @@ run-parallel@^1.1.9:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
"rxjs@file:../../node_modules/rxjs":
|
||||
version "7.8.1"
|
||||
version "7.8.2"
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
@@ -643,10 +691,10 @@ tslib@^2.1.0, tslib@^2.3.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
|
||||
integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==
|
||||
|
||||
typescript@5.8.1-rc:
|
||||
version "5.8.1-rc"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.1-rc.tgz#678b7586bf1d7fdec0dbf35708ab7bb39ce13c3d"
|
||||
integrity sha512-D8IlSOUk1E08jpFdK81reYkA1a/4XtEdV6MElOGdbu/uOy1RpEDqNO/onWmqUaLkTyeHmmU/QlWvjcM9cqF85g==
|
||||
typescript@5.8.2:
|
||||
version "5.8.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4"
|
||||
integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
|
||||
|
||||
update-browserslist-db@^1.1.1:
|
||||
version "1.1.1"
|
||||
|
||||
+5
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "19.2.0-next.3",
|
||||
"version": "19.2.1",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
@@ -35,7 +35,6 @@
|
||||
"symbol-extractor:check": "node tools/symbol-extractor/run_all_symbols_extractor_tests.js test",
|
||||
"symbol-extractor:update": "node tools/symbol-extractor/run_all_symbols_extractor_tests.js accept",
|
||||
"ts-circular-deps:check": "yarn -s ng-dev ts-circular-deps check --config ./packages/circular-deps-test.conf.js",
|
||||
"ts-circular-deps:approve": "yarn -s ng-dev ts-circular-deps approve --config ./packages/circular-deps-test.conf.js",
|
||||
"check-tooling-setup": "yarn tsc --project .ng-dev/tsconfig.json && yarn tsc --project scripts/tsconfig.json",
|
||||
"devtools:devserver": "ibazel run //devtools/src:devserver",
|
||||
"devtools:devserver:demo-standalone": "ibazel run //devtools/projects/demo-standalone/src:devserver",
|
||||
@@ -78,7 +77,7 @@
|
||||
"@types/babel__core": "7.20.5",
|
||||
"@types/babel__generator": "7.6.8",
|
||||
"@types/bluebird": "^3.5.27",
|
||||
"@types/chrome": "^0.0.304",
|
||||
"@types/chrome": "^0.0.307",
|
||||
"@types/convert-source-map": "^2.0.0",
|
||||
"@types/diff": "^7.0.0",
|
||||
"@types/dom-view-transitions": "^1.0.1",
|
||||
@@ -136,7 +135,7 @@
|
||||
"rollup-plugin-sourcemaps": "^0.6.3",
|
||||
"rxjs": "^7.0.0",
|
||||
"selenium-webdriver": "3.5.0",
|
||||
"selenium-webdriver4": "npm:selenium-webdriver@4.28.1",
|
||||
"selenium-webdriver4": "npm:selenium-webdriver@4.29.0",
|
||||
"semver-dsl": "^1.0.1",
|
||||
"shelljs": "^0.8.5",
|
||||
"source-map": "0.7.4",
|
||||
@@ -148,7 +147,7 @@
|
||||
"tslib": "^2.3.0",
|
||||
"tslint": "6.1.3",
|
||||
"tsx": "^4.7.2",
|
||||
"typescript": "5.8.1-rc",
|
||||
"typescript": "5.8.2",
|
||||
"webtreemap": "^2.0.1",
|
||||
"ws": "^8.15.0",
|
||||
"xhr2": "0.2.1",
|
||||
@@ -163,7 +162,7 @@
|
||||
"@angular/animations": "^19.2.0-next",
|
||||
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#ce04ec6cf7604014191821a637e60964a1a3bb4a",
|
||||
"@angular/core": "^19.2.0-next",
|
||||
"@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#e29eb33d02c473598361716df343c13016a6c943",
|
||||
"@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#252589f7bc8fe6ca13b31e02d506ec52e826cdd2",
|
||||
"@bazel/bazelisk": "^1.7.5",
|
||||
"@bazel/buildifier": "^8.0.0",
|
||||
"@bazel/ibazel": "0.16.2",
|
||||
|
||||
@@ -78,7 +78,7 @@ import {AnimationDslVisitor} from './animation_dsl_visitor';
|
||||
import {parseTransitionExpr} from './animation_transition_expr';
|
||||
|
||||
const SELF_TOKEN = ':self';
|
||||
const SELF_TOKEN_REGEX = new RegExp(`s*${SELF_TOKEN}s*,?`, 'g');
|
||||
const SELF_TOKEN_REGEX = /* @__PURE__ */ new RegExp(`s*${SELF_TOKEN}s*,?`, 'g');
|
||||
|
||||
/*
|
||||
* [Validation]
|
||||
|
||||
@@ -47,9 +47,9 @@ import {ElementInstructionMap} from './element_instruction_map';
|
||||
|
||||
const ONE_FRAME_IN_MILLISECONDS = 1;
|
||||
const ENTER_TOKEN = ':enter';
|
||||
const ENTER_TOKEN_REGEX = new RegExp(ENTER_TOKEN, 'g');
|
||||
const ENTER_TOKEN_REGEX = /* @__PURE__ */ new RegExp(ENTER_TOKEN, 'g');
|
||||
const LEAVE_TOKEN = ':leave';
|
||||
const LEAVE_TOKEN_REGEX = new RegExp(LEAVE_TOKEN, 'g');
|
||||
const LEAVE_TOKEN_REGEX = /* @__PURE__ */ new RegExp(LEAVE_TOKEN, 'g');
|
||||
|
||||
/*
|
||||
* The code within this file aims to generate web-animations-compatible keyframes from Angular's
|
||||
|
||||
@@ -174,7 +174,7 @@ export function validateStyleParams(
|
||||
}
|
||||
}
|
||||
|
||||
const PARAM_REGEX = new RegExp(
|
||||
const PARAM_REGEX = /* @__PURE__ */ new RegExp(
|
||||
`${SUBSTITUTION_EXPR_START}\\s*(.+?)\\s*${SUBSTITUTION_EXPR_END}`,
|
||||
'g',
|
||||
);
|
||||
|
||||
@@ -54,7 +54,7 @@ export declare type AnimateTimings = {
|
||||
*
|
||||
* - `transition()`
|
||||
* - `sequence()`
|
||||
* - `{@link animations/group group()}`
|
||||
* - `{@link /api/animations/group group()}`
|
||||
* - `query()`
|
||||
* - `animation()`
|
||||
* - `useAnimation()`
|
||||
@@ -118,7 +118,7 @@ export enum AnimationMetadataType {
|
||||
Sequence = 2,
|
||||
/**
|
||||
* Contains a set of animation steps.
|
||||
* See `{@link animations/group group()}`
|
||||
* See `{@link /api/animations/group group()}`
|
||||
*/
|
||||
Group = 3,
|
||||
/**
|
||||
@@ -403,7 +403,7 @@ export interface AnimationSequenceMetadata extends AnimationMetadata {
|
||||
|
||||
/**
|
||||
* Encapsulates an animation group.
|
||||
* Instantiated and returned by the `{@link animations/group group()}` function.
|
||||
* Instantiated and returned by the `group()` function.
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
@@ -636,7 +636,7 @@ export function trigger(name: string, definitions: AnimationMetadata[]): Animati
|
||||
* @returns An object that encapsulates the animation step.
|
||||
*
|
||||
* @usageNotes
|
||||
* Call within an animation `sequence()`, `{@link animations/group group()}`, or
|
||||
* Call within an animation `sequence()`, {@link /api/animations/group group()}, or
|
||||
* `transition()` call to specify an animation step
|
||||
* that applies given style data to the parent animation for a given amount of time.
|
||||
*
|
||||
@@ -740,10 +740,10 @@ export function group(
|
||||
* @usageNotes
|
||||
* When you pass an array of steps to a
|
||||
* `transition()` call, the steps run sequentially by default.
|
||||
* Compare this to the `{@link animations/group group()}` call, which runs animation steps in
|
||||
* Compare this to the {@link /api/animations/group group()} call, which runs animation steps in
|
||||
*parallel.
|
||||
*
|
||||
* When a sequence is used within a `{@link animations/group group()}` or a `transition()` call,
|
||||
* When a sequence is used within a {@link /api/animations/group group()} or a `transition()` call,
|
||||
* execution continues to the next instruction only after each of the inner animation
|
||||
* steps have completed.
|
||||
*
|
||||
|
||||
@@ -65,6 +65,10 @@ describe('@angular/core ng_package', () => {
|
||||
types: './index.d.ts',
|
||||
default: './fesm2022/core.mjs',
|
||||
},
|
||||
'./primitives/di': {
|
||||
types: './primitives/di/index.d.ts',
|
||||
default: './fesm2022/primitives/di.mjs',
|
||||
},
|
||||
'./primitives/event-dispatch': {
|
||||
types: './primitives/event-dispatch/index.d.ts',
|
||||
default: './fesm2022/primitives/event-dispatch.mjs',
|
||||
|
||||
@@ -70,7 +70,7 @@ License: MIT
|
||||
|
||||
/**
|
||||
* @license Angular v0.0.0
|
||||
* (c) 2010-2024 Google LLC. https://angular.io/
|
||||
* (c) 2010-2025 Google LLC. https://angular.io/
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
@@ -120,7 +120,7 @@ Hello
|
||||
|
||||
/**
|
||||
* @license Angular v0.0.0
|
||||
* (c) 2010-2024 Google LLC. https://angular.io/
|
||||
* (c) 2010-2025 Google LLC. https://angular.io/
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
@@ -149,7 +149,7 @@ export { A11yModule };
|
||||
|
||||
/**
|
||||
* @license Angular v0.0.0
|
||||
* (c) 2010-2024 Google LLC. https://angular.io/
|
||||
* (c) 2010-2025 Google LLC. https://angular.io/
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
@@ -190,7 +190,7 @@ export { MyService };
|
||||
|
||||
/**
|
||||
* @license Angular v0.0.0
|
||||
* (c) 2010-2024 Google LLC. https://angular.io/
|
||||
* (c) 2010-2025 Google LLC. https://angular.io/
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
@@ -220,7 +220,7 @@ export { SecondaryModule, a };
|
||||
|
||||
/**
|
||||
* @license Angular v0.0.0
|
||||
* (c) 2010-2024 Google LLC. https://angular.io/
|
||||
* (c) 2010-2025 Google LLC. https://angular.io/
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
@@ -249,7 +249,7 @@ export { MyModule };
|
||||
|
||||
/**
|
||||
* @license Angular v0.0.0
|
||||
* (c) 2010-2024 Google LLC. https://angular.io/
|
||||
* (c) 2010-2025 Google LLC. https://angular.io/
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
@@ -275,7 +275,7 @@ export { }
|
||||
|
||||
/**
|
||||
* @license Angular v0.0.0
|
||||
* (c) 2010-2024 Google LLC. https://angular.io/
|
||||
* (c) 2010-2025 Google LLC. https://angular.io/
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
@@ -331,7 +331,7 @@ export { }
|
||||
|
||||
/**
|
||||
* @license Angular v0.0.0
|
||||
* (c) 2010-2024 Google LLC. https://angular.io/
|
||||
* (c) 2010-2025 Google LLC. https://angular.io/
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ License: MIT
|
||||
|
||||
/**
|
||||
* @license Angular v0.0.0
|
||||
* (c) 2010-2024 Google LLC. https://angular.io/
|
||||
* (c) 2010-2025 Google LLC. https://angular.io/
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
@@ -68,7 +68,7 @@ export { VERSION };
|
||||
|
||||
/**
|
||||
* @license Angular v0.0.0
|
||||
* (c) 2010-2024 Google LLC. https://angular.io/
|
||||
* (c) 2010-2025 Google LLC. https://angular.io/
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
@@ -98,7 +98,7 @@ export { PortalModule, a };
|
||||
|
||||
/**
|
||||
* @license Angular v0.0.0
|
||||
* (c) 2010-2024 Google LLC. https://angular.io/
|
||||
* (c) 2010-2025 Google LLC. https://angular.io/
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
@@ -114,7 +114,7 @@ export { dispatchFakeEvent };
|
||||
|
||||
/**
|
||||
* @license Angular v0.0.0
|
||||
* (c) 2010-2024 Google LLC. https://angular.io/
|
||||
* (c) 2010-2025 Google LLC. https://angular.io/
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
@@ -157,7 +157,7 @@ export { }
|
||||
|
||||
/**
|
||||
* @license Angular v0.0.0
|
||||
* (c) 2010-2024 Google LLC. https://angular.io/
|
||||
* (c) 2010-2025 Google LLC. https://angular.io/
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
@@ -179,7 +179,7 @@ export { }
|
||||
|
||||
/**
|
||||
* @license Angular v0.0.0
|
||||
* (c) 2010-2024 Google LLC. https://angular.io/
|
||||
* (c) 2010-2025 Google LLC. https://angular.io/
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -46,9 +46,11 @@ export class JsonFileReporter extends Reporter {
|
||||
@Inject(Options.NOW) private _now: Function,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.textReporter = new TextReporterBase(this._columnWidth, this._description);
|
||||
}
|
||||
|
||||
private textReporter = new TextReporterBase(this._columnWidth, this._description);
|
||||
private textReporter: TextReporterBase;
|
||||
|
||||
override reportMeasureValues(measureValues: MeasureValues): Promise<any> {
|
||||
return Promise.resolve(null);
|
||||
|
||||
@@ -10,10 +10,7 @@ const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
baseDir: '../',
|
||||
goldenFile: '../goldens/circular-deps/packages.json',
|
||||
glob: `./**/*.ts`,
|
||||
// Command that will be displayed if the golden needs to be updated.
|
||||
approveCommand: 'yarn ts-circular-deps:approve',
|
||||
resolveModule: resolveModule,
|
||||
ignoreTypeOnlyChecks: true,
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ ng_module(
|
||||
),
|
||||
deps = [
|
||||
"//packages/core",
|
||||
"//packages/core/primitives/dom-navigation",
|
||||
"@npm//rxjs",
|
||||
],
|
||||
)
|
||||
|
||||
+261
-262
File diff suppressed because it is too large
Load Diff
@@ -239,7 +239,7 @@ function makeHttpResourceFn<TRaw>(responseType: 'arraybuffer' | 'blob' | 'json'
|
||||
injector,
|
||||
() => normalizeRequest(request, responseType),
|
||||
options?.defaultValue,
|
||||
options?.map as (value: unknown) => TResult,
|
||||
options?.parse as (value: unknown) => TResult,
|
||||
) as HttpResourceRef<TResult>;
|
||||
};
|
||||
}
|
||||
@@ -312,7 +312,7 @@ class HttpResourceImpl<T>
|
||||
injector: Injector,
|
||||
request: () => HttpRequest<T> | undefined,
|
||||
defaultValue: T,
|
||||
map?: (value: unknown) => T,
|
||||
parse?: (value: unknown) => T,
|
||||
) {
|
||||
super(
|
||||
request,
|
||||
@@ -342,7 +342,7 @@ class HttpResourceImpl<T>
|
||||
this._headers.set(event.headers);
|
||||
this._statusCode.set(event.status);
|
||||
try {
|
||||
send({value: map ? map(event.body) : (event.body as T)});
|
||||
send({value: parse ? parse(event.body) : (event.body as T)});
|
||||
} catch (error) {
|
||||
send({error});
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {Injector, ResourceRef, Signal, ValueEqualityFn, WritableResource} f
|
||||
import type {HttpHeaders} from './headers';
|
||||
import type {HttpParams} from './params';
|
||||
import type {HttpProgressEvent} from './response';
|
||||
import {HttpContext} from './context';
|
||||
|
||||
/**
|
||||
* The structure of an `httpResource` request which will be sent to the backend.
|
||||
@@ -50,6 +51,11 @@ export interface HttpResourceRequest {
|
||||
*/
|
||||
headers?: HttpHeaders | Record<string, string | ReadonlyArray<string>>;
|
||||
|
||||
/**
|
||||
* Context of the request stored in a dictionary of key-value pairs.
|
||||
*/
|
||||
context?: HttpContext;
|
||||
|
||||
/**
|
||||
* If `true`, progress events will be enabled for the request and delivered through the
|
||||
* `HttpResource.progress` signal.
|
||||
@@ -81,12 +87,12 @@ export interface HttpResourceOptions<TResult, TRaw> {
|
||||
/**
|
||||
* Transform the result of the HTTP request before it's delivered to the resource.
|
||||
*
|
||||
* `map` receives the value from the HTTP layer as its raw type (e.g. as `unknown` for JSON data).
|
||||
* `parse` receives the value from the HTTP layer as its raw type (e.g. as `unknown` for JSON data).
|
||||
* It can be used to validate or transform the type of the resource, and return a more specific
|
||||
* type. This is also useful for validating backend responses using a runtime schema validation
|
||||
* library such as Zod.
|
||||
*/
|
||||
map?: (value: TRaw) => TResult;
|
||||
parse?: (value: TRaw) => TResult;
|
||||
|
||||
/**
|
||||
* Value that the resource will take when in Idle, Loading, or Error states.
|
||||
|
||||
@@ -154,7 +154,7 @@ describe('httpResource', () => {
|
||||
},
|
||||
{
|
||||
injector: TestBed.inject(Injector),
|
||||
map: (value) => JSON.stringify(value),
|
||||
parse: (value) => JSON.stringify(value),
|
||||
},
|
||||
);
|
||||
TestBed.flushEffects();
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
} from '@angular/core';
|
||||
|
||||
/**
|
||||
* Instantiates a {@link Component} type and inserts its Host View into the current View.
|
||||
* Instantiates a {@link /api/core/Component Component} type and inserts its Host View into the current View.
|
||||
* `NgComponentOutlet` provides a declarative approach for dynamic component creation.
|
||||
*
|
||||
* `NgComponentOutlet` requires a component type, if a falsy value is set the view will clear and
|
||||
|
||||
@@ -26,8 +26,8 @@ import {DOCUMENT} from '../dom_tokens';
|
||||
* when they need to interact with the DOM APIs like pushState, popState, etc.
|
||||
*
|
||||
* {@link LocationStrategy} in turn is used by the {@link Location} service which is used directly
|
||||
* by the {@link Router} in order to navigate between routes. Since all interactions between {@link
|
||||
* Router} /
|
||||
* by the {@link /api/router/Router Router} in order to navigate between routes. Since all interactions between
|
||||
* {@link /api/router/Router Router} /
|
||||
* {@link Location} / {@link LocationStrategy} and DOM APIs flow through the `PlatformLocation`
|
||||
* class, they are all platform-agnostic.
|
||||
*
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
export interface NavigationEventMap {
|
||||
navigate: NavigateEvent;
|
||||
navigatesuccess: Event;
|
||||
navigateerror: ErrorEvent;
|
||||
currententrychange: NavigationCurrentEntryChangeEvent;
|
||||
}
|
||||
|
||||
export interface NavigationResult {
|
||||
committed: Promise<NavigationHistoryEntry>;
|
||||
finished: Promise<NavigationHistoryEntry>;
|
||||
}
|
||||
|
||||
export declare class Navigation extends EventTarget {
|
||||
entries(): NavigationHistoryEntry[];
|
||||
readonly currentEntry: NavigationHistoryEntry | null;
|
||||
updateCurrentEntry(options: NavigationUpdateCurrentEntryOptions): void;
|
||||
readonly transition: NavigationTransition | null;
|
||||
|
||||
readonly canGoBack: boolean;
|
||||
readonly canGoForward: boolean;
|
||||
|
||||
navigate(url: string, options?: NavigationNavigateOptions): NavigationResult;
|
||||
reload(options?: NavigationReloadOptions): NavigationResult;
|
||||
|
||||
traverseTo(key: string, options?: NavigationOptions): NavigationResult;
|
||||
back(options?: NavigationOptions): NavigationResult;
|
||||
forward(options?: NavigationOptions): NavigationResult;
|
||||
|
||||
onnavigate: ((this: Navigation, ev: NavigateEvent) => any) | null;
|
||||
onnavigatesuccess: ((this: Navigation, ev: Event) => any) | null;
|
||||
onnavigateerror: ((this: Navigation, ev: ErrorEvent) => any) | null;
|
||||
oncurrententrychange: ((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any) | null;
|
||||
|
||||
addEventListener<K extends keyof NavigationEventMap>(
|
||||
type: K,
|
||||
listener: (this: Navigation, ev: NavigationEventMap[K]) => any,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
addEventListener(
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
removeEventListener<K extends keyof NavigationEventMap>(
|
||||
type: K,
|
||||
listener: (this: Navigation, ev: NavigationEventMap[K]) => any,
|
||||
options?: boolean | EventListenerOptions,
|
||||
): void;
|
||||
removeEventListener(
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: boolean | EventListenerOptions,
|
||||
): void;
|
||||
}
|
||||
|
||||
export declare class NavigationTransition {
|
||||
readonly navigationType: NavigationTypeString;
|
||||
readonly from: NavigationHistoryEntry;
|
||||
readonly finished: Promise<void>;
|
||||
}
|
||||
|
||||
export interface NavigationHistoryEntryEventMap {
|
||||
dispose: Event;
|
||||
}
|
||||
|
||||
export declare class NavigationHistoryEntry extends EventTarget {
|
||||
readonly key: string;
|
||||
readonly id: string;
|
||||
readonly url: string | null;
|
||||
readonly index: number;
|
||||
readonly sameDocument: boolean;
|
||||
|
||||
getState(): unknown;
|
||||
|
||||
ondispose: ((this: NavigationHistoryEntry, ev: Event) => any) | null;
|
||||
|
||||
addEventListener<K extends keyof NavigationHistoryEntryEventMap>(
|
||||
type: K,
|
||||
listener: (this: NavigationHistoryEntry, ev: NavigationHistoryEntryEventMap[K]) => any,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
addEventListener(
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
removeEventListener<K extends keyof NavigationHistoryEntryEventMap>(
|
||||
type: K,
|
||||
listener: (this: NavigationHistoryEntry, ev: NavigationHistoryEntryEventMap[K]) => any,
|
||||
options?: boolean | EventListenerOptions,
|
||||
): void;
|
||||
removeEventListener(
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: boolean | EventListenerOptions,
|
||||
): void;
|
||||
}
|
||||
|
||||
type NavigationTypeString = 'reload' | 'push' | 'replace' | 'traverse';
|
||||
|
||||
export interface NavigationUpdateCurrentEntryOptions {
|
||||
state: unknown;
|
||||
}
|
||||
|
||||
export interface NavigationOptions {
|
||||
info?: unknown;
|
||||
}
|
||||
|
||||
export interface NavigationNavigateOptions extends NavigationOptions {
|
||||
state?: unknown;
|
||||
history?: 'auto' | 'push' | 'replace';
|
||||
}
|
||||
|
||||
export interface NavigationReloadOptions extends NavigationOptions {
|
||||
state?: unknown;
|
||||
}
|
||||
|
||||
export declare class NavigationCurrentEntryChangeEvent extends Event {
|
||||
constructor(type: string, eventInit?: NavigationCurrentEntryChangeEventInit);
|
||||
|
||||
readonly navigationType: NavigationTypeString | null;
|
||||
readonly from: NavigationHistoryEntry;
|
||||
}
|
||||
|
||||
export interface NavigationCurrentEntryChangeEventInit extends EventInit {
|
||||
navigationType?: NavigationTypeString | null;
|
||||
from: NavigationHistoryEntry;
|
||||
}
|
||||
|
||||
export declare class NavigateEvent extends Event {
|
||||
constructor(type: string, eventInit?: NavigateEventInit);
|
||||
|
||||
readonly navigationType: NavigationTypeString;
|
||||
readonly canIntercept: boolean;
|
||||
readonly userInitiated: boolean;
|
||||
readonly hashChange: boolean;
|
||||
readonly destination: NavigationDestination;
|
||||
readonly signal: AbortSignal;
|
||||
readonly formData: FormData | null;
|
||||
readonly downloadRequest: string | null;
|
||||
readonly info?: unknown;
|
||||
|
||||
intercept(options?: NavigationInterceptOptions): void;
|
||||
scroll(): void;
|
||||
}
|
||||
|
||||
export interface NavigateEventInit extends EventInit {
|
||||
navigationType?: NavigationTypeString;
|
||||
canIntercept?: boolean;
|
||||
userInitiated?: boolean;
|
||||
hashChange?: boolean;
|
||||
destination: NavigationDestination;
|
||||
signal: AbortSignal;
|
||||
formData?: FormData | null;
|
||||
downloadRequest?: string | null;
|
||||
info?: unknown;
|
||||
}
|
||||
|
||||
export interface NavigationInterceptOptions {
|
||||
handler?: () => Promise<void>;
|
||||
focusReset?: 'after-transition' | 'manual';
|
||||
scroll?: 'after-transition' | 'manual';
|
||||
}
|
||||
|
||||
export declare class NavigationDestination {
|
||||
readonly url: string;
|
||||
readonly key: string | null;
|
||||
readonly id: string | null;
|
||||
readonly index: number;
|
||||
readonly sameDocument: boolean;
|
||||
|
||||
getState(): unknown;
|
||||
}
|
||||
@@ -6,20 +6,19 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
import {
|
||||
NavigateEvent,
|
||||
Navigation,
|
||||
NavigationCurrentEntryChangeEvent,
|
||||
NavigationHistoryEntry,
|
||||
NavigationNavigateOptions,
|
||||
NavigationOptions,
|
||||
NavigationReloadOptions,
|
||||
NavigationResult,
|
||||
NavigationTransition,
|
||||
NavigationUpdateCurrentEntryOptions,
|
||||
} from './navigation_types';
|
||||
Injectable,
|
||||
ɵNavigateEvent as NavigateEvent,
|
||||
ɵNavigation as Navigation,
|
||||
ɵNavigationCurrentEntryChangeEvent as NavigationCurrentEntryChangeEvent,
|
||||
ɵNavigationHistoryEntry as NavigationHistoryEntry,
|
||||
ɵNavigationNavigateOptions as NavigationNavigateOptions,
|
||||
ɵNavigationOptions as NavigationOptions,
|
||||
ɵNavigationReloadOptions as NavigationReloadOptions,
|
||||
ɵNavigationResult as NavigationResult,
|
||||
ɵNavigationTransition as NavigationTransition,
|
||||
ɵNavigationUpdateCurrentEntryOptions as NavigationUpdateCurrentEntryOptions,
|
||||
} from '@angular/core';
|
||||
|
||||
/**
|
||||
* This class wraps the platform Navigation API which allows server-specific and test
|
||||
|
||||
@@ -10,6 +10,7 @@ ng_module(
|
||||
deps = [
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
"@npm//rxjs",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -6,972 +6,4 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {
|
||||
NavigateEvent,
|
||||
Navigation,
|
||||
NavigationCurrentEntryChangeEvent,
|
||||
NavigationDestination,
|
||||
NavigationHistoryEntry,
|
||||
NavigationInterceptOptions,
|
||||
NavigationNavigateOptions,
|
||||
NavigationOptions,
|
||||
NavigationReloadOptions,
|
||||
NavigationResult,
|
||||
NavigationTransition,
|
||||
NavigationTypeString,
|
||||
NavigationUpdateCurrentEntryOptions,
|
||||
} from './navigation_types';
|
||||
|
||||
/**
|
||||
* Fake implementation of user agent history and navigation behavior. This is a
|
||||
* high-fidelity implementation of browser behavior that attempts to emulate
|
||||
* things like traversal delay.
|
||||
*/
|
||||
export class FakeNavigation implements Navigation {
|
||||
/**
|
||||
* The fake implementation of an entries array. Only same-document entries
|
||||
* allowed.
|
||||
*/
|
||||
private readonly entriesArr: FakeNavigationHistoryEntry[] = [];
|
||||
|
||||
/**
|
||||
* The current active entry index into `entriesArr`.
|
||||
*/
|
||||
private currentEntryIndex = 0;
|
||||
|
||||
/**
|
||||
* The current navigate event.
|
||||
*/
|
||||
private navigateEvent: InternalFakeNavigateEvent | undefined = undefined;
|
||||
|
||||
/**
|
||||
* A Map of pending traversals, so that traversals to the same entry can be
|
||||
* re-used.
|
||||
*/
|
||||
private readonly traversalQueue = new Map<string, InternalNavigationResult>();
|
||||
|
||||
/**
|
||||
* A Promise that resolves when the previous traversals have finished. Used to
|
||||
* simulate the cross-process communication necessary for traversals.
|
||||
*/
|
||||
private nextTraversal = Promise.resolve();
|
||||
|
||||
/**
|
||||
* A prospective current active entry index, which includes unresolved
|
||||
* traversals. Used by `go` to determine where navigations are intended to go.
|
||||
*/
|
||||
private prospectiveEntryIndex = 0;
|
||||
|
||||
/**
|
||||
* A test-only option to make traversals synchronous, rather than emulate
|
||||
* cross-process communication.
|
||||
*/
|
||||
private synchronousTraversals = false;
|
||||
|
||||
/** Whether to allow a call to setInitialEntryForTesting. */
|
||||
private canSetInitialEntry = true;
|
||||
|
||||
/** `EventTarget` to dispatch events. */
|
||||
private eventTarget: EventTarget;
|
||||
|
||||
/** The next unique id for created entries. Replace recreates this id. */
|
||||
private nextId = 0;
|
||||
|
||||
/** The next unique key for created entries. Replace inherits this id. */
|
||||
private nextKey = 0;
|
||||
|
||||
/** Whether this fake is disposed. */
|
||||
private disposed = false;
|
||||
|
||||
/** Equivalent to `navigation.currentEntry`. */
|
||||
get currentEntry(): FakeNavigationHistoryEntry {
|
||||
return this.entriesArr[this.currentEntryIndex];
|
||||
}
|
||||
|
||||
get canGoBack(): boolean {
|
||||
return this.currentEntryIndex > 0;
|
||||
}
|
||||
|
||||
get canGoForward(): boolean {
|
||||
return this.currentEntryIndex < this.entriesArr.length - 1;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly window: Window,
|
||||
startURL: `http${string}`,
|
||||
) {
|
||||
this.eventTarget = this.window.document.createElement('div');
|
||||
// First entry.
|
||||
this.setInitialEntryForTesting(startURL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the initial entry.
|
||||
*/
|
||||
private setInitialEntryForTesting(
|
||||
url: `http${string}`,
|
||||
options: {historyState: unknown; state?: unknown} = {historyState: null},
|
||||
) {
|
||||
if (!this.canSetInitialEntry) {
|
||||
throw new Error(
|
||||
'setInitialEntryForTesting can only be called before any ' + 'navigation has occurred',
|
||||
);
|
||||
}
|
||||
const currentInitialEntry = this.entriesArr[0];
|
||||
this.entriesArr[0] = new FakeNavigationHistoryEntry(new URL(url).toString(), {
|
||||
index: 0,
|
||||
key: currentInitialEntry?.key ?? String(this.nextKey++),
|
||||
id: currentInitialEntry?.id ?? String(this.nextId++),
|
||||
sameDocument: true,
|
||||
historyState: options?.historyState,
|
||||
state: options.state,
|
||||
});
|
||||
}
|
||||
|
||||
/** Returns whether the initial entry is still eligible to be set. */
|
||||
canSetInitialEntryForTesting(): boolean {
|
||||
return this.canSetInitialEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to emulate traversals as synchronous rather than
|
||||
* asynchronous.
|
||||
*/
|
||||
setSynchronousTraversalsForTesting(synchronousTraversals: boolean) {
|
||||
this.synchronousTraversals = synchronousTraversals;
|
||||
}
|
||||
|
||||
/** Equivalent to `navigation.entries()`. */
|
||||
entries(): FakeNavigationHistoryEntry[] {
|
||||
return this.entriesArr.slice();
|
||||
}
|
||||
|
||||
/** Equivalent to `navigation.navigate()`. */
|
||||
navigate(url: string, options?: NavigationNavigateOptions): FakeNavigationResult {
|
||||
const fromUrl = new URL(this.currentEntry.url!);
|
||||
const toUrl = new URL(url, this.currentEntry.url!);
|
||||
|
||||
let navigationType: NavigationTypeString;
|
||||
if (!options?.history || options.history === 'auto') {
|
||||
// Auto defaults to push, but if the URLs are the same, is a replace.
|
||||
if (fromUrl.toString() === toUrl.toString()) {
|
||||
navigationType = 'replace';
|
||||
} else {
|
||||
navigationType = 'push';
|
||||
}
|
||||
} else {
|
||||
navigationType = options.history;
|
||||
}
|
||||
|
||||
const hashChange = isHashChange(fromUrl, toUrl);
|
||||
|
||||
const destination = new FakeNavigationDestination({
|
||||
url: toUrl.toString(),
|
||||
state: options?.state,
|
||||
sameDocument: hashChange,
|
||||
historyState: null,
|
||||
});
|
||||
const result = new InternalNavigationResult();
|
||||
|
||||
this.userAgentNavigate(destination, result, {
|
||||
navigationType,
|
||||
cancelable: true,
|
||||
canIntercept: true,
|
||||
// Always false for navigate().
|
||||
userInitiated: false,
|
||||
hashChange,
|
||||
info: options?.info,
|
||||
});
|
||||
|
||||
return {
|
||||
committed: result.committed,
|
||||
finished: result.finished,
|
||||
};
|
||||
}
|
||||
|
||||
/** Equivalent to `history.pushState()`. */
|
||||
pushState(data: unknown, title: string, url?: string): void {
|
||||
this.pushOrReplaceState('push', data, title, url);
|
||||
}
|
||||
|
||||
/** Equivalent to `history.replaceState()`. */
|
||||
replaceState(data: unknown, title: string, url?: string): void {
|
||||
this.pushOrReplaceState('replace', data, title, url);
|
||||
}
|
||||
|
||||
private pushOrReplaceState(
|
||||
navigationType: NavigationTypeString,
|
||||
data: unknown,
|
||||
_title: string,
|
||||
url?: string,
|
||||
): void {
|
||||
const fromUrl = new URL(this.currentEntry.url!);
|
||||
const toUrl = url ? new URL(url, this.currentEntry.url!) : fromUrl;
|
||||
|
||||
const hashChange = isHashChange(fromUrl, toUrl);
|
||||
|
||||
const destination = new FakeNavigationDestination({
|
||||
url: toUrl.toString(),
|
||||
sameDocument: true,
|
||||
historyState: data,
|
||||
});
|
||||
const result = new InternalNavigationResult();
|
||||
|
||||
this.userAgentNavigate(destination, result, {
|
||||
navigationType,
|
||||
cancelable: true,
|
||||
canIntercept: true,
|
||||
// Always false for pushState() or replaceState().
|
||||
userInitiated: false,
|
||||
hashChange,
|
||||
skipPopState: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** Equivalent to `navigation.traverseTo()`. */
|
||||
traverseTo(key: string, options?: NavigationOptions): FakeNavigationResult {
|
||||
const fromUrl = new URL(this.currentEntry.url!);
|
||||
const entry = this.findEntry(key);
|
||||
if (!entry) {
|
||||
const domException = new DOMException('Invalid key', 'InvalidStateError');
|
||||
const committed = Promise.reject(domException);
|
||||
const finished = Promise.reject(domException);
|
||||
committed.catch(() => {});
|
||||
finished.catch(() => {});
|
||||
return {
|
||||
committed,
|
||||
finished,
|
||||
};
|
||||
}
|
||||
if (entry === this.currentEntry) {
|
||||
return {
|
||||
committed: Promise.resolve(this.currentEntry),
|
||||
finished: Promise.resolve(this.currentEntry),
|
||||
};
|
||||
}
|
||||
if (this.traversalQueue.has(entry.key)) {
|
||||
const existingResult = this.traversalQueue.get(entry.key)!;
|
||||
return {
|
||||
committed: existingResult.committed,
|
||||
finished: existingResult.finished,
|
||||
};
|
||||
}
|
||||
|
||||
const hashChange = isHashChange(fromUrl, new URL(entry.url!, this.currentEntry.url!));
|
||||
const destination = new FakeNavigationDestination({
|
||||
url: entry.url!,
|
||||
state: entry.getState(),
|
||||
historyState: entry.getHistoryState(),
|
||||
key: entry.key,
|
||||
id: entry.id,
|
||||
index: entry.index,
|
||||
sameDocument: entry.sameDocument,
|
||||
});
|
||||
this.prospectiveEntryIndex = entry.index;
|
||||
const result = new InternalNavigationResult();
|
||||
this.traversalQueue.set(entry.key, result);
|
||||
this.runTraversal(() => {
|
||||
this.traversalQueue.delete(entry.key);
|
||||
this.userAgentNavigate(destination, result, {
|
||||
navigationType: 'traverse',
|
||||
cancelable: true,
|
||||
canIntercept: true,
|
||||
// Always false for traverseTo().
|
||||
userInitiated: false,
|
||||
hashChange,
|
||||
info: options?.info,
|
||||
});
|
||||
});
|
||||
return {
|
||||
committed: result.committed,
|
||||
finished: result.finished,
|
||||
};
|
||||
}
|
||||
|
||||
/** Equivalent to `navigation.back()`. */
|
||||
back(options?: NavigationOptions): FakeNavigationResult {
|
||||
if (this.currentEntryIndex === 0) {
|
||||
const domException = new DOMException('Cannot go back', 'InvalidStateError');
|
||||
const committed = Promise.reject(domException);
|
||||
const finished = Promise.reject(domException);
|
||||
committed.catch(() => {});
|
||||
finished.catch(() => {});
|
||||
return {
|
||||
committed,
|
||||
finished,
|
||||
};
|
||||
}
|
||||
const entry = this.entriesArr[this.currentEntryIndex - 1];
|
||||
return this.traverseTo(entry.key, options);
|
||||
}
|
||||
|
||||
/** Equivalent to `navigation.forward()`. */
|
||||
forward(options?: NavigationOptions): FakeNavigationResult {
|
||||
if (this.currentEntryIndex === this.entriesArr.length - 1) {
|
||||
const domException = new DOMException('Cannot go forward', 'InvalidStateError');
|
||||
const committed = Promise.reject(domException);
|
||||
const finished = Promise.reject(domException);
|
||||
committed.catch(() => {});
|
||||
finished.catch(() => {});
|
||||
return {
|
||||
committed,
|
||||
finished,
|
||||
};
|
||||
}
|
||||
const entry = this.entriesArr[this.currentEntryIndex + 1];
|
||||
return this.traverseTo(entry.key, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to `history.go()`.
|
||||
* Note that this method does not actually work precisely to how Chrome
|
||||
* does, instead choosing a simpler model with less unexpected behavior.
|
||||
* Chrome has a few edge case optimizations, for instance with repeated
|
||||
* `back(); forward()` chains it collapses certain traversals.
|
||||
*/
|
||||
go(direction: number): void {
|
||||
const targetIndex = this.prospectiveEntryIndex + direction;
|
||||
if (targetIndex >= this.entriesArr.length || targetIndex < 0) {
|
||||
return;
|
||||
}
|
||||
this.prospectiveEntryIndex = targetIndex;
|
||||
this.runTraversal(() => {
|
||||
// Check again that destination is in the entries array.
|
||||
if (targetIndex >= this.entriesArr.length || targetIndex < 0) {
|
||||
return;
|
||||
}
|
||||
const fromUrl = new URL(this.currentEntry.url!);
|
||||
const entry = this.entriesArr[targetIndex];
|
||||
const hashChange = isHashChange(fromUrl, new URL(entry.url!, this.currentEntry.url!));
|
||||
const destination = new FakeNavigationDestination({
|
||||
url: entry.url!,
|
||||
state: entry.getState(),
|
||||
historyState: entry.getHistoryState(),
|
||||
key: entry.key,
|
||||
id: entry.id,
|
||||
index: entry.index,
|
||||
sameDocument: entry.sameDocument,
|
||||
});
|
||||
const result = new InternalNavigationResult();
|
||||
this.userAgentNavigate(destination, result, {
|
||||
navigationType: 'traverse',
|
||||
cancelable: true,
|
||||
canIntercept: true,
|
||||
// Always false for go().
|
||||
userInitiated: false,
|
||||
hashChange,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Runs a traversal synchronously or asynchronously */
|
||||
private runTraversal(traversal: () => void) {
|
||||
if (this.synchronousTraversals) {
|
||||
traversal();
|
||||
return;
|
||||
}
|
||||
|
||||
// Each traversal occupies a single timeout resolution.
|
||||
// This means that Promises added to commit and finish should resolve
|
||||
// before the next traversal.
|
||||
this.nextTraversal = this.nextTraversal.then(() => {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
traversal();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Equivalent to `navigation.addEventListener()`. */
|
||||
addEventListener(
|
||||
type: string,
|
||||
callback: EventListenerOrEventListenerObject,
|
||||
options?: AddEventListenerOptions | boolean,
|
||||
) {
|
||||
this.eventTarget.addEventListener(type, callback, options);
|
||||
}
|
||||
|
||||
/** Equivalent to `navigation.removeEventListener()`. */
|
||||
removeEventListener(
|
||||
type: string,
|
||||
callback: EventListenerOrEventListenerObject,
|
||||
options?: EventListenerOptions | boolean,
|
||||
) {
|
||||
this.eventTarget.removeEventListener(type, callback, options);
|
||||
}
|
||||
|
||||
/** Equivalent to `navigation.dispatchEvent()` */
|
||||
dispatchEvent(event: Event): boolean {
|
||||
return this.eventTarget.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/** Cleans up resources. */
|
||||
dispose() {
|
||||
// Recreate eventTarget to release current listeners.
|
||||
// `document.createElement` because NodeJS `EventTarget` is incompatible with Domino's `Event`.
|
||||
this.eventTarget = this.window.document.createElement('div');
|
||||
this.disposed = true;
|
||||
}
|
||||
|
||||
/** Returns whether this fake is disposed. */
|
||||
isDisposed() {
|
||||
return this.disposed;
|
||||
}
|
||||
|
||||
/** Implementation for all navigations and traversals. */
|
||||
private userAgentNavigate(
|
||||
destination: FakeNavigationDestination,
|
||||
result: InternalNavigationResult,
|
||||
options: InternalNavigateOptions,
|
||||
) {
|
||||
// The first navigation should disallow any future calls to set the initial
|
||||
// entry.
|
||||
this.canSetInitialEntry = false;
|
||||
if (this.navigateEvent) {
|
||||
this.navigateEvent.cancel(new DOMException('Navigation was aborted', 'AbortError'));
|
||||
this.navigateEvent = undefined;
|
||||
}
|
||||
|
||||
const navigateEvent = createFakeNavigateEvent({
|
||||
navigationType: options.navigationType,
|
||||
cancelable: options.cancelable,
|
||||
canIntercept: options.canIntercept,
|
||||
userInitiated: options.userInitiated,
|
||||
hashChange: options.hashChange,
|
||||
signal: result.signal,
|
||||
destination,
|
||||
info: options.info,
|
||||
sameDocument: destination.sameDocument,
|
||||
skipPopState: options.skipPopState,
|
||||
result,
|
||||
userAgentCommit: () => {
|
||||
this.userAgentCommit();
|
||||
},
|
||||
});
|
||||
|
||||
this.navigateEvent = navigateEvent;
|
||||
this.eventTarget.dispatchEvent(navigateEvent);
|
||||
navigateEvent.dispatchedNavigateEvent();
|
||||
if (navigateEvent.commitOption === 'immediate') {
|
||||
navigateEvent.commit(/* internal= */ true);
|
||||
}
|
||||
}
|
||||
|
||||
/** Implementation to commit a navigation. */
|
||||
private userAgentCommit() {
|
||||
if (!this.navigateEvent) {
|
||||
return;
|
||||
}
|
||||
const from = this.currentEntry;
|
||||
if (!this.navigateEvent.sameDocument) {
|
||||
const error = new Error('Cannot navigate to a non-same-document URL.');
|
||||
this.navigateEvent.cancel(error);
|
||||
throw error;
|
||||
}
|
||||
if (
|
||||
this.navigateEvent.navigationType === 'push' ||
|
||||
this.navigateEvent.navigationType === 'replace'
|
||||
) {
|
||||
this.userAgentPushOrReplace(this.navigateEvent.destination, {
|
||||
navigationType: this.navigateEvent.navigationType,
|
||||
});
|
||||
} else if (this.navigateEvent.navigationType === 'traverse') {
|
||||
this.userAgentTraverse(this.navigateEvent.destination);
|
||||
}
|
||||
this.navigateEvent.userAgentNavigated(this.currentEntry);
|
||||
const currentEntryChangeEvent = createFakeNavigationCurrentEntryChangeEvent({
|
||||
from,
|
||||
navigationType: this.navigateEvent.navigationType,
|
||||
});
|
||||
this.eventTarget.dispatchEvent(currentEntryChangeEvent);
|
||||
if (!this.navigateEvent.skipPopState) {
|
||||
const popStateEvent = createPopStateEvent({
|
||||
state: this.navigateEvent.destination.getHistoryState(),
|
||||
});
|
||||
this.window.dispatchEvent(popStateEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/** Implementation for a push or replace navigation. */
|
||||
private userAgentPushOrReplace(
|
||||
destination: FakeNavigationDestination,
|
||||
{navigationType}: {navigationType: NavigationTypeString},
|
||||
) {
|
||||
if (navigationType === 'push') {
|
||||
this.currentEntryIndex++;
|
||||
this.prospectiveEntryIndex = this.currentEntryIndex;
|
||||
}
|
||||
const index = this.currentEntryIndex;
|
||||
const key = navigationType === 'push' ? String(this.nextKey++) : this.currentEntry.key;
|
||||
const entry = new FakeNavigationHistoryEntry(destination.url, {
|
||||
id: String(this.nextId++),
|
||||
key,
|
||||
index,
|
||||
sameDocument: true,
|
||||
state: destination.getState(),
|
||||
historyState: destination.getHistoryState(),
|
||||
});
|
||||
if (navigationType === 'push') {
|
||||
this.entriesArr.splice(index, Infinity, entry);
|
||||
} else {
|
||||
this.entriesArr[index] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
/** Implementation for a traverse navigation. */
|
||||
private userAgentTraverse(destination: FakeNavigationDestination) {
|
||||
this.currentEntryIndex = destination.index;
|
||||
}
|
||||
|
||||
/** Utility method for finding entries with the given `key`. */
|
||||
private findEntry(key: string) {
|
||||
for (const entry of this.entriesArr) {
|
||||
if (entry.key === key) return entry;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
set onnavigate(_handler: ((this: Navigation, ev: NavigateEvent) => any) | null) {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
|
||||
get onnavigate(): ((this: Navigation, ev: NavigateEvent) => any) | null {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
|
||||
set oncurrententrychange(
|
||||
_handler: ((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any) | null,
|
||||
) {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
|
||||
get oncurrententrychange():
|
||||
| ((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any)
|
||||
| null {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
|
||||
set onnavigatesuccess(_handler: ((this: Navigation, ev: Event) => any) | null) {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
|
||||
get onnavigatesuccess(): ((this: Navigation, ev: Event) => any) | null {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
|
||||
set onnavigateerror(_handler: ((this: Navigation, ev: ErrorEvent) => any) | null) {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
|
||||
get onnavigateerror(): ((this: Navigation, ev: ErrorEvent) => any) | null {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
|
||||
get transition(): NavigationTransition | null {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
|
||||
updateCurrentEntry(_options: NavigationUpdateCurrentEntryOptions): void {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
|
||||
reload(_options?: NavigationReloadOptions): NavigationResult {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake equivalent of the `NavigationResult` interface with
|
||||
* `FakeNavigationHistoryEntry`.
|
||||
*/
|
||||
interface FakeNavigationResult extends NavigationResult {
|
||||
readonly committed: Promise<FakeNavigationHistoryEntry>;
|
||||
readonly finished: Promise<FakeNavigationHistoryEntry>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake equivalent of `NavigationHistoryEntry`.
|
||||
*/
|
||||
export class FakeNavigationHistoryEntry implements NavigationHistoryEntry {
|
||||
readonly sameDocument;
|
||||
|
||||
readonly id: string;
|
||||
readonly key: string;
|
||||
readonly index: number;
|
||||
private readonly state: unknown;
|
||||
private readonly historyState: unknown;
|
||||
|
||||
ondispose: ((this: NavigationHistoryEntry, ev: Event) => any) | null = null;
|
||||
|
||||
constructor(
|
||||
readonly url: string | null,
|
||||
{
|
||||
id,
|
||||
key,
|
||||
index,
|
||||
sameDocument,
|
||||
state,
|
||||
historyState,
|
||||
}: {
|
||||
id: string;
|
||||
key: string;
|
||||
index: number;
|
||||
sameDocument: boolean;
|
||||
historyState: unknown;
|
||||
state?: unknown;
|
||||
},
|
||||
) {
|
||||
this.id = id;
|
||||
this.key = key;
|
||||
this.index = index;
|
||||
this.sameDocument = sameDocument;
|
||||
this.state = state;
|
||||
this.historyState = historyState;
|
||||
}
|
||||
|
||||
getState(): unknown {
|
||||
// Budget copy.
|
||||
return this.state ? JSON.parse(JSON.stringify(this.state)) : this.state;
|
||||
}
|
||||
|
||||
getHistoryState(): unknown {
|
||||
// Budget copy.
|
||||
return this.historyState ? JSON.parse(JSON.stringify(this.historyState)) : this.historyState;
|
||||
}
|
||||
|
||||
addEventListener(
|
||||
type: string,
|
||||
callback: EventListenerOrEventListenerObject,
|
||||
options?: AddEventListenerOptions | boolean,
|
||||
) {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
|
||||
removeEventListener(
|
||||
type: string,
|
||||
callback: EventListenerOrEventListenerObject,
|
||||
options?: EventListenerOptions | boolean,
|
||||
) {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
|
||||
dispatchEvent(event: Event): boolean {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
}
|
||||
|
||||
/** `NavigationInterceptOptions` with experimental commit option. */
|
||||
export interface ExperimentalNavigationInterceptOptions extends NavigationInterceptOptions {
|
||||
commit?: 'immediate' | 'after-transition';
|
||||
}
|
||||
|
||||
/** `NavigateEvent` with experimental commit function. */
|
||||
export interface ExperimentalNavigateEvent extends NavigateEvent {
|
||||
intercept(options?: ExperimentalNavigationInterceptOptions): void;
|
||||
|
||||
commit(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake equivalent of `NavigateEvent`.
|
||||
*/
|
||||
export interface FakeNavigateEvent extends ExperimentalNavigateEvent {
|
||||
readonly destination: FakeNavigationDestination;
|
||||
}
|
||||
|
||||
interface InternalFakeNavigateEvent extends FakeNavigateEvent {
|
||||
readonly sameDocument: boolean;
|
||||
readonly skipPopState?: boolean;
|
||||
readonly commitOption: 'after-transition' | 'immediate';
|
||||
readonly result: InternalNavigationResult;
|
||||
|
||||
commit(internal?: boolean): void;
|
||||
cancel(reason: Error): void;
|
||||
dispatchedNavigateEvent(): void;
|
||||
userAgentNavigated(entry: FakeNavigationHistoryEntry): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fake equivalent of `NavigateEvent`. This is not a class because ES5
|
||||
* transpiled JavaScript cannot extend native Event.
|
||||
*/
|
||||
function createFakeNavigateEvent({
|
||||
cancelable,
|
||||
canIntercept,
|
||||
userInitiated,
|
||||
hashChange,
|
||||
navigationType,
|
||||
signal,
|
||||
destination,
|
||||
info,
|
||||
sameDocument,
|
||||
skipPopState,
|
||||
result,
|
||||
userAgentCommit,
|
||||
}: {
|
||||
cancelable: boolean;
|
||||
canIntercept: boolean;
|
||||
userInitiated: boolean;
|
||||
hashChange: boolean;
|
||||
navigationType: NavigationTypeString;
|
||||
signal: AbortSignal;
|
||||
destination: FakeNavigationDestination;
|
||||
info: unknown;
|
||||
sameDocument: boolean;
|
||||
skipPopState?: boolean;
|
||||
result: InternalNavigationResult;
|
||||
userAgentCommit: () => void;
|
||||
}) {
|
||||
const event = new Event('navigate', {bubbles: false, cancelable}) as {
|
||||
-readonly [P in keyof InternalFakeNavigateEvent]: InternalFakeNavigateEvent[P];
|
||||
};
|
||||
event.canIntercept = canIntercept;
|
||||
event.userInitiated = userInitiated;
|
||||
event.hashChange = hashChange;
|
||||
event.navigationType = navigationType;
|
||||
event.signal = signal;
|
||||
event.destination = destination;
|
||||
event.info = info;
|
||||
event.downloadRequest = null;
|
||||
event.formData = null;
|
||||
|
||||
event.sameDocument = sameDocument;
|
||||
event.skipPopState = skipPopState;
|
||||
event.commitOption = 'immediate';
|
||||
|
||||
let handlerFinished: Promise<void> | undefined = undefined;
|
||||
let interceptCalled = false;
|
||||
let dispatchedNavigateEvent = false;
|
||||
let commitCalled = false;
|
||||
|
||||
event.intercept = function (
|
||||
this: InternalFakeNavigateEvent,
|
||||
options?: ExperimentalNavigationInterceptOptions,
|
||||
): void {
|
||||
interceptCalled = true;
|
||||
event.sameDocument = true;
|
||||
const handler = options?.handler;
|
||||
if (handler) {
|
||||
handlerFinished = handler();
|
||||
}
|
||||
if (options?.commit) {
|
||||
event.commitOption = options.commit;
|
||||
}
|
||||
if (options?.focusReset !== undefined || options?.scroll !== undefined) {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
};
|
||||
|
||||
event.scroll = function (this: InternalFakeNavigateEvent): void {
|
||||
throw new Error('unimplemented');
|
||||
};
|
||||
|
||||
event.commit = function (this: InternalFakeNavigateEvent, internal = false) {
|
||||
if (!internal && !interceptCalled) {
|
||||
throw new DOMException(
|
||||
`Failed to execute 'commit' on 'NavigateEvent': intercept() must be ` +
|
||||
`called before commit().`,
|
||||
'InvalidStateError',
|
||||
);
|
||||
}
|
||||
if (!dispatchedNavigateEvent) {
|
||||
throw new DOMException(
|
||||
`Failed to execute 'commit' on 'NavigateEvent': commit() may not be ` +
|
||||
`called during event dispatch.`,
|
||||
'InvalidStateError',
|
||||
);
|
||||
}
|
||||
if (commitCalled) {
|
||||
throw new DOMException(
|
||||
`Failed to execute 'commit' on 'NavigateEvent': commit() already ` + `called.`,
|
||||
'InvalidStateError',
|
||||
);
|
||||
}
|
||||
commitCalled = true;
|
||||
|
||||
userAgentCommit();
|
||||
};
|
||||
|
||||
// Internal only.
|
||||
event.cancel = function (this: InternalFakeNavigateEvent, reason: Error) {
|
||||
result.committedReject(reason);
|
||||
result.finishedReject(reason);
|
||||
};
|
||||
|
||||
// Internal only.
|
||||
event.dispatchedNavigateEvent = function (this: InternalFakeNavigateEvent) {
|
||||
dispatchedNavigateEvent = true;
|
||||
if (event.commitOption === 'after-transition') {
|
||||
// If handler finishes before commit, call commit.
|
||||
handlerFinished?.then(
|
||||
() => {
|
||||
if (!commitCalled) {
|
||||
event.commit(/* internal */ true);
|
||||
}
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
Promise.all([result.committed, handlerFinished]).then(
|
||||
([entry]) => {
|
||||
result.finishedResolve(entry);
|
||||
},
|
||||
(reason) => {
|
||||
result.finishedReject(reason);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// Internal only.
|
||||
event.userAgentNavigated = function (
|
||||
this: InternalFakeNavigateEvent,
|
||||
entry: FakeNavigationHistoryEntry,
|
||||
) {
|
||||
result.committedResolve(entry);
|
||||
};
|
||||
|
||||
return event as InternalFakeNavigateEvent;
|
||||
}
|
||||
|
||||
/** Fake equivalent of `NavigationCurrentEntryChangeEvent`. */
|
||||
export interface FakeNavigationCurrentEntryChangeEvent extends NavigationCurrentEntryChangeEvent {
|
||||
readonly from: FakeNavigationHistoryEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fake equivalent of `NavigationCurrentEntryChange`. This does not use
|
||||
* a class because ES5 transpiled JavaScript cannot extend native Event.
|
||||
*/
|
||||
function createFakeNavigationCurrentEntryChangeEvent({
|
||||
from,
|
||||
navigationType,
|
||||
}: {
|
||||
from: FakeNavigationHistoryEntry;
|
||||
navigationType: NavigationTypeString;
|
||||
}) {
|
||||
const event = new Event('currententrychange', {
|
||||
bubbles: false,
|
||||
cancelable: false,
|
||||
}) as {
|
||||
-readonly [P in keyof NavigationCurrentEntryChangeEvent]: NavigationCurrentEntryChangeEvent[P];
|
||||
};
|
||||
event.from = from;
|
||||
event.navigationType = navigationType;
|
||||
return event as FakeNavigationCurrentEntryChangeEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fake equivalent of `PopStateEvent`. This does not use a class
|
||||
* because ES5 transpiled JavaScript cannot extend native Event.
|
||||
*/
|
||||
function createPopStateEvent({state}: {state: unknown}) {
|
||||
const event = new Event('popstate', {
|
||||
bubbles: false,
|
||||
cancelable: false,
|
||||
}) as {-readonly [P in keyof PopStateEvent]: PopStateEvent[P]};
|
||||
event.state = state;
|
||||
return event as PopStateEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake equivalent of `NavigationDestination`.
|
||||
*/
|
||||
export class FakeNavigationDestination implements NavigationDestination {
|
||||
readonly url: string;
|
||||
readonly sameDocument: boolean;
|
||||
readonly key: string | null;
|
||||
readonly id: string | null;
|
||||
readonly index: number;
|
||||
|
||||
private readonly state?: unknown;
|
||||
private readonly historyState: unknown;
|
||||
|
||||
constructor({
|
||||
url,
|
||||
sameDocument,
|
||||
historyState,
|
||||
state,
|
||||
key = null,
|
||||
id = null,
|
||||
index = -1,
|
||||
}: {
|
||||
url: string;
|
||||
sameDocument: boolean;
|
||||
historyState: unknown;
|
||||
state?: unknown;
|
||||
key?: string | null;
|
||||
id?: string | null;
|
||||
index?: number;
|
||||
}) {
|
||||
this.url = url;
|
||||
this.sameDocument = sameDocument;
|
||||
this.state = state;
|
||||
this.historyState = historyState;
|
||||
this.key = key;
|
||||
this.id = id;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
getState(): unknown {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
getHistoryState(): unknown {
|
||||
return this.historyState;
|
||||
}
|
||||
}
|
||||
|
||||
/** Utility function to determine whether two UrlLike have the same hash. */
|
||||
function isHashChange(from: URL, to: URL): boolean {
|
||||
return (
|
||||
to.hash !== from.hash &&
|
||||
to.hostname === from.hostname &&
|
||||
to.pathname === from.pathname &&
|
||||
to.search === from.search
|
||||
);
|
||||
}
|
||||
|
||||
/** Internal utility class for representing the result of a navigation. */
|
||||
class InternalNavigationResult {
|
||||
committedResolve!: (entry: FakeNavigationHistoryEntry) => void;
|
||||
committedReject!: (reason: Error) => void;
|
||||
finishedResolve!: (entry: FakeNavigationHistoryEntry) => void;
|
||||
finishedReject!: (reason: Error) => void;
|
||||
readonly committed: Promise<FakeNavigationHistoryEntry>;
|
||||
readonly finished: Promise<FakeNavigationHistoryEntry>;
|
||||
get signal(): AbortSignal {
|
||||
return this.abortController.signal;
|
||||
}
|
||||
private readonly abortController = new AbortController();
|
||||
|
||||
constructor() {
|
||||
this.committed = new Promise<FakeNavigationHistoryEntry>((resolve, reject) => {
|
||||
this.committedResolve = resolve;
|
||||
this.committedReject = reject;
|
||||
});
|
||||
|
||||
this.finished = new Promise<FakeNavigationHistoryEntry>(async (resolve, reject) => {
|
||||
this.finishedResolve = resolve;
|
||||
this.finishedReject = (reason: Error) => {
|
||||
reject(reason);
|
||||
this.abortController.abort(reason);
|
||||
};
|
||||
});
|
||||
// All rejections are handled.
|
||||
this.committed.catch(() => {});
|
||||
this.finished.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/** Internal options for performing a navigate. */
|
||||
interface InternalNavigateOptions {
|
||||
navigationType: NavigationTypeString;
|
||||
cancelable: boolean;
|
||||
canIntercept: boolean;
|
||||
userInitiated: boolean;
|
||||
hashChange: boolean;
|
||||
info?: unknown;
|
||||
skipPopState?: boolean;
|
||||
}
|
||||
export {ɵFakeNavigation as FakeNavigation} from '@angular/core/testing';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user