Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 58ee039d5e | |||
| 3d0ee73892 | |||
| 8bb5937e71 | |||
| c969422da0 | |||
| 1a606b0bfc | |||
| 288a4fac01 | |||
| 889a02b1c3 | |||
| 23fa43d044 | |||
| c979dfd462 | |||
| 915b0e0b16 | |||
| 463945003d | |||
| 06ae22680e | |||
| f917b6e4d0 | |||
| 87d042ba8a | |||
| 4b561f8f35 | |||
| b9fb98c67c | |||
| 8a005eada5 | |||
| ae15dd94ef | |||
| 7792aad8da | |||
| 142c2dffb0 | |||
| 56f0fc1f9f | |||
| 9e52c1c840 | |||
| e904f34020 | |||
| be16d7fcc3 | |||
| 04c504ab40 | |||
| 91483ab5b3 | |||
| e088c80854 | |||
| 14d442a6c3 | |||
| 1999e31851 | |||
| f1349e1d0f | |||
| f3014c5392 | |||
| bbabf5451a | |||
| edbb403171 | |||
| d3e14b35d3 | |||
| 4bfe707ac2 | |||
| 4f11d6eee2 | |||
| a1cb9dfc0d | |||
| f02f351cda | |||
| e9837c94d0 | |||
| 5682527d94 | |||
| 4c1f3cf843 | |||
| daf0317bdc | |||
| 6cfc4d8c85 | |||
| da9149ea10 | |||
| bbe655ff14 | |||
| 7c0a7a02c0 | |||
| 35b2728994 | |||
| 653e898f78 | |||
| c0855f0e25 | |||
| c94a897248 | |||
| b666d2c20f | |||
| 634d55a4f6 | |||
| 5c08ae68ad | |||
| 60df722d29 | |||
| a531106578 | |||
| d7dca6dbb6 | |||
| f6eec4813f | |||
| 94c6ec0221 | |||
| a7b774c4a2 | |||
| 5673f6b04d | |||
| aedf8322b0 | |||
| e076eb2ae6 | |||
| aa08ecc538 | |||
| 4b82b9815d | |||
| bbbaf7a1b8 | |||
| 729bf5b828 | |||
| 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 |
@@ -5,6 +5,9 @@ inputs:
|
||||
serviceKey:
|
||||
description: 'The service key used for firebase deployments.'
|
||||
required: true
|
||||
githubReleaseTrainReadToken:
|
||||
description: 'GitHub access token for reading release trains without rate limits.'
|
||||
required: true
|
||||
configPath:
|
||||
description: 'The path to the firebase config file.'
|
||||
required: true
|
||||
|
||||
@@ -13,3 +13,8 @@ export function getCredentialFilePath(): string {
|
||||
}
|
||||
return credentialFilePath;
|
||||
}
|
||||
|
||||
/** Github access token. Used for querying the active release trains. */
|
||||
export const githubReleaseTrainReadToken: string = getInput('githubReleaseTrainReadToken', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {fetchLongTermSupportBranchesFromNpm, ActiveReleaseTrains} from '@angular/ng-dev';
|
||||
import {ReleaseConfig} from '@angular/ng-dev';
|
||||
import {GitClient} from '@angular/ng-dev';
|
||||
import {AuthenticatedGitClient} from '@angular/ng-dev';
|
||||
|
||||
export interface Deployment {
|
||||
branch: string;
|
||||
@@ -14,7 +14,7 @@ export interface Deployment {
|
||||
export type Deployments = Map<string, Deployment>;
|
||||
|
||||
export async function getDeployments(): Promise<Deployments> {
|
||||
const {github} = await GitClient.get();
|
||||
const {github} = await AuthenticatedGitClient.get();
|
||||
const releaseTrains = await ActiveReleaseTrains.fetch({
|
||||
api: github,
|
||||
name: 'angular',
|
||||
|
||||
@@ -2,7 +2,8 @@ import {getInput, setFailed} from '@actions/core';
|
||||
import {context} from '@actions/github';
|
||||
import {deployToFirebase, setupRedirect} from './deploy';
|
||||
import {getDeployments} from './deployments';
|
||||
import {GithubConfig, setConfig} from '@angular/ng-dev';
|
||||
import {AuthenticatedGitClient, GithubConfig, setConfig} from '@angular/ng-dev';
|
||||
import {githubReleaseTrainReadToken} from './credential';
|
||||
|
||||
const refMatcher = /refs\/heads\/(.*)/;
|
||||
|
||||
@@ -14,6 +15,9 @@ async function deployDocs() {
|
||||
owner: 'angular',
|
||||
},
|
||||
});
|
||||
|
||||
AuthenticatedGitClient.configure(githubReleaseTrainReadToken);
|
||||
|
||||
if (context.eventName !== 'push') {
|
||||
throw Error();
|
||||
}
|
||||
|
||||
@@ -11344,6 +11344,9 @@ function getCredentialFilePath() {
|
||||
}
|
||||
return credentialFilePath;
|
||||
}
|
||||
var githubReleaseTrainReadToken = (0, import_core.getInput)("githubReleaseTrainReadToken", {
|
||||
required: true
|
||||
});
|
||||
|
||||
//
|
||||
async function deployToFirebase(deployment, configPath, distDirPath) {
|
||||
@@ -12977,7 +12980,7 @@ AuthenticatedGitClient._authenticatedInstance = null;
|
||||
|
||||
//
|
||||
async function getDeployments() {
|
||||
const { github } = await GitClient.get();
|
||||
const { github } = await AuthenticatedGitClient.get();
|
||||
const releaseTrains = await ActiveReleaseTrains.fetch({
|
||||
api: github,
|
||||
name: "angular",
|
||||
@@ -13033,6 +13036,7 @@ async function deployDocs() {
|
||||
owner: "angular"
|
||||
}
|
||||
});
|
||||
AuthenticatedGitClient.configure(githubReleaseTrainReadToken);
|
||||
if (import_github3.context.eventName !== "push") {
|
||||
throw Error();
|
||||
}
|
||||
|
||||
@@ -197,5 +197,6 @@ jobs:
|
||||
uses: ./.github/actions/deploy-docs-site
|
||||
with:
|
||||
serviceKey: ${{ secrets.ANGULAR_DEV_SITE_DEPLOY }}
|
||||
githubReleaseTrainReadToken: ${{ secrets.DOCS_DEPLOY_GITHUB_RELEASE_TRAIN_TOKEN }}
|
||||
configPath: 'adev/firebase.json'
|
||||
distDir: 'dist/bin/adev/build/browser'
|
||||
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
- name: Run CI tests for framework
|
||||
run: yarn tsx ./scripts/build/build-packages-dist.mts
|
||||
- name: Archive build artifacts
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
with:
|
||||
name: pr-artifacts-${{ github.event.number }}
|
||||
path: dist/packages-dist/
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
# Upload the results as artifacts.
|
||||
- name: 'Upload artifact'
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
@@ -47,6 +47,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: 'Upload to code-scanning'
|
||||
uses: github/codeql-action/upload-sarif@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
|
||||
uses: github/codeql-action/upload-sarif@2d790406f505036ef40ecba973cc774a50395aac # v3.25.13
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -597,7 +597,8 @@ groups:
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
'adev/**/{.*,*}'
|
||||
'adev/**/{.*,*}',
|
||||
'aio/**/{.*,*}'
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
|
||||
@@ -1,3 +1,104 @@
|
||||
<a name="18.1.2"></a>
|
||||
# 18.1.2 (2024-07-24)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [463945003d](https://github.com/angular/angular/commit/463945003dcf253c64809ffdcddabedb87e78e06) | fix | limit the number of chained instructions ([#57069](https://github.com/angular/angular/pull/57069)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e904f34020](https://github.com/angular/angular/commit/e904f3402053ce9098854a31f2652020dd79e8f9) | fix | add warning for unused let declarations ([#57033](https://github.com/angular/angular/pull/57033)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [9e52c1c840](https://github.com/angular/angular/commit/9e52c1c8402dd5a54cf73caafff4a5b81d73fabd) | fix | `afterNextRender` hooks return that callback value. ([#57031](https://github.com/angular/angular/pull/57031)) |
|
||||
| [b9fb98c67c](https://github.com/angular/angular/commit/b9fb98c67c29d8e5697b72788f09f1263e8130f4) | fix | tree shake dev mode error message ([#57035](https://github.com/angular/angular/pull/57035)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="18.1.1"></a>
|
||||
# 18.1.1 (2024-07-17)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [a1cb9dfc0d](https://github.com/angular/angular/commit/a1cb9dfc0d3539d16020a53dd4c32311240a6265) | fix | Don't run preconnect assertion on the server. ([#56213](https://github.com/angular/angular/pull/56213)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [daf0317bdc](https://github.com/angular/angular/commit/daf0317bdcef79445eead4a3e0e1be75671f43fb) | fix | JIT mode incorrectly interpreting host directive configuration in partial compilation ([#57002](https://github.com/angular/angular/pull/57002)) |
|
||||
| [d7dca6dbb6](https://github.com/angular/angular/commit/d7dca6dbb6d8afc77a988de0b7471ac4e078762e) | fix | use strict equality for 'code' comparison ([#56944](https://github.com/angular/angular/pull/56944)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c94a897248](https://github.com/angular/angular/commit/c94a8972488f62656f5d0368b8763776e2cd39c6) | fix | avoid emitting references to typecheck files in TS 5.4 ([#56961](https://github.com/angular/angular/pull/56961)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5682527d94](https://github.com/angular/angular/commit/5682527d949b078e92ff7588c7e25dad37fffc52) | fix | not all callbacks running when registered at the same time ([#56981](https://github.com/angular/angular/pull/56981)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [b666d2c20f](https://github.com/angular/angular/commit/b666d2c20f932f435fa6c51e1d74d7bca53381f2) | fix | fix common module removal ([#56968](https://github.com/angular/angular/pull/56968)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<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 +110,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 +127,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 +149,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 +165,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
|
||||
|
||||
@@ -353,9 +353,9 @@ DEPRECATED: <what is deprecated>
|
||||
Closes #<pr number>
|
||||
```
|
||||
|
||||
Breaking Change section should start with the phrase "BREAKING CHANGE: " followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
|
||||
Breaking Change section should start with the phrase `BREAKING CHANGE: ` followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
|
||||
|
||||
Similarly, a Deprecation section should start with "DEPRECATED: " followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path.
|
||||
Similarly, a Deprecation section should start with `DEPRECATED: ` followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path.
|
||||
|
||||
|
||||
### Revert commits
|
||||
|
||||
@@ -75,7 +75,7 @@ yarn_install(
|
||||
"//:.yarnrc",
|
||||
"//:tools/npm-patches/@bazel+jasmine+5.8.1.patch",
|
||||
"//tools:postinstall-patches.js",
|
||||
"//tools/esm-interop:patches/npm/@angular+build-tooling+0.0.0-239d56b71911f9fa1eeefb6e4505dbe7b0cd81a7.patch",
|
||||
"//tools/esm-interop:patches/npm/@angular+build-tooling+0.0.0-d30a56c19bafaac67cf44e605ed8c2c0e45b0a51.patch",
|
||||
"//tools/esm-interop:patches/npm/@bazel+concatjs+5.8.1.patch",
|
||||
"//tools/esm-interop:patches/npm/@bazel+esbuild+5.7.1.patch",
|
||||
"//tools/esm-interop:patches/npm/@bazel+protractor+5.7.1.patch",
|
||||
@@ -143,10 +143,10 @@ cldr_xml_data_repository(
|
||||
# sass rules
|
||||
http_archive(
|
||||
name = "io_bazel_rules_sass",
|
||||
sha256 = "47c50aa960ddf875a2a2dd7efd1c839e5f8598725c00014680122b0e48d0ec7f",
|
||||
strip_prefix = "rules_sass-b222c61b3d3879ec45b66062b2c706a72f3d80bb",
|
||||
sha256 = "cd83736ea65d0df064283aea5922dbaf132dd2b3aa54e7151aae7edaa9572c3e",
|
||||
strip_prefix = "rules_sass-83022b98114c07e9588089c7fe8f76bc0262c7e7",
|
||||
urls = [
|
||||
"https://github.com/bazelbuild/rules_sass/archive/b222c61b3d3879ec45b66062b2c706a72f3d80bb.zip",
|
||||
"https://github.com/bazelbuild/rules_sass/archive/83022b98114c07e9588089c7fe8f76bc0262c7e7.zip",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ APPLICATION_FILES = [
|
||||
|
||||
TEST_FILES = APPLICATION_FILES + [
|
||||
"karma.conf.js",
|
||||
"test-main.ts",
|
||||
"tsconfig.spec.json",
|
||||
] + glob(
|
||||
["**/*.spec.ts"],
|
||||
@@ -78,6 +79,7 @@ APPLICATION_DEPS = [
|
||||
"@npm//@lezer/highlight",
|
||||
"@npm//@lezer/javascript",
|
||||
"@npm//@lezer/common",
|
||||
"@npm//@stackblitz/sdk",
|
||||
"@npm//@xterm/xterm",
|
||||
"@npm//@xterm/addon-fit",
|
||||
"@npm//algoliasearch",
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -84,10 +84,10 @@
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": ["zone.js", "zone.js/testing"],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"include": ["src/app"],
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"main": "test-main.ts",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["@angular/docs/styles/global-styles.scss"],
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"ignore": ["**/.*"],
|
||||
"headers": [
|
||||
{
|
||||
"source": "assets/tutorials/common/*.jpg",
|
||||
"source": "assets/images/tutorials/common/*.jpg",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Resource-Policy",
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
TitleStrategy,
|
||||
createUrlTreeFromSnapshot,
|
||||
provideRouter,
|
||||
withComponentInputBinding,
|
||||
withInMemoryScrolling,
|
||||
withViewTransitions,
|
||||
} from '@angular/router';
|
||||
@@ -70,6 +71,7 @@ export const appConfig: ApplicationConfig = {
|
||||
}
|
||||
},
|
||||
}),
|
||||
withComponentInputBinding(),
|
||||
),
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
provideClientHydration(),
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {PROGRESS_BAR_DELAY, ProgressBarComponent} from './progress-bar.component';
|
||||
import {RouterTestingHarness, RouterTestingModule} from '@angular/router/testing';
|
||||
@@ -25,13 +25,13 @@ describe('ProgressBarComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should call progressBar.complete() on route change', fakeAsync(async () => {
|
||||
it('should call progressBar.complete() on route change', async () => {
|
||||
const progressBarCompleteSpy = spyOn(component.progressBar, 'complete');
|
||||
|
||||
const harness = await RouterTestingHarness.create();
|
||||
await harness.navigateByUrl('/');
|
||||
|
||||
tick(PROGRESS_BAR_DELAY);
|
||||
await new Promise((resolve) => setTimeout(resolve, PROGRESS_BAR_DELAY));
|
||||
expect(progressBarCompleteSpy).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
@use '@angular/docs/styles/media-queries' as mq;
|
||||
|
||||
:host {
|
||||
z-index: 100;
|
||||
|
||||
@include mq.for-tablet-landscape-up {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
// Render secondary nav below primary nav
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
@include mq.for-tablet-landscape-down {
|
||||
position: fixed;
|
||||
// Render secondary nav above mobile menu
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@include mq.for-phone-only {
|
||||
transform: translateX(0);
|
||||
z-index: 99;
|
||||
transition: transform 0.3s ease;
|
||||
&:has(.docs-nav-secondary--open) {
|
||||
transform: translateX(82px);
|
||||
@@ -40,7 +37,10 @@
|
||||
transition: transform 0.45s ease;
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition: transform 0.45s ease, background-color 0.3s ease, border-color 0.3s ease;
|
||||
transition:
|
||||
transform 0.45s ease,
|
||||
background-color 0.3s ease,
|
||||
border-color 0.3s ease;
|
||||
}
|
||||
|
||||
@include mq.for-tablet-landscape-down {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ChangeDetectionStrategy, Component, Inject} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, Inject, inject} from '@angular/core';
|
||||
import {MAT_SNACK_BAR_DATA, MatSnackBarAction, MatSnackBarRef} from '@angular/material/snack-bar';
|
||||
|
||||
export interface ErrorSnackBarData {
|
||||
@@ -37,10 +37,8 @@ export class ErrorSnackBar {
|
||||
protected message: string;
|
||||
protected actionText?: string;
|
||||
|
||||
constructor(
|
||||
protected snackBarRef: MatSnackBarRef<ErrorSnackBar>,
|
||||
@Inject(MAT_SNACK_BAR_DATA) public data: ErrorSnackBarData,
|
||||
) {
|
||||
constructor(protected snackBarRef: MatSnackBarRef<ErrorSnackBar>) {
|
||||
const data = inject(MAT_SNACK_BAR_DATA) as ErrorSnackBarData;
|
||||
this.message = data.message;
|
||||
this.actionText = data.actionText;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export class VersionManager {
|
||||
// version: 'rc',
|
||||
// },
|
||||
{
|
||||
url: this.getAdevDocsUrl(this.currentMajorVersion),
|
||||
url: 'https://angular.dev/',
|
||||
displayName: this.getVersion(this.currentMajorVersion),
|
||||
version: this.currentVersionMode,
|
||||
},
|
||||
|
||||
@@ -65,6 +65,14 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="adev-editor-download-button"
|
||||
type="button"
|
||||
(click)="openCurrentCodeInStackBlitz()"
|
||||
aria-label="Open current code in editor in StackBlitz"
|
||||
>
|
||||
<docs-icon>launch</docs-icon>
|
||||
</button>
|
||||
<button
|
||||
class="adev-editor-download-button"
|
||||
type="button"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {NgFor, NgIf} from '@angular/common';
|
||||
import {Location, NgFor, NgIf} from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {MatTabGroup, MatTabsModule} from '@angular/material/tabs';
|
||||
import {Title} from '@angular/platform-browser';
|
||||
import {debounceTime, map} from 'rxjs';
|
||||
|
||||
import {TerminalType} from '../terminal/terminal-handler.service';
|
||||
@@ -28,6 +29,7 @@ import {EmbeddedTutorialManager} from '../embedded-tutorial-manager.service';
|
||||
import {CodeMirrorEditor} from './code-mirror-editor.service';
|
||||
import {DiagnosticWithLocation, DiagnosticsState} from './services/diagnostics-state.service';
|
||||
import {DownloadManager} from '../download-manager.service';
|
||||
import {StackBlitzOpener} from '../stackblitz-opener.service';
|
||||
import {ClickOutside, IconComponent} from '@angular/docs';
|
||||
|
||||
export const REQUIRED_FILES = new Set([
|
||||
@@ -36,6 +38,8 @@ export const REQUIRED_FILES = new Set([
|
||||
'src/app/app.component.ts',
|
||||
]);
|
||||
|
||||
const ANGULAR_DEV = 'https://angular.dev';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-tutorial-code-editor',
|
||||
standalone: true,
|
||||
@@ -73,6 +77,9 @@ export class CodeEditor implements AfterViewInit, OnDestroy {
|
||||
private readonly codeMirrorEditor = inject(CodeMirrorEditor);
|
||||
private readonly diagnosticsState = inject(DiagnosticsState);
|
||||
private readonly downloadManager = inject(DownloadManager);
|
||||
private readonly stackblitzOpener = inject(StackBlitzOpener);
|
||||
private readonly title = inject(Title);
|
||||
private readonly location = inject(Location);
|
||||
private readonly embeddedTutorialManager = inject(EmbeddedTutorialManager);
|
||||
|
||||
private readonly errors$ = this.diagnosticsState.diagnostics$.pipe(
|
||||
@@ -110,6 +117,16 @@ export class CodeEditor implements AfterViewInit, OnDestroy {
|
||||
this.codeMirrorEditor.disable();
|
||||
}
|
||||
|
||||
async openCurrentCodeInStackBlitz(): Promise<void> {
|
||||
const title = this.title.getTitle();
|
||||
|
||||
const path = this.location.path();
|
||||
const editorUrl = `${ANGULAR_DEV}${path}`;
|
||||
const description = `Angular.dev example generated from [${editorUrl}](${editorUrl})`;
|
||||
|
||||
await this.stackblitzOpener.openCurrentSolutionInStackBlitz({title, description});
|
||||
}
|
||||
|
||||
async downloadCurrentCodeEditorState(): Promise<void> {
|
||||
const name = this.embeddedTutorialManager.tutorialId();
|
||||
await this.downloadManager.downloadCurrentStateOfTheSolution(name);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {signal} from '@angular/core';
|
||||
import {TestBed, fakeAsync, tick} from '@angular/core/testing';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {BehaviorSubject, Subject} from 'rxjs';
|
||||
import {EditorState} from '@codemirror/state';
|
||||
import type {FileSystemTree} from '@webcontainer/api';
|
||||
@@ -142,17 +142,19 @@ describe('CodeMirrorEditor', () => {
|
||||
expect(service.currentFile().content).toBe(newContent);
|
||||
});
|
||||
|
||||
it('should write the changed file content to the sandbox filesystem', fakeAsync(() => {
|
||||
it('should write the changed file content to the sandbox filesystem', () => {
|
||||
jasmine.clock().install();
|
||||
jasmine.clock().mockDate();
|
||||
const newContent = 'new content';
|
||||
|
||||
const nodeRuntimeSandboxSpy = spyOn(fakeNodeRuntimeSandbox, 'writeFile');
|
||||
|
||||
dispatchDocumentChange(newContent);
|
||||
|
||||
tick(EDITOR_CONTENT_CHANGE_DELAY_MILLIES);
|
||||
jasmine.clock().tick(EDITOR_CONTENT_CHANGE_DELAY_MILLIES);
|
||||
|
||||
expect(nodeRuntimeSandboxSpy).toHaveBeenCalledWith(service.currentFile().filename, newContent);
|
||||
}));
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
it('should add created file to code editor', async () => {
|
||||
const newFile = 'new-component.component.ts';
|
||||
|
||||
@@ -107,7 +107,10 @@ export class NodeRuntimeSandbox {
|
||||
|
||||
console.timeEnd('Load time');
|
||||
} catch (error: any) {
|
||||
this.setErrorState(error.message);
|
||||
// If we're already in an error state, throw away the most recent error which may have happened because
|
||||
// we were in the error state already and tried to do more things after terminating.
|
||||
const message = this.nodeRuntimeState.error()?.message ?? error.message;
|
||||
this.setErrorState(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {TestBed, fakeAsync, tick} from '@angular/core/testing';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {DebugElement, signal} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {of} from 'rxjs';
|
||||
@@ -64,16 +64,16 @@ describe('Preview', () => {
|
||||
};
|
||||
};
|
||||
|
||||
it('should set iframe src on init', fakeAsync(() => {
|
||||
it('should set iframe src on init', async () => {
|
||||
const {component, PREVIEW_URL} = beforeEach();
|
||||
|
||||
component.ngAfterViewInit();
|
||||
|
||||
tick(100);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(component.previewIframe?.nativeElement).toBeTruthy();
|
||||
expect(component.previewIframe?.nativeElement?.src).toBe(PREVIEW_URL);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not render loading elements if the loadingStep is READY or ERROR', () => {
|
||||
const {fixture, fakeNodeRuntimeState, getLoadingElementsWrapper} = beforeEach();
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {StackBlitzOpener} from './stackblitz-opener.service';
|
||||
|
||||
describe('StackBlitzOpener', () => {
|
||||
let service: StackBlitzOpener;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(StackBlitzOpener);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {EnvironmentInjector, Injectable, inject} from '@angular/core';
|
||||
import sdk, {Project, ProjectFiles} from '@stackblitz/sdk';
|
||||
import {injectAsync} from '../core/services/inject-async';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StackBlitzOpener {
|
||||
private readonly environmentInjector = inject(EnvironmentInjector);
|
||||
|
||||
/**
|
||||
* Generate a StackBlitz project from the current state of the solution in the EmbeddedEditor
|
||||
*/
|
||||
async openCurrentSolutionInStackBlitz(
|
||||
projectMetadata: Pick<Project, 'title' | 'description'>,
|
||||
): Promise<void> {
|
||||
const nodeRuntimeSandbox = await injectAsync(this.environmentInjector, () =>
|
||||
import('./node-runtime-sandbox.service').then((c) => c.NodeRuntimeSandbox),
|
||||
);
|
||||
|
||||
const runtimeFiles = await nodeRuntimeSandbox.getSolutionFiles();
|
||||
|
||||
const stackblitzProjectFiles: ProjectFiles = {};
|
||||
runtimeFiles.forEach((file) => {
|
||||
// Leading slashes are incompatible with StackBlitz SDK they are removed
|
||||
const path = file.path.replace(/^\//, '');
|
||||
|
||||
stackblitzProjectFiles[path] =
|
||||
typeof file.content !== 'string' ? new TextDecoder().decode(file.content) : file.content;
|
||||
});
|
||||
|
||||
sdk.openProject({
|
||||
...projectMetadata,
|
||||
template: 'node',
|
||||
files: stackblitzProjectFiles,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
|
||||
import {Terminal} from './terminal.component';
|
||||
@@ -61,12 +61,12 @@ describe('Terminal', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should call resizeToFitParent on window resize', fakeAsync(() => {
|
||||
it('should call resizeToFitParent on window resize', async () => {
|
||||
fakeWindow.dispatchEvent(new Event('resize'));
|
||||
|
||||
// debounce time
|
||||
tick(50);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(terminalHandlerSpy.resizeToFitParent).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
.docs-code {
|
||||
pre {
|
||||
margin-block: 0;
|
||||
padding-block: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,13 +119,13 @@
|
||||
transition: background-color 0.3s ease;
|
||||
font-family: monospace;
|
||||
|
||||
&.hljs-ln-line {
|
||||
&.line {
|
||||
font-weight: 400;
|
||||
text-align: left;
|
||||
padding-block: 0.25rem;
|
||||
}
|
||||
|
||||
&.hljs-ln-line-highlighted {
|
||||
&.shiki-ln-line-highlighted {
|
||||
background-color: var(--senary-contrast);
|
||||
}
|
||||
&:hover {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import {HarnessLoader} from '@angular/cdk/testing';
|
||||
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
|
||||
import {TestBed, waitForAsync} from '@angular/core/testing';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {MatTabGroupHarness} from '@angular/material/tabs/testing';
|
||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {ReferenceScrollHandler} from '../services/reference-scroll-handler.service';
|
||||
@@ -68,15 +68,15 @@ describe('ApiReferenceDetailsPage', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render tabs for all elements with tab attribute', waitForAsync(async () => {
|
||||
it('should render tabs for all elements with tab attribute', async () => {
|
||||
const matTabGroup = await loader.getHarness(MatTabGroupHarness);
|
||||
|
||||
const tabs = await matTabGroup.getTabs();
|
||||
|
||||
expect(tabs.length).toBe(4);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should display members cards when API tab is active', waitForAsync(async () => {
|
||||
it('should display members cards when API tab is active', async () => {
|
||||
const matTabGroup = await loader.getHarness(MatTabGroupHarness);
|
||||
const tabs = await matTabGroup.getTabs();
|
||||
|
||||
@@ -94,7 +94,7 @@ describe('ApiReferenceDetailsPage', () => {
|
||||
|
||||
membersCard = harness.fixture.debugElement.query(By.css('.docs-reference-members-container'));
|
||||
expect(membersCard).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
|
||||
it('should setup scroll listeners when API members are loaded', () => {
|
||||
const setupListenersSpy = spyOn(fakeApiReferenceScrollHandler, 'setupListeners');
|
||||
|
||||
@@ -1,30 +1,13 @@
|
||||
<div class="adev-reference-list-page">
|
||||
<header>
|
||||
<h6>Getting Started</h6>
|
||||
<h1 tabindex="-1">API Reference</h1>
|
||||
<h1>API Reference</h1>
|
||||
</header>
|
||||
|
||||
@if (featuredGroup().items.length) {
|
||||
<adev-api-items-section [group]="featuredGroup()" class="adev-featured-list" />
|
||||
<adev-api-items-section [group]="featuredGroup()" class="adev-featured-list" />
|
||||
}
|
||||
|
||||
<form class="adev-reference-list-form">
|
||||
<docs-text-field
|
||||
name="query"
|
||||
placeholder="Filter"
|
||||
[ngModel]="query()"
|
||||
(ngModelChange)="query.set($event)"
|
||||
/>
|
||||
|
||||
<div class="adev-reference-list-form-part-two">
|
||||
<docs-slide-toggle
|
||||
buttonId="includeDeprecated"
|
||||
label="Show @deprecated"
|
||||
name="includeDeprecated"
|
||||
[ngModel]="includeDeprecated()"
|
||||
(ngModelChange)="includeDeprecated.set($event)"
|
||||
/>
|
||||
</div>
|
||||
<ul class="adev-reference-list-legend">
|
||||
@for (itemType of itemTypes; track itemType) {
|
||||
<li
|
||||
@@ -37,10 +20,24 @@
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
<docs-text-field name="query" placeholder="Filter" [(ngModel)]="query" />
|
||||
|
||||
<div class="adev-reference-list-form-part-two">
|
||||
<docs-slide-toggle
|
||||
buttonId="includeDeprecated"
|
||||
label="Show @deprecated"
|
||||
name="includeDeprecated"
|
||||
[(ngModel)]="includeDeprecated"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@for (group of filteredGroups(); track group.id) {
|
||||
<adev-api-items-section [group]="group" />
|
||||
} @empty {
|
||||
<div class="adev-reference-list-empty">
|
||||
<p>No API items found.</p>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
|
||||
@for (group of filteredGroups(); track group.id) {
|
||||
<adev-api-items-section [group]="group" />
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
.adev-reference-list-legend {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
padding-block-end: 0.5rem;
|
||||
margin-block-start: 0;
|
||||
padding-inline: 0;
|
||||
cursor: pointer;
|
||||
@@ -57,7 +56,10 @@
|
||||
border-radius: 0.25rem;
|
||||
margin-inline-end: 0.5rem;
|
||||
margin-block-end: 0.5rem;
|
||||
transition: color 0.3s ease, background 0.3s ease, border 0.3s ease;
|
||||
transition:
|
||||
color 0.3s ease,
|
||||
background 0.3s ease,
|
||||
border 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-contrast);
|
||||
@@ -74,8 +76,8 @@
|
||||
|
||||
.adev-reference-list-form {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap-reverse;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
padding-block-start: 1.5rem;
|
||||
padding-block-end: 2rem;
|
||||
@@ -88,6 +90,13 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.adev-reference-list-empty {
|
||||
text-align: center;
|
||||
p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.adev-featured-list {
|
||||
display: block;
|
||||
padding-block-end: 1rem;
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import ApiReferenceList, {ALL_STATUSES_KEY} from './api-reference-list.component';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {ApiReferenceManager} from './api-reference-manager.service';
|
||||
import {signal} from '@angular/core';
|
||||
import {ApiItemType} from '../interfaces/api-item-type';
|
||||
import {RouterTestingHarness} from '@angular/router/testing';
|
||||
import {provideRouter} from '@angular/router';
|
||||
|
||||
describe('ApiReferenceList', () => {
|
||||
let component: ApiReferenceList;
|
||||
@@ -52,8 +53,11 @@ describe('ApiReferenceList', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ApiReferenceList, RouterTestingModule],
|
||||
providers: [{provide: ApiReferenceManager, useValue: fakeApiReferenceManager}],
|
||||
imports: [ApiReferenceList],
|
||||
providers: [
|
||||
{provide: ApiReferenceManager, useValue: fakeApiReferenceManager},
|
||||
provideRouter([{path: 'api', component: ApiReferenceList}]),
|
||||
],
|
||||
});
|
||||
fixture = TestBed.createComponent(ApiReferenceList);
|
||||
component = fixture.componentInstance;
|
||||
@@ -90,25 +94,27 @@ describe('ApiReferenceList', () => {
|
||||
});
|
||||
|
||||
it('should display only class items when user selects Class in the Type select', () => {
|
||||
component.type.set(ApiItemType.CLASS);
|
||||
fixture.componentInstance.type.set(ApiItemType.CLASS);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.type()).toEqual(ApiItemType.CLASS);
|
||||
expect(component.filteredGroups()![0].items).toEqual([fakeItem2]);
|
||||
});
|
||||
|
||||
it('should set selected type when provided type is different than selected', () => {
|
||||
it('should set selected type when provided type is different than selected', async () => {
|
||||
expect(component.type()).toBe(ALL_STATUSES_KEY);
|
||||
component.filterByItemType(ApiItemType.BLOCK);
|
||||
|
||||
await RouterTestingHarness.create(`/api?type=${ApiItemType.BLOCK}`);
|
||||
expect(component.type()).toBe(ApiItemType.BLOCK);
|
||||
});
|
||||
|
||||
it('should reset selected type when provided type is equal to selected', () => {
|
||||
it('should reset selected type when provided type is equal to selected', async () => {
|
||||
component.filterByItemType(ApiItemType.BLOCK);
|
||||
|
||||
const harness = await RouterTestingHarness.create(`/api?type=${ApiItemType.BLOCK}`);
|
||||
expect(component.type()).toBe(ApiItemType.BLOCK);
|
||||
|
||||
component.filterByItemType(ApiItemType.BLOCK);
|
||||
|
||||
harness.navigateByUrl(`/api`);
|
||||
expect(component.type()).toBe(ALL_STATUSES_KEY);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,17 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ChangeDetectionStrategy, Component, computed, inject, signal} from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
ElementRef,
|
||||
inject,
|
||||
model,
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import ApiItemsSection from '../api-items-section/api-items-section.component';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {SlideToggle, TextField} from '@angular/docs';
|
||||
@@ -38,12 +48,22 @@ export const ALL_STATUSES_KEY = 'All';
|
||||
})
|
||||
export default class ApiReferenceList {
|
||||
private readonly apiReferenceManager = inject(ApiReferenceManager);
|
||||
filterInput = viewChild.required(TextField, {read: ElementRef});
|
||||
|
||||
private readonly allGroups = this.apiReferenceManager.apiGroups;
|
||||
|
||||
private filterEffect = effect(() => {
|
||||
if (matchMedia('(hover: hover) and (pointer:fine)').matches) {
|
||||
// Lord forgive me for I have sinned
|
||||
// Use the CVA to focus when https://github.com/angular/angular/issues/31133 is implemented
|
||||
this.filterInput().nativeElement.querySelector('input').focus();
|
||||
}
|
||||
});
|
||||
|
||||
query = signal('');
|
||||
includeDeprecated = signal(false);
|
||||
type = signal(ALL_STATUSES_KEY);
|
||||
|
||||
type = model<string | undefined>(ALL_STATUSES_KEY);
|
||||
|
||||
featuredGroup = this.apiReferenceManager.featuredGroup;
|
||||
filteredGroups = computed((): ApiItemsGroup[] => {
|
||||
@@ -58,7 +78,9 @@ export default class ApiReferenceList {
|
||||
? apiItem.title.toLocaleLowerCase().includes(this.query().toLocaleLowerCase())
|
||||
: true) &&
|
||||
(this.includeDeprecated() ? true : apiItem.isDeprecated === this.includeDeprecated()) &&
|
||||
(this.type() === ALL_STATUSES_KEY || apiItem.itemType === this.type())
|
||||
(this.type() === undefined ||
|
||||
this.type() === ALL_STATUSES_KEY ||
|
||||
apiItem.itemType === this.type())
|
||||
);
|
||||
}),
|
||||
}))
|
||||
@@ -67,6 +89,6 @@ export default class ApiReferenceList {
|
||||
itemTypes = Object.values(ApiItemType);
|
||||
|
||||
filterByItemType(itemType: ApiItemType): void {
|
||||
this.type.set(this.type() === itemType ? ALL_STATUSES_KEY : itemType);
|
||||
this.type.update((currentType) => (currentType === itemType ? ALL_STATUSES_KEY : itemType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
&:has(.hljs-ln-line-highlighted) {
|
||||
&:has(.shiki-ln-line-highlighted) {
|
||||
&::before {
|
||||
background: var(--red-to-pink-to-purple-horizontal-gradient);
|
||||
}
|
||||
@@ -95,8 +95,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.hljs-ln-line-argument,
|
||||
.hljs-ln-line-option {
|
||||
.shiki-ln-line-argument,
|
||||
.shiki-ln-line-option {
|
||||
padding: 0.1rem 0.2rem 0.2rem;
|
||||
margin-inline: 0.1rem;
|
||||
color: var(--quaternary-contrast);
|
||||
@@ -110,12 +110,12 @@
|
||||
background: var(--septenary-contrast);
|
||||
}
|
||||
|
||||
&.hljs-ln-line-highlighted {
|
||||
&.shiki-ln-line-highlighted {
|
||||
color: var(--primary-contrast);
|
||||
background: var(--senary-contrast);
|
||||
}
|
||||
}
|
||||
.hljs-ln-line-argument {
|
||||
.shiki-ln-line-argument {
|
||||
margin-inline-start: 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,6 @@ export const API_REFERENCE_TAB_API_LABEL = 'API';
|
||||
export const API_REFERENCE_TAB_QUERY_PARAM = 'tab';
|
||||
export const API_TAB_CLASS_NAME = '.docs-reference-api-tab';
|
||||
export const API_REFERENCE_MEMBER_CARD_CLASS_NAME = '.docs-reference-member-card';
|
||||
export const API_TAB_ACTIVE_CODE_LINE = 'hljs-ln-line-highlighted';
|
||||
export const HIGHLIGHT_JS_CODE_LINE_CLASS_NAME = 'hljs-ln-line';
|
||||
export const API_TAB_ACTIVE_CODE_LINE = 'shiki-ln-line-highlighted';
|
||||
export const HIGHLIGHT_JS_CODE_LINE_CLASS_NAME = 'shiki-ln-line';
|
||||
export const MEMBER_ID_ATTRIBUTE = 'member-id';
|
||||
|
||||
@@ -35,6 +35,7 @@ export function mapApiManifestToRoutes(): Route[] {
|
||||
),
|
||||
},
|
||||
data: {
|
||||
label: api.name,
|
||||
displaySecondaryNav: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {fromEvent} from 'rxjs';
|
||||
import {auditTime} from 'rxjs/operators';
|
||||
import {auditTime, skipWhile} from 'rxjs/operators';
|
||||
import {
|
||||
API_REFERENCE_DETAILS_PAGE_MEMBERS_CLASS_NAME,
|
||||
API_REFERENCE_MEMBER_CARD_CLASS_NAME,
|
||||
@@ -114,7 +114,10 @@ export class ReferenceScrollHandler implements OnDestroy, ReferenceScrollHandler
|
||||
this.getAllMemberCards().forEach((card) => {
|
||||
this.cardOffsetTop.set(card.id, card.offsetTop);
|
||||
fromEvent(card, 'click')
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.pipe(
|
||||
skipWhile((event) => event.target instanceof HTMLAnchorElement),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.router.navigate([], {fragment: card.id, replaceUrl: true});
|
||||
});
|
||||
|
||||
@@ -31,6 +31,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
path: 'overview',
|
||||
contentPath: 'introduction/what-is-angular',
|
||||
},
|
||||
{
|
||||
label: 'Installation',
|
||||
path: 'installation',
|
||||
contentPath: 'introduction/installation',
|
||||
},
|
||||
{
|
||||
label: 'Essentials',
|
||||
children: [
|
||||
@@ -228,6 +233,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: [
|
||||
@@ -473,6 +483,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
{
|
||||
label: 'Performance',
|
||||
children: [
|
||||
{
|
||||
label: 'Overview',
|
||||
path: 'guide/performance',
|
||||
contentPath: 'guide/performance/overview',
|
||||
},
|
||||
{
|
||||
label: 'Deferrable views',
|
||||
path: 'guide/defer',
|
||||
|
||||
@@ -20,6 +20,7 @@ copy_to_directory(
|
||||
"//adev/src/content/guide/http",
|
||||
"//adev/src/content/guide/i18n",
|
||||
"//adev/src/content/guide/ngmodules",
|
||||
"//adev/src/content/guide/performance",
|
||||
"//adev/src/content/guide/pipes",
|
||||
"//adev/src/content/guide/routing",
|
||||
"//adev/src/content/guide/signals",
|
||||
@@ -54,7 +55,7 @@ copy_to_directory(
|
||||
"//packages/elements:elements_docs",
|
||||
"//packages/forms:forms_docs",
|
||||
"//packages/localize:localize_docs",
|
||||
"//packages/localize/init:localize_docs",
|
||||
"//packages/localize/src/localize:localize_init_docs",
|
||||
"//packages/platform-browser:platform-browser_docs",
|
||||
"//packages/platform-browser-dynamic:platform-browser_dynamic_docs",
|
||||
"//packages/platform-browser-dynamic/testing:platform-browser_dynamic_testing_docs",
|
||||
|
||||
|
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 |
@@ -127,7 +127,7 @@ The `RouterLinkActive` directive provides the `ariaCurrentWhenActive` input whic
|
||||
|
||||
The following example shows how to apply the `active-page` class to active links as well as setting their `aria-current` attribute to `"page"` when they are active:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<nav>
|
||||
<a routerLink="home"
|
||||
routerLinkActive="active-page"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ In addition, using the `onActionClick` property on the `data` object, you can ti
|
||||
{"action": "foo", "title": "Open new tab"},
|
||||
{"action": "bar", "title": "Focus last"},
|
||||
{"action": "baz", "title": "Navigate last"},
|
||||
{"action": "qux", "title": "Send request in the background"}
|
||||
{"action": "qux", "title": "Send request in the background"},
|
||||
{"action": "other", "title": "Just notify existing clients"}
|
||||
],
|
||||
"data": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -89,7 +89,7 @@ The heroes list gradually re-enters the page as you delete each letter in the fi
|
||||
|
||||
The HTML template contains a trigger called `filterAnimation`.
|
||||
|
||||
<docs-code header="src/app/hero-list-page.component.html" path="adev/src/content/examples/animations/src/app/hero-list-page.component.html" visibleRegion="filter-animations"/>
|
||||
<docs-code header="src/app/hero-list-page.component.html" path="adev/src/content/examples/animations/src/app/hero-list-page.component.html" visibleRegion="filter-animations" language="angular-html"/>
|
||||
|
||||
The `filterAnimation` in the component's decorator contains three transitions.
|
||||
|
||||
|
||||
@@ -43,14 +43,14 @@ rendered, or **projected**, at the location of that `<ng-content>`:
|
||||
export class CustomCard {/* ... */}
|
||||
```
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<!-- Using the component -->
|
||||
<custom-card>
|
||||
<p>This is the projected content</p>
|
||||
</custom-card>
|
||||
```
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<!-- The rendered DOM -->
|
||||
<custom-card>
|
||||
<div class="card-shadow">
|
||||
@@ -78,7 +78,7 @@ Angular supports projecting multiple different elements into different `<ng-cont
|
||||
based on CSS selector. Expanding the card example from above, you could create two placeholders for
|
||||
a card title and a card body by using the `select` attribute:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<!-- Component template -->
|
||||
<div class="card-shadow">
|
||||
<ng-content select="card-title"></ng-content>
|
||||
@@ -87,7 +87,7 @@ a card title and a card body by using the `select` attribute:
|
||||
</div>
|
||||
```
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<!-- Using the component -->
|
||||
<custom-card>
|
||||
<card-title>Hello</card-title>
|
||||
@@ -95,7 +95,7 @@ a card title and a card body by using the `select` attribute:
|
||||
</custom-card>
|
||||
```
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<!-- Rendered DOM -->
|
||||
<custom-card>
|
||||
<div class="card-shadow">
|
||||
@@ -113,7 +113,7 @@ If you include one or more `<ng-content>` placeholders with a `select` attribute
|
||||
one `<ng-content>` placeholder without a `select` attribute, the latter captures all elements that
|
||||
did not match a `select` attribute:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<!-- Component template -->
|
||||
<div class="card-shadow">
|
||||
<ng-content select="card-title"></ng-content>
|
||||
@@ -123,7 +123,7 @@ did not match a `select` attribute:
|
||||
</div>
|
||||
```
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<!-- Using the component -->
|
||||
<custom-card>
|
||||
<card-title>Hello</card-title>
|
||||
@@ -132,7 +132,7 @@ did not match a `select` attribute:
|
||||
</custom-card>
|
||||
```
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<!-- Rendered DOM -->
|
||||
<custom-card>
|
||||
<div class="card-shadow">
|
||||
@@ -153,7 +153,7 @@ Angular supports a special attribute, `ngProjectAs`, that allows you to specify
|
||||
any element. Whenever an element with `ngProjectAs` is checked against an `<ng-content>`
|
||||
placeholder, Angular compares against the `ngProjectAs` value instead of the element's identity:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<!-- Component template -->
|
||||
<div class="card-shadow">
|
||||
<ng-content select="card-title"></ng-content>
|
||||
@@ -162,7 +162,7 @@ placeholder, Angular compares against the `ngProjectAs` value instead of the ele
|
||||
</div>
|
||||
```
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<!-- Using the component -->
|
||||
<custom-card>
|
||||
<h3 ngProjectAs="card-title">Hello</h3>
|
||||
@@ -171,7 +171,7 @@ placeholder, Angular compares against the `ngProjectAs` value instead of the ele
|
||||
</custom-card>
|
||||
```
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<!-- Rendered DOM -->
|
||||
<custom-card>
|
||||
<div class="card-shadow">
|
||||
|
||||
@@ -17,14 +17,14 @@ The contents of a component's template are rendered inside its host element.
|
||||
export class ProfilePhoto {}
|
||||
```
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<!-- Using the component -->
|
||||
<h3>Your profile photo</h3>
|
||||
<profile-photo />
|
||||
<button>Upload a new profile photo</button>
|
||||
```
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<!-- Rendered DOM -->
|
||||
<h3>Your profile photo</h3>
|
||||
<profile-photo>
|
||||
@@ -116,7 +116,7 @@ The component may _also_ define host bindings for the same properties or attribu
|
||||
export class ProfilePhoto { /* ... */ }
|
||||
```
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<profile-photo role="group" [id]="otherId" />
|
||||
```
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export class CustomSlider {
|
||||
|
||||
This lets you bind to the property in a template:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<custom-slider [value]="50" />
|
||||
```
|
||||
|
||||
@@ -62,7 +62,7 @@ function trimString(value: string | undefined) {
|
||||
}
|
||||
</docs-code>
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<custom-slider [label]="systemVolume" />
|
||||
```
|
||||
|
||||
@@ -121,7 +121,7 @@ export class CustomSlider {
|
||||
}
|
||||
</docs-code>
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<custom-slider [sliderValue]="50" />
|
||||
```
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export class MyComp {
|
||||
An output is automatically recognized by Angular whenever you use the `output` function as an initializer of a class member.
|
||||
Parent components can listen to outputs in templates by using the event binding syntax.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<my-comp (onNameChange)="showNewName($event)" />
|
||||
```
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export class ExpandablePanel {
|
||||
}
|
||||
</docs-code>
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<expandable-panel (panelClosed)="savePanelState()" />
|
||||
```
|
||||
|
||||
@@ -46,7 +46,7 @@ this.thumbDropped.emit({
|
||||
|
||||
When defining an event listener in a template, you can access the event data from the `$event` variable:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<custom-slider (valueChanged)="logValue($event)" />
|
||||
```
|
||||
|
||||
@@ -61,7 +61,7 @@ export class CustomSlider {
|
||||
}
|
||||
</docs-code>
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<custom-slider (valueChanged)="saveVolume()" />
|
||||
```
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ export class InnerItem {
|
||||
|
||||
In the example above, clicking the "Load content" button results in the following DOM structure
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<outer-container>
|
||||
<p>This is the start of the outer container</p>
|
||||
<inner-item>
|
||||
|
||||
@@ -6,7 +6,7 @@ Deferrable views can be used in component template to defer the loading of selec
|
||||
|
||||
Deferrable views support a series of [triggers](guide/defer#triggers), [prefetching](guide/defer#prefetching), and several sub blocks used for [placeholder](guide/defer#placeholder), [loading](guide/defer#loading), and [error](guide/defer#error) state management. You can also create custom conditions with [`when`](guide/defer#when) and [`prefetch when`](guide/defer#prefetching).
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@defer {
|
||||
<large-component />
|
||||
}
|
||||
@@ -44,7 +44,7 @@ Note: For the best user experience, you should always specify a `@placeholder` b
|
||||
|
||||
The `@placeholder` block accepts an optional parameter to specify the `minimum` amount of time that this placeholder should be shown. This `minimum` parameter is specified in time increments of milliseconds (ms) or seconds (s). This parameter exists to prevent fast flickering of placeholder content in the case that the deferred dependencies are fetched quickly. The `minimum` timer for the `@placeholder` block begins after the initial render of this `@placeholder` block completes.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@defer {
|
||||
<large-component />
|
||||
} @placeholder (minimum 500ms) {
|
||||
@@ -62,7 +62,7 @@ For example, you could show a loading spinner. Once loading has been triggered,
|
||||
|
||||
The `@loading` block accepts two optional parameters to specify the `minimum` amount of time that this placeholder should be shown and amount of time to wait `after` loading begins before showing the loading template. `minimum` and `after` parameters are specified in time increments of milliseconds (ms) or seconds (s). Just like `@placeholder`, these parameters exist to prevent fast flickering of content in the case that the deferred dependencies are fetched quickly. Both the `minimum` and `after` timers for the `@loading` block begins immediately after the loading has been triggered.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@defer {
|
||||
<large-component />
|
||||
} @loading (after 100ms; minimum 1s) {
|
||||
@@ -74,7 +74,7 @@ The `@loading` block accepts two optional parameters to specify the `minimum` am
|
||||
|
||||
The `@error` block allows you to declare content that will be shown if deferred loading fails. Similar to `@placeholder` and `@loading`, the dependencies of the `@error` block are eagerly loaded. The `@error` block is optional.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@defer {
|
||||
<calendar-cmp />
|
||||
} @error {
|
||||
@@ -93,7 +93,7 @@ Multiple event triggers can be defined at once. For example: `on interaction; on
|
||||
|
||||
Note: Multiple `on` triggers are always OR conditions. Similarly, `on` mixed with `when` conditions are also OR conditions.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@defer (on viewport; on timer(5s)) {
|
||||
<calendar-cmp />
|
||||
} @placeholder {
|
||||
@@ -106,7 +106,7 @@ Note: Multiple `on` triggers are always OR conditions. Similarly, `on` mixed wit
|
||||
|
||||
Note: if the `when` condition switches back to `false`, the defer block is not reverted back to the placeholder. The swap is a one-time operation. If the content within the block should be conditionally rendered, an `if` condition can be used within the block itself.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@defer (when cond) {
|
||||
<calendar-cmp />
|
||||
}
|
||||
@@ -114,7 +114,7 @@ Note: if the `when` condition switches back to `false`, the defer block is not r
|
||||
|
||||
You could also use both `when` and `on` together in one statement, and the swap will be triggered if either condition is met.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@defer (on viewport; when cond) {
|
||||
<calendar-cmp />
|
||||
} @placeholder {
|
||||
@@ -132,7 +132,7 @@ You could also use both `when` and `on` together in one statement, and the swap
|
||||
|
||||
By default, the placeholder will act as the element watched for entering viewport as long as it is a single root element node.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@defer (on viewport) {
|
||||
<calendar-cmp />
|
||||
} @placeholder {
|
||||
@@ -142,7 +142,7 @@ By default, the placeholder will act as the element watched for entering viewpor
|
||||
|
||||
Alternatively, you can specify a [template reference variable](guide/templates/reference-variables) in the same template as the `@defer` block as the element that is watched to enter the viewport. This variable is passed in as a parameter on the viewport trigger.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<div #greeting>Hello!</div>
|
||||
|
||||
@defer (on viewport(greeting)) {
|
||||
@@ -156,7 +156,7 @@ Alternatively, you can specify a [template reference variable](guide/templates/r
|
||||
|
||||
By default, the placeholder will act as the interaction element as long as it is a single root element node.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@defer (on interaction) {
|
||||
<calendar-cmp />
|
||||
} @placeholder {
|
||||
@@ -166,7 +166,7 @@ By default, the placeholder will act as the interaction element as long as it is
|
||||
|
||||
Alternatively, you can specify a [template reference variable](guide/templates/reference-variables) as the element that triggers interaction. This variable is passed in as a parameter on the interaction trigger.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<button type="button" #greeting>Hello!</button>
|
||||
|
||||
@defer (on interaction(greeting)) {
|
||||
@@ -182,7 +182,7 @@ Alternatively, you can specify a [template reference variable](guide/templates/r
|
||||
|
||||
By default, the placeholder will act as the hover element as long as it is a single root element node.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@defer (on hover) {
|
||||
<calendar-cmp />
|
||||
} @placeholder {
|
||||
@@ -192,7 +192,7 @@ By default, the placeholder will act as the hover element as long as it is a sin
|
||||
|
||||
Alternatively, you can specify a [template reference variable](guide/templates/reference-variables) as the hover element. This variable is passed in as a parameter on the hover trigger.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<div #greeting>Hello!</div>
|
||||
|
||||
@defer (on hover(greeting)) {
|
||||
@@ -206,7 +206,7 @@ Alternatively, you can specify a [template reference variable](guide/templates/r
|
||||
|
||||
`immediate` triggers the deferred load immediately, meaning once the client has finished rendering, the defer chunk would then start fetching right away.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@defer (on immediate) {
|
||||
<calendar-cmp />
|
||||
} @placeholder {
|
||||
@@ -218,7 +218,7 @@ Alternatively, you can specify a [template reference variable](guide/templates/r
|
||||
|
||||
`timer(x)` would trigger after a specified duration. The duration is required and can be specified in `ms` or `s`.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@defer (on timer(500ms)) {
|
||||
<calendar-cmp />
|
||||
}
|
||||
@@ -232,7 +232,7 @@ In this case, `when` and `on` associated with defer controls when to render, and
|
||||
|
||||
In the example below, the prefetching starts when a browser becomes idle and the contents of the block is rendered on interaction.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@defer (on interaction; prefetch on idle) {
|
||||
<calendar-cmp />
|
||||
} @placeholder {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -59,7 +59,7 @@ export class AdminMenu { }
|
||||
By explicitly specifying the inputs and outputs, consumers of the component with `hostDirective` can
|
||||
bind them in a template:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
|
||||
<admin-menu menuId="top-menu" (menuClosed)="logMenuClosed()">
|
||||
```
|
||||
@@ -81,7 +81,7 @@ component:
|
||||
export class AdminMenu { }
|
||||
```
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
|
||||
<admin-menu id="top-menu" (closed)="logMenuClosed()">
|
||||
```
|
||||
|
||||
@@ -10,7 +10,7 @@ In this guide you'll build a structural directive which fetches data from a give
|
||||
|
||||
The following is an example of using this directive directly on an `<ng-template>` would look like:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<ng-template select let-data [selectFrom]="source">
|
||||
<p>The data is: {{ data }}</p>
|
||||
</ng-template>
|
||||
@@ -30,7 +30,7 @@ Structural directives can be applied directly on an element by prefixing the dir
|
||||
|
||||
You can use this with `SelectDirective` as follows:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<p *select="let data from source">The data is: {{data}}</p>
|
||||
```
|
||||
|
||||
@@ -38,7 +38,7 @@ This example shows the flexibility of structural directive shorthand syntax, whi
|
||||
|
||||
When used in this way, only the structural directive and its bindings are applied to the `<ng-template>`. Any other attributes or bindings on the `<p>` tag are left alone. For example, these two forms are equivalent:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<!-- Shorthand syntax: -->
|
||||
<p class="data-view" *select="let data from source">The data is: {{data}}</p>
|
||||
|
||||
@@ -80,7 +80,7 @@ import {Directive, TemplateRef, ViewContainerRef} from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
standalone: true,
|
||||
selector: 'select',
|
||||
selector: '[select]',
|
||||
})
|
||||
export class SelectDirective {
|
||||
constructor(private templateRef: TemplateRef, private ViewContainerRef: ViewContainerRef) {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -104,7 +130,7 @@ For a full reference on hydration related errors, visit the [Errors Reference Gu
|
||||
|
||||
Some components may not work properly with hydration enabled due to some of the aforementioned issues, like [Direct DOM Manipulation](#direct-dom-manipulation). As a workaround, you can add the `ngSkipHydration` attribute to a component's tag in order to skip hydrating the entire component.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<app-example ngSkipHydration />
|
||||
```
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
load("@npm//@angular/docs:index.bzl", "generate_guides")
|
||||
|
||||
generate_guides(
|
||||
name = "performance",
|
||||
srcs = glob([
|
||||
"*.md",
|
||||
]),
|
||||
data = [
|
||||
"//adev/src/assets/images:overview.svg",
|
||||
],
|
||||
visibility = ["//adev:__subpackages__"],
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
<docs-decorative-header title="Performance" imgSrc="adev/src/assets/images/overview.svg"> <!-- markdownlint-disable-line -->
|
||||
Learn about different ways you can optimize the performance of your application.
|
||||
</docs-decorative-header>
|
||||
|
||||
One of the top priorities of any developer is ensuring that their application is as performant as possible. These guides are here to help you follow best practices for building performant applications.
|
||||
|
||||
That said, please note that these best practices will only take the performance of your application so far. At the end of the day, we encourage you to measure performance in order to best understand what custom optimizations are best for your application.
|
||||
|
||||
| Guides Types | Description |
|
||||
| :---------------------------------------- | :--------------------------------------------------------------------------------------------------------- |
|
||||
| [Deferrable views](/guide/defer) | Defer loading of select dependencies within a template by wrapping corresponding parts in a `@defer` block. |
|
||||
| [Image optimization](/guide/image-optimization) | Use the `NgOptimizedImage` directive to adopt best practices for loading images. |
|
||||
| [Server-side rendering](/guide/ssr) | Learn how to leverage rendering pages on the server to improve load times. |
|
||||
| [Build-time prerendering](/guide/prerendering) | Also known as static-side generation (SSG), is an alternate rendering method to improve load times. |
|
||||
| [Hydration](/guide/hydration) | A process to improve application performance by restoring its state after server-side rendering and reusing existing DOM structure as much as possible. |
|
||||
@@ -23,5 +23,5 @@ The following are commonly used built-in pipes for data formatting:
|
||||
- [`AsyncPipe`](api/common/AsyncPipe): Subscribe and unsubscribe to an asynchronous source such as an observable.
|
||||
- [`JsonPipe`](api/common/JsonPipe): Display a component object property to the screen as JSON for debugging.
|
||||
|
||||
Note: For a complete list of built-in pipes, see the [pipes API documentation](/api/common#pipes "Pipes API reference summary").
|
||||
Note: For a complete list of built-in pipes, see the [pipes API documentation](/api?type=pipe "Pipes API reference summary").
|
||||
To learn more about using pipes for internationalization (i18n) efforts, see [formatting data based on locale](guide/i18n/format-data-locale).
|
||||
|
||||
@@ -93,7 +93,7 @@ const routes: Routes = [
|
||||
|
||||
Now that you have defined your routes, add them to your application. First, add links to the two components. Assign the anchor tag that you want to add the route to the `routerLink` attribute. Set the value of the attribute to the component to show when a user clicks on each link. Next, update your component template to include `<router-outlet>`. This element informs Angular to update the application view with the component for the selected route.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<h1>Angular Router App</h1>
|
||||
<nav>
|
||||
<ul>
|
||||
@@ -260,7 +260,7 @@ This means you're adding a second `<router-outlet>` to your app, because it is i
|
||||
In this example, there are two additional child components, `child-a`, and `child-b`.
|
||||
Here, `FirstComponent` has its own `<nav>` and a second `<router-outlet>` in addition to the one in `AppComponent`.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<h2>First Component</h2>
|
||||
|
||||
<nav>
|
||||
@@ -358,7 +358,7 @@ The following example shows a relative route to another component, `second-compo
|
||||
`FirstComponent` and `SecondComponent` are at the same level in the tree, however, the link to `SecondComponent` is situated within the `FirstComponent`, meaning that the router has to go up a level and then into the second directory to find the `SecondComponent`.
|
||||
Rather than writing out the whole path to get to `SecondComponent`, use the `../` notation to go up a level.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<h2>First Component</h2>
|
||||
|
||||
<nav>
|
||||
@@ -516,13 +516,13 @@ A link parameters array holds the following ingredients for router navigation:
|
||||
|
||||
Bind the `RouterLink` directive to such an array like this:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<a [routerLink]="['/heroes']">Heroes</a>
|
||||
```
|
||||
|
||||
The following is a two-element array when specifying a route parameter:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<a [routerLink]="['/hero', hero.id]">
|
||||
<span class="badge">{{ hero.id }}</span>{{ hero.name }}
|
||||
</a>
|
||||
@@ -530,7 +530,7 @@ The following is a two-element array when specifying a route parameter:
|
||||
|
||||
Provide optional route parameters in an object, as in `{ foo: 'foo' }`:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<a [routerLink]="['/crisis-center', { foo: 'foo' }]">Crisis Center</a>
|
||||
```
|
||||
|
||||
@@ -539,7 +539,7 @@ However, with a child router, such as in the crisis center, you create new link
|
||||
|
||||
The following minimal `RouterLink` example builds upon a specified default child route for the crisis center.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<a [routerLink]="['/crisis-center']">Crisis Center</a>
|
||||
```
|
||||
|
||||
@@ -552,7 +552,7 @@ Review the following:
|
||||
|
||||
Consider the following router link that navigates from the root of the application down to the Dragon Crisis:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<a [routerLink]="['/crisis-center', 1]">Dragon Crisis</a>
|
||||
```
|
||||
|
||||
@@ -589,7 +589,7 @@ The router can compose a "natural" URL that is indistinguishable from one that w
|
||||
|
||||
Here's the Crisis Center URL in this "HTML5 pushState" style:
|
||||
|
||||
```http
|
||||
```text
|
||||
localhost:3002/crisis-center
|
||||
```
|
||||
|
||||
@@ -597,7 +597,7 @@ Older browsers send page requests to the server when the location URL changes un
|
||||
Routers can take advantage of this exception by composing in-application route URLs with hashes.
|
||||
Here's a "hash URL" that routes to the Crisis Center.
|
||||
|
||||
```http
|
||||
```text
|
||||
localhost:3002/src/#/crisis-center
|
||||
```
|
||||
|
||||
@@ -660,7 +660,7 @@ While the router uses the [HTML5 pushState](https://developer.mozilla.org/docs/W
|
||||
|
||||
The preferred way to configure the strategy is to add a [`<base href>` element](https://developer.mozilla.org/docs/Web/HTML/Element/base 'base href') tag in the `<head>` of the `index.html`.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<base href="/">
|
||||
```
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ You should see only the application title.
|
||||
To view the `crisis-list` component, add `crisis-list` to the end of the path in your browser's address bar.
|
||||
For example:
|
||||
|
||||
<docs-code language="https">
|
||||
<docs-code language="text">
|
||||
http://localhost:4200/crisis-list
|
||||
</docs-code>
|
||||
|
||||
@@ -161,7 +161,7 @@ Notice that the `crisis-list` component displays.
|
||||
Angular is using the route you defined to dynamically load the component.
|
||||
You can load the `heroes-list` component the same way:
|
||||
|
||||
<docs-code language="https">
|
||||
<docs-code language="text">
|
||||
http://localhost:4200/heroes-list
|
||||
</docs-code>
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ This allows users to bind to your input using `[studentAge]`, while inside your
|
||||
Signal inputs are read-only signals.
|
||||
As with signals declared via `signal()`, you access the current value of the input by calling the input signal.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<p>First name: {{firstName()}}</p>
|
||||
<p>Last name: {{lastName()}}</p>
|
||||
```
|
||||
@@ -126,7 +126,7 @@ These values are then parsed to a `boolean` with the transform, resulting in boo
|
||||
|
||||
That way, you are only dealing with `boolean` inside your component when calling `this.disabled()`, while users of your component can pass an empty string as a shorthand to mark your component as disabled.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<my-custom-comp disabled>
|
||||
```
|
||||
|
||||
|
||||
@@ -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() }}`,
|
||||
|
||||
@@ -6,7 +6,7 @@ Angular templates support _control flow blocks_ that let you conditionally show,
|
||||
|
||||
The `@if` block conditionally displays its content when its condition expression is truthy:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@if (a > b) {
|
||||
{{a}} is greater than {{b}}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ The `@if` block conditionally displays its content when its condition expression
|
||||
The `@if` block might have one or more associated `@else` blocks. Immediately after an `@if` block, you can optionally
|
||||
specify any number of `@else if` blocks and one `@else` block:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@if (a > b) {
|
||||
{{a}} is greater than {{b}}
|
||||
} @else if (b > a) {
|
||||
@@ -30,7 +30,7 @@ specify any number of `@else if` blocks and one `@else` block:
|
||||
The new built-in `@if` conditional supports referencing of expression results to keep a solution for common coding
|
||||
patterns:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@if (users$ | async; as users) {
|
||||
{{ users.length }}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ The `@for` repeatedly renders content of a block for each item in a collection.
|
||||
JavaScript [iterable](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Iteration_protocols) but there
|
||||
are performance advantages of using a regular `Array`. A basic `@for` loop looks like:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@for (item of items; track item.id) {
|
||||
{{ item.name }}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ Inside `@for` contents, several implicit variables are always available:
|
||||
|
||||
These variables are always available with these names, but can be aliased via a `let` segment:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@for (item of items; track item.id; let idx = $index, e = $even) {
|
||||
Item #{{ idx }}: {{ item.name }}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ The aliasing is especially useful in case of using nested `@for` blocks where co
|
||||
You can optionally include an `@empty` section immediately after the `@for` block content. The content of the `@empty`
|
||||
block displays when there are no items:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@for (item of items; track item.name) {
|
||||
<li> {{ item.name }}</li>
|
||||
} @empty {
|
||||
@@ -104,7 +104,7 @@ block displays when there are no items:
|
||||
|
||||
The syntax for `switch` is very similar to `if`, and is inspired by the JavaScript `switch` statement:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@switch (condition) {
|
||||
@case (caseA) {
|
||||
Case A.
|
||||
@@ -147,7 +147,7 @@ The `track` setting replaces `NgFor`'s concept of a `trackBy` function. Because
|
||||
better experience than passing a `trackBy` function, and directly use an expression representing the key instead.
|
||||
Migrating from `trackBy` to `track` is possible by invoking the `trackBy` function:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
@for (item of items; track itemId($index, item)) {
|
||||
{{ item.name }}
|
||||
}
|
||||
|
||||
@@ -35,13 +35,13 @@ You can bind to keyboard events using Angular's binding syntax. You can specify
|
||||
|
||||
Combinations of keys can be separated by a `.` (period). For example, `keydown.enter` will allow you to bind events to the `enter` key. You can also use modifier keys, such as `shift`, `alt`, `control`, and the `command` keys from Mac. The following example shows how to bind a keyboard event to `keydown.shift.t`.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<input (keydown.shift.t)="onKeydown($event)" />
|
||||
```
|
||||
|
||||
Depending on the operating system, some key combinations might create special characters instead of the key combination that you expect. MacOS, for example, creates special characters when you use the option and shift keys together. If you bind to `keydown.shift.alt.t`, on macOS, that combination produces a `ˇ` character instead of a `t`, which doesn't match the binding and won't trigger your event handler. To bind to `keydown.shift.alt.t` on macOS, use the `code` keyboard event field to get the correct behavior, such as `keydown.code.shiftleft.altleft.keyt` shown in this example.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<input (keydown.code.shiftleft.altleft.keyt)="onKeydown($event)" />
|
||||
```
|
||||
|
||||
@@ -57,7 +57,7 @@ This is an advanced technique that is not necessary for most applications. You m
|
||||
|
||||
For example, to make a scroll event passive, it may be necessary to set the special zone.js flag in `src/index.html`:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# 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.
|
||||
|
||||
```angular-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:
|
||||
|
||||
|
||||
```angular-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.
|
||||
|
||||
```angular-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:
|
||||
|
||||
```angular-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. |
|
||||
|
||||
@@ -72,7 +72,7 @@ In this case, the `*ngIf` on `<span>` creates a new template scope, which includ
|
||||
|
||||
However, accessing a template variable from a child scope in the parent template doesn't work:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<input *ngIf="true" #ref2 type="text" [(ngModel)]="secondExample" />
|
||||
<span>Value: {{ ref2?.value }}</span> <!-- doesn't work -->
|
||||
```
|
||||
@@ -85,7 +85,7 @@ A _template input variable_ is a variable with a value that is set when an insta
|
||||
|
||||
Template input variables can be seen in action in the long-form usage of `NgFor`:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<ul>
|
||||
<ng-template ngFor let-hero [ngForOf]="heroes">
|
||||
<li>{{hero.name}}
|
||||
@@ -99,7 +99,7 @@ When an `<ng-template>` is instantiated, multiple named values can be passed whi
|
||||
|
||||
`NgFor` for example also provides access to the `index` of each hero in the array:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<ul>
|
||||
<ng-template ngFor let-hero let-i="index" [ngForOf]="heroes">
|
||||
<li>Hero number {{i}}: {{hero.name}}
|
||||
|
||||
@@ -15,7 +15,7 @@ Two-way binding combines [property binding](guide/templates/property-binding) wi
|
||||
Angular's two-way binding syntax is a combination of square brackets and parentheses, `[()]`.
|
||||
The `[()]` syntax combines the brackets of property binding, `[]`, with the parentheses of event binding, `()`, as follows.
|
||||
|
||||
<docs-code header="src/app/app.component.html" path="adev/src/content/examples/two-way-binding/src/app/app.component.html" visibleRegion="two-way-syntax"/>
|
||||
<docs-code header="src/app/app.component.html" path="adev/src/content/examples/two-way-binding/src/app/app.component.html" visibleRegion="two-way-syntax" language="angular-html"/>
|
||||
|
||||
## How two-way binding works
|
||||
|
||||
@@ -29,7 +29,7 @@ The `sizeChange` event is an `@Output()`, which lets data flow out of the `sizer
|
||||
Next, there are two methods, `dec()` to decrease the font size and `inc()` to increase the font size.
|
||||
These two methods use `resize()` to change the value of the `size` property within min/max value constraints, and to emit an event that conveys the new `size` value.
|
||||
|
||||
<docs-code header="src/app/sizer.component.ts" path="adev/src/content/examples/two-way-binding/src/app/sizer/sizer.component.ts" visibleRegion="sizer-component"/>
|
||||
<docs-code header="src/app/sizer.component.ts" path="adev/src/content/examples/two-way-binding/src/app/sizer/sizer.component.ts" visibleRegion="sizer-component" language="angular-ts"/>
|
||||
|
||||
The `sizerComponent` template has two buttons that each bind the click event to the `inc()` and `dec()` methods.
|
||||
When the user clicks one of the buttons, the `sizerComponent` calls the corresponding method.
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -42,7 +42,7 @@ These notifications include:
|
||||
### `OnPush`-compatible components
|
||||
|
||||
One way to ensure that a component is using the correct notification mechanisms from above is to
|
||||
use [ChangeDetectionStrategy.OnPush](../best-practices/skipping-subtrees#using-onpush).
|
||||
use [ChangeDetectionStrategy.OnPush](/adev/src/content/best-practices/runtime-performance/skipping-subtrees#using-onpush).
|
||||
|
||||
The `OnPush` change detection strategy is not required, but it is a recommended step towards zoneless compatibility for application components. It is not always possible for library components to use `ChangeDetectionStrategy.OnPush`.
|
||||
When a library component is a host for user-components which might use `ChangeDetectionStratey.Default`, it cannot use `OnPush` because that would prevent the child component from being refreshed if it is not `OnPush` compatible and relies on ZoneJS to trigger change detection. Components can use the `Default` strategy as long as they notify Angular when change detection needs to run (calling `markForCheck`, using signals, `AsyncPipe`, etc.).
|
||||
|
||||
@@ -87,7 +87,7 @@ export class TodoListItem {
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<!-- todo-list-item.component.html -->
|
||||
<li>(TODO) Read Angular Essentials Guide</li>
|
||||
```
|
||||
|
||||
@@ -68,7 +68,7 @@ Another common scenario developers encounter is the need to render a list of ite
|
||||
|
||||
Similar to JavaScript’s `for...of` loops, Angular provides the `@for` block for rendering repeated elements.
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<!-- ingredient-list.component.html -->
|
||||
<ul>
|
||||
@for (ingredient of ingredientList; track ingredient.name) {
|
||||
|
||||
@@ -11,7 +11,7 @@ You can add an event handler to an element by:
|
||||
1. Adding an attribute with the events name inside of parentheses
|
||||
2. Specify what JavaScript statement you want to run when it fires
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<button (click)="save()">Save</button>
|
||||
```
|
||||
|
||||
@@ -45,7 +45,7 @@ Other common examples of event listeners include:
|
||||
|
||||
If you need to access the [event](https://developer.mozilla.org/docs/Web/API/Event) object, Angular provides an implicit `$event` variable that you can pass to a function:
|
||||
|
||||
```html
|
||||
```angular-html
|
||||
<button (click)="createUser($event)">Submit</button>
|
||||
```
|
||||
|
||||
|
||||