Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 07a0b87a4c | |||
| 551ffbf9f2 | |||
| 4d87b9e899 | |||
| 0da9eae24c | |||
| 15d2e8d54c | |||
| 3a453d6040 | |||
| 8bd4c074af | |||
| 5f97d6aec2 | |||
| 2f8bf933f4 | |||
| ae9505b702 | |||
| 0f4219cf0c | |||
| f689214178 | |||
| aef166f763 | |||
| 02f5e73a8d | |||
| 20f5e1291e | |||
| 8adc6b9425 | |||
| 35fa9b0f77 | |||
| 56ec97d2e1 | |||
| ba325c59c0 | |||
| 447a36ef95 | |||
| 8326e62313 | |||
| 9b35726e42 | |||
| cdebf751e4 | |||
| 66e582551e | |||
| 1efe907881 | |||
| 1074134d4b | |||
| ecc56447b9 | |||
| 85da4b3b2a | |||
| 3fa21cbbc5 | |||
| 1ee64a5c45 | |||
| 3235d6d6dd | |||
| 6ad2fe6ab0 | |||
| 0e0fb7bb61 | |||
| 8a0a00e212 | |||
| 092bfe95cc | |||
| c4384bf269 | |||
| 52852abb56 | |||
| 0f612dcaf8 | |||
| 9acccb11ff | |||
| e914767efb | |||
| 58be8e6bf8 | |||
| 9cc4dbeadd | |||
| 7f5d70a74a |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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: [
|
||||
|
||||
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 258 KiB |
|
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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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?
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||