Compare commits

...

43 Commits

Author SHA1 Message Date
Pawel Kozlowski 07a0b87a4c release: cut the v18.1.0 release 2024-07-10 15:00:47 +02:00
Harish Krishnan 551ffbf9f2 docs: Update components-scenarios.md (#56921)
Fix grammatical issues

PR Close #56921
2024-07-10 11:29:17 +02:00
cexbrayat 4d87b9e899 fix(core): rename the equality function option in toSignal (#56769) (#56922)
The option introduced in 5df3e78c99 has been named `equals` whereas the existing option in `signal` is named `equal`.
This commit renames the new option to `equal` as well to keep the naming coherent across these APIs.

PR Close #56769

PR Close #56922
2024-07-10 11:09:07 +02:00
Matthieu Riegler 0da9eae24c docs: update version table for 18.1 (#56920)
PR Close #56920
2024-07-10 09:34:48 +02:00
Andrew Scott 15d2e8d54c docs: Update component testing scenarios (#56388)
This update goes through half of the component-scenarios guide and makes
relevant updates to examples.

Updates include:

* Use signals in examples
* Advise and use `await fixture.whenStable` more frequently than
  `detectChanges`
* Use and advise mocks and spies more sparingly
* Remove `waitForAsync` and jasmine `done` guidance - The ecosystem has evolved
  and these aren't needed
* Remove marble testing for rxjs - this belongs in rxjs documentation,
  not in Angular
* Remove class-only component testing. This approach is not advisable
  for components.

related to #48510

PR Close #56388
2024-07-10 09:11:21 +02:00
Marcos Paulo dos Santos 3a453d6040 docs: update compat table with 13.4.x (#56915)
PR Close #56915
2024-07-10 09:10:29 +02:00
Andrew Scott 8bd4c074af fix(core): toSignal equal option should be passed to inner computed (#56903)
The user-defined equality function needs to be passed to the inner
computed it will still use `Object.is` and prevent notifications.

PR Close #56903
2024-07-09 19:52:18 +02:00
Jessica Janiuk 5f97d6aec2 fix(migrations): Fix cf migration let condition semicolon order (#56913)
In rare cases people may put let statements before else statements and omit semicolons, causing migration issues.

PR Close #56913
2024-07-09 18:17:52 +02:00
Matthieu Riegler 2f8bf933f4 refactor(compiler-cli): escape missing decorators. (#56794)
Escaping decorators is required as else they are considered as new JSDocs tags.

fixes #56569

PR Close #56794
2024-07-09 13:13:53 +02:00
Ofer Segev ae9505b702 docs: update dependency-injection-context.md (#56736)
HeroService is not part of this example, and is only introduced in the next example. Use type Service1 for this.service1, as its type
PR Close #56736
2024-07-09 13:05:57 +02:00
Matthieu Riegler 0f4219cf0c refactor(platform-server): Add private profiler. (#56274)
The commit adds a private profiler to investigate SSR performance.

PR Close #56274
2024-07-09 12:47:46 +02:00
Kristiyan Kostadinov f689214178 docs: fix wrong link in template overview (#56911)
One of the links added in #56886 was incomplete.

PR Close #56911
2024-07-09 12:13:12 +02:00
Daniel Puckowski aef166f763 fix(compiler): fix CSS animation rule scope (#56800)
It is valid CSS to list keyframe names in an animation declaration only
separating the names with a comma and no whitespace. This is typical of
production builds. Updated a couple of regexes and added a couple of
tests to account for this scenario.

Fixes #53038

PR Close #56800
2024-07-09 09:44:56 +02:00
Kristiyan Kostadinov 02f5e73a8d docs: add documentation page for let declarations (#56886)
Adds a docs page for the new `@let` syntax in Angular 18.1.

PR Close #56886
2024-07-09 09:38:23 +02:00
Matthieu Riegler 20f5e1291e docs: add a FAQ on how to find the LCP image (#56779)
PR Close #56779
2024-07-09 09:37:20 +02:00
Andrew Kushnir 8adc6b9425 test(core): simplify @defer-based test to make CI more stable (#56904)
This commit updates one of the `@defer`-based test to use the `on immediate` trigger to make a test mor
e stable without the need to add mocks for the `on idle` (default) condition.

PR Close #56904
2024-07-09 09:36:12 +02:00
vladboisa 35fa9b0f77 docs(docs-infra): add missing context with link (#56896)
Added missing link documenting ng global features.

Fixes #56488

PR Close #56896
2024-07-08 18:04:00 +02:00
Paul Gschwendtner 56ec97d2e1 refactor(compiler-cli): do not truncate/reduce types in API docs (#56572)
Fixes that e.g. signal input APIs docs were removing `undefined` from
the shorthand `input<T>()` documentation.

PR Close #56572
2024-07-08 16:26:40 +02:00
Matthieu Riegler ba325c59c0 docs: remove private symbol from docs (#56851)
fixes #56850

PR Close #56851
2024-07-08 09:54:14 +02:00
Charles Lyding 447a36ef95 refactor(localize): reduce deep imports in ng add schematic (#56839)
The `@angular/schematic` package has a `utility` export path which can be
used to access common utility rules and helpers. The previous deep imports
into the `@angular/schematic` package have been switched to the actual
export path where possible. This lowers the potential for breakage from
internal package changes.

PR Close #56839
2024-07-08 09:51:53 +02:00
Sasidharan SD 8326e62313 docs: fix previous configuration link (#56880)
PR Close #56880
2024-07-08 09:48:02 +02:00
cexbrayat 9b35726e42 fix(common): typo in warning for NgOptimizedDirective (#56817)
The warning message for the `PLACEHOLDER_DIMENSION_LIMIT_EXCEEDED` check has a repetition of `of`.

PR Close #56817
2024-07-05 13:03:56 +02:00
Kristiyan Kostadinov cdebf751e4 fix(compiler-cli): used before declared diagnostic not firing for control flow blocks (#56843)
When we process `@if` and `@for` blocks, we create a scope around their expressions in order to encapsulate the aliases to them. The problem is that this doesn't represent the actual structure since the expression is part of the outer scope. This surfaces by not raising the "used before declared" diagnostic for `@let` declarations.

These changes resolve the issue by processing the expression as a part of the parent scope.

Fixes #56842.

PR Close #56843
2024-07-05 13:02:58 +02:00
Kristiyan Kostadinov 66e582551e fix(compiler-cli): avoid duplicate diagnostics for let declarations read before definition (#56843)
Fixes that in some cases `@let` declarations that are read before they're defined were producing multiple diagnostics.

PR Close #56843
2024-07-05 13:02:58 +02:00
Dan Nisenson 1efe907881 docs: fix broken link (#56848)
fix link @ /guide/forms/reactive-forms#reactive-forms-api-summary
prev: /api/forms
current: /api#forms

PR Close #56848
2024-07-05 12:27:35 +02:00
Corbin Crutchley 1074134d4b docs: add window title to reference pages (#56840)
PR Close #56840
2024-07-04 14:29:29 +02:00
Alan Agius ecc56447b9 perf(http): execute fetch outside of Angular zone (#56820)
In this update, the fetch backend now executes fetch operations outside of the Angular zone. This adjustment primarily aims to decrease Continuous Delivery (CD) cycles on Node.js. The decision was influenced by Undici, the Node.js fetch implementation, which relies on `setTimeouts` to manage response timeouts.

PR Close #56820
2024-07-04 12:10:35 +02:00
Michael van der Luit 85da4b3b2a docs: update app-shell docs to reflect v17 (#56836)
Fixes the build commands as previously mentioned here: https://github.com/angular/angular/pull/52294

PR Close #56836
2024-07-04 11:32:14 +02:00
Harish Krishnan 3fa21cbbc5 docs: Update image-optimization.md (#56819)
Fix the mistyped attribute rel
PR Close #56819
2024-07-04 09:56:18 +02:00
Matthieu Riegler 1ee64a5c45 refactor(common): Fire priority check on stable. (#56776)
To support routing on app init, the directive will now fire the priority check when the apps become stable.

fixes #56757

PR Close #56776
2024-07-03 14:40:48 +00:00
Andrew Kushnir 3235d6d6dd fix(core): establish proper defer injector hierarchy for components attached to ApplicationRef (#56763)
This commit updates the logic that create an injector for defer blocks (when it's needed) to account for a situation when a component is instantiated without a connection to the current component tree. This can happen if a component is created using its factory function or via `createComponent()` call.

Resolves #56372.

PR Close #56763
2024-07-03 14:08:44 +00:00
Krishna Sahu 6ad2fe6ab0 docs: add toSignal import inside the demo example (#56804)
PR Close #56804
2024-07-03 14:07:51 +00:00
komura-c 0e0fb7bb61 docs: fix missing image and baseUrl in tutorials/first-app (#56766)
PR Close #56766
2024-07-02 20:21:41 +00:00
Andrew Kushnir 8a0a00e212 docs: add Event Replay section to the Hydration guide (#56770)
PR Close #56770
2024-07-02 20:19:53 +00:00
Matthieu Riegler 092bfe95cc fix(core): support injection of object with null constructor. (#56553)
This is debug only code, it shouldn't have any consequences on prod build.

fixes #56552

PR Close #56553
2024-07-02 17:33:32 +00:00
Andrew Scott c4384bf269 fix(router): routes should not get stale providers (#56798)
This fixes a bug with RouterOutlet and its context where it would reuse
providers from a previously activated route.

fixes #56774

PR Close #56798
2024-07-02 17:10:52 +00:00
Angular Robot 52852abb56 build: update github/codeql-action action to v3.25.11 (#56803)
See associated pull request for more information.

PR Close #56803
2024-07-02 13:04:27 -04:00
Ben Hong 0f612dcaf8 docs: update playground app to use hello world for easier onboarding (#56813)
PR Close #56813
2024-07-02 12:57:31 -04:00
Ben Elliott 9acccb11ff docs: Update interceptors.md to replace public class with export class in DI section (#56807)
PR Close #56807
2024-07-02 16:44:12 +00:00
Ben Hong e914767efb docs: add documentation aroun build error for docs (#56814)
PR Close #56814
2024-07-02 16:43:07 +00:00
Angular Robot 58be8e6bf8 build: update all non-major dependencies (#56801)
See associated pull request for more information.

PR Close #56801
2024-07-02 15:26:28 +00:00
Pawel Kozlowski 9cc4dbeadd refactor(common): remove remaining code for NgSwitch equality patch (#56806)
This change is the final cleanup after removing NgSwitch's equality patch
that was used in G3 to allow == matching for a certain period of time. This
code is no longer needed as both the external ecosystem and G3 are using
=== equality for matching NgSwitch cases.

PR Close #56806
2024-07-02 14:16:24 +00:00
Jessica Janiuk 7f5d70a74a release: cut the v18.1.0-rc.0 release 2024-07-01 14:01:57 -07:00
97 changed files with 1190 additions and 812 deletions
+1 -1
View File
@@ -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@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
uses: github/codeql-action/upload-sarif@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11
with:
sarif_file: results.sarif
+57 -108
View File
@@ -1,3 +1,60 @@
<a name="18.1.0"></a>
# 18.1.0 (2024-07-10)
### common
| Commit | Type | Description |
| -- | -- | -- |
| [f25653e231](https://github.com/angular/angular/commit/f25653e2311152d30b14d25acb0dccb4e2b5ea56) | fix | typo in NgOptimizedImage warning ([#56756](https://github.com/angular/angular/pull/56756)) |
| [9b35726e42](https://github.com/angular/angular/commit/9b35726e42ebdeed138a25581e0a7eefff466206) | fix | typo in warning for NgOptimizedDirective ([#56817](https://github.com/angular/angular/pull/56817)) |
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [fd6cd0422d](https://github.com/angular/angular/commit/fd6cd0422d2d761d2c6cc0cd41838fbba8a3f010) | feat | Add extended diagnostic to warn when there are uncalled functions in event bindings ([#56295](https://github.com/angular/angular/pull/56295)) |
| [341a116d61](https://github.com/angular/angular/commit/341a116d611c095ed414c82612adb529e7be310c) | fix | allow more characters in let declaration name ([#56764](https://github.com/angular/angular/pull/56764)) |
| [2a1291e942](https://github.com/angular/angular/commit/2a1291e942a3cd645ee635e72e7d83722383d39b) | fix | give precedence to local let declarations over parent ones ([#56752](https://github.com/angular/angular/pull/56752)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [66e582551e](https://github.com/angular/angular/commit/66e582551eb081e422e0df41badce1821c46dc62) | fix | avoid duplicate diagnostics for let declarations read before definition ([#56843](https://github.com/angular/angular/pull/56843)) |
| [4d18c5bfd5](https://github.com/angular/angular/commit/4d18c5bfd54c53655955c8cd90472081ade40b34) | fix | flag all conflicts between let declarations and local symbols ([#56752](https://github.com/angular/angular/pull/56752)) |
| [9e21582456](https://github.com/angular/angular/commit/9e215824565f0d30da7edb20087c4460069a6660) | fix | Show template syntax errors in local compilation modified ([#55855](https://github.com/angular/angular/pull/55855)) |
| [5996502921](https://github.com/angular/angular/commit/599650292107f8856c7cd41791bd0856f9d14eb1) | fix | type check let declarations nested inside nodes ([#56752](https://github.com/angular/angular/pull/56752)) |
| [cdebf751e4](https://github.com/angular/angular/commit/cdebf751e4949048b01acc92de2517f46fcd5d37) | fix | used before declared diagnostic not firing for control flow blocks ([#56843](https://github.com/angular/angular/pull/56843)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [ea3c802056](https://github.com/angular/angular/commit/ea3c80205653af109c688a4d4808143b34591d54) | feat | Add a schematic to migrate afterRender phase flag ([#55648](https://github.com/angular/angular/pull/55648)) |
| [5df3e78c99](https://github.com/angular/angular/commit/5df3e78c9907f522f2f96c087b10ca12d57f7028) | feat | add equality function to rxjs-interop `toSignal` ([#56447](https://github.com/angular/angular/pull/56447)) |
| [0a48d584f2](https://github.com/angular/angular/commit/0a48d584f2ffeebb9402032182d4fc13a260c5cf) | feat | add support for let syntax ([#56715](https://github.com/angular/angular/pull/56715)) |
| [352e0782ec](https://github.com/angular/angular/commit/352e0782ec37d2adcc662cfc69c83d38058a34bf) | feat | expose signal input metadata in `ComponentMirror` ([#56402](https://github.com/angular/angular/pull/56402)) |
| [a655e46447](https://github.com/angular/angular/commit/a655e46447962bf56bf0184e3104328b9f7c1531) | feat | Redesign the `afterRender` & `afterNextRender` phases API ([#55648](https://github.com/angular/angular/pull/55648)) |
| [e5a6f91722](https://github.com/angular/angular/commit/e5a6f917225aafa7c5c860f280d2aafe3615727e) | feat | support TypeScript 5.5 ([#56096](https://github.com/angular/angular/pull/56096)) |
| [38effcc63e](https://github.com/angular/angular/commit/38effcc63eea360e948dc22860add72d3aa02288) | fix | Add back phase flag option as a deprecated API ([#55648](https://github.com/angular/angular/pull/55648)) |
| [86bcfd3e49](https://github.com/angular/angular/commit/86bcfd3e498b8ec1de1a2a1ad0847fe567f7e9d4) | fix | improve docs on afterRender hooks ([#56522](https://github.com/angular/angular/pull/56522)) |
| [b2445a0953](https://github.com/angular/angular/commit/b2445a095314aa66da038d3093e6a1b18fe5768b) | fix | link errors to ADEV ([#55554](https://github.com/angular/angular/pull/55554)) ([#56038](https://github.com/angular/angular/pull/56038)) |
| [03a2acd2a3](https://github.com/angular/angular/commit/03a2acd2a3bdc87aaeb6b835a7c1016f800b31cb) | fix | properly remove imports in the afterRender phase migration ([#56524](https://github.com/angular/angular/pull/56524)) |
| [4d87b9e899](https://github.com/angular/angular/commit/4d87b9e899381894a1de90f251da58613a96eed0) | fix | rename the equality function option in toSignal ([#56769](https://github.com/angular/angular/pull/56769)) ([#56922](https://github.com/angular/angular/pull/56922)) |
| [8bd4c074af](https://github.com/angular/angular/commit/8bd4c074afe85b739dff4d3c4dcc19384c42b85e) | fix | toSignal equal option should be passed to inner computed ([#56903](https://github.com/angular/angular/pull/56903)) |
### forms
| Commit | Type | Description |
| -- | -- | -- |
| [00bde8b1c2](https://github.com/angular/angular/commit/00bde8b1c2d1511da40526a374d4e94d31e0d575) | fix | Make `NgControlStatus` host bindings `OnPush` compatible ([#55720](https://github.com/angular/angular/pull/55720)) |
### http
| Commit | Type | Description |
| -- | -- | -- |
| [cc21989132](https://github.com/angular/angular/commit/cc21989132bc64b981df83cb6ff6e1506b42a1d0) | fix | Make `Content-Type` header case insensitive ([#56541](https://github.com/angular/angular/pull/56541)) |
### language-service
| Commit | Type | Description |
| -- | -- | -- |
| [b400e2e4d4](https://github.com/angular/angular/commit/b400e2e4d4c27a9c8d8e91b52852ef7b64f7591a) | feat | autocompletion for the component not imported ([#55595](https://github.com/angular/angular/pull/55595)) |
| [67b2c336bc](https://github.com/angular/angular/commit/67b2c336bc0bdce3f7ae054c094990a9831f5b20) | fix | import the default exported component correctly ([#56432](https://github.com/angular/angular/pull/56432)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [a13f5da773](https://github.com/angular/angular/commit/a13f5da77303f4ab2f1543df1de1f416216b5a9c) | feat | Allow `UrlTree` as an input to `routerLink` ([#56265](https://github.com/angular/angular/pull/56265)) |
| [1d3a7529b4](https://github.com/angular/angular/commit/1d3a7529b4fa3617a5d6a97e742cb13818253a14) | feat | Set a different browser URL from the one for route matching ([#53318](https://github.com/angular/angular/pull/53318)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.0.5"></a>
# 18.0.5 (2024-06-26)
### core
@@ -9,51 +66,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.0-next.4"></a>
# 18.1.0-next.4 (2024-06-26)
### core
| Commit | Type | Description |
| -- | -- | -- |
| [0a48d584f2](https://github.com/angular/angular/commit/0a48d584f2ffeebb9402032182d4fc13a260c5cf) | feat | add support for let syntax ([#56715](https://github.com/angular/angular/pull/56715)) |
| [29ca6d10cc](https://github.com/angular/angular/commit/29ca6d10cc3cd75ebdf64658dafcb3ce579af343) | fix | improve support for i18n hydration of projected content ([#56192](https://github.com/angular/angular/pull/56192)) |
| [5be16d06bd](https://github.com/angular/angular/commit/5be16d06bd504b1a72b7fb6f8d6e0c1b93a8188b) | fix | prevent calling devMode only function on `@defer` error. ([#56559](https://github.com/angular/angular/pull/56559)) |
### http
| Commit | Type | Description |
| -- | -- | -- |
| [cc21989132](https://github.com/angular/angular/commit/cc21989132bc64b981df83cb6ff6e1506b42a1d0) | fix | Make `Content-Type` header case insensitive ([#56541](https://github.com/angular/angular/pull/56541)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.0-next.3"></a>
# 18.1.0-next.3 (2024-06-20)
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [0b867e83b6](https://github.com/angular/angular/commit/0b867e83b624cbfe449ed3b7daf87642337824f0) | fix | insert constant statements after the first group of imports ([#56431](https://github.com/angular/angular/pull/56431)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [5df3e78c99](https://github.com/angular/angular/commit/5df3e78c9907f522f2f96c087b10ca12d57f7028) | feat | add equality function to rxjs-interop `toSignal` ([#56447](https://github.com/angular/angular/pull/56447)) |
| [352e0782ec](https://github.com/angular/angular/commit/352e0782ec37d2adcc662cfc69c83d38058a34bf) | feat | expose signal input metadata in `ComponentMirror` ([#56402](https://github.com/angular/angular/pull/56402)) |
| [bf6df6f186](https://github.com/angular/angular/commit/bf6df6f18658dd0d477271f7eb969317ce1df024) | fix | do not activate event replay when no events are registered ([#56509](https://github.com/angular/angular/pull/56509)) |
| [86bcfd3e49](https://github.com/angular/angular/commit/86bcfd3e498b8ec1de1a2a1ad0847fe567f7e9d4) | fix | improve docs on afterRender hooks ([#56522](https://github.com/angular/angular/pull/56522)) |
| [03a2acd2a3](https://github.com/angular/angular/commit/03a2acd2a3bdc87aaeb6b835a7c1016f800b31cb) | fix | properly remove imports in the afterRender phase migration ([#56524](https://github.com/angular/angular/pull/56524)) |
### forms
| Commit | Type | Description |
| -- | -- | -- |
| [00bde8b1c2](https://github.com/angular/angular/commit/00bde8b1c2d1511da40526a374d4e94d31e0d575) | fix | Make `NgControlStatus` host bindings `OnPush` compatible ([#55720](https://github.com/angular/angular/pull/55720)) |
### language-service
| Commit | Type | Description |
| -- | -- | -- |
| [67b2c336bc](https://github.com/angular/angular/commit/67b2c336bc0bdce3f7ae054c094990a9831f5b20) | fix | import the default exported component correctly ([#56432](https://github.com/angular/angular/pull/56432)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [1d3a7529b4](https://github.com/angular/angular/commit/1d3a7529b4fa3617a5d6a97e742cb13818253a14) | feat | Set a different browser URL from the one for route matching ([#53318](https://github.com/angular/angular/pull/53318)) |
| [0d52c6b182](https://github.com/angular/angular/commit/0d52c6b18275433923a5fe94f3fc24323b73d0be) | fix | Delay the view transition to ensure renders in microtasks complete ([#56494](https://github.com/angular/angular/pull/56494)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.0.4"></a>
# 18.0.4 (2024-06-20)
### compiler-cli
@@ -71,39 +83,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.0-next.2"></a>
# 18.1.0-next.2 (2024-06-12)
### benchpress
| Commit | Type | Description |
| -- | -- | -- |
| [31d9fa40bdb](https://github.com/angular/angular/commit/31d9fa40bdbd1b815a819527312994b6ab1b532f) | fix | adjust supported browser names for headless chrome ([#56360](https://github.com/angular/angular/pull/56360)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [ea3c8020565](https://github.com/angular/angular/commit/ea3c80205653af109c688a4d4808143b34591d54) | feat | Add a schematic to migrate afterRender phase flag ([#55648](https://github.com/angular/angular/pull/55648)) |
| [a655e464479](https://github.com/angular/angular/commit/a655e46447962bf56bf0184e3104328b9f7c1531) | feat | Redesign the `afterRender` & `afterNextRender` phases API ([#55648](https://github.com/angular/angular/pull/55648)) |
| [38effcc63ee](https://github.com/angular/angular/commit/38effcc63eea360e948dc22860add72d3aa02288) | fix | Add back phase flag option as a deprecated API ([#55648](https://github.com/angular/angular/pull/55648)) |
| [d5c6ee432fc](https://github.com/angular/angular/commit/d5c6ee432fcd467c09b4d5d5366e731f5c91e8d4) | fix | async EventEmitter should contribute to app stability ([#56308](https://github.com/angular/angular/pull/56308)) |
| [4c7d5d8acd8](https://github.com/angular/angular/commit/4c7d5d8acd8a714fe89366f76dc69f91356f0a06) | fix | signals should be tracked when embeddedViewRef.detectChanges is called ([#55719](https://github.com/angular/angular/pull/55719)) |
### language-service
| Commit | Type | Description |
| -- | -- | -- |
| [b400e2e4d4c](https://github.com/angular/angular/commit/b400e2e4d4c27a9c8d8e91b52852ef7b64f7591a) | feat | autocompletion for the component not imported ([#55595](https://github.com/angular/angular/pull/55595)) |
### localize
| Commit | Type | Description |
| -- | -- | -- |
| [81486c2f477](https://github.com/angular/angular/commit/81486c2f4770361b4aecdc891107f70446e43bc3) | fix | add `@angular/localize/init` as polyfill in `angular.json` ([#56300](https://github.com/angular/angular/pull/56300)) |
### migrations
| Commit | Type | Description |
| -- | -- | -- |
| [96aa5c8a2c9](https://github.com/angular/angular/commit/96aa5c8a2c9dd607dbcffa5d7923df4c21636510) | fix | resolve error in standalone migration ([#56302](https://github.com/angular/angular/pull/56302)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [a13f5da7730](https://github.com/angular/angular/commit/a13f5da77303f4ab2f1543df1de1f416216b5a9c) | feat | Allow `UrlTree` as an input to `routerLink` ([#56265](https://github.com/angular/angular/pull/56265)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.0.3"></a>
# 18.0.3 (2024-06-12)
### benchpress
@@ -126,26 +105,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.0-next.1"></a>
# 18.1.0-next.1 (2024-06-05)
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [9e21582456](https://github.com/angular/angular/commit/9e215824565f0d30da7edb20087c4460069a6660) | fix | Show template syntax errors in local compilation modified ([#55855](https://github.com/angular/angular/pull/55855)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [8d75627ddd](https://github.com/angular/angular/commit/8d75627dddb82380dc8b3455a13f128e88309aa6) | fix | Do not migrate `HttpClientModule` imports on components. ([#56067](https://github.com/angular/angular/pull/56067)) |
| [1081c8d623](https://github.com/angular/angular/commit/1081c8d6233ba1ff09187b95a09b0644e130cdf8) | fix | don't coerce all producers to consumers on liveness change ([#56140](https://github.com/angular/angular/pull/56140)) |
| [b6fb53cdc7](https://github.com/angular/angular/commit/b6fb53cdc7c9f9dad5a113a68e72a2b723626bef) | fix | Fix shouldPreventDefaultBeforeDispatching bug ([#56188](https://github.com/angular/angular/pull/56188)) |
| [31f3975e4b](https://github.com/angular/angular/commit/31f3975e4b1f418c1a6b2516618f44093ba20175) | fix | handle missing `withI18nSupport()` call for components that use i18n blocks ([#56175](https://github.com/angular/angular/pull/56175)) |
### migrations
| Commit | Type | Description |
| -- | -- | -- |
| [75e811c18b](https://github.com/angular/angular/commit/75e811c18b16f42c6f72ac32e44c4bc4d37fc3b1) | fix | do not generate aliased variables with the same name ([#56154](https://github.com/angular/angular/pull/56154)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.0.2"></a>
# 18.0.2 (2024-06-05)
### core
@@ -162,16 +121,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.0-next.0"></a>
# 18.1.0-next.0 (2024-05-29)
### core
| Commit | Type | Description |
| -- | -- | -- |
| [e5a6f91722](https://github.com/angular/angular/commit/e5a6f917225aafa7c5c860f280d2aafe3615727e) | feat | support TypeScript 5.5 ([#56096](https://github.com/angular/angular/pull/56096)) |
| [b2445a0953](https://github.com/angular/angular/commit/b2445a095314aa66da038d3093e6a1b18fe5768b) | fix | link errors to ADEV ([#55554](https://github.com/angular/angular/pull/55554)) ([#56038](https://github.com/angular/angular/pull/56038)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.0.1"></a>
# 18.0.1 (2024-05-29)
### compiler
+16
View File
@@ -24,6 +24,8 @@ yarn
yarn docs
```
If you are having issues with the docs building, see the [FAQs](#faqs) section.
## Contributing
Want to report a bug, contribute some code, or improve the documentation? Excellent!
@@ -35,3 +37,17 @@ And if you're new, check out one of our issues labeled as <kbd>[help wanted](htt
### Code of Conduct
Help us keep Angular open and inclusive. Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md).
## FAQs
### The build is failing and I'm seeing `bazel:bazel failed: missing input file` messages.
This is most likely due to a bazel dependency / caching issue. To resolve this, run the following command:
```
# Try this first
yarn bazel clean
# If that doesn't work, try it with the expunge flag
yarn bazel clean --expunge
```
+1 -1
View File
@@ -5,7 +5,7 @@
"ignore": ["**/.*"],
"headers": [
{
"source": "assets/tutorials/common/*.jpg",
"source": "assets/images/tutorials/common/*.jpg",
"headers": [
{
"key": "Cross-Origin-Resource-Policy",
@@ -35,6 +35,7 @@ export function mapApiManifestToRoutes(): Route[] {
),
},
data: {
label: api.name,
displaySecondaryNav: true,
},
});
+5
View File
@@ -228,6 +228,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
path: 'guide/templates/control-flow',
contentPath: 'guide/templates/control-flow',
},
{
label: 'Local template variables with @let',
path: 'guide/templates/let-template-variables',
contentPath: 'guide/templates/let-template-variables',
},
{
label: 'Pipes',
children: [
Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

@@ -48,7 +48,7 @@ src
<docs-code language="shell">
ng run my-app:app-shell:development
ng build --configuration=development
</docs-code>
@@ -56,7 +56,7 @@ Or to use the production configuration.
<docs-code language="shell">
ng run my-app:app-shell:production
ng build
</docs-code>
@@ -15,7 +15,6 @@ describe('BannerComponent (AutoChangeDetect)', () => {
beforeEach(() => {
// #docregion auto-detect
TestBed.configureTestingModule({
imports: [BannerComponent],
providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
});
// #enddocregion auto-detect
@@ -30,15 +29,18 @@ describe('BannerComponent (AutoChangeDetect)', () => {
expect(h1.textContent).toContain(comp.title);
});
it('should still see original title after comp.title change', () => {
it('should still see original title after comp.title change', async () => {
const oldTitle = comp.title;
comp.title = 'Test Title';
// Displayed title is old because Angular didn't hear the change :(
const newTitle = 'Test Title';
comp.title.set(newTitle);
// Displayed title is old because Angular didn't yet run change detection
expect(h1.textContent).toContain(oldTitle);
await fixture.whenStable();
expect(h1.textContent).toContain(newTitle);
});
it('should display updated title after detectChanges', () => {
comp.title = 'Test Title';
comp.title.set('Test Title');
fixture.detectChanges(); // detect changes explicitly
expect(h1.textContent).toContain(comp.title);
});
@@ -1,13 +1,13 @@
import {Component} from '@angular/core';
import {Component, signal} from '@angular/core';
// #docregion component
@Component({
standalone: true,
selector: 'app-banner',
template: '<h1>{{title}}</h1>',
template: '<h1>{{title()}}</h1>',
styles: ['h1 { color: green; font-size: 350%}'],
})
export class BannerComponent {
title = 'Test Tour of Heroes';
title = signal('Test Tour of Heroes');
}
// #enddocregion component
@@ -12,19 +12,6 @@ import {DashboardHeroComponent} from './dashboard-hero.component';
beforeEach(addMatchers);
describe('DashboardHeroComponent class only', () => {
// #docregion class-only
it('raises the selected event when clicked', () => {
const comp = new DashboardHeroComponent();
const hero: Hero = {id: 42, name: 'Test'};
comp.hero = hero;
comp.selected.pipe(first()).subscribe((selectedHero: Hero) => expect(selectedHero).toBe(hero));
comp.click();
});
// #enddocregion class-only
});
describe('DashboardHeroComponent when tested directly', () => {
let comp: DashboardHeroComponent;
let expectedHero: Hero;
@@ -32,19 +19,18 @@ describe('DashboardHeroComponent when tested directly', () => {
let heroDe: DebugElement;
let heroEl: HTMLElement;
beforeEach(waitForAsync(() => {
beforeEach(() => {
// #docregion setup, config-testbed
TestBed.configureTestingModule({
providers: appProviders,
imports: [DashboardHeroComponent],
})
// #enddocregion setup, config-testbed
.compileComponents();
}));
});
// #enddocregion setup, config-testbed
});
beforeEach(() => {
beforeEach(async () => {
// #docregion setup
fixture = TestBed.createComponent(DashboardHeroComponent);
fixture.autoDetectChanges();
comp = fixture.componentInstance;
// find the hero's DebugElement and element
@@ -55,10 +41,10 @@ describe('DashboardHeroComponent when tested directly', () => {
expectedHero = {id: 42, name: 'Test Name'};
// simulate the parent setting the input property with that hero
comp.hero = expectedHero;
fixture.componentRef.setInput('hero', expectedHero);
// trigger initial data binding
fixture.detectChanges();
// wait for initial data binding
await fixture.whenStable();
// #enddocregion setup
});
@@ -72,7 +58,7 @@ describe('DashboardHeroComponent when tested directly', () => {
// #docregion click-test
it('should raise selected event when clicked (triggerEventHandler)', () => {
let selectedHero: Hero | undefined;
comp.selected.pipe(first()).subscribe((hero: Hero) => (selectedHero = hero));
comp.selected.subscribe((hero: Hero) => (selectedHero = hero));
// #docregion trigger-event-handler
heroDe.triggerEventHandler('click');
@@ -84,7 +70,7 @@ describe('DashboardHeroComponent when tested directly', () => {
// #docregion click-test-2
it('should raise selected event when clicked (element.click)', () => {
let selectedHero: Hero | undefined;
comp.selected.pipe(first()).subscribe((hero: Hero) => (selectedHero = hero));
comp.selected.subscribe((hero: Hero) => (selectedHero = hero));
heroEl.click();
expect(selectedHero).toBe(expectedHero);
@@ -94,7 +80,7 @@ describe('DashboardHeroComponent when tested directly', () => {
// #docregion click-test-3
it('should raise selected event when clicked (click helper with DebugElement)', () => {
let selectedHero: Hero | undefined;
comp.selected.pipe(first()).subscribe((hero: Hero) => (selectedHero = hero));
comp.selected.subscribe((hero: Hero) => (selectedHero = hero));
click(heroDe); // click helper with DebugElement
@@ -104,7 +90,7 @@ describe('DashboardHeroComponent when tested directly', () => {
it('should raise selected event when clicked (click helper with native element)', () => {
let selectedHero: Hero | undefined;
comp.selected.pipe(first()).subscribe((hero: Hero) => (selectedHero = hero));
comp.selected.subscribe((hero: Hero) => (selectedHero = hero));
click(heroEl); // click helper with native element
@@ -1,5 +1,5 @@
// #docregion
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Component, input, output} from '@angular/core';
import {UpperCasePipe} from '@angular/common';
import {Hero} from '../model/hero';
@@ -10,7 +10,7 @@ import {Hero} from '../model/hero';
selector: 'dashboard-hero',
template: `
<button type="button" (click)="click()" class="hero">
{{ hero.name | uppercase }}
{{ hero().name | uppercase }}
</button>
`,
styleUrls: ['./dashboard-hero.component.css'],
@@ -18,10 +18,10 @@ import {Hero} from '../model/hero';
})
// #docregion class
export class DashboardHeroComponent {
@Input() hero!: Hero;
@Output() selected = new EventEmitter<Hero>();
hero = input.required<Hero>();
selected = output<Hero>();
click() {
this.selected.emit(this.hero);
this.selected.emit(this.hero());
}
}
// #enddocregion component, class
@@ -172,7 +172,8 @@ function heroModuleSetup() {
}));
// #docregion title-case-pipe
it('should convert hero name to Title Case', () => {
it('should convert hero name to Title Case', async () => {
harness.fixture.autoDetectChanges();
// get the name's input and display elements from the DOM
const hostElement: HTMLElement = harness.routeNativeElement!;
const nameInput: HTMLInputElement = hostElement.querySelector('input')!;
@@ -184,8 +185,8 @@ function heroModuleSetup() {
// Dispatch a DOM event so that Angular learns of input value change.
nameInput.dispatchEvent(new Event('input'));
// Tell Angular to update the display binding through the title pipe
harness.detectChanges();
// Wait for Angular to update the display binding through the title pipe
await harness.fixture.whenStable();
expect(nameDisplay.textContent).toBe('Quick Brown Fox');
});
@@ -1,7 +1,7 @@
import {Injectable} from '@angular/core';
import {Injectable, signal} from '@angular/core';
@Injectable()
@Injectable({providedIn: 'root'})
export class UserService {
isLoggedIn = true;
user = {name: 'Sam Spade'};
isLoggedIn = signal(true);
user = signal({name: 'Sam Spade'});
}
@@ -3,7 +3,7 @@ import {fakeAsync, ComponentFixture, TestBed, tick, waitForAsync} from '@angular
import {asyncData, asyncError} from '../../testing';
import {of, throwError} from 'rxjs';
import {Subject, defer, of, throwError} from 'rxjs';
import {last} from 'rxjs/operators';
import {TwainComponent} from './twain.component';
@@ -25,21 +25,21 @@ describe('TwainComponent', () => {
// #docregion setup
beforeEach(() => {
TestBed.configureTestingModule({
imports: [TwainComponent],
providers: [TwainService],
});
testQuote = 'Test Quote';
// #docregion spy
// Create a fake TwainService object with a `getQuote()` spy
const twainService = jasmine.createSpyObj('TwainService', ['getQuote']);
const twainService = TestBed.inject(TwainService);
// Make the spy return a synchronous Observable with the test data
getQuoteSpy = twainService.getQuote.and.returnValue(of(testQuote));
getQuoteSpy = spyOn(twainService, 'getQuote').and.returnValue(of(testQuote));
// #enddocregion spy
TestBed.configureTestingModule({
imports: [TwainComponent],
providers: [{provide: TwainService, useValue: twainService}],
});
fixture = TestBed.createComponent(TwainComponent);
fixture.autoDetectChanges();
component = fixture.componentInstance;
quoteEl = fixture.nativeElement.querySelector('.twain');
});
@@ -54,8 +54,8 @@ describe('TwainComponent', () => {
// The quote would not be immediately available if the service were truly async.
// #docregion sync-test
it('should show quote after component initialized', () => {
fixture.detectChanges(); // onInit()
it('should show quote after component initialized', async () => {
await fixture.whenStable(); // onInit()
// sync spy result shows testQuote immediately after init
expect(quoteEl.textContent).toBe(testQuote);
@@ -67,12 +67,20 @@ describe('TwainComponent', () => {
// Use `fakeAsync` because the component error calls `setTimeout`
// #docregion error-test
it('should display error when TwainService fails', fakeAsync(() => {
// tell spy to return an error observable
getQuoteSpy.and.returnValue(throwError(() => new Error('TwainService test failure')));
// tell spy to return an error observable after a timeout
getQuoteSpy.and.returnValue(
defer(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('TwainService test failure');
});
});
}),
);
fixture.detectChanges(); // onInit()
// sync spy errors immediately after init
tick(); // flush the component's setTimeout()
tick(); // flush the setTimeout()
fixture.detectChanges(); // update errorMessage within setTimeout()
@@ -120,46 +128,18 @@ describe('TwainComponent', () => {
}));
// #enddocregion fake-async-test
// #docregion waitForAsync-test
it('should show quote after getQuote (waitForAsync)', waitForAsync(() => {
// #docregion async-test
it('should show quote after getQuote (async)', async () => {
fixture.detectChanges(); // ngOnInit()
expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');
fixture.whenStable().then(() => {
// wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).withContext('should not show error').toBeNull();
});
}));
// #enddocregion waitForAsync-test
// #docregion quote-done-test
it('should show last quote (quote done)', (done: DoneFn) => {
fixture.detectChanges();
component.quote.pipe(last()).subscribe(() => {
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).withContext('should not show error').toBeNull();
done();
});
await fixture.whenStable();
// wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).withContext('should not show error').toBeNull();
});
// #enddocregion quote-done-test
// #docregion spy-done-test
it('should show quote after getQuote (spy done)', (done: DoneFn) => {
fixture.detectChanges();
// the spy's most recent call returns the observable with the test quote
getQuoteSpy.calls.mostRecent().returnValue.subscribe(() => {
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).withContext('should not show error').toBeNull();
done();
});
});
// #enddocregion spy-done-test
// #enddocregion async-test
it('should display error when TwainService fails', fakeAsync(() => {
// tell spy to return an async error observable
@@ -1,5 +1,5 @@
// #docregion
import {Component, OnInit} from '@angular/core';
import {Component, OnInit, signal} from '@angular/core';
import {AsyncPipe} from '@angular/common';
import {sharedImports} from '../shared/shared';
@@ -16,16 +16,16 @@ import {TwainService} from './twain.service';
<i>{{ quote | async }}</i>
</p>
<button type="button" (click)="getQuote()">Next quote</button>
@if (errorMessage) {
<p class="error">{{ errorMessage }}</p>
@if (errorMessage()) {
<p class="error">{{ errorMessage() }}</p>
}`,
// #enddocregion template
styles: ['.twain { font-style: italic; } .error { color: red; }'],
imports: [AsyncPipe, sharedImports],
})
export class TwainComponent implements OnInit {
errorMessage!: string;
quote!: Observable<string>;
errorMessage = signal('');
quote?: Observable<string>;
constructor(private twainService: TwainService) {}
@@ -35,12 +35,11 @@ export class TwainComponent implements OnInit {
// #docregion get-quote
getQuote() {
this.errorMessage = '';
this.errorMessage.set('');
this.quote = this.twainService.getQuote().pipe(
startWith('...'),
catchError((err: any) => {
// Wait a turn because errorMessage already set once this turn
setTimeout(() => (this.errorMessage = err.message || err.toString()));
this.errorMessage.set(err.message || err.toString());
return of('...'); // reset message to placeholder
}),
);
@@ -11,41 +11,6 @@ class MockUserService {
}
// #enddocregion mock-user-service
describe('WelcomeComponent (class only)', () => {
let comp: WelcomeComponent;
let userService: UserService;
// #docregion class-only-before-each
beforeEach(() => {
TestBed.configureTestingModule({
// provide the component-under-test and dependent service
providers: [WelcomeComponent, {provide: UserService, useClass: MockUserService}],
});
// inject both the component and the dependent service.
comp = TestBed.inject(WelcomeComponent);
userService = TestBed.inject(UserService);
});
// #enddocregion class-only-before-each
// #docregion class-only-tests
it('should not have welcome message after construction', () => {
expect(comp.welcome).toBe('');
});
it('should welcome logged in user after Angular calls ngOnInit', () => {
comp.ngOnInit();
expect(comp.welcome).toContain(userService.user.name);
});
it('should ask user to log in if not logged in after ngOnInit', () => {
userService.isLoggedIn = false;
comp.ngOnInit();
expect(comp.welcome).not.toContain(userService.user.name);
expect(comp.welcome).toContain('log in');
});
// #enddocregion class-only-tests
});
describe('WelcomeComponent', () => {
let comp: WelcomeComponent;
let fixture: ComponentFixture<WelcomeComponent>;
@@ -53,40 +18,17 @@ describe('WelcomeComponent', () => {
let userService: UserService; // the TestBed injected service
let el: HTMLElement; // the DOM element with the welcome message
// #docregion setup, user-service-stub
let userServiceStub: Partial<UserService>;
// #enddocregion user-service-stub
// #docregion setup
beforeEach(() => {
// stub UserService for test purposes
// #docregion user-service-stub
userServiceStub = {
isLoggedIn: true,
user: {name: 'Test User'},
};
// #enddocregion user-service-stub
// #docregion config-test-module
TestBed.configureTestingModule({
imports: [WelcomeComponent],
// #enddocregion setup
// providers: [ UserService ], // NO! Don't provide the real service!
// Provide a test-double instead
// #docregion setup
providers: [{provide: UserService, useValue: userServiceStub}],
});
// #enddocregion config-test-module
fixture = TestBed.createComponent(WelcomeComponent);
fixture.autoDetectChanges();
comp = fixture.componentInstance;
// #enddocregion setup
// #docregion injected-service
// UserService actually injected into the component
userService = fixture.debugElement.injector.get(UserService);
// #enddocregion injected-service
componentUserService = userService;
// #docregion setup
// #docregion inject-from-testbed
// UserService from the root injector
userService = TestBed.inject(UserService);
@@ -98,22 +40,22 @@ describe('WelcomeComponent', () => {
// #enddocregion setup
// #docregion tests
it('should welcome the user', () => {
fixture.detectChanges();
it('should welcome the user', async () => {
await fixture.whenStable();
const content = el.textContent;
expect(content).withContext('"Welcome ..."').toContain('Welcome');
expect(content).withContext('expected name').toContain('Test User');
});
it('should welcome "Bubba"', () => {
userService.user.name = 'Bubba'; // welcome message hasn't been shown yet
fixture.detectChanges();
it('should welcome "Bubba"', async () => {
userService.user.set({name: 'Bubba'}); // welcome message hasn't been shown yet
await fixture.whenStable();
expect(el.textContent).toContain('Bubba');
});
it('should request login if not logged in', () => {
userService.isLoggedIn = false; // welcome message hasn't been shown yet
fixture.detectChanges();
it('should request login if not logged in', async () => {
userService.isLoggedIn.set(false); // welcome message hasn't been shown yet
await fixture.whenStable();
const content = el.textContent;
expect(content).withContext('not welcomed').not.toContain('Welcome');
expect(content)
@@ -1,21 +1,21 @@
// #docregion
import {Component, OnInit} from '@angular/core';
import {Component, OnInit, signal} from '@angular/core';
import {UserService} from '../model/user.service';
@Component({
standalone: true,
selector: 'app-welcome',
template: '<h3 class="welcome"><i>{{welcome}}</i></h3>',
template: '<h3 class="welcome"><i>{{welcome()}}</i></h3>',
})
// #docregion class
export class WelcomeComponent implements OnInit {
welcome = '';
welcome = signal('');
constructor(private userService: UserService) {}
ngOnInit(): void {
this.welcome = this.userService.isLoggedIn
? 'Welcome, ' + this.userService.user.name
: 'Please log in.';
this.welcome.set(
this.userService.isLoggedIn() ? 'Welcome, ' + this.userService.user().name : 'Please log in.',
);
}
}
// #enddocregion class
@@ -23,7 +23,7 @@ class MyComponent {
private service2: Service2 = inject(Service2); // In context
constructor() {
this.service1 = inject(HeroService) // In context
this.service1 = inject(Service1) // In context
}
}
</docs-code>
@@ -395,7 +395,7 @@ Initially, the form contains one `Alias` field. To add another field, click the
## Reactive forms API summary
The following table lists the base classes and services used to create and manage reactive form controls.
For complete syntax details, see the API reference documentation for the [Forms package](api/forms "API reference").
For complete syntax details, see the API reference documentation for the [Forms package](api#forms "API reference").
### Classes
+1 -1
View File
@@ -172,7 +172,7 @@ A DI-based interceptor is an injectable class which implements the `HttpIntercep
<docs-code language="ts">
@Injectable()
public class LoggingInterceptor implements HttpInterceptor {
export class LoggingInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, handler: HttpHandler): Observable<HttpEvent<any>> {
console.log('Request URL: ' + req.url);
return handler.handle(req);
+30 -4
View File
@@ -12,7 +12,15 @@ Without hydration enabled, server-side rendered Angular applications will destro
## How do you enable hydration in Angular
Before you can get started with hydration, you must have a server-side rendered (SSR) application. Follow the [Angular SSR Guide](guide/ssr) to enable server-side rendering first. Once you have SSR working with your application, you can enable hydration by visiting your main app component or module and importing `provideClientHydration` from `@angular/platform-browser`. You'll then add that provider to your app's bootstrapping providers list.
Hydration can be enabled for server-side rendered (SSR) applications only. Follow the [Angular SSR Guide](guide/ssr) to enable server-side rendering first.
### Using Angular CLI
If you've used Angular CLI to enable SSR (either by enabling it during application creation or later via `ng add @angular/ssr`), the code that enables hydration should already be included into your application.
### Manual setup
If you have a custom setup and didn't use Angular CLI to enable SSR, you can enable hydration manually by visiting your main application component or module and importing `provideClientHydration` from `@angular/platform-browser`. You'll then add that provider to your app's bootstrapping providers list.
```typescript
import {
@@ -43,13 +51,31 @@ export class AppModule {}
IMPORTANT: Make sure that the `provideClientHydration()` call is also included into a set of providers that is used to bootstrap an application on the **server**. In applications with the default project structure (generated by the `ng new` command), adding a call to the root `AppModule` should be sufficient, since this module is imported by the server module. If you use a custom setup, add the `provideClientHydration()` call to the providers list in the server bootstrap configuration.
After you've followed these steps and have started up your server, load your application in the browser.
### Verify that hydration is enabled
After you've configured hydration and have started up your server, load your application in the browser.
HELPFUL: You will likely need to fix instances of Direct DOM Manipulation before hydration will fully work either by switching to Angular constructs or by using `ngSkipHydration`. See [Constraints](#constraints), [Direct DOM Manipulation](#direct-dom-manipulation), and [How to skip hydration for particular components](#how-to-skip-hydration-for-particular-components) for more details.
While running an application in dev mode, you can confirm hydration is enabled by opening the Developer Tools in your browser and viewing the console. You should see a message that includes hydration-related stats, such as the number of components and nodes hydrated.
While running an application in dev mode, you can confirm hydration is enabled by opening the Developer Tools in your browser and viewing the console. You should see a message that includes hydration-related stats, such as the number of components and nodes hydrated. Angular calculates the stats based on all components rendered on a page, including those that come from third-party libraries.
HELPFUL: Angular calculates the stats based on all components rendered on a page, including those that come from third-party libraries.
You can also use [Angular DevTools browser extension](tools/devtools) to see hydration status of components on a page. Angular DevTools also allows to enable an overlay to indicate which parts of the page were hydrated. If there is a hydration mismatch error - DevTools would also highlight a component that caused the error.
## Capturing and replaying events
When an application is rendered on the server, it is visible in a browser as soon as produced HTML loads. Users may assume that they can interact with the page, but event listeners are not attached until hydration completes. Starting from v18, you can enable the Event Replay feature that allows to capture all events that happen before hydration and replay those events once hydration has completed. You can enable it using the `withEventReplay()` function, for example:
```typescript
import {provideClientHydration, withEventReplay} from '@angular/platform-browser';
bootstrapApplication(App, {
providers: [
provideClientHydration(withEventReplay())
]
});
```
IMPORTANT: the Event Replay feature is currently in [Developer Preview](/reference/releases#developer-preview).
## Constraints
+16 -2
View File
@@ -194,7 +194,7 @@ NgOptimizedImage includes a number of features designed to improve loading perfo
A [`preconnect` resource hint](https://web.dev/preconnect-and-dns-prefetch) for your image origin ensures that the LCP image loads as quickly as possible.
Preconnect links are automatically generated for domains provided as an argument to a [loader](#optional-set-up-a-loader). If an image origin cannot be automatically identified, and no preconnect link is detected for the LCP image, `NgOptimizedImage` will warn during development. In that case, you should manually add a resource hint to `index.html`. Within the `<head>` of the document, add a `link` tag with `rel="preload"`, as shown below:
Preconnect links are automatically generated for domains provided as an argument to a [loader](#optional-set-up-a-loader). If an image origin cannot be automatically identified, and no preconnect link is detected for the LCP image, `NgOptimizedImage` will warn during development. In that case, you should manually add a resource hint to `index.html`. Within the `<head>` of the document, add a `link` tag with `rel="preconnect"`, as shown below:
<docs-code language="html">
@@ -432,4 +432,18 @@ For maintenance reasons, we don't currently plan to support additional built-in
No, but this is on our roadmap, so stay tuned.
If you're waiting on this feature, please upvote the Github issue [here](https://github.com/angular/angular/issues/56594).
If you're waiting on this feature, please upvote the Github issue [here](https://github.com/angular/angular/issues/56594).
### How do I find my LCP image with Chrome DevTools?
1. Using the performance tab of the Chrome DevTools, click on the "start profiling and reload page" button on the top left. It looks like a page refresh icon.
2. This will trigger a profiling snapshot of your Angular application.
3. Once the profiling result is available, select "LCP" in the timings section.
4. A summary entry should appear in the panel at the bottom. You can find the LCP element in the row for "related node". Clicking on it will reveal the element in the Elements panel.
<img alt="LCP in the Chrome DevTools" src="assets/images/guide/image-optimization/devtools-lcp.png">
NOTE: This only identifies the LCP element within the viewport of the page you are testing. It is also recommended to use mobile emulation to identify the LCP element for smaller screens.
@@ -12,6 +12,7 @@ Use the `toSignal` function to create a signal which tracks the value of an Obse
import { Component } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { interval } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
template: `{{ counter() }}`,
@@ -0,0 +1,104 @@
# Local template variables
Angular's `@let` syntax allows you to define a local variable and re-use it across the template.
IMPORTANT: the `@let` syntax is currently in [Developer Preview](/reference/releases#developer-preview).
## Syntax
`@let` declarations are similar to [JavaScript's `let`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let) and
their values can be any valid Angular expression. The expressions will be re-evaluated any time the
template is executed.
```html
@let name = user.name;
@let greeting = 'Hello, ' + name;
@let data = data$ | async;
@let pi = 3.1459;
@let coordinates = {x: 50, y: 100};
@let longExpression = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit ' +
'sed do eiusmod tempor incididunt ut labore et dolore magna ' +
'Ut enim ad minim veniam...';
```
### Referencing the value of `@let`
Once you've declared the `@let`, you can reuse it anywhere in the template:
```html
@let user = user$ | async;
@if (user) {
<h1>Hello, {{user.name}}</h1>
<user-avatar [photo]="user.photo"/>
<ul>
@for (snack of user.favoriteSnacks; track snack.id) {
<li>{{snack.name}}</li>
}
</ul>
<button (click)="update(user)">Update profile</button>
}
```
## Assignability
A key difference between `@let` and JavaScript's `let` is that `@let` cannot be re-assigned
within the template, however its value will be recomputed when Angular runs change detection.
```html
@let value = 1;
<!-- Invalid -->
<button (click)="value = value + 1">Increment the value</button>
```
## Scope
`@let` declarations are scoped to the current view and its descendants. Since they are not
hoisted, they **cannot** be accessed by parent views or siblings:
```html
@let topLevel = value;
<div>
@let insideDiv = value;
</div>
{{topLevel}} <!-- Valid -->
{{insideDiv}} <!-- Valid -->
@if (condition) {
{{topLevel + insideDiv}} <!-- Valid -->
@let nested = value;
@if (condition) {
{{topLevel + insideDiv + nested}} <!-- Valid -->
}
}
<div *ngIf="condition">
{{topLevel + insideDiv}} <!-- Valid -->
@let nestedNgIf = value;
<div *ngIf="condition">
{{topLevel + insideDiv + nestedNgIf}} <!-- Valid -->
</div>
</div>
{{nested}} <!-- Error, not hoisted from @if -->
{{nestedNgIf}} <!-- Error, not hoisted from *ngIf -->
```
## Syntax definition
The `@let` syntax is formally defined as:
* The `@let` keyword.
* Followed by one or more whitespaces, not including new lines.
* Followed by a valid JavaScript name and zero or more whitespaces.
* Followed by the = symbol and zero or more whitespaces.
* Followed by an Angular expression which can be multi-line.
* Terminated by the `;` symbol.
+16 -14
View File
@@ -32,17 +32,19 @@ For more information, see the [Security](best-practices/security) page.
You might also be interested in the following:
| Topics | Details |
| :------------------------------------------------------------------------ | :-------------------------------------------------------------------- |
| [Interpolation](guide/templates/interpolation) | Learn how to use interpolation and expressions in HTML. |
| [Template statements](guide/templates/template-statements) | Respond to events in your templates. |
| [Binding syntax](guide/templates/binding) | Use binding to coordinate values in your application. |
| [Property binding](guide/templates/property-binding) | Set properties of target elements or directive `@Input()` decorators. |
| [Attribute, class, and style bindings](guide/templates/attribute-binding) | Set the value of attributes, classes, and styles. |
| [Event binding](guide/templates/event-binding) | Listen for events and your HTML. |
| [Two-way binding](guide/templates/two-way-binding) | Share data between a class and its template. |
| [Built-in directives](guide/directives) | Listen to and modify the behavior and layout of HTML. |
| [Template reference variables](guide/templates/reference-variables) | Use special variables to reference a DOM element within a template. |
| [Inputs](guide/components/inputs) | Accepting data with input properties |
| [Outputs](guide/components/outputs) | Custom events with outputs |
| [SVG in templates](guide/templates/svg-in-templates) | Dynamically generate interactive graphics. |
| Topics | Details |
| :------------------------------------------------------------------------ | :---------------------------------------------------------------------------- |
| [Interpolation](guide/templates/interpolation) | Learn how to use interpolation and expressions in HTML. |
| [Template statements](guide/templates/template-statements) | Respond to events in your templates. |
| [Binding syntax](guide/templates/binding) | Use binding to coordinate values in your application. |
| [Property binding](guide/templates/property-binding) | Set properties of target elements or directive `@Input()` decorators. |
| [Attribute, class, and style bindings](guide/templates/attribute-binding) | Set the value of attributes, classes, and styles. |
| [Event binding](guide/templates/event-binding) | Listen for events and your HTML. |
| [Two-way binding](guide/templates/two-way-binding) | Share data between a class and its template. |
| [Control flow](guide/templates/control-flow) | Angular's syntax for conditionally showing, hiding, and repeating elements. |
| [Local template variables](guide/templates/let-template-variables) | Define and reuse variables in your template. |
| [Built-in directives](guide/directives) | Listen to and modify the behavior and layout of HTML. |
| [Template reference variables](guide/templates/reference-variables) | Use special variables to reference a DOM element within a template. |
| [Inputs](guide/components/inputs) | Accepting data with input properties |
| [Outputs](guide/components/outputs) | Custom events with outputs |
| [SVG in templates](guide/templates/svg-in-templates) | Dynamically generate interactive graphics. |
@@ -9,69 +9,11 @@ Such tests require creating the component's host element in the browser DOM, as
The Angular `TestBed` facilitates this kind of testing as you'll see in the following sections.
But in many cases, *testing the component class alone*, without DOM involvement, can validate much of the component's behavior in a straightforward, more obvious way.
## Component class testing
Test a component class on its own as you would test a service class.
Component class testing should be kept very clean and simple.
It should test only a single unit.
At first glance, you should be able to understand what the test is testing.
Consider this `LightswitchComponent` which toggles a light on and off (represented by an on-screen message) when the user clicks the button.
<docs-code header="app/demo/demo.ts (LightswitchComp)" path="adev/src/content/examples/testing/src/app/demo/demo.ts" visibleRegion="LightswitchComp"/>
You might decide only to test that the `clicked()` method toggles the light's *on/off* state and sets the message appropriately.
This component class has no dependencies.
To test these types of classes, follow the same steps as you would for a service that has no dependencies:
1. Create a component using the new keyword.
1. Poke at its API.
1. Assert expectations on its public state.
<docs-code header="app/demo/demo.spec.ts (Lightswitch tests)" path="adev/src/content/examples/testing/src/app/demo/demo.spec.ts" visibleRegion="Lightswitch"/>
Here is the `DashboardHeroComponent` from the *Tour of Heroes* tutorial.
<docs-code header="app/dashboard/dashboard-hero.component.ts (component)" path="adev/src/content/examples/testing/src/app/dashboard/dashboard-hero.component.ts" visibleRegion="class"/>
It appears within the template of a parent component, which binds a *hero* to the `@Input` property and listens for an event raised through the *selected* `@Output` property.
You can test that the class code works without creating the `DashboardHeroComponent` or its parent component.
<docs-code header="app/dashboard/dashboard-hero.component.spec.ts (class tests)" path="adev/src/content/examples/testing/src/app/dashboard/dashboard-hero.component.spec.ts" visibleRegion="class-only"/>
When a component has dependencies, you might want to use the `TestBed` to both create the component and its dependencies.
The following `WelcomeComponent` depends on the `UserService` to know the name of the user to greet.
IMPORTANT: Remember to either *import* or *provide* each standalone component you want to test.
<docs-code header="app/welcome/welcome.component.ts" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.ts" visibleRegion="class"/>
You might start by creating a mock of the `UserService` that meets the minimum needs of this component.
<docs-code header="app/welcome/welcome.component.spec.ts (MockUserService)" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.spec.ts" visibleRegion="mock-user-service"/>
Then provide and inject *both the* **component** *and the service* in the `TestBed` configuration.
<docs-code header="app/welcome/welcome.component.spec.ts (class-only setup)" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.spec.ts" visibleRegion="class-only-before-each"/>
Then exercise the component class, remembering to call the [lifecycle hook methods](guide/components/lifecycle) as Angular does when running the application.
<docs-code header="app/welcome/welcome.component.spec.ts (class-only tests)" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.spec.ts" visibleRegion="class-only-tests"/>
## Component DOM testing
Testing the component *class* is as straightforward as [testing a service](guide/testing/services).
But a component is more than just its class.
A component is more than just its class.
A component interacts with the DOM and with other components.
The *class-only* tests can tell you about class behavior.
They cannot tell you if the component is going to render properly, respond to user input and gestures, or integrate with its parent and child components.
None of the preceding *class-only* tests can answer key questions about how the components actually behave on screen.
Classes alone cannot tell you if the component is going to render properly, respond to user input and gestures, or integrate with its parent and child components.
* Is `Lightswitch.clicked()` bound to anything such that the user can invoke it?
* Is the `Lightswitch.message` displayed?
@@ -37,15 +37,15 @@ expected '' to contain 'Test Tour of Heroes'.
Binding happens when Angular performs **change detection**.
In production, change detection kicks in automatically when Angular creates a component or the user enters a keystroke or an asynchronous activity \(for example, AJAX\) completes.
In production, change detection kicks in automatically when Angular creates a component or the user enters a keystroke, for example.
The `TestBed.createComponent` does *not* trigger change detection; a fact confirmed in the revised test:
The `TestBed.createComponent` does not trigger change detection by default; a fact confirmed in the revised test:
<docs-code path="adev/src/content/examples/testing/src/app/banner/banner.component.spec.ts" visibleRegion="test-w-o-detect-changes"/>
### `detectChanges()`
You must tell the `TestBed` to perform data binding by calling `fixture.detectChanges()`.
You can tell the `TestBed` to perform data binding by calling `fixture.detectChanges()`.
Only then does the `<h1>` have the expected title.
<docs-code path="adev/src/content/examples/testing/src/app/banner/banner.component.spec.ts" visibleRegion="expect-h1-default"/>
@@ -60,7 +60,7 @@ Here's another test that changes the component's `title` property *before* calli
### Automatic change detection
The `BannerComponent` tests frequently call `detectChanges`.
Some testers prefer that the Angular test environment run change detection automatically.
Many testers prefer that the Angular test environment run change detection automatically like it does in production.
That's possible by configuring the `TestBed` with the `ComponentFixtureAutoDetect` provider.
First import it from the testing utility library:
@@ -71,6 +71,10 @@ Then add it to the `providers` array of the testing module configuration:
<docs-code header="app/banner/banner.component.detect-changes.spec.ts (AutoDetect)" path="adev/src/content/examples/testing/src/app/banner/banner.component.detect-changes.spec.ts" visibleRegion="auto-detect"/>
HELPFUL: You can also use the `fixture.autoDetectChanges()` function instead if you only want to enable automatic change detection
after making updates to the state of the fixture's component. In addition, automatic change detection is on by default
when using `provideExperimentalZonelessChangeDetection` and turning it off is not recommended.
Here are three tests that illustrate how automatic change detection works.
<docs-code header="app/banner/banner.component.detect-changes.spec.ts (AutoDetect Tests)" path="adev/src/content/examples/testing/src/app/banner/banner.component.detect-changes.spec.ts" visibleRegion="auto-detect-tests"/>
@@ -78,24 +82,20 @@ Here are three tests that illustrate how automatic change detection works.
The first test shows the benefit of automatic change detection.
The second and third test reveal an important limitation.
The Angular testing environment does *not* know that the test changed the component's `title`.
The `ComponentFixtureAutoDetect` service responds to *asynchronous activities* such as promise resolution, timers, and DOM events.
But a direct, synchronous update of the component property is invisible.
The test must call `fixture.detectChanges()` manually to trigger another cycle of change detection.
The Angular testing environment does not run change detection synchronously when updates happen inside the test case that changed the component's `title`.
The test must call `await fixture.whenStable` to wait for another of change detection.
HELPFUL: Rather than wonder when the test fixture will or won't perform change detection, the samples in this guide *always call* `detectChanges()` *explicitly*.
There is no harm in calling `detectChanges()` more often than is strictly necessary.
HELPFUL: Angular does not know about direct updates to values that are not signals. The easiest way to ensure that
change detection will be scheduled is to use signals for values read in the template.
### Change an input value with `dispatchEvent()`
To simulate user input, find the input element and set its `value` property.
You will call `fixture.detectChanges()` to trigger Angular's change detection.
But there is an essential, intermediate step.
Angular doesn't know that you set the input element's `value` property.
It won't read that property until you raise the element's `input` event by calling `dispatchEvent()`.
*Then* you call `detectChanges()`.
The following example demonstrates the proper sequence.
@@ -138,32 +138,18 @@ It knows who the user is based on a property of the injected `UserService`:
<docs-code header="app/welcome/welcome.component.ts" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.ts"/>
The `WelcomeComponent` has decision logic that interacts with the service, logic that makes this component worth testing.
Here's the testing module configuration for the spec file:
<docs-code header="app/welcome/welcome.component.spec.ts" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.spec.ts" visibleRegion="config-test-module"/>
This time, in addition to declaring the *component-under-test*,
the configuration adds a `UserService` provider to the `providers` list.
But not the real `UserService`.
### Provide service test doubles
A *component-under-test* doesn't have to be injected with real services.
In fact, it is usually better if they are test doubles such as, stubs, fakes, spies, or mocks.
The purpose of the spec is to test the component, not the service, and real services can be trouble.
A *component-under-test* doesn't have to be provided with real services.
Injecting the real `UserService` could be a nightmare.
Injecting the real `UserService` could be difficult.
The real service might ask the user for login credentials and attempt to reach an authentication server.
These behaviors can be hard to intercept.
It is far easier and safer to create and register a test double in place of the real `UserService`.
This particular test suite supplies a minimal mock of the `UserService` that satisfies the needs of the `WelcomeComponent` and its tests:
<docs-code header="app/welcome/welcome.component.spec.ts" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.spec.ts" visibleRegion="user-service-stub"/>
These behaviors can be hard to intercept. Be aware that using test doubles makes the test behave differently from production so use them sparingly.
### Get injected services
The tests need access to the stub `UserService` injected into the `WelcomeComponent`.
The tests need access to the `UserService` injected into the `WelcomeComponent`.
Angular has a hierarchical injection system.
There can be injectors at multiple levels, from the root injector created by the `TestBed` down through the component tree.
@@ -174,11 +160,11 @@ The component injector is a property of the fixture's `DebugElement`.
<docs-code header="WelcomeComponent's injector" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.spec.ts" visibleRegion="injected-service"/>
HELPFUL: This is _usually_ not necessary. Services are often provided in the root or the TestBed overrides and can be retrieved more easily with `TestBed.inject()` (see below).
### `TestBed.inject()`
You *might* also be able to get the service from the root injector using `TestBed.inject()`.
This is easier to remember and less verbose.
But it only works when Angular injects the component with the service instance in the test's root injector.
This is easier to remember and less verbose than retrieving a service using the fixture's `DebugElement`.
In this test suite, the *only* provider of `UserService` is the root testing module, so it is safe to call `TestBed.inject()` as follows:
@@ -196,9 +182,9 @@ And here are some tests:
<docs-code header="app/welcome/welcome.component.spec.ts" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.spec.ts" visibleRegion="tests"/>
The first is a sanity test; it confirms that the stubbed `UserService` is called and working.
The first is a sanity test; it confirms that the `UserService` is called and working.
HELPFUL: The second parameter to the Jasmine matcher \(for example, `'expected name'`\) is an optional failure label.
HELPFUL: The withContext function \(for example, `'expected name'`\) is an optional failure label.
If the expectation fails, Jasmine appends this label to the expectation failure message.
In a spec with multiple expectations, it can help clarify what went wrong and which expectation failed.
@@ -224,7 +210,6 @@ The `TwainComponent` gets quotes from an injected `TwainService`.
The component starts the returned `Observable` with a placeholder value \(`'...'`\), before the service can return its first quote.
The `catchError` intercepts service errors, prepares an error message, and returns the placeholder value on the success channel.
It must wait a tick to set the `errorMessage` in order to avoid updating that message twice in the same change detection cycle.
These are all features you'll want to test.
@@ -246,18 +231,8 @@ Unlike the real `getQuote()` method, this spy bypasses the server and returns a
You can write many useful tests with this spy, even though its `Observable` is synchronous.
### Synchronous tests
HELPFUL: It is best to limit the usage of spies to only what is necessary for the test. Creating mocks or spies for more than what's necessary can be brittle. As the component and injectable evolves, the unrelated tests can fail because they no longer mock enough behaviors that would otherwise not affect the test.
A key advantage of a synchronous `Observable` is that you can often turn asynchronous processes into synchronous tests.
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.spec.ts" visibleRegion="sync-test"/>
Because the spy result returns synchronously, the `getQuote()` method updates the message on screen immediately *after* the first change detection cycle during which Angular calls `ngOnInit`.
You're not so lucky when testing the error path.
Although the service spy will return an error synchronously, the component method calls `setTimeout()`.
The test must wait at least one full turn of the JavaScript engine before the value becomes available.
The test must become *asynchronous*.
### Async test with `fakeAsync()`
@@ -283,12 +258,16 @@ There is no nested syntax \(like a `Promise.then()`\) to disrupt the flow of con
HELPFUL: Limitation: The `fakeAsync()` function won't work if the test body makes an `XMLHttpRequest` \(XHR\) call.
XHR calls within a test are rare, but if you need to call XHR, see the [`waitForAsync()`](#waitForAsync) section.
IMPORTANT: Be aware that asynchronous tasks that happen inside the `fakeAsync` zone need to be manually executed with `flush` or `tick`. If you attempt to
wait for them to complete (i.e. using `fixture.whenStable`) without using the
`fakeAsync` test helpers to advance time, your test will likely fail. See below for more information.
### The `tick()` function
You do have to call [tick()](api/core/testing/tick) to advance the virtual clock.
Calling [tick()](api/core/testing/tick) simulates the passage of time until all pending asynchronous activities finish.
In this case, it waits for the error handler's `setTimeout()`.
In this case, it waits for the observable's `setTimeout()`.
The [tick()](api/core/testing/tick) function accepts `millis` and `tickOptions` as parameters. The `millis` parameter specifies how much the virtual clock advances and defaults to `0` if not provided.
For example, if you have a `setTimeout(fn, 100)` in a `fakeAsync()` test, you need to use `tick(100)` to trigger the fn callback.
@@ -415,21 +394,11 @@ Then call `detectChanges()` to tell Angular to update the screen.
Then you can assert that the quote element displays the expected text.
### Async test with `waitForAsync()`
### Async test without `fakeAsync()`
To use `waitForAsync()` functionality, you must import `zone.js/testing` in your test setup file.
If you created your project with the Angular CLI, `zone-testing` is already imported in `src/test.ts`.
Here's the previous `fakeAsync()` test, re-written with the `async`.
Here's the previous `fakeAsync()` test, re-written with the `waitForAsync()` utility.
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.spec.ts" visibleRegion="waitForAsync-test"/>
The `waitForAsync()` utility hides some asynchronous boilerplate by arranging for the tester's code to run in a special *async test zone*.
You don't need to pass Jasmine's `done()` into the test and call `done()` because it is `undefined` in promise or observable callbacks.
But the test's asynchronous nature is revealed by the call to `fixture.whenStable()`, which breaks the linear flow of control.
When using an `intervalTimer()` such as `setInterval()` in `waitForAsync()`, remember to cancel the timer with `clearInterval()` after the test, otherwise the `waitForAsync()` never ends.
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.spec.ts" visibleRegion="async-test"/>
### `whenStable`
@@ -439,104 +408,6 @@ Instead of calling [tick()](api/core/testing/tick), it calls `fixture.whenStable
The `fixture.whenStable()` returns a promise that resolves when the JavaScript engine's task queue becomes empty.
In this example, the task queue becomes empty when the observable emits the first quote.
The test resumes within the promise callback, which calls `detectChanges()` to update the quote element with the expected text.
### Jasmine `done()`
While the `waitForAsync()` and `fakeAsync()` functions greatly simplify Angular asynchronous testing, you can still fall back to the traditional technique and pass `it` a function that takes a [`done` callback](https://jasmine.github.io/2.0/introduction.html#section-Asynchronous_Support).
You can't call `done()` in `waitForAsync()` or `fakeAsync()` functions, because the `done parameter` is `undefined`.
Now you are responsible for chaining promises, handling errors, and calling `done()` at the appropriate moments.
Writing test functions with `done()`, is more cumbersome than `waitForAsync()`and `fakeAsync()`, but it is occasionally necessary when code involves the `intervalTimer()` like `setInterval`.
Here are two more versions of the previous test, written with `done()`.
The first one subscribes to the `Observable` exposed to the template by the component's `quote` property.
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.spec.ts" visibleRegion="quote-done-test"/>
The RxJS `last()` operator emits the observable's last value before completing, which will be the test quote.
The `subscribe` callback calls `detectChanges()` to update the quote element with the test quote, in the same manner as the earlier tests.
In some tests, you're more interested in how an injected service method was called and what values it returned, than what appears on screen.
A service spy, such as the `qetQuote()` spy of the fake `TwainService`, can give you that information and make assertions about the state of the view.
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.spec.ts" visibleRegion="spy-done-test"/>
## Component marble tests
The previous `TwainComponent` tests simulated an asynchronous observable response from the `TwainService` with the `asyncData` and `asyncError` utilities.
These are short, simple functions that you can write yourself.
Unfortunately, they're too simple for many common scenarios.
An observable often emits multiple times, perhaps after a significant delay.
A component might coordinate multiple observables with overlapping sequences of values and errors.
**RxJS marble testing** is a great way to test observable scenarios, both simple and complex.
You've likely seen the [marble diagrams](https://rxmarbles.com) that illustrate how observables work.
Marble testing uses a similar marble language to specify the observable streams and expectations in your tests.
The following examples revisit two of the `TwainComponent` tests with marble testing.
Start by installing the `jasmine-marbles` npm package.
Then import the symbols you need.
<docs-code header="app/twain/twain.component.marbles.spec.ts (import marbles)" path="adev/src/content/examples/testing/src/app/twain/twain.component.marbles.spec.ts" visibleRegion="import-marbles"/>
Here's the complete test for getting a quote:
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.marbles.spec.ts" visibleRegion="get-quote-test"/>
Notice that the Jasmine test is synchronous.
There's no `fakeAsync()`.
Marble testing uses a test scheduler to simulate the passage of time in a synchronous test.
The beauty of marble testing is in the visual definition of the observable streams.
This test defines a [*cold* observable](#learn-about-marble-testing) that waits three [frames](#learn-about-marble-testing) \(`---`\), emits a value \(`x`\), and completes \(`|`\).
In the second argument you map the value marker \(`x`\) to the emitted value \(`testQuote`\).
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.marbles.spec.ts" visibleRegion="test-quote-marbles"/>
The marble library constructs the corresponding observable, which the test sets as the `getQuote` spy's return value.
When you're ready to activate the marble observables, you tell the `TestScheduler` to *flush* its queue of prepared tasks like this.
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.marbles.spec.ts" visibleRegion="test-scheduler-flush"/>
This step serves a purpose analogous to [tick()](api/core/testing/tick) and `whenStable()` in the earlier `fakeAsync()` and `waitForAsync()` examples.
The balance of the test is the same as those examples.
### Marble error testing
Here's the marble testing version of the `getQuote()` error test.
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.marbles.spec.ts" visibleRegion="error-test"/>
It's still an async test, calling `fakeAsync()` and [tick()](api/core/testing/tick), because the component itself calls `setTimeout()` when processing errors.
Look at the marble observable definition.
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.marbles.spec.ts" visibleRegion="error-marbles"/>
This is a *cold* observable that waits three frames and then emits an error, the hash \(`#`\) character indicates the timing of the error that is specified in the third argument.
The second argument is null because the observable never emits a value.
### Learn about marble testing
A *marble frame* is a virtual unit of testing time.
Each symbol \(`-`, `x`, `|`, `#`\) marks the passing of one frame.
A *cold* observable doesn't produce values until you subscribe to it.
Most of your application observables are cold.
All [*HttpClient*](guide/http) methods return cold observables.
A *hot* observable is already producing values *before* you subscribe to it.
The [`Router.events`](api/router/Router#events) observable, which reports router activity, is a *hot* observable.
RxJS marble testing is a rich subject, beyond the scope of this guide.
Learn about it on the web, starting with the [official documentation](https://rxjs.dev/guide/testing/marble-testing).
## Component with inputs and outputs
@@ -564,22 +435,12 @@ While testing a component this simple has little intrinsic value, it's worth kno
Use one of these approaches:
* Test it as used by `DashboardComponent`
* Test it as a stand-alone component
* Test it as a standalone component
* Test it as used by a substitute for `DashboardComponent`
A quick look at the `DashboardComponent` constructor discourages the first approach:
<docs-code header="app/dashboard/dashboard.component.ts (constructor)" path="adev/src/content/examples/testing/src/app/dashboard/dashboard.component.ts" visibleRegion="ctor"/>
The `DashboardComponent` depends on the Angular router and the `HeroService`.
You'd probably have to replace them both with test doubles, which is a lot of work.
The router seems particularly challenging.
HELPFUL: The [following discussion](#routing-component) covers testing components that require the router.
The immediate goal is to test the `DashboardHeroComponent`, not the `DashboardComponent`, so, try the second and third options.
### Test `DashboardHeroComponent` stand-alone
### Test `DashboardHeroComponent` standalone
Here's the meat of the spec file setup.
@@ -593,8 +454,6 @@ The following test verifies that the hero name is propagated to the template usi
Because the [template](#dashboard-hero-component) passes the hero name through the Angular `UpperCasePipe`, the test must match the element value with the upper-cased name.
HELPFUL: This small test demonstrates how Angular tests can verify a component's visual representation —something not possible with [component class tests](guide/testing/components-basics#component-class-testing)— at low cost and without resorting to much slower and more complicated end-to-end tests.
### Clicking
Clicking the hero should raise a `selected` event that the host component \(`DashboardComponent` presumably\) can hear:
@@ -656,6 +515,7 @@ Here's the previous test, rewritten using the click helper.
<docs-code header="app/dashboard/dashboard-hero.component.spec.ts (test with click helper)" path="adev/src/content/examples/testing/src/app/dashboard/dashboard-hero.component.spec.ts" visibleRegion="click-test-3"/>
<!-- TODO(atscott): Guide above this line updated on 06/11/2024. Continue updating sections below. -->
## Component inside a test host
The previous tests played the role of the host `DashboardComponent` themselves.
@@ -1066,7 +926,7 @@ Fortunately, the `HeroDetailService` delegates responsibility for remote data ac
<docs-code header="app/hero/hero-detail.service.ts (prototype)" path="adev/src/content/examples/testing/src/app/hero/hero-detail.service.ts" visibleRegion="prototype"/>
The [previous test configuration](#feature-module-import) replaces the real `HeroService` with a `TestHeroService` that intercepts server requests and fakes their responses.
The [previous test configuration](#import-a-feature-module) replaces the real `HeroService` with a `TestHeroService` that intercepts server requests and fakes their responses.
What if you aren't so lucky.
What if faking the `HeroService` is hard?
+2 -1
View File
@@ -9,6 +9,7 @@ This table covers [Angular versions under active support](reference/releases#act
| Angular | Node.js | TypeScript | RxJS |
| ------------------ | ------------------------------------ | -------------- | ------------------ |
| 18.1.x | ^18.19.1 \|\| ^20.11.1 \|\| ^22.0.0 | >=5.4.0 <5.6.0 | ^6.5.3 \|\| ^7.4.0 |
| 18.0.x | ^18.19.1 \|\| ^20.11.1 \|\| ^22.0.0 | >=5.4.0 <5.5.0 | ^6.5.3 \|\| ^7.4.0 |
| 17.3.x | ^18.13.0 \|\| ^20.9.0 | >=5.2.0 <5.5.0 | ^6.5.3 \|\| ^7.4.0 |
| 17.1.x \|\| 17.2.x | ^18.13.0 \|\| ^20.9.0 | >=5.2.0 <5.4.0 | ^6.5.3 \|\| ^7.4.0 |
@@ -28,7 +29,7 @@ guarantees. It is listed here for historical reference.
| 15.0.x | ^14.20.0 \|\| ^16.13.0 \|\| ^18.10.0 | ~4.8.2 | ^6.5.3 \|\| ^7.4.0 |
| 14.2.x \|\| 14.3.x | ^14.15.0 \|\| ^16.10.0 | >=4.6.2 <4.9.0 | ^6.5.3 \|\| ^7.4.0 |
| 14.0.x \|\| 14.1.x | ^14.15.0 \|\| ^16.10.0 | >=4.6.2 <4.8.0 | ^6.5.3 \|\| ^7.4.0 |
| 13.3.x | ^12.20.0 \|\| ^14.15.0 \|\| ^16.10.0 | >=4.4.3 <4.7.0 | ^6.5.3 \|\| ^7.4.0 |
| 13.3.x \|\| 13.4.x | ^12.20.0 \|\| ^14.15.0 \|\| ^16.10.0 | >=4.4.3 <4.7.0 | ^6.5.3 \|\| ^7.4.0 |
| 13.1.x \|\| 13.2.x | ^12.20.0 \|\| ^14.15.0 \|\| ^16.10.0 | >=4.4.3 <4.6.0 | ^6.5.3 \|\| ^7.4.0 |
| 13.0.x | ^12.20.0 \|\| ^14.15.0 \|\| ^16.10.0 | ~4.4.3 | ^6.5.3 \|\| ^7.4.0 |
| 12.2.x | ^12.14.0 \|\| ^14.15.0 | >=4.2.3 <4.4.0 | ^6.5.3 \|\| ^7.0.0 |
+1 -1
View File
@@ -110,7 +110,7 @@ at runtime which enables:
* Extra safety checks such as [`expression-changed-after-checked`](errors/NG0100) detection.
* More detailed error messages.
* Additional debugging utilities such as the global `ng` variable and [Angular DevTools](tools/devtools) support.
* Additional debugging utilities such as the global `ng` variable with [debugging functions](api#core-global) and [Angular DevTools](tools/devtools) support.
These features are helpful during development, but they require extra code in the app, which is
undesirable in production. To ensure these features do not negatively impact bundle size for end users, Angular CLI
@@ -21,7 +21,7 @@ import {HousingLocation} from '../housinglocation';
styleUrls: ['./home.component.css'],
})
export class HomeComponent {
readonly baseUrl = 'https://angular.dev/assets/tutorials/common';
readonly baseUrl = 'https://angular.dev/assets/images/tutorials/common';
housingLocation: HousingLocation = {
id: 9999,
@@ -21,7 +21,7 @@ import {HousingLocation} from '../housinglocation';
styleUrls: ['./home.component.css'],
})
export class HomeComponent {
readonly baseUrl = 'https://angular.dev/assets/tutorials/common';
readonly baseUrl = 'https://angular.dev/assets/images/tutorials/common';
housingLocation: HousingLocation = {
id: 9999,
@@ -21,7 +21,7 @@ import {HousingLocation} from '../housinglocation';
styleUrls: ['./home.component.css'],
})
export class HomeComponent {
readonly baseUrl = 'https://angular.dev/assets/tutorials/common';
readonly baseUrl = 'https://angular.dev/assets/images/tutorials/common';
housingLocation: HousingLocation = {
id: 9999,
@@ -21,7 +21,7 @@ import {HousingLocation} from '../housinglocation';
styleUrls: ['./home.component.css'],
})
export class HomeComponent {
readonly baseUrl = 'https://angular.dev/assets/tutorials/common';
readonly baseUrl = 'https://angular.dev/assets/images/tutorials/common';
housingLocation: HousingLocation = {
id: 9999,
@@ -24,7 +24,7 @@ import {HousingLocation} from '../housinglocation';
styleUrls: ['./home.component.css'],
})
export class HomeComponent {
readonly baseUrl = 'https://angular.dev/assets/tutorials/common';
readonly baseUrl = 'https://angular.dev/assets/images/tutorials/common';
housingLocationList: HousingLocation[] = [
{
@@ -4,7 +4,7 @@ import {HousingLocation} from './housinglocation';
providedIn: 'root',
})
export class HousingService {
readonly baseUrl = 'https://angular.dev/assets/tutorials/common';
readonly baseUrl = 'https://angular.dev/assets/images/tutorials/common';
protected housingLocationList: HousingLocation[] = [
{
@@ -5,7 +5,7 @@ import {HousingLocation} from './housinglocation';
providedIn: 'root',
})
export class HousingService {
readonly baseUrl = 'https://angular.dev/assets/tutorials/common';
readonly baseUrl = 'https://angular.dev/assets/images/tutorials/common';
protected housingLocationList: HousingLocation[] = [
{
@@ -5,7 +5,7 @@ import {HousingLocation} from './housinglocation';
providedIn: 'root',
})
export class HousingService {
readonly baseUrl = 'https://angular.dev/assets/tutorials/common';
readonly baseUrl = 'https://angular.dev/assets/images/tutorials/common';
protected housingLocationList: HousingLocation[] = [
{
@@ -5,7 +5,7 @@ import {HousingLocation} from './housinglocation';
providedIn: 'root',
})
export class HousingService {
readonly baseUrl = 'https://angular.dev/assets/tutorials/common';
readonly baseUrl = 'https://angular.dev/assets/images/tutorials/common';
protected housingLocationList: HousingLocation[] = [
{
@@ -5,7 +5,7 @@ import {HousingLocation} from './housinglocation';
providedIn: 'root',
})
export class HousingService {
readonly baseUrl = 'https://angular.dev/assets/tutorials/common';
readonly baseUrl = 'https://angular.dev/assets/images/tutorials/common';
protected housingLocationList: HousingLocation[] = [
{

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 269 B

@@ -60,7 +60,7 @@ export function toSignal<T, const U extends T>(source: Observable<T> | Subscriba
// @public
export interface ToSignalOptions<T> {
equals?: ValueEqualityFn<T>;
equal?: ValueEqualityFn<T>;
initialValue?: unknown;
injector?: Injector;
manualCleanup?: boolean;
+4 -3
View File
@@ -183,7 +183,7 @@ export class ChildActivationStart {
// @public
export class ChildrenOutletContexts {
constructor(parentInjector: EnvironmentInjector);
constructor(rootInjector: EnvironmentInjector);
// (undocumented)
getContext(childName: string): OutletContext | null;
// (undocumented)
@@ -531,13 +531,14 @@ export type OnSameUrlNavigation = 'reload' | 'ignore';
// @public
export class OutletContext {
constructor(injector: EnvironmentInjector);
constructor(rootInjector: EnvironmentInjector);
// (undocumented)
attachRef: ComponentRef<any> | null;
// (undocumented)
children: ChildrenOutletContexts;
// (undocumented)
injector: EnvironmentInjector;
get injector(): EnvironmentInjector;
set injector(_: EnvironmentInjector);
// (undocumented)
outlet: RouterOutletContract | null;
// (undocumented)
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "18.1.0-next.4",
"version": "18.1.0",
"private": true,
"description": "Angular - a web framework for modern web apps",
"homepage": "https://github.com/angular/angular",
@@ -86,7 +86,7 @@
"@types/jasminewd2": "^2.0.8",
"@types/node": "^16.11.7",
"@types/selenium-webdriver": "3.0.7",
"@types/selenium-webdriver4": "npm:@types/selenium-webdriver@4.1.23",
"@types/selenium-webdriver4": "npm:@types/selenium-webdriver@4.1.24",
"@types/semver": "^7.3.4",
"@types/shelljs": "^0.8.6",
"@types/systemjs": "6.13.5",
@@ -222,7 +222,7 @@
"// 3": "Ensure that transitive dependencies on `https-proxy-agent` are at minimum v5 as older versions patch NodeJS directly, breaking tools like webdriver which is used by the karma-sauce-launcher as an example.",
"// 4": "Ensure that a single instance of the `saucelabs` package is used. Protractor and the Karma sauce launcher pull this package as dependency. A single instance allows for e.g. easier patching in the Karma config.",
"resolutions": {
"**/https-proxy-agent": "7.0.4",
"**/https-proxy-agent": "7.0.5",
"**/saucelabs": "7.5.0"
}
}
@@ -114,7 +114,6 @@ export interface AnimationPlayer {
*
* @see {@link animate}
* @see {@link AnimationPlayer}
* @see {@link ɵAnimationGroupPlayer AnimationGroupPlayer}
*
* @publicApi
*/
+6 -1
View File
@@ -76,7 +76,12 @@ export class FetchBackend implements HttpBackend {
let response;
try {
const fetchPromise = this.fetchImpl(request.urlWithParams, {signal, ...init});
// Run fetch outside of Angular zone.
// This is due to Node.js fetch implementation (Undici) which uses a number of setTimeouts to check if
// the response should eventually timeout which causes extra CD cycles every 500ms
const fetchPromise = this.ngZone.runOutsideAngular(() =>
this.fetchImpl(request.urlWithParams, {signal, ...init}),
);
// Make sure Zone.js doesn't trigger false-positive unhandled promise
// error in case the Promise is rejected synchronously. See function
@@ -30,6 +30,8 @@ import {
ɵSafeValue as SafeValue,
ɵunwrapSafeValue as unwrapSafeValue,
ChangeDetectorRef,
ApplicationRef,
ɵwhenStable as whenStable,
} from '@angular/core';
import {RuntimeErrorCode} from '../../errors';
@@ -155,6 +157,13 @@ const PRIORITY_COUNT_THRESHOLD = 10;
*/
let IMGS_WITH_PRIORITY_ATTR_COUNT = 0;
/**
* This function is for testing purpose.
*/
export function resetImagePriorityCount() {
IMGS_WITH_PRIORITY_ATTR_COUNT = 0;
}
/**
* Config options used in rendering placeholder images.
*
@@ -449,12 +458,9 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
const checker = this.injector.get(PreconnectLinkChecker);
checker.assertPreconnect(this.getRewrittenSrc(), this.ngSrc);
// This leaves the Angular zone to avoid triggering unnecessary change detection cycles when
// document.addEventListener is invoked
if (!this.isServer) {
ngZone.runOutsideAngular(() => {
assetPriorityCountBelowThreshold();
});
const applicationRef = this.injector.get(ApplicationRef);
assetPriorityCountBelowThreshold(applicationRef);
}
}
}
@@ -1278,22 +1284,23 @@ function assertNoLoaderParamsWithoutLoader(dir: NgOptimizedImage, imageLoader: I
/**
* Warns if the priority attribute is used too often on page load
*/
function assetPriorityCountBelowThreshold() {
async function assetPriorityCountBelowThreshold(appRef: ApplicationRef) {
if (IMGS_WITH_PRIORITY_ATTR_COUNT === 0) {
document.addEventListener('DOMContentLoaded', () => {
if (IMGS_WITH_PRIORITY_ATTR_COUNT > PRIORITY_COUNT_THRESHOLD) {
console.warn(
formatRuntimeError(
RuntimeErrorCode.TOO_MANY_PRIORITY_ATTRIBUTES,
`NgOptimizedImage: The "priority" attribute is set to true more than ${PRIORITY_COUNT_THRESHOLD} times (${IMGS_WITH_PRIORITY_ATTR_COUNT} times). ` +
`Marking too many images as "high" priority can hurt your application's LCP (https://web.dev/lcp). ` +
`"Priority" should only be set on the image expected to be the page's LCP element.`,
),
);
}
});
IMGS_WITH_PRIORITY_ATTR_COUNT++;
await whenStable(appRef);
if (IMGS_WITH_PRIORITY_ATTR_COUNT > PRIORITY_COUNT_THRESHOLD) {
console.warn(
formatRuntimeError(
RuntimeErrorCode.TOO_MANY_PRIORITY_ATTRIBUTES,
`NgOptimizedImage: The "priority" attribute is set to true more than ${PRIORITY_COUNT_THRESHOLD} times (${IMGS_WITH_PRIORITY_ATTR_COUNT} times). ` +
`Marking too many images as "high" priority can hurt your application's LCP (https://web.dev/lcp). ` +
`"Priority" should only be set on the image expected to be the page's LCP element.`,
),
);
}
} else {
IMGS_WITH_PRIORITY_ATTR_COUNT++;
}
IMGS_WITH_PRIORITY_ATTR_COUNT++;
}
/**
@@ -1310,7 +1317,7 @@ function assertPlaceholderDimensions(dir: NgOptimizedImage, imgElement: HTMLImag
console.warn(
formatRuntimeError(
RuntimeErrorCode.PLACEHOLDER_DIMENSION_LIMIT_EXCEEDED,
`${imgDirectiveDetails(dir.ngSrc)} it uses a placeholder image, but at least one of ` +
`${imgDirectiveDetails(dir.ngSrc)} it uses a placeholder image, but at least one ` +
`of the dimensions attribute (height or width) exceeds the limit of ${PLACEHOLDER_DIMENSION_LIMIT}px. ` +
`To fix this, use a smaller image as a placeholder.`,
),
@@ -14,7 +14,6 @@ import {
Optional,
TemplateRef,
ViewContainerRef,
ɵformatRuntimeError as formatRuntimeError,
ɵRuntimeError as RuntimeError,
} from '@angular/core';
@@ -1,13 +0,0 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* A constant indicating a type of comparison that NgSwitch uses to match cases. Extracted to a
* separate file to facilitate G3 patches.
*/
export const NG_SWITCH_USE_STRICT_EQUALS = true;
@@ -30,6 +30,7 @@ import {
NgOptimizedImage,
PLACEHOLDER_BLUR_AMOUNT,
RECOMMENDED_SRCSET_DENSITY_CAP,
resetImagePriorityCount,
} from '../../src/directives/ng_optimized_image/ng_optimized_image';
import {PRECONNECT_CHECK_BLOCKLIST} from '../../src/directives/ng_optimized_image/preconnect_link_checker';
@@ -871,12 +872,16 @@ describe('Image directive', () => {
expect(img.getAttribute('fetchpriority')).toBe('auto');
});
it('should log a warning if the priority attribute is used too often', async () => {
withHead('', () => {
it(
'should log a warning if the priority attribute is used too often',
withHead('<link rel="preconnect" href="https://angular.io/">', async () => {
// We need to reset the count as previous test might have incremented it already
resetImagePriorityCount();
const imageLoader = () => {
// We need something different from the `localhost` (as we don't want to produce
// a preconnect warning for local environments).
return 'https://angular.io/assets/images/logos/angular/angular.svg';
return 'https://angular.io/assets/images/logos/path/img.png';
};
setupTestingModule({imageLoader});
@@ -900,9 +905,10 @@ describe('Image directive', () => {
const fixture = createTestComponent(template);
fixture.detectChanges();
// We manually fire the event that is listened by the directive
// as it won't fire in the context of our unit test
document.dispatchEvent(new Event('DOMContentLoaded'));
await fixture.whenStable();
// trick to wait for the whenStable() to fire in the directive
await Promise.resolve();
if (isBrowser) {
expect(consoleWarnSpy.calls.count()).toBe(1);
@@ -913,8 +919,8 @@ describe('Image directive', () => {
// The warning is only logged on browsers
expect(consoleWarnSpy.calls.count()).toBe(0);
}
});
});
}),
);
});
describe('meta data', () => {
@@ -32,7 +32,12 @@ export class FunctionExtractor {
// Is void a better type?
const signature = this.typeChecker.getSignatureFromDeclaration(this.declaration);
const returnType = signature
? this.typeChecker.typeToString(this.typeChecker.getReturnTypeOfSignature(signature))
? this.typeChecker.typeToString(
this.typeChecker.getReturnTypeOfSignature(signature),
undefined,
// This ensures that e.g. `T | undefined` is not reduced to `T`.
ts.TypeFormatFlags.NoTypeReduction | ts.TypeFormatFlags.NoTruncation,
)
: 'unknown';
const jsdocsTags = extractJsDocTags(this.declaration);
@@ -187,11 +187,14 @@ function getContainerVariableStatement(node: ts.VariableDeclaration): ts.Variabl
/** Filters the list signatures to valid initializer API signatures. */
function filterSignatureDeclarations(signatures: readonly ts.Signature[]) {
const result: Array<ts.FunctionDeclaration | ts.CallSignatureDeclaration> = [];
const result: Array<{
signature: ts.Signature;
decl: ts.FunctionDeclaration | ts.CallSignatureDeclaration;
}> = [];
for (const signature of signatures) {
const decl = signature.getDeclaration();
if (ts.isFunctionDeclaration(decl) || ts.isCallSignatureDeclaration(decl)) {
result.push(decl);
result.push({signature, decl});
}
}
return result;
@@ -213,17 +216,20 @@ function extractFunctionWithOverloads(
): FunctionWithOverloads {
return {
name,
signatures: filterSignatureDeclarations(signatures).map((s) => ({
signatures: filterSignatureDeclarations(signatures).map(({decl, signature}) => ({
name,
entryType: EntryType.Function,
description: extractJsDocDescription(s),
generics: extractGenerics(s),
description: extractJsDocDescription(decl),
generics: extractGenerics(decl),
isNewType: false,
jsdocTags: extractJsDocTags(s),
params: extractAllParams(s.parameters, typeChecker),
rawComment: extractRawJsDoc(s),
jsdocTags: extractJsDocTags(decl),
params: extractAllParams(decl.parameters, typeChecker),
rawComment: extractRawJsDoc(decl),
returnType: typeChecker.typeToString(
typeChecker.getReturnTypeOfSignature(typeChecker.getSignatureFromDeclaration(s)!),
typeChecker.getReturnTypeOfSignature(signature),
undefined,
// This ensures that e.g. `T | undefined` is not reduced to `T`.
ts.TypeFormatFlags.NoTypeReduction | ts.TypeFormatFlags.NoTruncation,
),
})),
// Implementation may be populated later.
@@ -15,7 +15,7 @@ import {JsDocTagEntry} from './entities';
* decorators in JsDoc blocks so that they're not parsed as JsDoc tags.
*/
const decoratorExpression =
/@(?=(Injectable|Component|Directive|Pipe|NgModule|Input|Output|HostBinding|HostListener|Inject|Optional|Self|Host|SkipSelf))/g;
/@(?=(Injectable|Component|Directive|Pipe|NgModule|Input|Output|HostBinding|HostListener|Inject|Optional|Self|Host|SkipSelf|ViewChild|ViewChildren|ContentChild|ContentChildren))/g;
/** Gets the set of JsDoc tags applied to a node. */
export function extractJsDocTags(node: ts.HasJSDoc): JsDocTagEntry[] {
@@ -1647,23 +1647,23 @@ class TcbIfOp extends TcbOp {
return ts.factory.createBlock(branchScope.render());
}
// We need to process the expression first so it gets its own scope that the body of the
// conditional will inherit from. We do this, because we need to declare a separate variable
// We process the expression first in the parent scope, but create a scope around the block
// that the body will inherit from. We do this, because we need to declare a separate variable
// for the case where the expression has an alias _and_ because we need the processed
// expression when generating the guard for the body.
const expressionScope = Scope.forNodes(this.tcb, this.scope, branch, [], null);
expressionScope.render().forEach((stmt) => this.scope.addStatement(stmt));
this.expressionScopes.set(branch, expressionScope);
const outerScope = Scope.forNodes(this.tcb, this.scope, branch, [], null);
outerScope.render().forEach((stmt) => this.scope.addStatement(stmt));
this.expressionScopes.set(branch, outerScope);
let expression = tcbExpression(branch.expression, this.tcb, expressionScope);
let expression = tcbExpression(branch.expression, this.tcb, this.scope);
if (branch.expressionAlias !== null) {
expression = ts.factory.createBinaryExpression(
ts.factory.createParenthesizedExpression(expression),
ts.SyntaxKind.AmpersandAmpersandToken,
expressionScope.resolve(branch.expressionAlias),
outerScope.resolve(branch.expressionAlias),
);
}
const bodyScope = this.getBranchScope(expressionScope, branch, index);
const bodyScope = this.getBranchScope(outerScope, branch, index);
return ts.factory.createIfStatement(
expression,
@@ -1889,7 +1889,7 @@ class TcbForOfOp extends TcbOp {
// It's common to have a for loop over a nullable value (e.g. produced by the `async` pipe).
// Add a non-null expression to allow such values to be assigned.
const expression = ts.factory.createNonNullExpression(
tcbExpression(this.block.expression, this.tcb, loopScope),
tcbExpression(this.block.expression, this.tcb, this.scope),
);
const trackTranslator = new TcbForLoopTrackTranslator(this.tcb, loopScope, this.block);
const trackExpression = trackTranslator.translate(this.block.trackBy);
@@ -2723,10 +2723,23 @@ class TcbExpressionTranslator {
// returned here to let it fall through resolution so it will be caught when the
// `ImplicitReceiver` is resolved in the branch below.
const target = this.tcb.boundTarget.getExpressionTarget(ast);
if (target instanceof TmplAstLetDeclaration) {
this.validateLetDeclarationAccess(target, ast);
const targetExpression = target === null ? null : this.getTargetNodeExpression(target, ast);
if (
target instanceof TmplAstLetDeclaration &&
!this.isValidLetDeclarationAccess(target, ast)
) {
this.tcb.oobRecorder.letUsedBeforeDefinition(this.tcb.id, ast, target);
// Cast the expression to `any` so we don't produce additional diagnostics.
// We don't use `markIgnoreForDiagnostics` here, because it won't prevent duplicate
// diagnostics for nested accesses in cases like `@let value = value.foo.bar.baz`.
if (targetExpression !== null) {
return ts.factory.createAsExpression(
targetExpression,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
}
}
return target === null ? null : this.getTargetNodeExpression(target, ast);
return targetExpression;
} else if (ast instanceof PropertyWrite && ast.receiver instanceof ImplicitReceiver) {
const target = this.tcb.boundTarget.getExpressionTarget(ast);
if (target === null) {
@@ -2859,7 +2872,7 @@ class TcbExpressionTranslator {
return expr;
}
protected validateLetDeclarationAccess(target: TmplAstLetDeclaration, ast: PropertyRead): void {
protected isValidLetDeclarationAccess(target: TmplAstLetDeclaration, ast: PropertyRead): boolean {
const targetStart = target.sourceSpan.start.offset;
const targetEnd = target.sourceSpan.end.offset;
const astStart = ast.sourceSpan.start;
@@ -2867,12 +2880,7 @@ class TcbExpressionTranslator {
// We only flag local references that occur before the declaration, because embedded views
// are updated before the child views. In practice this means that something like
// `<ng-template [ngIf]="true">{{value}}</ng-template> @let value = 1;` is valid.
if (
(targetStart > astStart || (astStart >= targetStart && astStart <= targetEnd)) &&
this.scope.isLocal(target)
) {
this.tcb.oobRecorder.letUsedBeforeDefinition(this.tcb.id, ast, target);
}
return (targetStart < astStart && astStart > targetEnd) || !this.scope.isLocal(target);
}
}
@@ -3200,9 +3208,10 @@ class TcbEventHandlerTranslator extends TcbExpressionTranslator {
return super.resolve(ast);
}
protected override validateLetDeclarationAccess(): void {
protected override isValidLetDeclarationAccess(): boolean {
// Event listeners are allowed to read `@let` declarations before
// they're declared since the callback won't be executed immediately.
return true;
}
}
@@ -18,11 +18,13 @@ import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing';
import {NgtscTestEnvironment} from '../env';
const inputFixture = `
export interface InputSignal<T> {}
export interface InputFunction {
/** No explicit initial value */
<T>(): void;
<T>(): InputSignal<T|undefined>;
/** With explicit initial value */
<T>(initialValue: T): void;
<T>(initialValue: T): InputSignal<T>;
required: {
/** Required, no options */
@@ -93,6 +95,13 @@ runInEachFileSystem(() => {
);
});
it('should extract individual return types', () => {
expect(test(inputFixture)?.callFunction.signatures[0].returnType).toBe(
'InputSignal<T | undefined>',
);
expect(test(inputFixture)?.callFunction.signatures[1].returnType).toBe('InputSignal<T>');
});
it('should extract container tags', () => {
expect(test(inputFixture)?.jsdocTags).toEqual([
jasmine.objectContaining({name: 'initializerApiFunction'}),
@@ -106,12 +115,12 @@ runInEachFileSystem(() => {
signatures: [
jasmine.objectContaining<FunctionEntry>({
generics: [{name: 'T', constraint: undefined, default: undefined}],
returnType: 'void',
returnType: 'InputSignal<T | undefined>',
}),
jasmine.objectContaining<FunctionEntry>({
generics: [{name: 'T', constraint: undefined, default: undefined}],
params: [jasmine.objectContaining<ParameterEntry>({name: 'initialValue', type: 'T'})],
returnType: 'void',
returnType: 'InputSignal<T>',
}),
],
});
@@ -5818,7 +5818,7 @@ suppress
]);
});
it('should not allow usages of aliased `if` block variables inside the tracking exprssion', () => {
it('should not allow usages of aliased `if` block variables inside the tracking expression', () => {
env.write(
'/test.ts',
`
@@ -7136,6 +7136,52 @@ suppress
);
});
it('should produce a single diagnostic if a @let declaration refers to properties on itself', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`
@let value = value.a.b.c;
\`,
standalone: true,
})
export class Main {}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(
`Cannot read @let declaration 'value' before it has been defined.`,
);
});
it('should produce a single diagnostic if a @let declaration invokes itself', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`
@let value = value();
\`,
standalone: true,
})
export class Main {}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(
`Cannot read @let declaration 'value' before it has been defined.`,
);
});
it('should allow event listeners to refer to a declaration before it has been defined', () => {
env.write(
'test.ts',
@@ -7296,6 +7342,88 @@ suppress
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
it('should report @let declaration used in the expression of a @if block before it is defined', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`
@if (value) {
Hello
}
@let value = 123;
\`,
standalone: true,
})
export class Main {}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(
`Cannot read @let declaration 'value' before it has been defined.`,
);
});
it('should report @let declaration used in the expression of a @for block before it is defined', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`
@for (current of value; track $index) {
{{current}}
}
@let value = [1, 2, 3];
\`,
standalone: true,
})
export class Main {}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(
`Cannot read @let declaration 'value' before it has been defined.`,
);
});
it('should report @let declaration used in the expression of a @switch block before it is defined', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`
@switch (value) {
@case (123) {
Hello
}
}
@let value = [1, 2, 3];
\`,
standalone: true,
})
export class Main {}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(
`Cannot read @let declaration 'value' before it has been defined.`,
);
});
});
});
});
+4 -4
View File
@@ -334,8 +334,8 @@ export class ShadowCss {
* animation declaration (with possibly multiple animation definitions)
*
* The regular expression can be divided in three parts
* - (^|\s+)
* simply captures how many (if any) leading whitespaces are present
* - (^|\s+|,)
* captures how many (if any) leading whitespaces are present or a comma
* - (?:(?:(['"])((?:\\\\|\\\2|(?!\2).)+)\2)|(-?[A-Za-z][\w\-]*))
* captures two different possible keyframes, ones which are quoted or ones which are valid css
* idents (custom properties excluded)
@@ -344,7 +344,7 @@ export class ShadowCss {
* semicolon or the end of the string
*/
private _animationDeclarationKeyframesRe =
/(^|\s+)(?:(?:(['"])((?:\\\\|\\\2|(?!\2).)+)\2)|(-?[A-Za-z][\w\-]*))(?=[,\s]|$)/g;
/(^|\s+|,)(?:(?:(['"])((?:\\\\|\\\2|(?!\2).)+)\2)|(-?[A-Za-z][\w\-]*))(?=[,\s]|$)/g;
/**
* Scope an animation rule so that the keyframes mentioned in such rule
@@ -364,7 +364,7 @@ export class ShadowCss {
unscopedKeyframesSet: ReadonlySet<string>,
): CssRule {
let content = rule.content.replace(
/((?:^|\s+|;)(?:-webkit-)?animation(?:\s*):(?:\s*))([^;]+)/g,
/((?:^|\s+|;)(?:-webkit-)?animation\s*:\s*),*([^;]+)/g,
(_, start, animationDeclarations) =>
start +
animationDeclarations.replace(
@@ -122,6 +122,73 @@ describe('ShadowCss, keyframes and animations', () => {
});
});
it('should handle (scope or not) animation definition containing some names which do not have a preceding space', () => {
const COMPONENT_VARIABLE = '%COMP%';
const HOST_ATTR = `_nghost-${COMPONENT_VARIABLE}`;
const CONTENT_ATTR = `_ngcontent-${COMPONENT_VARIABLE}`;
const css = `.test {
animation:my-anim 1s,my-anim2 2s, my-anim3 3s,my-anim4 4s;
}
@keyframes my-anim {
0% {color: red}
100% {color: blue}
}
@keyframes my-anim2 {
0% {font-size: 1em}
100% {font-size: 1.2em}
}
`;
const result = shim(css, CONTENT_ATTR, HOST_ATTR);
const animationLineMatch = result.match(/animation:[^;]+;/);
let animationLine = '';
if (animationLineMatch) {
animationLine = animationLineMatch[0];
}
['my-anim', 'my-anim2'].forEach((scoped) =>
expect(animationLine).toContain(`_ngcontent-%COMP%_${scoped}`),
);
['my-anim3', 'my-anim4'].forEach((nonScoped) => {
expect(animationLine).toContain(nonScoped);
expect(animationLine).not.toContain(`_ngcontent-%COMP%_${nonScoped}`);
});
});
it('should handle (scope or not) animation definitions preceded by an erroneous comma', () => {
const COMPONENT_VARIABLE = '%COMP%';
const HOST_ATTR = `_nghost-${COMPONENT_VARIABLE}`;
const CONTENT_ATTR = `_ngcontent-${COMPONENT_VARIABLE}`;
const css = `.test {
animation:, my-anim 1s,my-anim2 2s, my-anim3 3s,my-anim4 4s;
}
@keyframes my-anim {
0% {color: red}
100% {color: blue}
}
@keyframes my-anim2 {
0% {font-size: 1em}
100% {font-size: 1.2em}
}
`;
const result = shim(css, CONTENT_ATTR, HOST_ATTR);
const animationLineMatch = result.match(/animation:[^;]+;/);
let animationLine = '';
if (animationLineMatch) {
animationLine = animationLineMatch[0];
}
expect(result).not.toContain('animation:,');
['my-anim', 'my-anim2'].forEach((scoped) =>
expect(animationLine).toContain(`_ngcontent-%COMP%_${scoped}`),
);
['my-anim3', 'my-anim4'].forEach((nonScoped) => {
expect(animationLine).toContain(nonScoped);
expect(animationLine).not.toContain(`_ngcontent-%COMP%_${nonScoped}`);
});
});
it('should handle (scope or not) multiple animation definitions in a single declaration', () => {
const css = `
div {
+22 -19
View File
@@ -77,7 +77,7 @@ export interface ToSignalOptions<T> {
*
* Equality comparisons are executed against the initial value if one is provided.
*/
equals?: ValueEqualityFn<T>;
equal?: ValueEqualityFn<T>;
}
// Base case: no options -> `undefined` in the result type.
@@ -147,7 +147,7 @@ export function toSignal<T, U = undefined>(
? options?.injector?.get(DestroyRef) ?? inject(DestroyRef)
: null;
const equal = makeToSignalEquals(options?.equals);
const equal = makeToSignalEqual(options?.equal);
// Note: T is the Observable value type, and U is the initial value type. They don't have to be
// the same - the returned signal gives values of type `T`.
@@ -195,25 +195,28 @@ export function toSignal<T, U = undefined>(
// The actual returned signal is a `computed` of the `State` signal, which maps the various states
// to either values or errors.
return computed(() => {
const current = state();
switch (current.kind) {
case StateKind.Value:
return current.value;
case StateKind.Error:
throw current.error;
case StateKind.NoValue:
// This shouldn't really happen because the error is thrown on creation.
// TODO(alxhub): use a RuntimeError when we finalize the error semantics
throw new ɵRuntimeError(
ɵRuntimeErrorCode.REQUIRE_SYNC_WITHOUT_SYNC_EMIT,
'`toSignal()` called with `requireSync` but `Observable` did not emit synchronously.',
);
}
});
return computed(
() => {
const current = state();
switch (current.kind) {
case StateKind.Value:
return current.value;
case StateKind.Error:
throw current.error;
case StateKind.NoValue:
// This shouldn't really happen because the error is thrown on creation.
// TODO(alxhub): use a RuntimeError when we finalize the error semantics
throw new ɵRuntimeError(
ɵRuntimeErrorCode.REQUIRE_SYNC_WITHOUT_SYNC_EMIT,
'`toSignal()` called with `requireSync` but `Observable` did not emit synchronously.',
);
}
},
{equal: options?.equal},
);
}
function makeToSignalEquals<T>(
function makeToSignalEqual<T>(
userEquality: ValueEqualityFn<T> = Object.is,
): ValueEqualityFn<State<T>> {
return (a, b) =>
@@ -251,7 +251,7 @@ describe('toSignal()', () => {
const counter$ = new Subject<{value: number}>();
const counter = toSignal(counter$, {
initialValue: {value: 0},
equals: (a, b) => a.value === b.value,
equal: (a, b) => a.value === b.value,
});
let updates = 0;
@@ -274,6 +274,30 @@ describe('toSignal()', () => {
expect(updates).toBe(3);
}),
);
it(
'should update when values are reference equal but equality function says otherwise',
test(() => {
const numsSet = new Set<number>();
const nums$ = new BehaviorSubject<Set<number>>(numsSet);
const nums = toSignal(nums$, {
requireSync: true,
equal: () => false,
});
let updates = 0;
const tracker = computed(() => {
updates++;
return Array.from(nums()!.values());
});
expect(tracker()).toEqual([]);
numsSet.add(1);
nums$.next(numsSet); // same value as before
expect(tracker()).toEqual([1]);
expect(updates).toBe(2);
}),
);
});
describe('in a @Component', () => {
@@ -150,8 +150,19 @@ export class ElementToMigrate {
this.aliasAttrs = aliasAttrs;
}
normalizeConditionString(value: string): string {
value = this.insertSemicolon(value, value.indexOf(' else '));
value = this.insertSemicolon(value, value.indexOf(' then '));
value = this.insertSemicolon(value, value.indexOf(' let '));
return value.replace(';;', ';');
}
insertSemicolon(str: string, ix: number): string {
return ix > -1 ? `${str.slice(0, ix)};${str.slice(ix)}` : str;
}
getCondition(): string {
const chunks = this.attr.value.split(';');
const chunks = this.normalizeConditionString(this.attr.value).split(';');
let condition = chunks[0];
// checks for case of no usage of `;` in if else / if then else
@@ -1674,6 +1674,48 @@ describe('control flow migration', () => {
);
});
it('should migrate if/else with let variable in wrong place with no semicolons', async () => {
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
import {NgIf} from '@angular/common';
@Component({
templateUrl: './comp.html'
})
class Comp {
user$ = of({ name: 'Jane' }})
}
`,
);
writeFile(
'/comp.html',
[
`<div>`,
`<div *ngIf="user of users; let tooltipContent else noUserBlock">{{ user.name }}</div>`,
`<ng-template #noUserBlock>No user</ng-template>`,
`</div>`,
].join('\n'),
);
await runMigration();
const content = tree.readContent('/comp.html');
expect(content).toBe(
[
`<div>`,
` @if (user of users; as tooltipContent) {`,
` <div>{{ user.name }}</div>`,
` } @else {`,
` No user`,
` }`,
`</div>`,
].join('\n'),
);
});
it('should migrate if/then/else with alias', async () => {
writeFile(
'/comp.ts',
+67 -43
View File
@@ -10,7 +10,7 @@ import {setActiveConsumer} from '@angular/core/primitives/signals';
import {CachedInjectorService} from '../cached_injector_service';
import {NotificationSource} from '../change_detection/scheduling/zoneless_scheduling';
import {EnvironmentInjector, InjectionToken, Injector} from '../di';
import {EnvironmentInjector, InjectionToken, Injector, Provider} from '../di';
import {internalImportProvidersFrom} from '../di/provider_collection';
import {RuntimeError, RuntimeErrorCode} from '../errors';
import {findMatchingDehydratedView} from '../hydration/views';
@@ -90,7 +90,6 @@ import {
setLDeferBlockDetails,
setTDeferBlockDetails,
} from './utils';
import {isRouterOutletInjector} from '../render3/util/injector_utils';
/**
* **INTERNAL**, avoid referencing it in application code.
@@ -626,20 +625,73 @@ export function renderDeferBlockState(
}
/**
* Creates an instance of the `OutletInjector` using a private factory
* function available on the `OutletInjector` class.
*
* @param parentOutletInjector Parent OutletInjector, which should be used
* to produce a new instance.
* @param parentInjector An Injector, which should be used as a parent one
* for a newly created `OutletInjector` instance.
* Checks whether there is a cached injector associated with a given defer block
* declaration and returns if it exists. If there is no cached injector present -
* creates a new injector and stores in the cache.
*/
function createRouterOutletInjector(
parentOutletInjector: ChainedInjector,
function getOrCreateEnvironmentInjector(
parentInjector: Injector,
tDetails: TDeferBlockDetails,
providers: Provider[],
) {
const outletInjector = parentOutletInjector.injector as any;
return outletInjector.__ngOutletInjector(parentInjector);
return parentInjector
.get(CachedInjectorService)
.getOrCreateInjector(
tDetails,
parentInjector as EnvironmentInjector,
providers,
ngDevMode ? 'DeferBlock Injector' : '',
);
}
/**
* Creates a new injector, which contains providers collected from dependencies (NgModules) of
* defer-loaded components. This function detects different types of parent injectors and creates
* a new injector based on that.
*/
function createDeferBlockInjector(
parentInjector: Injector,
tDetails: TDeferBlockDetails,
providers: Provider[],
) {
// Check if the parent injector is an instance of a `ChainedInjector`.
//
// In this case, we retain the shape of the injector and use a newly created
// `EnvironmentInjector` as a parent in the `ChainedInjector`. That is needed to
// make sure that the primary injector gets consulted first (since it's typically
// a NodeInjector) and `EnvironmentInjector` tree is consulted after that.
if (parentInjector instanceof ChainedInjector) {
const origInjector = parentInjector.injector;
// Guaranteed to be an environment injector
const parentEnvInjector = parentInjector.parentInjector;
const envInjector = getOrCreateEnvironmentInjector(parentEnvInjector, tDetails, providers);
return new ChainedInjector(origInjector, envInjector);
}
const parentEnvInjector = parentInjector.get(EnvironmentInjector);
// If the `parentInjector` is *not* an `EnvironmentInjector` - we need to create
// a new `ChainedInjector` with the following setup:
//
// - the provided `parentInjector` becomes a primary injector
// - an existing (real) `EnvironmentInjector` becomes a parent injector for
// a newly-created one, which contains extra providers
//
// So the final order in which injectors would be consulted in this case would look like this:
//
// 1. Provided `parentInjector`
// 2. Newly-created `EnvironmentInjector` with extra providers
// 3. `EnvironmentInjector` from the `parentInjector`
if (parentEnvInjector !== parentInjector) {
const envInjector = getOrCreateEnvironmentInjector(parentEnvInjector, tDetails, providers);
return new ChainedInjector(parentInjector, envInjector);
}
// The `parentInjector` is an instance of an `EnvironmentInjector`.
// No need for special handling, we can use `parentInjector` as a
// parent injector directly.
return getOrCreateEnvironmentInjector(parentInjector, tDetails, providers);
}
/**
@@ -672,40 +724,12 @@ function applyDeferBlockState(
// newly loaded standalone components used within the block, which may
// import NgModules with providers. In order to make those providers
// available for components declared in that NgModule, we create an instance
// of environment injector to host those providers and pass this injector
// of an environment injector to host those providers and pass this injector
// to the logic that creates a view.
const tDetails = getTDeferBlockDetails(hostTView, tNode);
const providers = tDetails.providers;
if (providers && providers.length > 0) {
const parentInjector = hostLView[INJECTOR] as Injector;
// Note: we have a special case for Router's `OutletInjector`,
// since it's not an instance of the `EnvironmentInjector`, so
// we can't inject it. Once the `OutletInjector` is replaced
// with the `EnvironmentInjector` in Router's code, this special
// handling can be removed.
const isParentOutletInjector = isRouterOutletInjector(parentInjector);
const parentEnvInjector = isParentOutletInjector
? parentInjector
: parentInjector.get(EnvironmentInjector);
injector = parentEnvInjector
.get(CachedInjectorService)
.getOrCreateInjector(
tDetails,
parentEnvInjector as EnvironmentInjector,
providers,
ngDevMode ? 'DeferBlock Injector' : '',
);
// Note: this is a continuation of the special case for Router's `OutletInjector`.
// Since the `OutletInjector` handles `ActivatedRoute` and `ChildrenOutletContexts`
// dynamically (i.e. their values are not really stored statically in an injector),
// we need to "wrap" a defer injector into another `OutletInjector`, so we retain
// the dynamic resolution of the mentioned tokens.
if (isParentOutletInjector) {
injector = createRouterOutletInjector(parentInjector as ChainedInjector, injector);
}
injector = createDeferBlockInjector(hostLView[INJECTOR]!, tDetails, providers);
}
}
const dehydratedView = findMatchingDehydratedView(lContainer, activeBlockTNode.tView!.ssrId);
@@ -207,11 +207,13 @@ function handleInstanceCreatedByInjectorEvent(
// if our value is an instance of a standalone component, map the injector of that standalone
// component to the component class. Otherwise, this event is a noop.
let standaloneComponent: Type<unknown> | undefined = undefined;
let standaloneComponent: Type<unknown> | undefined | null = undefined;
if (typeof value === 'object') {
standaloneComponent = value?.constructor as Type<unknown>;
standaloneComponent = value?.constructor as Type<unknown> | undefined | null;
}
if (standaloneComponent === undefined || !isStandaloneComponent(standaloneComponent)) {
// We want to also cover if `standaloneComponent === null` in addition to `undefined`
if (standaloneComponent == undefined || !isStandaloneComponent(standaloneComponent)) {
return;
}
@@ -31,6 +31,9 @@ import {
ViewChildren,
ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR,
ɵRuntimeError as RuntimeError,
Injector,
ElementRef,
ViewChild,
} from '@angular/core';
import {getComponentDef} from '@angular/core/src/render3/definition';
import {
@@ -4113,6 +4116,101 @@ describe('@defer', () => {
`<child-cmp>Token A: ${tokenA} | Token B: ${tokenB}</child-cmp>`,
);
});
it(
'should provide access to tokens from a parent component ' +
'for components instantiated via `createComponent` call (when a corresponding NodeInjector is used in the call), ' +
'but attached to the ApplicationRef',
async () => {
const TokenA = new InjectionToken('A');
const TokenB = new InjectionToken('B');
@NgModule({
providers: [{provide: TokenB, useValue: 'TokenB value'}],
})
class MyModule {}
@Component({
selector: 'lazy',
standalone: true,
imports: [MyModule],
template: `
Lazy Component! Token: {{ token }}
`,
})
class Lazy {
token = inject(TokenA);
}
@Component({
standalone: true,
imports: [Lazy],
template: `
@defer (on immediate) {
<lazy />
}
`,
})
class Dialog {}
@Component({
standalone: true,
selector: 'app-root',
providers: [{provide: TokenA, useValue: 'TokenA from RootCmp'}],
template: `
<div #container></div>
`,
})
class RootCmp {
injector = inject(Injector);
appRef = inject(ApplicationRef);
envInjector = inject(EnvironmentInjector);
@ViewChild('container', {read: ElementRef}) container!: ElementRef;
openModal() {
const hostElement = this.container.nativeElement;
const componentRef = createComponent(Dialog, {
hostElement,
elementInjector: this.injector,
environmentInjector: this.envInjector,
});
this.appRef.attachView(componentRef.hostView);
componentRef.changeDetectorRef.detectChanges();
}
}
const deferDepsInterceptor = {
intercept() {
return () => {
return [dynamicImportOf(Lazy)];
};
},
};
TestBed.configureTestingModule({
providers: [
{provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor},
],
deferBlockBehavior: DeferBlockBehavior.Playthrough,
});
const fixture = TestBed.createComponent(RootCmp);
fixture.detectChanges();
fixture.componentInstance.openModal();
// The call above instantiates a component that uses a `@defer` block,
// so we need to wait for dynamic imports to complete.
await allPendingDynamicImports();
fixture.detectChanges();
// Verify that tokens from parent components are available for injection
// inside a component within a `@defer` block.
expect(fixture.nativeElement.innerHTML).toContain(
`<lazy> Lazy Component! Token: TokenA from RootCmp </lazy>`,
);
},
);
});
describe('NgModules', () => {
@@ -932,6 +932,9 @@
{
"name": "getOrCreateComponentTView"
},
{
"name": "getOrCreateEnvironmentInjector"
},
{
"name": "getOrCreateInjectable"
},
@@ -16,7 +16,6 @@ ts_library(
],
tsconfig = ":tsconfig",
deps = [
"@npm//@angular-devkit/core",
"@npm//@angular-devkit/schematics",
"@npm//@schematics/angular",
],
+38 -52
View File
@@ -8,23 +8,15 @@
* @fileoverview Schematics for `ng add @angular/localize` schematic.
*/
import {chain, noop, Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
import {
chain,
noop,
Rule,
SchematicContext,
SchematicsException,
Tree,
} from '@angular-devkit/schematics';
import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks';
import {
addPackageJsonDependency,
NodeDependencyType,
removePackageJsonDependency,
} from '@schematics/angular/utility/dependencies';
AngularBuilder,
addDependency,
readWorkspace,
updateWorkspace,
} from '@schematics/angular/utility';
import {removePackageJsonDependency} from '@schematics/angular/utility/dependencies';
import {JSONFile, JSONPath} from '@schematics/angular/utility/json-file';
import {getWorkspace, updateWorkspace} from '@schematics/angular/utility/workspace';
import {Builders} from '@schematics/angular/utility/workspace-models';
import {Schema} from './schema';
@@ -43,11 +35,11 @@ function addPolyfillToConfig(projectName: string): Rule {
for (const target of project.targets.values()) {
switch (target.builder) {
case Builders.Karma:
case Builders.Server:
case Builders.Browser:
case Builders.BrowserEsbuild:
case Builders.Application:
case AngularBuilder.Karma:
case AngularBuilder.Server:
case AngularBuilder.Browser:
case AngularBuilder.BrowserEsbuild:
case AngularBuilder.Application:
target.options ??= {};
const value = target.options['polyfills'];
if (typeof value === 'string') {
@@ -70,7 +62,7 @@ function addPolyfillToConfig(projectName: string): Rule {
function addTypeScriptConfigTypes(projectName: string): Rule {
return async (host: Tree) => {
const workspace = await getWorkspace(host);
const workspace = await readWorkspace(host);
const project = workspace.projects.get(projectName);
if (!project) {
throw new SchematicsException(`Invalid project name '${projectName}'.`);
@@ -80,11 +72,11 @@ function addTypeScriptConfigTypes(projectName: string): Rule {
const tsConfigFiles = new Set<string>();
for (const target of project.targets.values()) {
switch (target.builder) {
case Builders.Karma:
case Builders.Server:
case Builders.BrowserEsbuild:
case Builders.Browser:
case Builders.Application:
case AngularBuilder.Karma:
case AngularBuilder.Server:
case AngularBuilder.BrowserEsbuild:
case AngularBuilder.Browser:
case AngularBuilder.Application:
const value = target.options?.['tsConfig'];
if (typeof value === 'string') {
tsConfigFiles.add(value);
@@ -93,12 +85,15 @@ function addTypeScriptConfigTypes(projectName: string): Rule {
break;
}
if (target.builder === Builders.Browser || target.builder === Builders.BrowserEsbuild) {
if (
target.builder === AngularBuilder.Browser ||
target.builder === AngularBuilder.BrowserEsbuild
) {
const value = target.options?.['main'];
if (typeof value === 'string') {
addTripleSlashType(host, value);
}
} else if (target.builder === Builders.Application) {
} else if (target.builder === AngularBuilder.Application) {
const value = target.options?.['browser'];
if (typeof value === 'string') {
addTripleSlashType(host, value);
@@ -140,40 +135,31 @@ function addTripleSlashType(host: Tree, path: string): void {
}
}
function moveToDependencies(host: Tree, context: SchematicContext): void {
function moveToDependencies(host: Tree): Rule | void {
if (!host.exists('package.json')) {
return;
}
// Remove the previous dependency and add in a new one under the desired type.
removePackageJsonDependency(host, '@angular/localize');
addPackageJsonDependency(host, {
name: '@angular/localize',
type: NodeDependencyType.Default,
version: `~0.0.0-PLACEHOLDER`,
});
// Add a task to run the package manager. This is necessary because we updated
// "package.json" and we want lock files to reflect this.
context.addTask(new NodePackageInstallTask());
return addDependency('@angular/localize', `~0.0.0-PLACEHOLDER`);
}
export default function (options: Schema): Rule {
return () => {
// We favor the name option because the project option has a
// smart default which can be populated even when unspecified by the user.
const projectName = options.name ?? options.project;
// We favor the name option because the project option has a
// smart default which can be populated even when unspecified by the user.
const projectName = options.name ?? options.project;
if (!projectName) {
throw new SchematicsException('Option "project" is required.');
}
if (!projectName) {
throw new SchematicsException('Option "project" is required.');
}
return chain([
addTypeScriptConfigTypes(projectName),
addPolyfillToConfig(projectName),
// If `$localize` will be used at runtime then must install `@angular/localize`
// into `dependencies`, rather than the default of `devDependencies`.
options.useAtRuntime ? moveToDependencies : noop(),
]);
};
return chain([
addTypeScriptConfigTypes(projectName),
addPolyfillToConfig(projectName),
// If `$localize` will be used at runtime then must install `@angular/localize`
// into `dependencies`, rather than the default of `devDependencies`.
options.useAtRuntime ? moveToDependencies : noop(),
]);
}
@@ -11,3 +11,7 @@ export {
SERVER_RENDER_PROVIDERS as ɵSERVER_RENDER_PROVIDERS,
} from './server';
export {SERVER_CONTEXT as ɵSERVER_CONTEXT} from './utils';
export {
enableSsrProfiling as ɵenableSsrProfiling,
disableSsrProfiling as ɵdisableSsrProfiling,
} from './profiler';
+64
View File
@@ -0,0 +1,64 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
const PERFORMANCE_MARK_PREFIX = '🅰️';
let enablePerfLogging = false;
export function runAndMeasurePerf<T>(label: string, method: () => T): T {
if (!enablePerfLogging) {
return method();
}
const labelName = `${PERFORMANCE_MARK_PREFIX}:${label}`;
const startLabel = `start:${labelName}`;
const endLabel = `end:${labelName}`;
const end = () => {
/* tslint:disable:ban */
performance.mark(endLabel);
performance.measure(labelName, startLabel, endLabel);
performance.clearMarks(startLabel);
performance.clearMarks(endLabel);
/* tslint:enable:ban */
};
/* tslint:disable:ban */
performance.mark(startLabel);
/* tslint:enable:ban */
const returnValue = method();
if (returnValue instanceof Promise) {
return returnValue.finally(() => end()) as T;
} else {
end();
return returnValue;
}
}
let warningLogged = false;
/**
* This enables an internal performance profiler for SSR apps
*
* It should not be imported in application code
*/
export function enableSsrProfiling() {
if (
!warningLogged &&
(typeof performance === 'undefined' || !performance.mark || !performance.measure)
) {
warningLogged = true;
console.warn('Performance API is not supported on this platform');
return;
}
enablePerfLogging = true;
}
export function disableSsrProfiling() {
enablePerfLogging = false;
}
+10 -4
View File
@@ -26,6 +26,7 @@ import {PlatformState} from './platform_state';
import {platformServer} from './server';
import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG} from './tokens';
import {createScript} from './transfer_state';
import {runAndMeasurePerf} from './profiler';
/**
* Event dispatch (JSAction) script is inlined into the HTML by the build
@@ -145,7 +146,9 @@ function insertEventRecordScript(
// This is defined in packages/core/primitives/event-dispatch/contract_binary.ts
const replayScriptContents = `window.__jsaction_bootstrap('ngContracts', document.body, ${JSON.stringify(
appId,
)}, ${JSON.stringify(Array.from(regular))}${capture.size ? ',' + JSON.stringify(Array.from(capture)) : ''});`;
)}, ${JSON.stringify(Array.from(regular))}${
capture.size ? ',' + JSON.stringify(Array.from(capture)) : ''
});`;
const replayScript = createScript(doc, replayScriptContents, nonce);
@@ -272,7 +275,10 @@ export async function renderApplication<T>(
bootstrap: () => Promise<ApplicationRef>,
options: {document?: string | Document; url?: string; platformProviders?: Provider[]},
): Promise<string> {
const platformRef = createServerPlatform(options);
const applicationRef = await bootstrap();
return _render(platformRef, applicationRef);
return runAndMeasurePerf('renderApplication', async () => {
const platformRef = createServerPlatform(options);
const applicationRef = await bootstrap();
return _render(platformRef, applicationRef);
});
}
+20 -1
View File
@@ -9,6 +9,9 @@
import {Component} from '@angular/core';
import {RouterOutlet} from '../directives/router_outlet';
import {PRIMARY_OUTLET} from '../shared';
import {Route} from '../models';
export {ɵEmptyOutletComponent as EmptyOutletComponent};
/**
* This component is used internally within the router to be a placeholder when an empty
@@ -26,4 +29,20 @@ import {RouterOutlet} from '../directives/router_outlet';
})
export class ɵEmptyOutletComponent {}
export {ɵEmptyOutletComponent as EmptyOutletComponent};
/**
* Makes a copy of the config and adds any default required properties.
*/
export function standardizeConfig(r: Route): Route {
const children = r.children && r.children.map(standardizeConfig);
const c = children ? {...r, children} : {...r};
if (
!c.component &&
!c.loadComponent &&
(children || c.loadChildren) &&
c.outlet &&
c.outlet !== PRIMARY_OUTLET
) {
c.component = ɵEmptyOutletComponent;
}
return c;
}
@@ -221,10 +221,8 @@ export class ActivateRoutes {
advanceActivatedRoute(stored.route.value);
this.activateChildRoutes(futureNode, null, context.children);
} else {
const injector = getClosestRouteInjector(future.snapshot);
context.attachRef = null;
context.route = future;
context.injector = injector ?? context.injector;
if (context.outlet) {
// Activate the outlet when it has already been instantiated
// Otherwise it will get activated from its `ngOnInit` when instantiated
+2 -1
View File
@@ -54,8 +54,9 @@ import {
UrlSerializer,
UrlTree,
} from './url_tree';
import {standardizeConfig, validateConfig} from './utils/config';
import {validateConfig} from './utils/config';
import {afterNextNavigation} from './utils/navigations';
import {standardizeConfig} from './components/empty_outlet';
function defaultErrorHandler(error: any): never {
throw error;
+2 -1
View File
@@ -21,7 +21,8 @@ import {finalize, map, mergeMap, refCount, tap} from 'rxjs/operators';
import {DefaultExport, LoadedRouterConfig, Route, Routes} from './models';
import {wrapIntoObservable} from './utils/collection';
import {assertStandalone, standardizeConfig, validateConfig} from './utils/config';
import {assertStandalone, validateConfig} from './utils/config';
import {standardizeConfig} from './components/empty_outlet';
/**
* The DI token for a router configuration.
+11 -4
View File
@@ -10,6 +10,7 @@ import {ComponentRef, EnvironmentInjector, Injectable} from '@angular/core';
import {RouterOutletContract} from './directives/router_outlet';
import {ActivatedRoute} from './router_state';
import {getClosestRouteInjector} from './utils/config';
/**
* Store contextual information about a `RouterOutlet`
@@ -19,9 +20,15 @@ import {ActivatedRoute} from './router_state';
export class OutletContext {
outlet: RouterOutletContract | null = null;
route: ActivatedRoute | null = null;
children = new ChildrenOutletContexts(this.injector);
children = new ChildrenOutletContexts(this.rootInjector);
attachRef: ComponentRef<any> | null = null;
constructor(public injector: EnvironmentInjector) {}
get injector(): EnvironmentInjector {
return getClosestRouteInjector(this.route?.snapshot) ?? this.rootInjector;
}
// TODO(atscott): Only here to avoid a "breaking" change in a patch/minor. Remove in v19.
set injector(_: EnvironmentInjector) {}
constructor(private readonly rootInjector: EnvironmentInjector) {}
}
/**
@@ -35,7 +42,7 @@ export class ChildrenOutletContexts {
private contexts = new Map<string, OutletContext>();
/** @nodoc */
constructor(private parentInjector: EnvironmentInjector) {}
constructor(private rootInjector: EnvironmentInjector) {}
/** Called when a `RouterOutlet` directive is instantiated */
onChildOutletCreated(childName: string, outlet: RouterOutletContract): void {
@@ -75,7 +82,7 @@ export class ChildrenOutletContexts {
let context = this.getContext(childName);
if (!context) {
context = new OutletContext(this.parentInjector);
context = new OutletContext(this.rootInjector);
this.contexts.set(childName, context);
}
+1 -20
View File
@@ -15,7 +15,6 @@ import {
ɵRuntimeError as RuntimeError,
} from '@angular/core';
import {EmptyOutletComponent} from '../components/empty_outlet';
import {RuntimeErrorCode} from '../errors';
import {Route, Routes} from '../models';
import {ActivatedRouteSnapshot} from '../router_state';
@@ -222,24 +221,6 @@ function getFullPath(parentPath: string, currentRoute: Route): string {
}
}
/**
* Makes a copy of the config and adds any default required properties.
*/
export function standardizeConfig(r: Route): Route {
const children = r.children && r.children.map(standardizeConfig);
const c = children ? {...r, children} : {...r};
if (
!c.component &&
!c.loadComponent &&
(children || c.loadChildren) &&
c.outlet &&
c.outlet !== PRIMARY_OUTLET
) {
c.component = EmptyOutletComponent;
}
return c;
}
/** Returns the `route.outlet` or PRIMARY_OUTLET if none exists. */
export function getOutlet(route: Route): string {
return route.outlet || PRIMARY_OUTLET;
@@ -268,7 +249,7 @@ export function sortByMatchingOutlets(routes: Routes, outletName: string): Route
* also used for getting the correct injector to use for creating components.
*/
export function getClosestRouteInjector(
snapshot: ActivatedRouteSnapshot,
snapshot: ActivatedRouteSnapshot | undefined,
): EnvironmentInjector | null {
if (!snapshot) return null;
@@ -427,6 +427,44 @@ describe('injectors', () => {
fixture.detectChanges();
expect(childTokenValue).toEqual(null);
});
it('should not get sibling providers', async () => {
let childTokenValue: any = null;
const TOKEN = new InjectionToken<any>('');
@Component({
template: '',
standalone: true,
})
class Child {
constructor() {
childTokenValue = inject(TOKEN, {optional: true});
}
}
@Component({
template: '<router-outlet/>',
imports: [RouterOutlet],
standalone: true,
})
class App {}
TestBed.configureTestingModule({
providers: [
provideRouter([
{path: 'a', providers: [{provide: TOKEN, useValue: 'a value'}], component: Child},
{path: 'b', component: Child},
]),
],
});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
await TestBed.inject(Router).navigateByUrl('/a');
fixture.detectChanges();
expect(childTokenValue).toEqual('a value');
await TestBed.inject(Router).navigateByUrl('/b');
fixture.detectChanges();
expect(childTokenValue).toEqual(null);
});
});
function advance(fixture: ComponentFixture<unknown>, millis?: number): void {
+17 -9
View File
@@ -4607,11 +4607,10 @@
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a"
integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==
"@types/selenium-webdriver4@npm:@types/selenium-webdriver@4.1.23", "@types/selenium-webdriver@^4.1.21":
name "@types/selenium-webdriver4"
version "4.1.23"
resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.1.23.tgz#05a2794927db661f075ab443d5504b679b32f7f7"
integrity sha512-PgreEfCfafYLyTwvJTZvOspCq3JABnS51e+NSFFL5yoiMO7h04lWgLfr10NA7nl/yZbz4m76rBfOOdDfleb7pQ==
"@types/selenium-webdriver4@npm:@types/selenium-webdriver@4.1.24":
version "4.1.24"
resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.1.24.tgz#f19f4c73fa176ef6076805cf433a0c991bab9cd7"
integrity sha512-oR5MVATv+P2dmhceZJPDm99MmOC9yAK8YpIgJbHEFQ/MbSPC1lA6Ohw441WNUcQ6B1fMAOMu0u59fRKKj9AGGg==
dependencies:
"@types/node" "*"
"@types/ws" "*"
@@ -4626,6 +4625,15 @@
resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.26.tgz#fc7d87d580affa2e52685b2e881bc201819a5836"
integrity sha512-dyIGFKXfUFiwkMfNGn1+F6b80ZjR3uSYv1j6xVJSDlft5waZ2cwkHW4e7zNzvq7hiEackcgvBpmnXZrI1GltPg==
"@types/selenium-webdriver@^4.1.21":
name "@types/selenium-webdriver4"
version "4.1.23"
resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.1.23.tgz#05a2794927db661f075ab443d5504b679b32f7f7"
integrity sha512-PgreEfCfafYLyTwvJTZvOspCq3JABnS51e+NSFFL5yoiMO7h04lWgLfr10NA7nl/yZbz4m76rBfOOdDfleb7pQ==
dependencies:
"@types/node" "*"
"@types/ws" "*"
"@types/semver@^7.3.4", "@types/semver@^7.3.6", "@types/semver@^7.5.5":
version "7.5.8"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"
@@ -9693,10 +9701,10 @@ http2-wrapper@^1.0.0-beta.5.2:
quick-lru "^5.1.1"
resolve-alpn "^1.0.0"
https-proxy-agent@7.0.4, https-proxy-agent@^2.2.1, https-proxy-agent@^4.0.0, https-proxy-agent@^5.0.0, https-proxy-agent@^7.0.1, https-proxy-agent@^7.0.2, https-proxy-agent@^7.0.3, https-proxy-agent@^7.0.4:
version "7.0.4"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz#8e97b841a029ad8ddc8731f26595bad868cb4168"
integrity sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==
https-proxy-agent@7.0.4, https-proxy-agent@7.0.5, https-proxy-agent@^2.2.1, https-proxy-agent@^4.0.0, https-proxy-agent@^5.0.0, https-proxy-agent@^7.0.1, https-proxy-agent@^7.0.2, https-proxy-agent@^7.0.3, https-proxy-agent@^7.0.4:
version "7.0.5"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2"
integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==
dependencies:
agent-base "^7.0.2"
debug "4"