mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d00aa2bd4 | |||
| e86c31bf26 | |||
| 1804f73bec | |||
| 91df739b80 | |||
| 8d22cc953b | |||
| 5a693bafcd | |||
| 6bcce117fb | |||
| 31c25e2e85 | |||
| 88832c84f8 | |||
| 13fb0afe93 | |||
| 86a56dc279 | |||
| bcb1b7ea25 | |||
| b9d29381bb | |||
| a810a319d1 | |||
| 35510746b7 | |||
| a5f82b8315 | |||
| bc55749698 | |||
| d846326b07 | |||
| e245d40c4d | |||
| dc9c99636d | |||
| 1523061137 | |||
| 03188ddc9f | |||
| 736c4ab7e6 | |||
| 3fd6897a67 | |||
| db157e4aff | |||
| 70af5e8abd | |||
| 66821c4ed5 | |||
| b74fb76d1a | |||
| 66d09558b6 | |||
| ae1c8a1f7a | |||
| 9f6fda6815 | |||
| 1e5d76bfd9 | |||
| 22dd53ca97 | |||
| 1cd4f54aef | |||
| 9d8ea2cc9a | |||
| 69c0d48a0d | |||
| 7e38336dc7 | |||
| 34c4e401ba | |||
| f6d8e642b0 | |||
| 8206972189 | |||
| d3170031b6 | |||
| 9b7d0e5034 | |||
| cea6588bb3 | |||
| 6b8202eab6 | |||
| eb1cbbf2eb | |||
| 8538bdce1c | |||
| 582a417bd2 | |||
| 5c6d6df34b | |||
| 29ceeffd40 | |||
| 1a84668f0c | |||
| 551a2a1f46 | |||
| 84c6579a4e | |||
| 2b17b2db88 | |||
| fd3573d99d | |||
| ef38852213 | |||
| 2232a62bb2 | |||
| ad5053b518 | |||
| ca32fc1000 | |||
| b8bd49341d | |||
| 251c8f2740 | |||
| dada86e43d | |||
| 782e01594e | |||
| ff12fe55ac | |||
| 0b07f47bd6 | |||
| cc1378d54b | |||
| daaf32937f | |||
| 37e8aadf87 | |||
| 72696e244e | |||
| 300f61feb3 | |||
| 7f4ac78994 | |||
| e6fe77cc97 | |||
| 525e1605a6 | |||
| 4a41831326 | |||
| 6d8b156b45 | |||
| 82cf38ad95 | |||
| 711f873e31 | |||
| 3fe8562b38 | |||
| 9627e02bde | |||
| a7b9ff5a58 | |||
| 1b6f780e2d | |||
| d56f1a35ad | |||
| 232b21db55 | |||
| 1d6e71dd78 | |||
| 0c7f70e8ea | |||
| 1ef4ea3e39 | |||
| 395919ffeb | |||
| 49113ac0ef | |||
| 027c7c0c95 | |||
| 7f444e1c7f | |||
| 34f89fb6c2 | |||
| 68282dff9f | |||
| 099bf577ee | |||
| fd05135da9 | |||
| c0f52272ed | |||
| d1736efc32 | |||
| 73b0ada729 | |||
| 0fb2724194 | |||
| 6652ec0115 | |||
| 938a7f3edd | |||
| fc434c1d0a | |||
| 8282c09e2d | |||
| 1e079a8994 | |||
| 7ab78d5c89 | |||
| 49a133aeaf | |||
| c08321988d | |||
| c93d158aae | |||
| 327edb9001 | |||
| 3ec0a10ca0 | |||
| 0b7192f441 | |||
| 5000a6d2c0 | |||
| f9a58c1da3 | |||
| 6ed6498854 | |||
| 629905d537 | |||
| baf92da96e | |||
| 1c6553e97d | |||
| c39f7708a6 | |||
| 2a1e6ec1bb | |||
| 73ba918cce | |||
| bca94ee0bb |
@@ -36,6 +36,18 @@ jobs:
|
||||
# that the action was triggered by a team member.
|
||||
ref: ${{steps.comment-branch.outputs.head_sha}}
|
||||
|
||||
# We cannot use `angular/dev-infra/github-actions/npm/checkout-and-setup-node` here
|
||||
# because it does not support checking out from a fork (as it lacks a `repository` input).
|
||||
# Thus, we checkout and setup Node/pnpm manually.
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
|
||||
+11
-10
@@ -36,8 +36,9 @@
|
||||
|
||||
version: 3
|
||||
|
||||
#availability:
|
||||
# users_unavailable: []
|
||||
availability:
|
||||
users_unavailable:
|
||||
- devversion
|
||||
|
||||
# Meta field that goes unused by PullApprove to allow for defining aliases to be
|
||||
# used throughout the config.
|
||||
@@ -103,7 +104,7 @@ groups:
|
||||
- atscott
|
||||
- crisbeto
|
||||
- devversion
|
||||
- thePunderWoman
|
||||
- ~thePunderWoman
|
||||
- kirjs
|
||||
- JoostK
|
||||
- ~amishne
|
||||
@@ -150,7 +151,7 @@ groups:
|
||||
- crisbeto
|
||||
- devversion
|
||||
- kirjs
|
||||
- thePunderWoman
|
||||
- ~thePunderWoman
|
||||
- ~pkozlowski-opensource
|
||||
- JeanMeche
|
||||
- ~amishne
|
||||
@@ -205,7 +206,7 @@ groups:
|
||||
- crisbeto
|
||||
- devversion
|
||||
- kirjs
|
||||
- thePunderWoman
|
||||
- ~thePunderWoman
|
||||
- ~pkozlowski-opensource
|
||||
- ~amishne
|
||||
- ~leonsenft
|
||||
@@ -256,7 +257,7 @@ groups:
|
||||
- crisbeto
|
||||
- kirjs
|
||||
- JeanMeche
|
||||
- thePunderWoman
|
||||
- ~thePunderWoman
|
||||
- devversion
|
||||
- josephperrott
|
||||
- ~pkozlowski-opensource
|
||||
@@ -375,7 +376,7 @@ groups:
|
||||
- ~AndrewKushnir
|
||||
- ~alxhub
|
||||
- atscott
|
||||
- thePunderWoman
|
||||
- ~thePunderWoman
|
||||
- ~pkozlowski-opensource
|
||||
- kirjs
|
||||
- crisbeto
|
||||
@@ -411,7 +412,7 @@ groups:
|
||||
- ~AndrewKushnir
|
||||
- atscott
|
||||
- kirjs
|
||||
- thePunderWoman
|
||||
- ~thePunderWoman
|
||||
- ~pkozlowski-opensource
|
||||
- ~amishne
|
||||
- ~leonsenft
|
||||
@@ -459,7 +460,7 @@ groups:
|
||||
- ~AndrewKushnir
|
||||
- andrewseguin
|
||||
- dgp1130
|
||||
- thePunderWoman
|
||||
- ~thePunderWoman
|
||||
- josephperrott
|
||||
|
||||
# =========================================================
|
||||
@@ -478,7 +479,7 @@ groups:
|
||||
users:
|
||||
- ~pkozlowski-opensource # Pawel Kozlowski
|
||||
- ~alxhub # Alex Rickabaugh
|
||||
- thePunderWoman # Jessica Janiuk
|
||||
- ~thePunderWoman # Jessica Janiuk
|
||||
- ~AndrewKushnir # Andrew Kushnir
|
||||
- atscott # Andrew Scott
|
||||
|
||||
|
||||
+157
@@ -1,3 +1,160 @@
|
||||
<a name="21.2.18"></a>
|
||||
# 21.2.18 (2026-07-08)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [8d22cc953b](https://github.com/angular/angular/commit/8d22cc953bb9f970c6f86fb0f6e4d0665b874bfa) | fix | update babel dependencies to latest v7 |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [6bcce117fb](https://github.com/angular/angular/commit/6bcce117fbc616e4e721f351e9e038202e916522) | fix | avoid caching missing locale data |
|
||||
| [5a693bafcd](https://github.com/angular/angular/commit/5a693bafcd49ef11ce687bd91a209e1a46652f42) | fix | reject dynamic script host elements |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [91df739b80](https://github.com/angular/angular/commit/91df739b8022e2177bcc7214ed9a4e94098a4831) | fix | prevent caching of responses with Set-Cookie headers |
|
||||
### service-worker
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1804f73bec](https://github.com/angular/angular/commit/1804f73becf425b5b1ccee9751d6e3fbd10e1f0b) | fix | preserve referrer in asset requests |
|
||||
| [e86c31bf26](https://github.com/angular/angular/commit/e86c31bf26359aacce814167506dbdae585a7eb8) | fix | preserve referrer policy in asset requests |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.17"></a>
|
||||
# 21.2.17 (2026-06-10)
|
||||
## Deprecations
|
||||
### platform-server
|
||||
- XHR support in `@angular/platform-server` is deprecated. Use standard `fetch` APIs instead.
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [86a56dc279](https://github.com/angular/angular/commit/86a56dc279e71159d09a073a3cb138f49131995b) | fix | Limits date format string length |
|
||||
| [d846326b07](https://github.com/angular/angular/commit/d846326b071e0a4ab090e068d934b182926c6b15) | fix | skip transfer cache for uncacheable HTTP traffic |
|
||||
| [bc55749698](https://github.com/angular/angular/commit/bc55749698ce3917160cd8e9f7108f3c5d1c0b32) | fix | use cryptographically secure SHA-256 for transfer cache key generation |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [dc9c99636d](https://github.com/angular/angular/commit/dc9c99636d3471ed5a3c5cda54b95f604cd2b9a4) | fix | sanitize two-way properties |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1523061137](https://github.com/angular/angular/commit/152306113760e13196653699b42046d9f4129a37) | fix | harden TransferState restoration against DOM clobbering |
|
||||
| [88832c84f8](https://github.com/angular/angular/commit/88832c84f8a3cd88d80adcde539a6f91a1f30b74) | fix | validate lowercase SVG animation attribute names ([#69269](https://github.com/angular/angular/pull/69269)) |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [bcb1b7ea25](https://github.com/angular/angular/commit/bcb1b7ea2575b140f7bf202ad4f779e402cd6094) | fix | preserve empty referrer option in HttpRequest |
|
||||
| [a810a319d1](https://github.com/angular/angular/commit/a810a319d10a8254307eb8f0598e7a888ce09ec0) | fix | Rejects non-HTTP(S) URLs in JSONP requests |
|
||||
| [e245d40c4d](https://github.com/angular/angular/commit/e245d40c4d05665ab4814c594b8e0849b6e88a2d) | fix | skip transfer cache for fetch credentialed requests |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [35510746b7](https://github.com/angular/angular/commit/35510746b7d6b5c3de41de04c0586fc286c9e748) | fix | harden platform location origin validation during SSR |
|
||||
| [13fb0afe93](https://github.com/angular/angular/commit/13fb0afe93b45e3c2383969f70d3ee1f0146ecfb) | refactor | deprecate ServerXhr ([#69255](https://github.com/angular/angular/pull/69255)) |
|
||||
### service-worker
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [b9d29381bb](https://github.com/angular/angular/commit/b9d29381bb4442164b19d9b7e0baa147a7b25629) | fix | Strips sensitive headers on cross-origin redirects |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.16"></a>
|
||||
# 21.2.16 (2026-06-03)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f6d8e642b0](https://github.com/angular/angular/commit/f6d8e642b0b215d2f9dbf1060abd24348c6cbf66) | fix | only strip a literal /index.html suffix from URLs |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [ae1c8a1f7a](https://github.com/angular/angular/commit/ae1c8a1f7a7f1d4832da3b22e3763864fa5ff098) | fix | move projection attributes into constants |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3fd6897a67](https://github.com/angular/angular/commit/3fd6897a67fd6acdc01fcde0452a98c3e0f81e21) | fix | harden inherit definition feature against polluted prototypes |
|
||||
| [7e38336dc7](https://github.com/angular/angular/commit/7e38336dc73e14d98cc6465f54e1b7d6271facb2) | fix | use Object.create(null) for LOCALE_DATA as a hardening measure |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [66821c4ed5](https://github.com/angular/angular/commit/66821c4ed5f580912a1609fc1e06a86f8793c2cf) | fix | throw on suspicious URLs and restrict protocol-relative URLs |
|
||||
| [d3170031b6](https://github.com/angular/angular/commit/d3170031b6f35508f960cba18586843925bb61ec) | fix | update domino to latest version |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.15"></a>
|
||||
# 21.2.15 (2026-05-28)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7f4ac78994](https://github.com/angular/angular/commit/7f4ac78994bff1576ab33f3ce48f95c17f40b4d8) | fix | add upper bounds for digitsInfo |
|
||||
| [300f61feb3](https://github.com/angular/angular/commit/300f61feb3a534bfddf16fcbd240f97b32249699) | fix | sanitize placeholder |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0b07f47bd6](https://github.com/angular/angular/commit/0b07f47bd6598ae6bd5b75a375e2c817a3c0f243) | fix | normalize tag names with custom namespaces in DomElementSchemaRegistry ([#68925](https://github.com/angular/angular/pull/68925)) |
|
||||
| [eb1cbbf2eb](https://github.com/angular/angular/commit/eb1cbbf2eb5833219a367a61c04eb07aaa36cc29) | fix | prevent namespaced SVG <style> elements from being stripped |
|
||||
| [cc1378d54b](https://github.com/angular/angular/commit/cc1378d54bd93f3882d732261be8e66720eb71b2) | fix | sanitize dynamic href and xlink:href bindings on SVG a elements ([#68925](https://github.com/angular/angular/pull/68925)) |
|
||||
| [782e01594e](https://github.com/angular/angular/commit/782e01594e2ad9134c7385dcf3b518101b23ccab) | fix | strip namespaced SVG script elements during template compilation ([#68925](https://github.com/angular/angular/pull/68925)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [ff12fe55ac](https://github.com/angular/angular/commit/ff12fe55ace5e861ba261afb4c0480ff3c40a192) | fix | normalize tag names in runtime i18n attribute security context lookup ([#68925](https://github.com/angular/angular/pull/68925)) |
|
||||
| [e6fe77cc97](https://github.com/angular/angular/commit/e6fe77cc97fd10351687416f938bf754aff4eb9f) | fix | sanitize meta selectors |
|
||||
| [daaf32937f](https://github.com/angular/angular/commit/daaf32937fd5c46e411b26f7c082613716fe9550) | fix | support prefix-insensitive DOM schema lookups and compile-time i18n attribute validation ([#68925](https://github.com/angular/angular/pull/68925)) |
|
||||
| [dada86e43d](https://github.com/angular/angular/commit/dada86e43d847204f714d1a933084617ab941c0a) | fix | synchronize core sanitization schema with compiler ([#68925](https://github.com/angular/angular/pull/68925)) |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [582a417bd2](https://github.com/angular/angular/commit/582a417bd27fdaf989e5065dbcdf1ad752faf70c) | fix | exclude withCredentials requests from transfer cache |
|
||||
| [5c6d6df34b](https://github.com/angular/angular/commit/5c6d6df34bbeff3ce98f3b35875444f925cc8f51) | fix | skip TransferCache for cookie-bearing requests by default |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [37e8aadf87](https://github.com/angular/angular/commit/37e8aadf87b4facfcaf002a1557f8c393a362d97) | fix | prevent SSRF bypasses via backslash URLs in HttpClient |
|
||||
| [72696e244e](https://github.com/angular/angular/commit/72696e244ed7646cca9ab9afc7769a2163943bda) | fix | secure location and document initialization against SSRF and path hijack |
|
||||
### service-worker
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [b8bd49341d](https://github.com/angular/angular/commit/b8bd49341ddcee10d119a9d4aa8e5736e4e5da53) | fix | Preserves explicit 'credentials: omit' in asset requests |
|
||||
| [ca32fc1000](https://github.com/angular/angular/commit/ca32fc10001301e6174804f9abcfba62252334f4) | fix | Preserves HTTP cache mode in asset group requests |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.14"></a>
|
||||
# 21.2.14 (2026-05-20)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [68282dff9f](https://github.com/angular/angular/commit/68282dff9f9ef46540cca4bd38fc1ab739c8a783) | fix | strip namespaced SVG script elements during template compilation |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c0f52272ed](https://github.com/angular/angular/commit/c0f52272ed337d4776bd4178cbbdc7f32037500f) | fix | do not insert todo when migrating void @Output |
|
||||
| [938a7f3edd](https://github.com/angular/angular/commit/938a7f3eddda97a39edb9edcc8b4dd970858b3a2) | fix | makes resource URL sanitizer lookup case-insensitive |
|
||||
| [0fb2724194](https://github.com/angular/angular/commit/0fb272419407a64a0a47096b03a911f4e7e83d79) | fix | reject script element as a dynamic component host |
|
||||
| [49113ac0ef](https://github.com/angular/angular/commit/49113ac0eff852d987b5acb28a9bbda0242842cd) | fix | visit ICU expressions in signal migration schematics |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [099bf577ee](https://github.com/angular/angular/commit/099bf577ee8f0bab60593a8fd2a1de7d298e3cd6) | fix | skip scroll-to-top on initial navigation when hydrating |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.13"></a>
|
||||
# 21.2.13 (2026-05-13)
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1c6553e97d](https://github.com/angular/angular/commit/1c6553e97d9655d8c48fbf625987fae86f9cd947) | fix | disallow event attribute bindings in host bindings unconditionally |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [629905d537](https://github.com/angular/angular/commit/629905d537f59dc3c264c49f6347e3599dea0215) | fix | add `allowedHosts` option to `renderModule` and `renderApplication` |
|
||||
| [0b7192f441](https://github.com/angular/angular/commit/0b7192f4410d055191ac9b15bff57d1d0b9a644f) | fix | forward BEFORE_APP_SERIALIZED errors to ErrorHandler |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.12"></a>
|
||||
# 21.2.12 (2026-05-06)
|
||||
### core
|
||||
|
||||
@@ -10,6 +10,8 @@ import {get} from 'node:https';
|
||||
import {posix} from 'node:path';
|
||||
|
||||
const GITHUB_API = 'https://api.github.com/repos/';
|
||||
const SHA_REGEX = /^[0-9a-f]{40}$/i;
|
||||
const BRANCH_REGEX = /^(?!.*\.\.)[a-zA-Z0-9/_.-]+$/;
|
||||
|
||||
export class GithubClient {
|
||||
#token;
|
||||
@@ -30,6 +32,12 @@ export class GithubClient {
|
||||
* @returns Promise<string[]>
|
||||
*/
|
||||
async getAffectedFiles(baseSha, headSha) {
|
||||
if (!SHA_REGEX.test(baseSha)) {
|
||||
throw new Error(`Invalid base SHA: ${baseSha}`);
|
||||
}
|
||||
if (!SHA_REGEX.test(headSha)) {
|
||||
throw new Error(`Invalid head SHA: ${headSha}`);
|
||||
}
|
||||
const {files} = JSON.parse(await this.#httpGet(`${this.#api}/compare/${baseSha}...${headSha}`));
|
||||
return files.map((f) => f.filename);
|
||||
}
|
||||
@@ -41,6 +49,9 @@ export class GithubClient {
|
||||
* @returns Promise<string>
|
||||
*/
|
||||
async getShaForBranch(branch) {
|
||||
if (!BRANCH_REGEX.test(branch)) {
|
||||
throw new Error(`Invalid branch name: ${branch}`);
|
||||
}
|
||||
const sha = await this.#httpGet(`${this.#api}/commits/${branch}`, {
|
||||
headers: {Accept: 'application/vnd.github.VERSION.sha'},
|
||||
});
|
||||
@@ -49,7 +60,7 @@ export class GithubClient {
|
||||
throw new Error(`Unable to extract the SHA for '${branch}'.`);
|
||||
}
|
||||
|
||||
return sha;
|
||||
return sha.trim();
|
||||
}
|
||||
|
||||
#httpGet(url, options = {}) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
//tslint:disable:no-console
|
||||
import assert from 'node:assert';
|
||||
import {execSync} from 'node:child_process';
|
||||
import {execFileSync} from 'node:child_process';
|
||||
import {existsSync, constants as fsConstants} from 'node:fs';
|
||||
import {
|
||||
copyFile,
|
||||
@@ -41,6 +41,16 @@ export async function updateAssets({repo, assetsPath, destPath}) {
|
||||
await readFile(buildInfoPath, 'utf-8'),
|
||||
);
|
||||
|
||||
const shaRegex = /^[0-9a-f]{40}$/i;
|
||||
const branchRegex = /^(?!.*\.\.)[a-zA-Z0-9/_.-]+$/;
|
||||
|
||||
if (!shaRegex.test(storedSha)) {
|
||||
throw new Error(`Invalid SHA in build info: ${storedSha}`);
|
||||
}
|
||||
if (!branchRegex.test(storedBranch)) {
|
||||
throw new Error(`Invalid branch name in build info: ${storedBranch}`);
|
||||
}
|
||||
|
||||
assert(process.env.ANGULAR_READONLY_GITHUB_TOKEN);
|
||||
const githubApi = new GithubClient(
|
||||
repo,
|
||||
@@ -63,6 +73,10 @@ export async function updateAssets({repo, assetsPath, destPath}) {
|
||||
downstreamBranch = storedBranch;
|
||||
}
|
||||
|
||||
if (!shaRegex.test(latestSha)) {
|
||||
throw new Error(`Invalid SHA resolved: ${latestSha}`);
|
||||
}
|
||||
|
||||
console.log(`Comparing ${storedSha}...${latestSha}.`);
|
||||
const affectedFiles = await githubApi.getAffectedFiles(storedSha, latestSha);
|
||||
const changedFiles = affectedFiles.filter((file) => file.startsWith(`${assetsPath}/`));
|
||||
@@ -78,14 +92,18 @@ export async function updateAssets({repo, assetsPath, destPath}) {
|
||||
|
||||
try {
|
||||
const execOptions = {cwd: temporaryDir, stdio: 'inherit'};
|
||||
execSync('git init', execOptions);
|
||||
execSync(`git remote add origin https://github.com/${repo}.git`, execOptions);
|
||||
execFileSync('git', ['init'], execOptions);
|
||||
execFileSync(
|
||||
'git',
|
||||
['remote', 'add', 'origin', `https://github.com/${repo}.git`],
|
||||
execOptions,
|
||||
);
|
||||
// fetch a commit
|
||||
execSync(`git fetch origin ${latestSha}`, execOptions);
|
||||
execFileSync('git', ['fetch', 'origin', latestSha], execOptions);
|
||||
// reset this repository's main branch to the commit of interest
|
||||
execSync('git reset --hard FETCH_HEAD', execOptions);
|
||||
execFileSync('git', ['reset', '--hard', 'FETCH_HEAD'], execOptions);
|
||||
// get sha when files where changed
|
||||
shaWhenFilesChanged = execSync(`git rev-list -1 ${latestSha} "${assetsPath}/"`, {
|
||||
shaWhenFilesChanged = execFileSync('git', ['rev-list', '-1', latestSha, `${assetsPath}/`], {
|
||||
encoding: 'utf8',
|
||||
cwd: temporaryDir,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
[href]="item.path"
|
||||
target="_blank"
|
||||
[matTooltip]="item.label"
|
||||
[matTooltipDisabled]="itemLabel.length < 27"
|
||||
[matTooltipDisabled]="itemLabel.length < labelTruncationThreshold"
|
||||
matTooltipPosition="after"
|
||||
[attr.aria-label]="item.label"
|
||||
[matTooltipClass]="'API-tooltip'"
|
||||
@@ -55,7 +55,7 @@
|
||||
}"
|
||||
(click)="emitClickOnLink(item)"
|
||||
[matTooltip]="item.label"
|
||||
[matTooltipDisabled]="itemLabel.length < 27"
|
||||
[matTooltipDisabled]="itemLabel.length < labelTruncationThreshold"
|
||||
matTooltipPosition="after"
|
||||
[attr.aria-label]="item.label"
|
||||
[matTooltipClass]="'API-tooltip'"
|
||||
@@ -76,7 +76,7 @@
|
||||
<div
|
||||
class="docs-secondary-nav-header"
|
||||
[matTooltip]="item.label"
|
||||
[matTooltipDisabled]="itemLabel.length < 27"
|
||||
[matTooltipDisabled]="itemLabel.length < labelTruncationThreshold"
|
||||
matTooltipPosition="after"
|
||||
[attr.aria-label]="item.label"
|
||||
[matTooltipClass]="'API-tooltip'"
|
||||
@@ -106,7 +106,7 @@
|
||||
item.children && item.level === expandableLevel() && !item.isExpanded
|
||||
"
|
||||
[matTooltip]="item.label"
|
||||
[matTooltipDisabled]="itemLabel.length < 27"
|
||||
[matTooltipDisabled]="itemLabel.length < labelTruncationThreshold"
|
||||
matTooltipPosition="after"
|
||||
[attr.aria-label]="item.label"
|
||||
[matTooltipClass]="'API-tooltip'"
|
||||
|
||||
@@ -39,6 +39,8 @@ export class NavigationList {
|
||||
|
||||
readonly linkClicked = output<void>();
|
||||
|
||||
protected readonly labelTruncationThreshold = 27;
|
||||
|
||||
private readonly navigationState = inject(NavigationState);
|
||||
private readonly crossCategoryOrigin = this.navigationState.crossCategoryOrigin;
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
ParameterEntry,
|
||||
PipeEntry,
|
||||
TypeAliasEntry,
|
||||
EntryType,
|
||||
} from '../entities.mjs';
|
||||
|
||||
import {CliCommand, CliOption} from '../cli-entities.mjs';
|
||||
@@ -105,6 +104,7 @@ export type FunctionEntryRenderable = FunctionEntry &
|
||||
export type FunctionSignatureMetadataRenderable = FunctionSignatureMetadata &
|
||||
DocEntryRenderable & {
|
||||
params: ParameterEntryRenderable[];
|
||||
htmlReturnDescription?: string;
|
||||
};
|
||||
|
||||
/** Documentation entity for a block augmented with transformed content for rendering. */
|
||||
|
||||
@@ -23,6 +23,7 @@ import {RawHtml} from './raw-html';
|
||||
export function ClassMethodInfo(props: {
|
||||
entry: FunctionSignatureMetadataRenderable;
|
||||
hideUsageNotes?: boolean;
|
||||
hideDescription?: boolean;
|
||||
}) {
|
||||
const entry = props.entry;
|
||||
|
||||
@@ -30,7 +31,9 @@ export function ClassMethodInfo(props: {
|
||||
<div
|
||||
className={`${REFERENCE_MEMBER_CARD_ITEM} ${entry.deprecated ? 'docs-reference-card-item-deprecated' : ''}`}
|
||||
>
|
||||
<RawHtml value={entry.htmlDescription} className={'docs-function-definition'} />
|
||||
{!props.hideDescription && (
|
||||
<RawHtml value={entry.htmlDescription} className={'docs-function-definition'} />
|
||||
)}
|
||||
{/* In case when method is overloaded we need to indicate which overload is deprecated */}
|
||||
{entry.deprecated ? (
|
||||
<div>
|
||||
@@ -45,6 +48,9 @@ export function ClassMethodInfo(props: {
|
||||
<div className={'docs-return-type'}>
|
||||
<span className={PARAM_KEYWORD_CLASS_NAME}>@returns</span>
|
||||
<CodeSymbol code={entry.returnType} />
|
||||
{entry.htmlReturnDescription && (
|
||||
<RawHtml value={entry.htmlReturnDescription} className="docs-parameter-description" />
|
||||
)}
|
||||
</div>
|
||||
{entry.htmlUsageNotes && !props.hideUsageNotes ? (
|
||||
<div className={'docs-usage-notes'}>
|
||||
|
||||
@@ -32,31 +32,43 @@ import {SectionUsageNotes} from './section-usage-notes';
|
||||
export const signatureCard = (
|
||||
name: string,
|
||||
signature: FunctionSignatureMetadataRenderable,
|
||||
opts: {id: string; printSignaturesAsHeader: boolean; hideUsageNotes?: boolean},
|
||||
opts: {
|
||||
id: string;
|
||||
printSignaturesAsHeader: boolean;
|
||||
hideUsageNotes?: boolean;
|
||||
hideHeader?: boolean;
|
||||
hideDescription?: boolean;
|
||||
},
|
||||
) => {
|
||||
return (
|
||||
<div id={opts.id} class={REFERENCE_MEMBER_CARD}>
|
||||
<header class={REFERENCE_MEMBER_CARD_HEADER}>
|
||||
{opts.printSignaturesAsHeader ? (
|
||||
<HighlightTypeScript
|
||||
code={printInitializerFunctionSignatureLine(
|
||||
name,
|
||||
signature,
|
||||
// Always omit types in signature headers, to keep them short.
|
||||
true,
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<h3>{name}</h3>
|
||||
<div>
|
||||
<CodeSymbol code={signature.returnType} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</header>
|
||||
{!opts.hideHeader && (
|
||||
<header class={REFERENCE_MEMBER_CARD_HEADER}>
|
||||
{opts.printSignaturesAsHeader ? (
|
||||
<HighlightTypeScript
|
||||
code={printInitializerFunctionSignatureLine(
|
||||
name,
|
||||
signature,
|
||||
// Always omit types in signature headers, to keep them short.
|
||||
true,
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<h3>{name}</h3>
|
||||
<div>
|
||||
<CodeSymbol code={signature.returnType} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</header>
|
||||
)}
|
||||
<div class={REFERENCE_MEMBER_CARD_BODY}>
|
||||
<ClassMethodInfo entry={signature} hideUsageNotes={opts.hideUsageNotes} />
|
||||
<ClassMethodInfo
|
||||
entry={signature}
|
||||
hideUsageNotes={opts.hideUsageNotes}
|
||||
hideDescription={opts.hideDescription}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -64,8 +76,8 @@ export const signatureCard = (
|
||||
|
||||
/** Component to render a function API reference document. */
|
||||
export function FunctionReference(entry: FunctionEntryRenderable) {
|
||||
// Use signatures as header if there are multiple signatures.
|
||||
const printSignaturesAsHeader = entry.signatures.length > 1;
|
||||
const hideSignatureCardDescription = !printSignaturesAsHeader;
|
||||
|
||||
return (
|
||||
<div className={API_REFERENCE_CONTAINER}>
|
||||
@@ -73,14 +85,15 @@ export function FunctionReference(entry: FunctionEntryRenderable) {
|
||||
<DeprecationWarning entry={entry} />
|
||||
<SectionApi entry={entry} />
|
||||
<div className={REFERENCE_MEMBERS}>
|
||||
{entry.signatures.length > 1 &&
|
||||
entry.signatures.map((s, i) =>
|
||||
signatureCard(s.name, getFunctionMetadataRenderable(s, entry.moduleName, entry.repo), {
|
||||
id: `${s.name}_${i}`,
|
||||
printSignaturesAsHeader,
|
||||
hideUsageNotes: true,
|
||||
}),
|
||||
)}
|
||||
{entry.signatures.map((s, i) =>
|
||||
signatureCard(s.name, getFunctionMetadataRenderable(s, entry.moduleName, entry.repo), {
|
||||
id: `${s.name}_${i}`,
|
||||
printSignaturesAsHeader,
|
||||
hideHeader: !printSignaturesAsHeader,
|
||||
hideUsageNotes: hideSignatureCardDescription,
|
||||
hideDescription: hideSignatureCardDescription,
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SectionDescription entry={entry} />
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
import {h} from 'preact';
|
||||
import {ParameterEntryRenderable} from '../entities/renderables.mjs';
|
||||
import {RawHtml} from './raw-html';
|
||||
import {PARAM_GROUP_CLASS_NAME} from '../styling/css-classes.mjs';
|
||||
import {CodeSymbol} from './code-symbols';
|
||||
import {RawHtml} from './raw-html';
|
||||
|
||||
/** Component to render a function or method parameter reference doc fragment. */
|
||||
export function Parameter(props: {param: ParameterEntryRenderable}) {
|
||||
@@ -21,7 +21,9 @@ export function Parameter(props: {param: ParameterEntryRenderable}) {
|
||||
{/*TODO: isOptional, isRestParam*/}
|
||||
<span class="docs-param-keyword">@param</span>
|
||||
<span class="docs-param-name">{param.name}</span>
|
||||
<CodeSymbol code={param.type} />
|
||||
<span class="docs-param-type">
|
||||
<CodeSymbol code={param.type} />
|
||||
</span>
|
||||
<RawHtml value={param.htmlDescription} className="docs-parameter-description" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -43,4 +43,17 @@ describe('CLI docs to html', () => {
|
||||
expect(cliTocs[0].textContent).toContain('ng component [name] [options]');
|
||||
expect(cliTocs[1].textContent).toContain('ng c [name] [options]');
|
||||
});
|
||||
|
||||
it('should rewrite absolute angular.dev hrefs in option descriptions to root-relative', async () => {
|
||||
const renderableJson = await getRenderable(entryJson, '', 'angular/cli');
|
||||
const localFragment = JSDOM.fragment(renderEntry(renderableJson));
|
||||
const hrefs = Array.from(localFragment.querySelectorAll('a')).map((a) =>
|
||||
a.getAttribute('href'),
|
||||
);
|
||||
// Absolute angular.dev URLs are rewritten to root-relative.
|
||||
expect(hrefs).toContain('/cli-test-rewrite');
|
||||
expect(hrefs).not.toContain('https://angular.dev/cli-test-rewrite');
|
||||
// Subdomains are intentionally left as external.
|
||||
expect(hrefs).toContain('https://next.angular.dev/cli-test-preview');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
"name": "generate",
|
||||
"command": "ng generate <schematic>",
|
||||
"shortDescription": "Generates and/or modifies files based on a schematic.",
|
||||
"aliases": [
|
||||
"g"
|
||||
],
|
||||
"aliases": ["g"],
|
||||
"deprecated": false,
|
||||
"options": [
|
||||
{
|
||||
@@ -16,9 +14,7 @@
|
||||
{
|
||||
"name": "dry-run",
|
||||
"type": "boolean",
|
||||
"aliases": [
|
||||
"d"
|
||||
],
|
||||
"aliases": ["d"],
|
||||
"default": false,
|
||||
"description": "Run through and reports activity without writing out results."
|
||||
},
|
||||
@@ -44,6 +40,12 @@
|
||||
"type": "string",
|
||||
"description": "The [collection:schematic] to run.",
|
||||
"positional": 0
|
||||
},
|
||||
{
|
||||
"name": "test-angular-dev-href-rewrite",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "See [angular.dev test](https://angular.dev/cli-test-rewrite) and [preview test](https://next.angular.dev/cli-test-preview)."
|
||||
}
|
||||
],
|
||||
"subcommands": [
|
||||
@@ -75,17 +77,13 @@
|
||||
{
|
||||
"name": "inline-style",
|
||||
"type": "boolean",
|
||||
"aliases": [
|
||||
"s"
|
||||
],
|
||||
"aliases": ["s"],
|
||||
"description": "Include styles inline in the root component.ts file. Only CSS styles can be included inline. Default is false, meaning that an external styles file is created and referenced in the root component.ts file."
|
||||
},
|
||||
{
|
||||
"name": "inline-template",
|
||||
"type": "boolean",
|
||||
"aliases": [
|
||||
"t"
|
||||
],
|
||||
"aliases": ["t"],
|
||||
"description": "Include template inline in the root component.ts file. Default is false, meaning that an external template file is created and referenced in the root component.ts file. "
|
||||
},
|
||||
{
|
||||
@@ -103,9 +101,7 @@
|
||||
{
|
||||
"name": "prefix",
|
||||
"type": "string",
|
||||
"aliases": [
|
||||
"p"
|
||||
],
|
||||
"aliases": ["p"],
|
||||
"default": "app",
|
||||
"description": "A prefix to apply to generated selectors."
|
||||
},
|
||||
@@ -135,9 +131,7 @@
|
||||
{
|
||||
"name": "skip-tests",
|
||||
"type": "boolean",
|
||||
"aliases": [
|
||||
"S"
|
||||
],
|
||||
"aliases": ["S"],
|
||||
"default": false,
|
||||
"description": "Do not create \"spec.ts\" test files for the application."
|
||||
},
|
||||
@@ -163,29 +157,17 @@
|
||||
"name": "style",
|
||||
"type": "string",
|
||||
"default": "css",
|
||||
"enum": [
|
||||
"css",
|
||||
"scss",
|
||||
"sass",
|
||||
"less"
|
||||
],
|
||||
"enum": ["css", "scss", "sass", "less"],
|
||||
"description": "The file extension or preprocessor to use for style files."
|
||||
},
|
||||
{
|
||||
"name": "view-encapsulation",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Emulated",
|
||||
"None",
|
||||
"ShadowDom",
|
||||
"ExperimentalIsolatedShadowDom"
|
||||
],
|
||||
"enum": ["Emulated", "None", "ShadowDom", "ExperimentalIsolatedShadowDom"],
|
||||
"description": "The view encapsulation strategy to use in the new application."
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
"app"
|
||||
],
|
||||
"aliases": ["app"],
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
@@ -216,9 +198,7 @@
|
||||
"description": "Adds a developer-defined type to the filename, in the format \"name.type.ts\"."
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
"cl"
|
||||
],
|
||||
"aliases": ["cl"],
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
@@ -229,22 +209,15 @@
|
||||
{
|
||||
"name": "change-detection",
|
||||
"type": "string",
|
||||
"aliases": [
|
||||
"c"
|
||||
],
|
||||
"aliases": ["c"],
|
||||
"default": "Default",
|
||||
"enum": [
|
||||
"Default",
|
||||
"OnPush"
|
||||
],
|
||||
"enum": ["Default", "OnPush"],
|
||||
"description": "The change detection strategy to use in the new component."
|
||||
},
|
||||
{
|
||||
"name": "display-block",
|
||||
"type": "boolean",
|
||||
"aliases": [
|
||||
"b"
|
||||
],
|
||||
"aliases": ["b"],
|
||||
"default": false,
|
||||
"description": "Specifies if the style will contain `:host { display: block; }`."
|
||||
},
|
||||
@@ -269,27 +242,21 @@
|
||||
{
|
||||
"name": "inline-style",
|
||||
"type": "boolean",
|
||||
"aliases": [
|
||||
"s"
|
||||
],
|
||||
"aliases": ["s"],
|
||||
"default": false,
|
||||
"description": "Include styles inline in the component.ts file. Only CSS styles can be included inline. By default, an external styles file is created and referenced in the component.ts file."
|
||||
},
|
||||
{
|
||||
"name": "inline-template",
|
||||
"type": "boolean",
|
||||
"aliases": [
|
||||
"t"
|
||||
],
|
||||
"aliases": ["t"],
|
||||
"default": false,
|
||||
"description": "Include template inline in the component.ts file. By default, an external template file is created and referenced in the component.ts file."
|
||||
},
|
||||
{
|
||||
"name": "module",
|
||||
"type": "string",
|
||||
"aliases": [
|
||||
"m"
|
||||
],
|
||||
"aliases": ["m"],
|
||||
"description": "The declaring NgModule."
|
||||
},
|
||||
{
|
||||
@@ -301,9 +268,7 @@
|
||||
{
|
||||
"name": "prefix",
|
||||
"type": "string",
|
||||
"aliases": [
|
||||
"p"
|
||||
],
|
||||
"aliases": ["p"],
|
||||
"description": "The prefix to apply to the generated component selector."
|
||||
},
|
||||
{
|
||||
@@ -344,13 +309,7 @@
|
||||
"name": "style",
|
||||
"type": "string",
|
||||
"default": "css",
|
||||
"enum": [
|
||||
"css",
|
||||
"scss",
|
||||
"sass",
|
||||
"less",
|
||||
"none"
|
||||
],
|
||||
"enum": ["css", "scss", "sass", "less", "none"],
|
||||
"description": "The file extension or preprocessor to use for style files, or 'none' to skip generating the style file."
|
||||
},
|
||||
{
|
||||
@@ -362,21 +321,12 @@
|
||||
{
|
||||
"name": "view-encapsulation",
|
||||
"type": "string",
|
||||
"aliases": [
|
||||
"v"
|
||||
],
|
||||
"enum": [
|
||||
"Emulated",
|
||||
"None",
|
||||
"ShadowDom",
|
||||
"ExperimentalIsolatedShadowDom"
|
||||
],
|
||||
"aliases": ["v"],
|
||||
"enum": ["Emulated", "None", "ShadowDom", "ExperimentalIsolatedShadowDom"],
|
||||
"description": "The view encapsulation strategy to use in the new component."
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
"c"
|
||||
],
|
||||
"aliases": ["c"],
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
@@ -392,10 +342,7 @@
|
||||
{
|
||||
"name": "type",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"karma",
|
||||
"browserslist"
|
||||
],
|
||||
"enum": ["karma", "browserslist"],
|
||||
"description": "Specifies which type of configuration file to create.",
|
||||
"positional": 0
|
||||
}
|
||||
@@ -423,9 +370,7 @@
|
||||
{
|
||||
"name": "module",
|
||||
"type": "string",
|
||||
"aliases": [
|
||||
"m"
|
||||
],
|
||||
"aliases": ["m"],
|
||||
"description": "The declaring NgModule."
|
||||
},
|
||||
{
|
||||
@@ -437,9 +382,7 @@
|
||||
{
|
||||
"name": "prefix",
|
||||
"type": "string",
|
||||
"aliases": [
|
||||
"p"
|
||||
],
|
||||
"aliases": ["p"],
|
||||
"description": "A prefix to apply to generated selectors."
|
||||
},
|
||||
{
|
||||
@@ -471,9 +414,7 @@
|
||||
"description": "Whether the generated directive is standalone."
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
"d"
|
||||
],
|
||||
"aliases": ["d"],
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
@@ -498,9 +439,7 @@
|
||||
"description": "Adds a developer-defined type to the filename, in the format \"name.type.ts\"."
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
"e"
|
||||
],
|
||||
"aliases": ["e"],
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
@@ -537,9 +476,7 @@
|
||||
{
|
||||
"name": "implements",
|
||||
"type": "array",
|
||||
"aliases": [
|
||||
"guardType"
|
||||
],
|
||||
"aliases": ["guardType"],
|
||||
"description": "Specifies which type of guard to create."
|
||||
},
|
||||
{
|
||||
@@ -560,9 +497,7 @@
|
||||
"description": "Do not create \"spec.ts\" test files for the new guard."
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
"g"
|
||||
],
|
||||
"aliases": ["g"],
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
@@ -631,9 +566,7 @@
|
||||
"positional": 1
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
"i"
|
||||
],
|
||||
"aliases": ["i"],
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
@@ -656,9 +589,7 @@
|
||||
{
|
||||
"name": "prefix",
|
||||
"type": "string",
|
||||
"aliases": [
|
||||
"p"
|
||||
],
|
||||
"aliases": ["p"],
|
||||
"default": "lib",
|
||||
"description": "A prefix to apply to generated selectors."
|
||||
},
|
||||
@@ -692,9 +623,7 @@
|
||||
"description": "Creates a library based upon the standalone API, without NgModules."
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
"lib"
|
||||
],
|
||||
"aliases": ["lib"],
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
@@ -711,9 +640,7 @@
|
||||
{
|
||||
"name": "module",
|
||||
"type": "string",
|
||||
"aliases": [
|
||||
"m"
|
||||
],
|
||||
"aliases": ["m"],
|
||||
"description": "The declaring NgModule."
|
||||
},
|
||||
{
|
||||
@@ -742,16 +669,11 @@
|
||||
"name": "routing-scope",
|
||||
"type": "string",
|
||||
"default": "Child",
|
||||
"enum": [
|
||||
"Child",
|
||||
"Root"
|
||||
],
|
||||
"enum": ["Child", "Root"],
|
||||
"description": "The scope for the new routing module."
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
"m"
|
||||
],
|
||||
"aliases": ["m"],
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
@@ -774,9 +696,7 @@
|
||||
{
|
||||
"name": "module",
|
||||
"type": "string",
|
||||
"aliases": [
|
||||
"m"
|
||||
],
|
||||
"aliases": ["m"],
|
||||
"description": "The declaring NgModule."
|
||||
},
|
||||
{
|
||||
@@ -809,9 +729,7 @@
|
||||
"description": "Whether the generated pipe is standalone."
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
"p"
|
||||
],
|
||||
"aliases": ["p"],
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
@@ -849,9 +767,7 @@
|
||||
"description": "Do not create \"spec.ts\" test files for the new resolver."
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
"r"
|
||||
],
|
||||
"aliases": ["r"],
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
@@ -883,9 +799,7 @@
|
||||
"description": "Do not create \"spec.ts\" test files for the new service."
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
"s"
|
||||
],
|
||||
"aliases": ["s"],
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
|
||||
@@ -463,6 +463,7 @@
|
||||
"isRestParam": false
|
||||
}
|
||||
],
|
||||
"returnDescription": "A reference that can be used to unregister callbacks registered by this call.",
|
||||
"rawComment": "/**\n * Register callbacks to be invoked the next time the application finishes rendering, during the\n * specified phases. The available phases are:\n * - `earlyRead`\n * Use this phase to **read** from the DOM before a subsequent `write` callback, for example to\n * perform custom layout that the browser doesn't natively support. Prefer the `read` phase if\n * reading can wait until after the write phase. **Never** write to the DOM in this phase.\n * - `write`\n * Use this phase to **write** to the DOM. **Never** read from the DOM in this phase.\n * - `mixedReadWrite`\n * Use this phase to read from and write to the DOM simultaneously. **Never** use this phase if\n * it is possible to divide the work among the other phases instead.\n * - `read`\n * Use this phase to **read** from the DOM. **Never** write to the DOM in this phase.\n *\n * <div class=\"docs-alert docs-alert-critical\">\n *\n * You should prefer using the `read` and `write` phases over the `earlyRead` and `mixedReadWrite`\n * phases when possible, to avoid performance degradation.\n *\n * </div>\n *\n * Note that:\n * - Callbacks run in the following phase order *once, after the next render*:\n * 1. `earlyRead`\n * 2. `write`\n * 3. `mixedReadWrite`\n * 4. `read`\n * - Callbacks in the same phase run in the order they are registered.\n * - Callbacks run on browser platforms only, they will not run on the server.\n *\n * The first phase callback to run as part of this spec will receive no parameters. Each\n * subsequent phase callback in this spec will receive the return value of the previously run\n * phase callback as a parameter. This can be used to coordinate work across multiple phases.\n *\n * Angular is unable to verify or enforce that phases are used correctly, and instead\n * relies on each developer to follow the guidelines documented for each value and\n * carefully choose the appropriate one, refactoring their code if necessary. By doing\n * so, Angular is better able to minimize the performance degradation associated with\n * manual DOM access, ensuring the best experience for the end users of your application\n * or library.\n *\n * <div class=\"docs-alert docs-alert-important\">\n *\n * Components are not guaranteed to be [hydrated](guide/hydration) before the callback runs.\n * You must use caution when directly reading or writing the DOM and layout.\n *\n * </div>\n *\n * @param spec The callback functions to register\n * @param options Options to control the behavior of the callback\n *\n * @usageNotes\n *\n * Use `afterNextRender` to read or write the DOM once,\n * for example to initialize a non-Angular library.\n *\n * ### Example\n * ```angular-ts\n * @Component({\n * selector: 'my-chart-cmp',\n * template: `<div #chart>{{ ... }}</div>`,\n * })\n * export class MyChartCmp {\n * @ViewChild('chart') chartRef: ElementRef;\n * chart: MyChart|null;\n *\n * constructor() {\n * afterNextRender({\n * write: () => {\n * this.chart = new MyChart(this.chartRef.nativeElement);\n * }\n * });\n * }\n * }\n * ```\n *\n * @developerPreview\n */",
|
||||
"returnType": "AfterRenderRef"
|
||||
},
|
||||
@@ -497,6 +498,7 @@
|
||||
"isRestParam": false
|
||||
}
|
||||
],
|
||||
"returnDescription": "A reference that can be used to unregister the callback registered by this call.",
|
||||
"rawComment": "/**\n * Register a callback to be invoked the next time the application finishes rendering, during the\n * `mixedReadWrite` phase.\n *\n * <div class=\"docs-alert docs-alert-critical\">\n *\n * You should prefer specifying an explicit phase for the callback instead, or you risk significant\n * performance degradation.\n *\n * </div>\n *\n * Note that the callback will run\n * - in the order it was registered\n * - on browser platforms only\n * - during the `mixedReadWrite` phase\n *\n * <div class=\"docs-alert docs-alert-important\">\n *\n * Components are not guaranteed to be [hydrated](guide/hydration) before the callback runs.\n * You must use caution when directly reading or writing the DOM and layout.\n *\n * </div>\n *\n * @param callback A callback function to register\n * @param options Options to control the behavior of the callback\n *\n * @usageNotes\n *\n * Use `afterNextRender` to read or write the DOM once,\n * for example to initialize a non-Angular library.\n *\n * ### Example\n * ```angular-ts\n * @Component({\n * selector: 'my-chart-cmp',\n * template: `<div #chart>{{ ... }}</div>`,\n * })\n * export class MyChartCmp {\n * @ViewChild('chart') chartRef: ElementRef;\n * chart: MyChart|null;\n *\n * constructor() {\n * afterNextRender({\n * write: () => {\n * this.chart = new MyChart(this.chartRef.nativeElement);\n * }\n * });\n * }\n * }\n * ```\n *\n * @publicApi 20.0\n */",
|
||||
"returnType": "AfterRenderRef"
|
||||
}
|
||||
|
||||
@@ -53,11 +53,14 @@ export function getCliCardsRenderable(command: CliCommand): CliCardRenderable[]
|
||||
return cards;
|
||||
}
|
||||
|
||||
// Rewrite absolute angular.dev hrefs to root-relative so links stay in-site.
|
||||
const angularDevHrefRegex = /(href=["'])https?:\/\/angular\.dev\//g;
|
||||
|
||||
function getRenderableOptions(items: CliOption[]): CliOptionRenderable[] {
|
||||
return items.map((option) => ({
|
||||
...option,
|
||||
deprecated: option.deprecated ? {version: undefined} : undefined,
|
||||
description: marked.parse(option.description) as string,
|
||||
description: (marked.parse(option.description) as string).replace(angularDevHrefRegex, '$1/'),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
setEntryFlags,
|
||||
} from './jsdoc-transforms.mjs';
|
||||
import {addModuleName} from './module-name.mjs';
|
||||
import {addRenderableFunctionParams} from './params-transforms.mjs';
|
||||
import {addHtmlReturnDescription, addRenderableFunctionParams} from './params-transforms.mjs';
|
||||
import {addRepo} from './repo.mjs';
|
||||
|
||||
/** Given an unprocessed function entry, get the fully renderable function entry. */
|
||||
@@ -50,11 +50,13 @@ export function getFunctionMetadataRenderable(
|
||||
repo: string,
|
||||
): FunctionSignatureMetadataRenderable {
|
||||
return addHtmlAdditionalLinks(
|
||||
addRenderableFunctionParams(
|
||||
addHtmlUsageNotes(
|
||||
setEntryFlags(
|
||||
addHtmlJsDocTagComments(
|
||||
addHtmlDescription(addRepo(addModuleName(entry, moduleName), repo)),
|
||||
addHtmlReturnDescription(
|
||||
addRenderableFunctionParams(
|
||||
addHtmlUsageNotes(
|
||||
setEntryFlags(
|
||||
addHtmlJsDocTagComments(
|
||||
addHtmlDescription(addRepo(addModuleName(entry, moduleName), repo)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -112,7 +112,7 @@ export function addHtmlUsageNotes<T extends HasJsDocTags>(entry: T): T & HasHtml
|
||||
}
|
||||
|
||||
/** Given a markdown JsDoc text, gets the rendered HTML. */
|
||||
function getHtmlForJsDocText(text: string): string {
|
||||
export function getHtmlForJsDocText(text: string): string {
|
||||
const mdToParse = convertLinks(wrapExampleHtmlElementsWithCode(text));
|
||||
const parsed = parseMarkdown(mdToParse, {
|
||||
apiEntries: getSymbolsAsApiEntries(),
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {HasModuleName, HasParams, HasRenderableParams} from '../entities/traits.mjs';
|
||||
import {addHtmlDescription} from './jsdoc-transforms.mjs';
|
||||
import {addHtmlDescription, getHtmlForJsDocText} from './jsdoc-transforms.mjs';
|
||||
import {addModuleName} from './module-name.mjs';
|
||||
|
||||
export function addRenderableFunctionParams<T extends HasParams & HasModuleName>(
|
||||
@@ -22,3 +22,13 @@ export function addRenderableFunctionParams<T extends HasParams & HasModuleName>
|
||||
params,
|
||||
};
|
||||
}
|
||||
|
||||
/** Converts `returnDescription` to `htmlReturnDescription` for rendering. */
|
||||
export function addHtmlReturnDescription<
|
||||
T extends {returnDescription?: string; moduleName: string},
|
||||
>(entry: T): T & {htmlReturnDescription?: string} {
|
||||
const htmlReturnDescription = entry.returnDescription
|
||||
? getHtmlForJsDocText(entry.returnDescription)
|
||||
: undefined;
|
||||
return {...entry, htmlReturnDescription};
|
||||
}
|
||||
|
||||
@@ -24,43 +24,43 @@ $screen-xxl: 1800px;
|
||||
}
|
||||
|
||||
@mixin for-tablet-portrait-up {
|
||||
@media (min-width: calc($screen-xs + 0.01px)) {
|
||||
@media (min-width: ($screen-xs + 1px)) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin for-tablet {
|
||||
@media (min-width: calc($screen-xs + 0.01px)) and (max-width: $screen-md) {
|
||||
@media (min-width: ($screen-xs + 1px)) and (max-width: $screen-md) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin for-tablet-up {
|
||||
@media (min-width: calc($screen-sm + 0.01px)) {
|
||||
@media (min-width: ($screen-sm + 1px)) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin for-tablet-landscape-up {
|
||||
@media (min-width: calc($screen-md + 0.01px)) {
|
||||
@media (min-width: ($screen-md + 1px)) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin for-desktop-up {
|
||||
@media (min-width: calc($screen-lg + 0.01px)) {
|
||||
@media (min-width: ($screen-lg + 1px)) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin for-large-desktop-up {
|
||||
@media (min-width: calc($screen-xl + 0.01px)) {
|
||||
@media (min-width: ($screen-xl + 1px)) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin for-extra-large-desktop-up {
|
||||
@media (min-width: calc($screen-xxl + 0.01px)) {
|
||||
@media (min-width: ($screen-xxl + 1px)) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ $screen-xxl: 1800px;
|
||||
}
|
||||
|
||||
@mixin for-large-desktop-down {
|
||||
@media (max-width: calc($screen-xl )) {
|
||||
@media (max-width: $screen-xl) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +340,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.docs-param-type {
|
||||
display: inline-block;
|
||||
margin-inline-end: 0.5rem;
|
||||
}
|
||||
|
||||
.docs-parameter-description {
|
||||
p:first-child {
|
||||
margin-block-start: 0;
|
||||
@@ -357,7 +362,7 @@
|
||||
padding-block: 1rem;
|
||||
|
||||
// & does not follow a function definition
|
||||
&:not(.docs-function-definition + .docs-return-type) {
|
||||
&:not(.docs-function-definition + .docs-return-type):not(:first-child) {
|
||||
border-block-start: 1px solid var(--senary-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,26 +80,11 @@ $code-font-size: 0.875rem;
|
||||
&:not(pre *) {
|
||||
position: relative;
|
||||
padding: 0 0.3rem;
|
||||
// Fallback for older browsers
|
||||
background: #e62600;
|
||||
background: var(--red-to-orange-horizontal-gradient);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
background: var(--subtle-purple);
|
||||
color: var(--hot-pink);
|
||||
max-width: max-content;
|
||||
border-radius: 0.25rem;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--subtle-purple);
|
||||
border-radius: 0.25rem;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
a:not(.docs-anchor) > & {
|
||||
position: relative;
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/angular/angular/issues"
|
||||
title="Post issues and suggestions on github."
|
||||
title="Post issues and suggestions on GitHub."
|
||||
>
|
||||
Report Issues
|
||||
</a>
|
||||
@@ -99,6 +99,9 @@
|
||||
<li>
|
||||
<a href="https://angular.az/" title="Azərbaycanca">Azərbaycanca</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://docs.angular.lat/" title="Español">Español</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://angular-docs.tr/" title="Türkçe">Türkçe</a>
|
||||
</li>
|
||||
|
||||
@@ -392,11 +392,11 @@
|
||||
<a
|
||||
[href]="ngLinks.GITHUB"
|
||||
cdkMenuItem
|
||||
title="Angular Github"
|
||||
title="Angular GitHub"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<!-- Github Icon -->
|
||||
<!-- GitHub Icon -->
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
|
||||
@@ -79,12 +79,15 @@
|
||||
|
||||
:host {
|
||||
--item-attr-base-mix: 80%;
|
||||
--item-attr-text: var(--page-background);
|
||||
|
||||
.docs-light-mode & {
|
||||
--item-attr-base-mix: 20%;
|
||||
--item-attr-text: var(--primary-contrast);
|
||||
}
|
||||
@media screen and (prefers-color-scheme: light) {
|
||||
--item-attr-base-mix: 20%;
|
||||
--item-attr-text: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +113,7 @@
|
||||
var(--symbolic-yellow) var(--item-attr-base-mix),
|
||||
var(--octonary-contrast)
|
||||
);
|
||||
color: var(--page-background);
|
||||
color: var(--item-attr-text);
|
||||
}
|
||||
|
||||
&.adev-experimental {
|
||||
@@ -119,7 +122,7 @@
|
||||
var(--symbolic-green) var(--item-attr-base-mix),
|
||||
var(--octonary-contrast)
|
||||
);
|
||||
color: var(--page-background);
|
||||
color: var(--item-attr-text);
|
||||
}
|
||||
|
||||
&.adev-deprecated {
|
||||
|
||||
@@ -787,7 +787,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
possibleIn: 1000,
|
||||
necessaryAsOf: 1000,
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'v10 NodeJS 12',
|
||||
step: 'v10 Node.js 12',
|
||||
action: 'Make sure you are using [Node 12 or later](https://nodejs.org/dist/latest-v12.x/).',
|
||||
},
|
||||
{
|
||||
@@ -852,7 +852,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Advanced,
|
||||
step: 'closure-jsdoc-comments',
|
||||
action:
|
||||
"Angular's NPM packages no longer contain jsdoc comments, which are necessary for use with closure compiler (extremely uncommon). This support was experimental and only worked in some use cases. There will be an alternative recommended path announced shortly.",
|
||||
"Angular's npm packages no longer contain jsdoc comments, which are necessary for use with closure compiler (extremely uncommon). This support was experimental and only worked in some use cases. There will be an alternative recommended path announced shortly.",
|
||||
},
|
||||
{
|
||||
possibleIn: 1000,
|
||||
|
||||
@@ -517,6 +517,13 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
category: 'Signal Forms',
|
||||
status: 'new',
|
||||
},
|
||||
{
|
||||
label: 'Field metadata',
|
||||
path: 'guide/forms/signals/field-metadata',
|
||||
contentPath: 'guide/forms/signals/field-metadata',
|
||||
category: 'Signal Forms',
|
||||
status: 'new',
|
||||
},
|
||||
{
|
||||
label: 'Async operations',
|
||||
path: 'guide/forms/signals/async-operations',
|
||||
|
||||
@@ -18,11 +18,11 @@ Here is a set of instructions to help LLMs generate correct code that follows An
|
||||
|
||||
## Rules Files
|
||||
|
||||
Several editors, such as <a href="https://studio.firebase.google.com?utm_source=adev&utm_medium=website&utm_campaign=BUILD_WITH_AI_ANGULAR&utm_term=angular_devrel&utm_content=build_with_ai_angular_firebase_studio">Firebase Studio</a> have rules files useful for providing critical context to LLMs.
|
||||
Several editors, such as <a href="https://studio.firebase.google.com?utm_source=adev&utm_medium=website&utm_campaign=BUILD_WITH_AI_ANGULAR&utm_term=angular_devrel&utm_content=build_with_ai_angular_firebase_studio" target="_blank">Firebase Studio</a> have rules files useful for providing critical context to LLMs.
|
||||
|
||||
| Environment/IDE | Rules File | Installation Instructions |
|
||||
| :------------------- | :--------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Firebase Studio | <a download href="/assets/context/airules.md" target="_blank">airules.md</a> | <a href="https://firebase.google.com/docs/studio/set-up-gemini#custom-instructions">Configure `airules.md`</a> |
|
||||
| Firebase Studio | <a download href="/assets/context/airules.md" target="_blank">airules.md</a> | <a href="https://firebase.google.com/docs/studio/set-up-gemini#custom-instructions" target="_blank">Configure `airules.md`</a> |
|
||||
| Copilot powered IDEs | <a download="copilot-instructions.md" href="/assets/context/guidelines.md" target="_blank">copilot-instructions.md</a> | <a href="https://code.visualstudio.com/docs/copilot/copilot-customization#_custom-instructions" target="_blank">Configure `.github/copilot-instructions.md`</a> |
|
||||
| Cursor | <a download href="/assets/context/angular-20.mdc" target="_blank">cursor.md</a> | <a href="https://docs.cursor.com/context/rules" target="_blank">Configure `cursorrules.md`</a> |
|
||||
| JetBrains IDEs | <a download href="/assets/context/guidelines.md" target="_blank">guidelines.md</a> | <a href="https://www.jetbrains.com/help/junie/customize-guidelines.html" target="_blank">Configure `guidelines.md`</a> |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"branchName": "refs/heads/21.2.x",
|
||||
"sha": "faea5e033fd1bc0a5dec7e430434414d1cf6243d"
|
||||
"sha": "887bb70d938ed041e4d1d61f6683c995cb91709c"
|
||||
}
|
||||
@@ -255,7 +255,7 @@
|
||||
"readonly",
|
||||
"input"
|
||||
],
|
||||
"description": "The value of the menu item, used as the default aria-label",
|
||||
"description": "The value of the menu item.",
|
||||
"jsdocTags": [],
|
||||
"inputAlias": "value",
|
||||
"isRequiredInput": true
|
||||
@@ -413,7 +413,7 @@
|
||||
"name": "V"
|
||||
}
|
||||
],
|
||||
"description": "An item in a Menu.\n\n`ngMenuItem` directives can be used in `ngMenu` and `ngMenuBar` to represent a choice\nor action a user can take. They can also act as triggers for sub-menus.\n\n```html\n<div ngMenu (itemSelected)=\"doAction()\">\n <div ngMenuItem >Action Item</div>\n <div ngMenuItem [submenu]=\"anotherMenu\">Submenu Trigger</div>\n</div>\n```",
|
||||
"description": "An item in a Menu.\n\n`ngMenuItem` directives can be used in `ngMenu` and `ngMenuBar` to represent a choice\nor action a user can take. They can also act as triggers for sub-menus.\n\n```html\n<div ngMenu (itemSelected)=\"doAction()\">\n <div ngMenuItem>Action Item</div>\n <div ngMenuItem [submenu]=\"anotherMenu\">Submenu Trigger</div>\n</div>\n```",
|
||||
"jsdocTags": [
|
||||
{
|
||||
"name": "developerPreview",
|
||||
@@ -428,7 +428,7 @@
|
||||
"comment": "[MenuBar](guide/aria/menubar)"
|
||||
}
|
||||
],
|
||||
"rawComment": "/**\n * An item in a Menu.\n *\n * `ngMenuItem` directives can be used in `ngMenu` and `ngMenuBar` to represent a choice\n * or action a user can take. They can also act as triggers for sub-menus.\n *\n * ```html\n * <div ngMenu (itemSelected)=\"doAction()\">\n * <div ngMenuItem >Action Item</div>\n * <div ngMenuItem [submenu]=\"anotherMenu\">Submenu Trigger</div>\n * </div>\n * ```\n *\n * @developerPreview 21.0\n *\n * @see [Menu](guide/aria/menu)\n * @see [MenuBar](guide/aria/menubar)\n */",
|
||||
"rawComment": "/**\n * An item in a Menu.\n *\n * `ngMenuItem` directives can be used in `ngMenu` and `ngMenuBar` to represent a choice\n * or action a user can take. They can also act as triggers for sub-menus.\n *\n * ```html\n * <div ngMenu (itemSelected)=\"doAction()\">\n * <div ngMenuItem>Action Item</div>\n * <div ngMenuItem [submenu]=\"anotherMenu\">Submenu Trigger</div>\n * </div>\n * ```\n *\n * @developerPreview 21.0\n *\n * @see [Menu](guide/aria/menu)\n * @see [MenuBar](guide/aria/menubar)\n */",
|
||||
"implements": [],
|
||||
"isStandalone": true,
|
||||
"selector": "[ngMenuItem]",
|
||||
@@ -441,7 +441,7 @@
|
||||
"source": {
|
||||
"filePath": "/src/aria/menu/menu-item.ts",
|
||||
"startLine": 34,
|
||||
"endLine": 109
|
||||
"endLine": 108
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -63,6 +63,9 @@ For example:
|
||||
|
||||
For full details of these and other tools, see the [Angular CDK accessibility overview](https://material.angular.dev/cdk/a11y/overview).
|
||||
|
||||
For custom-styled components that need reusable WAI-ARIA interaction patterns, [Angular Aria](guide/aria/overview) provides headless directives for patterns such as accordion, combobox, listbox, menu, tabs, and toolbar.
|
||||
These directives handle keyboard interaction, ARIA attributes, focus management, and screen reader support while letting you provide the HTML structure and styling for your application.
|
||||
|
||||
### Augmenting native elements
|
||||
|
||||
Native HTML elements capture several standard interaction patterns that are important to accessibility.
|
||||
|
||||
@@ -85,7 +85,7 @@ For example, an asset group that matches `/foo.js` should appear before one that
|
||||
Each asset group specifies both a group of resources and a policy that governs them.
|
||||
This policy determines when the resources are fetched and what happens when changes are detected.
|
||||
|
||||
Asset groups follow the Typescript interface shown here:
|
||||
Asset groups follow the TypeScript interface shown here:
|
||||
|
||||
```ts
|
||||
interface AssetGroup {
|
||||
@@ -179,7 +179,7 @@ The first data group that matches the requested resource handles the request.
|
||||
It is recommended that you put the more specific data groups higher in the list.
|
||||
For example, a data group that matches `/api/foo.json` should appear before one that matches `/api/*.json`.
|
||||
|
||||
Data groups follow this Typescript interface:
|
||||
Data groups follow this TypeScript interface:
|
||||
|
||||
```ts
|
||||
export interface DataGroup {
|
||||
|
||||
@@ -283,7 +283,7 @@ When the service worker's request for `ngsw.json` returns a `404`, then the serv
|
||||
|
||||
<!-- vale Angular.Google_Acronyms = NO -->
|
||||
|
||||
A small script, `safety-worker.js`, is also included in the `@angular/service-worker` NPM package.
|
||||
A small script, `safety-worker.js`, is also included in the `@angular/service-worker` npm package.
|
||||
When loaded, it un-registers itself from the browser and removes the service worker caches.
|
||||
This script can be used as a last resort to get rid of unwanted service workers already installed on client pages.
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ This sample comes from the Angular documentation's "[Example Angular Internation
|
||||
|
||||
> See the scripts in `package.json` for an explanation of these commands.
|
||||
|
||||
## Run in Stackblitz
|
||||
## Run in StackBlitz
|
||||
|
||||
Stackblitz compiles and runs the English version by default.
|
||||
StackBlitz compiles and runs the English version by default.
|
||||
|
||||
To see the example translate to French with Angular i18n:
|
||||
|
||||
@@ -24,4 +24,4 @@ To see the example translate to French with Angular i18n:
|
||||
}
|
||||
```
|
||||
|
||||
1. Click the "Fork" button in the stackblitz header. That makes a new copy for you with this change and re-runs the example in French.
|
||||
1. Click the "Fork" button in the StackBlitz header. That makes a new copy for you with this change and re-runs the example in French.
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ export class ProfileEditorComponent {
|
||||
// #enddocregion add-alias
|
||||
// #docregion on-submit
|
||||
onSubmit() {
|
||||
// TODO: Use EventEmitter with form value
|
||||
// TODO: Use output() with form value
|
||||
console.warn(this.profileForm.value);
|
||||
}
|
||||
// #enddocregion on-submit
|
||||
|
||||
@@ -17,12 +17,12 @@ The functions that control complex animation sequences are:
|
||||
|
||||
## The query() function
|
||||
|
||||
Most complex animations rely on the `query()` function to find child elements and apply animations to them, basic examples of such are:
|
||||
Most complex animations rely on the `query()` function to find child elements and apply animations to them. Basic examples include:
|
||||
|
||||
| Examples | Details |
|
||||
| :------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `query()` followed by `animate()` | Used to query simple HTML elements and directly apply animations to them. |
|
||||
| `query()` followed by `animateChild()` | Used to query child elements, which themselves have animations metadata applied to them and trigger such animation \(which would be otherwise be blocked by the current/parent element's animation\). |
|
||||
| Examples | Details |
|
||||
| :------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `query()` followed by `animate()` | Used to query simple HTML elements and directly apply animations to them. |
|
||||
| `query()` followed by `animateChild()` | Used to query child elements, which themselves have animation metadata applied to them and trigger such animations \(which would otherwise be blocked by the current/parent element's animation\). |
|
||||
|
||||
The first argument of `query()` is a [css selector](https://developer.mozilla.org/docs/Web/CSS/CSS_Selectors) string which can also contain the following Angular-specific tokens:
|
||||
|
||||
@@ -43,7 +43,7 @@ You can also see an illustration of this in the animations example \(introduced
|
||||
|
||||
## Animate multiple elements using query() and stagger() functions
|
||||
|
||||
After having queried child elements via `query()`, the `stagger()` function lets you define a timing gap between each queried item that is animated and thus animates elements with a delay between them.
|
||||
After querying child elements via `query()`, the `stagger()` function lets you define a timing gap between each item, animating elements with a delay between them.
|
||||
|
||||
The following example demonstrates how to use the `query()` and `stagger()` functions to animate a list \(of heroes\) adding each in sequence, with a slight delay, from top to bottom.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ CSS offers a robust set of tools for you to create beautiful and engaging animat
|
||||
|
||||
## How to write animations in native CSS
|
||||
|
||||
If you've never written any native CSS animations, there are a number of excellent guides to get you started. Here's a few of them:
|
||||
If you've never written any native CSS animations, there are a number of excellent guides to get you started. Here are a few of them:
|
||||
[MDN's CSS Animations guide](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animations/Using_CSS_animations)
|
||||
[W3Schools CSS3 Animations guide](https://www.w3schools.com/css/css3_animations.asp)
|
||||
[The Complete CSS Animations Tutorial](https://www.lambdatest.com/blog/css-animations-tutorial/)
|
||||
@@ -14,7 +14,7 @@ and a couple of videos:
|
||||
[Learn CSS Animation in 9 Minutes](https://www.youtube.com/watch?v=z2LQYsZhsFw)
|
||||
[Net Ninja CSS Animation Tutorial Playlist](https://www.youtube.com/watch?v=jgw82b5Y2MU&list=PL4cUxeGkcC9iGYgmEd2dm3zAKzyCGDtM5)
|
||||
|
||||
Check some of these various guides and tutorials out, and then come back to this guide.
|
||||
Check out some of these guides and tutorials, then come back to this guide.
|
||||
|
||||
## Creating Reusable Animations
|
||||
|
||||
@@ -28,7 +28,7 @@ Adding the class `animated-class` to an element would trigger the animation on t
|
||||
|
||||
### Animating State and Styles
|
||||
|
||||
You may want to animate between two different states, for example when an element is opened or closed. You can accomplish this by using CSS classes either using a keyframe animation or transition styling.
|
||||
You may want to animate between two different states, for example when an element is opened or closed. You can accomplish this by using CSS classes, either with a keyframe animation or transition styling.
|
||||
|
||||
<docs-code header="animations.css" path="adev/src/content/examples/animations/src/app/animations.css" region="animation-states"/>
|
||||
|
||||
@@ -38,7 +38,7 @@ You can see similar examples in the template guide for [animating styles directl
|
||||
|
||||
### Transitions, Timing, and Easing
|
||||
|
||||
Animating often requires adjusting timing, delays and easeing behaviors. This can be done using several css properties or shorthand properties.
|
||||
Animating often requires adjusting timing, delays, and easing behaviors. This can be done using several css properties or shorthand properties.
|
||||
|
||||
Specify `animation-duration`, `animation-delay`, and `animation-timing-function` for a keyframe animation in CSS, or alternatively use the `animation` shorthand property.
|
||||
|
||||
@@ -62,7 +62,7 @@ Animations can be triggered by toggling CSS styles or classes. Once a class is p
|
||||
|
||||
### Animating Auto Height
|
||||
|
||||
You can use css-grid to animate to auto height.
|
||||
You can use CSS Grid to animate to auto height.
|
||||
|
||||
<docs-code-multifile preview path="adev/src/content/examples/animations/src/app/native-css/auto-height.ts">
|
||||
<docs-code header="auto-height.ts" path="adev/src/content/examples/animations/src/app/native-css/auto-height.ts" />
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Animating your applications with `animate.enter` and `animate.leave`
|
||||
|
||||
Well-designed animations can make your application more fun and straightforward to use, but they aren't just cosmetic.
|
||||
Animations can improve your application and user experience in a number of ways:
|
||||
Well-designed animations can make your application more intuitive and engaging, but they aren't just cosmetic.
|
||||
Animations can improve your application and the user experience in a number of ways:
|
||||
|
||||
- Without animations, web page transitions can seem abrupt and jarring
|
||||
- Motion greatly enhances the user experience, so animations give users a chance to detect the application's response to their actions
|
||||
- Good animations can smoothly direct the user's attention throughout a workflow
|
||||
|
||||
Angular provides `animate.enter` and `animate.leave` to animate your application's elements. These two features apply enter and leave CSS classes at the appropriate times or call functions to apply animations from third party libraries. `animate.enter` and `animate.leave` are not directives. They are special API supported directly by the Angular compiler. They can be used on elements directly and can also be used as a host binding.
|
||||
Angular provides `animate.enter` and `animate.leave` to animate your application's elements. These two features apply enter and leave CSS classes at the appropriate times or call functions to apply animations from third party libraries. `animate.enter` and `animate.leave` are not directives. They are special API supported directly by the Angular compiler. They can be used directly on elements and also as a host binding.
|
||||
|
||||
## `animate.enter`
|
||||
|
||||
@@ -19,7 +19,7 @@ You can use `animate.enter` to animate elements as they _enter_ the DOM. You can
|
||||
<docs-code header="enter.css" path="adev/src/content/examples/animations/src/app/enter-and-leave/enter.css"/>
|
||||
</docs-code-multifile>
|
||||
|
||||
When the animation completes, Angular removes the class or classes that you specified in `animate.enter` from the DOM. Animation classes are only be present while the animation is active.
|
||||
When the animation completes, Angular removes the class or classes that you specified in `animate.enter` from the DOM. Animation classes are only present while the animation is active.
|
||||
|
||||
NOTE: When using multiple keyframe animations or transition properties on an element, Angular removes all classes only _after_ the longest animation has completed.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ The `@angular/animations` package is deprecated as of v20.2, which also introduc
|
||||
|
||||
## How to write animations in native CSS
|
||||
|
||||
If you've never written any native CSS animations, there are a number of excellent guides to get you started. Here's a few of them:
|
||||
If you've never written any native CSS animations, there are a number of excellent guides to get you started. Here are a few of them:
|
||||
[MDN's CSS Animations guide](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animations/Using_CSS_animations)
|
||||
[W3Schools CSS3 Animations guide](https://www.w3schools.com/css/css3_animations.asp)
|
||||
[The Complete CSS Animations Tutorial](https://www.lambdatest.com/blog/css-animations-tutorial/)
|
||||
@@ -14,7 +14,7 @@ and a couple of videos:
|
||||
[Learn CSS Animation in 9 Minutes](https://www.youtube.com/watch?v=z2LQYsZhsFw)
|
||||
[Net Ninja CSS Animation Tutorial Playlist](https://www.youtube.com/watch?v=jgw82b5Y2MU&list=PL4cUxeGkcC9iGYgmEd2dm3zAKzyCGDtM5)
|
||||
|
||||
Check some of these various guides and tutorials out, and then come back to this guide.
|
||||
Check out some of these guides and tutorials, then come back to this guide.
|
||||
|
||||
## Creating Reusable Animations
|
||||
|
||||
@@ -40,7 +40,7 @@ The animations package allowed you to define various states using the [`state()`
|
||||
|
||||
<docs-code header="open-close.ts" path="adev/src/content/examples/animations/src/app/open-close.ts" region="state1"/>
|
||||
|
||||
This same behavior can be accomplished natively by using CSS classes either using a keyframe animation or transition styling.
|
||||
This same behavior can be accomplished natively by using CSS classes, either with a keyframe animation or transition styling.
|
||||
|
||||
#### With Native CSS
|
||||
|
||||
@@ -102,7 +102,7 @@ The animations package offers the ability to animate things that have been histo
|
||||
<docs-code header="auto-height.css" path="adev/src/content/examples/animations/src/app/animations-package/auto-height.css" />
|
||||
</docs-code-multifile>
|
||||
|
||||
You can use css-grid to animate to auto height.
|
||||
You can use CSS Grid to animate to auto height.
|
||||
|
||||
#### With Native CSS
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
IMPORTANT: The `@angular/animations` package is now deprecated. The Angular team recommends using native CSS with `animate.enter` and `animate.leave` for animations for all new code. Learn more at the new enter and leave [animation guide](guide/animations). Also see [Migrating away from Angular's Animations package](guide/animations/migration) to learn how you can start migrating to pure CSS animations in your apps.
|
||||
|
||||
Animation provides the illusion of motion: HTML elements change styling over time.
|
||||
Well-designed animations can make your application more fun and straightforward to use, but they aren't just cosmetic.
|
||||
Animations can improve your application and user experience in a number of ways:
|
||||
Animation provides the illusion of motion: HTML elements change styles over time.
|
||||
Well-designed animations can make your application more intuitive and engaging, but they aren't just cosmetic.
|
||||
Animations can improve your application and the user experience in a number of ways:
|
||||
|
||||
- Without animations, web page transitions can seem abrupt and jarring
|
||||
- Motion greatly enhances the user experience, so animations give users a chance to detect the application's response to their actions
|
||||
@@ -70,7 +70,7 @@ You put the trigger that defines an animation within the `animations` metadata p
|
||||
Let's animate a transition that changes a single HTML element from one state to another.
|
||||
For example, you can specify that a button displays either **Open** or **Closed** based on the user's last action.
|
||||
When the button is in the `open` state, it's visible and yellow.
|
||||
When it's the `closed` state, it's translucent and blue.
|
||||
When it's in the `closed` state, it's translucent and blue.
|
||||
|
||||
In HTML, these attributes are set using ordinary CSS styles such as color and opacity.
|
||||
In Angular, use the `style()` function to specify a set of CSS styles for use with animations.
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
IMPORTANT: The `@angular/animations` package is now deprecated. The Angular team recommends using native CSS with `animate.enter` and `animate.leave` for animations for all new code. Learn more at the new enter and leave [animation guide](guide/animations). Also see [Migrating away from Angular's Animations package](guide/animations/migration) to learn how you can start migrating to pure CSS animations in your apps.
|
||||
|
||||
This topic provides some examples of how to create reusable animations.
|
||||
This topic provides examples of how to create reusable animations.
|
||||
|
||||
## Create reusable animations
|
||||
|
||||
To create a reusable animation, use the [`animation()`](api/animations/animation) function to define an animation in a separate `.ts` file and declare this animation definition as a `const` export variable.
|
||||
You can then import and reuse this animation in any of your application components using the [`useAnimation()`](api/animations/useAnimation) function.
|
||||
You can then import and reuse this animation in your application components using the [`useAnimation()`](api/animations/useAnimation) function.
|
||||
|
||||
<docs-code header="animations.ts" path="adev/src/content/examples/animations/src/app/animations.1.ts" region="animation-const"/>
|
||||
|
||||
@@ -20,7 +20,7 @@ For example, the following snippet exports the animation `trigger`.
|
||||
|
||||
<docs-code header="animations.1.ts" path="adev/src/content/examples/animations/src/app/animations.1.ts" region="trigger-const"/>
|
||||
|
||||
From this point, you can import reusable animation variables in your component class.
|
||||
From this point, you can import reusable animation variables into your component class.
|
||||
For example, the following code snippet imports the `transitionAnimation` variable and uses it via the `useAnimation()` function.
|
||||
|
||||
<docs-code header="open-close.ts" path="adev/src/content/examples/animations/src/app/open-close.3.ts" region="reusable"/>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
IMPORTANT: The `@angular/animations` package is now deprecated. The Angular team recommends using native CSS with `animate.enter` and `animate.leave` for animations for all new code. Learn more at the new enter and leave [animation guide](guide/animations). Also see [Migrating away from Angular's Animations package](guide/animations/migration) to learn how you can start migrating to pure CSS animations in your apps.
|
||||
|
||||
This guide goes into depth on special transition states such as the `*` wildcard and `void`. It shows how these special states are used for elements entering and leaving a view.
|
||||
This guide goes into depth on special transition states such as the `*` wildcard and `void`. It also shows how these states are used for elements entering and leaving a view.
|
||||
This section also explores multiple animation triggers, animation callbacks, and sequence-based animation using keyframes.
|
||||
|
||||
## Predefined states and wildcard matching
|
||||
@@ -99,7 +99,7 @@ Use the aliases `:enter` and `:leave` to target HTML elements that are inserted
|
||||
|
||||
The `:enter` transition runs when any `*ngIf` or `*ngFor` views are placed on the page, and `:leave` runs when those views are removed from the page.
|
||||
|
||||
IMPORTANT: Entering/leaving behaviors can sometime be confusing.
|
||||
IMPORTANT: Entering/leaving behaviors can sometimes be confusing.
|
||||
As a rule of thumb consider that any element being added to the DOM by Angular passes via the `:enter` transition. Only elements being directly removed from the DOM by Angular pass via the `:leave` transition. For example, an element's view is removed from the DOM because its parent is being removed from the DOM.
|
||||
|
||||
This example has a special trigger for the enter and leave animation called `myInsertRemoveTrigger`.
|
||||
@@ -239,7 +239,7 @@ You can combine keyframes with `duration`, `delay`, and `easing` within a single
|
||||
|
||||
### Keyframes with a pulsation
|
||||
|
||||
Use keyframes to create a pulse effect in your animations by defining styles at specific offset throughout the animation.
|
||||
Use keyframes to create a pulse effect in your animations by defining styles at specific offsets throughout the animation.
|
||||
|
||||
Here's an example of using keyframes to create a pulse effect:
|
||||
|
||||
@@ -254,7 +254,7 @@ The code snippet for this animation might look like this.
|
||||
|
||||
### Animatable properties and units
|
||||
|
||||
Angular animations support builds on top of web animations, so you can animate any property that the browser considers animatable.
|
||||
Angular animations are built on top of web animations, so you can animate any property that the browser considers animatable.
|
||||
This includes positions, sizes, transforms, colors, borders, and more.
|
||||
The W3C maintains a list of animatable properties on its [CSS Transitions](https://www.w3.org/TR/css-transitions-1) page.
|
||||
|
||||
|
||||
@@ -249,3 +249,29 @@ placeholder, Angular compares against the `ngProjectAs` value instead of the ele
|
||||
```
|
||||
|
||||
`ngProjectAs` supports only static values and cannot be bound to dynamic expressions.
|
||||
|
||||
## Caveats
|
||||
|
||||
### Projected content lives in the parent's view
|
||||
|
||||
Even though projected content is _rendered_ inside the receiving component, it is still owned by the component that declared it. Angular tracks it as part of the parent's view, which has a couple of side effects worth knowing about.
|
||||
|
||||
**Change detection:** Projected content is checked when the _parent_ runs change detection. If the receiving component uses `OnPush`, Angular can skip checking that component's own template — but it won't skip the projected content, because that belongs to the parent.
|
||||
|
||||
```angular-html
|
||||
<!-- Parent template (default change detection) -->
|
||||
<onpush-wrapper>
|
||||
<!-- Still checked on every parent cycle, OnPush doesn't help here -->
|
||||
<expensive-component />
|
||||
</onpush-wrapper>
|
||||
```
|
||||
|
||||
**Dependency injection:** Projected content gets its dependencies from the parent's injector, not from the receiving component's `viewProviders`. See [Providers and viewProviders](guide/di/hierarchical-dependency-injection) for details.
|
||||
|
||||
### Some library components don't support projected children
|
||||
|
||||
Certain components — menus, tabs, lists — use `ContentChildren` to find their children and wire up behavior like keyboard navigation, focus management, or ARIA attributes. They're written assuming they own their children directly, so projecting external content into them tends to break things in subtle ways.
|
||||
|
||||
For example, wrapping `<mat-menu-item>` elements in an extra layer and projecting them into `<mat-menu>` can silently break keyboard navigation and screen reader support. The query still finds the items, but the internal setup that makes them interactive may not work correctly when the items come from a different view context.
|
||||
|
||||
If a library component manages its children's behavior, check its docs before reaching for content projection — it may not be supported.
|
||||
|
||||
@@ -65,7 +65,25 @@ and their own.
|
||||
|
||||
### Forwarding injected dependencies
|
||||
|
||||
If a base class injects dependencies as constructor parameters, the child class must explicitly class these dependencies to `super`.
|
||||
When a base class uses `inject()` as a property initializer, the child class inherits the property automatically. No `super` forwarding is needed.
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
/*...*/
|
||||
})
|
||||
export class ListboxBase {
|
||||
protected element = inject(ElementRef);
|
||||
}
|
||||
|
||||
@Component({
|
||||
/*...*/
|
||||
})
|
||||
export class CustomListbox extends ListboxBase {
|
||||
// `element` is inherited from `ListboxBase`.
|
||||
}
|
||||
```
|
||||
|
||||
If a base class injects dependencies as constructor parameters, the child class must explicitly pass these dependencies to `super`.
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
|
||||
@@ -34,8 +34,8 @@ render an Angular component, the framework automatically includes its associated
|
||||
lazy-loading a component.
|
||||
|
||||
Angular works with any tool that outputs CSS,
|
||||
including [Sass](https://sass-lang.com), [less](https://lesscss.org),
|
||||
and [stylus](https://stylus-lang.com).
|
||||
including [Sass](https://sass-lang.com), [Less](https://lesscss.org),
|
||||
and [Stylus](https://stylus-lang.com).
|
||||
|
||||
## Style scoping
|
||||
|
||||
|
||||
@@ -634,7 +634,7 @@ It doesn't need to continue searching the `ElementInjector` tree, nor does it ne
|
||||
### `providers` vs. `viewProviders`
|
||||
|
||||
The `viewProviders` field is conceptually similar to `providers`, but there is one notable difference.
|
||||
Configured providers in `viewProviders` are not visible to projected content that ends up as a logical children of the component.
|
||||
Providers in `viewProviders` are only visible inside the component's own view — content projected into the component via `<ng-content>` cannot see them.
|
||||
|
||||
To see the difference between using `providers` and `viewProviders`, add another component to the example and call it `Inspector`.
|
||||
`Inspector` will be a child of the `Child`.
|
||||
@@ -705,6 +705,11 @@ These four bindings demonstrate the difference between `providers` and `viewProv
|
||||
Remember that the dog emoji <code>🐶</code> is declared inside the `<#VIEW>` of `Child` and isn't visible to the projected content.
|
||||
Instead, the projected content sees the whale <code>🐳</code>.
|
||||
|
||||
You might wonder why the projected `<app-inspector>` can still see <code>🐳</code> from `App`'s `viewProviders`.
|
||||
The reason is that Angular DI tracks **where a component was declared**, not where it ends up being rendered.
|
||||
`<app-inspector>` lives in `App`'s template — inside `App`'s `<#VIEW>` — so `App`'s `viewProviders` are fair game.
|
||||
Projecting it into `Child` cuts off access to `Child`'s `viewProviders` (<code>🐶</code>), but `App`'s providers (<code>🐳</code>) are still reachable up the tree.
|
||||
|
||||
However, in the next output section though, the `Inspector` is an actual child component of `Child`, `Inspector` is inside the `<#VIEW>`, so when it asks for the `AnimalService`, it sees the dog <code>🐶</code>.
|
||||
|
||||
The `AnimalService` in the logical tree would look like this:
|
||||
@@ -741,8 +746,10 @@ The `AnimalService` in the logical tree would look like this:
|
||||
</app-root>
|
||||
```
|
||||
|
||||
The projected content of `<app-inspector>` sees the whale <code>🐳</code>, not the dog <code>🐶</code>, because the dog <code>🐶</code> is inside the `<app-child>` `<#VIEW>`.
|
||||
The `<app-inspector>` can only see the dog <code>🐶</code> if it is also within the `<#VIEW>`.
|
||||
The projected `<app-inspector>` gets <code>🐳</code> because <code>🐶</code> belongs to `Child`'s view and projected content can't reach it.
|
||||
<code>🐳</code> is accessible because `<app-inspector>` was declared in `App`'s template, so it can still walk up to `App`'s `viewProviders`.
|
||||
|
||||
The `<app-inspector>` that lives directly inside `Child`'s template (not projected) gets <code>🐶</code> — it's inside the `<#VIEW>`, so no boundary to cross.
|
||||
|
||||
### Visibility of provided tokens
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ The `ProfileEditor` component accepts input from the user, but in a real scenari
|
||||
|
||||
<docs-code header="profile-editor.component.html (submit event)" path="adev/src/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.html" region="ng-submit"/>
|
||||
|
||||
The `onSubmit()` method in the `ProfileEditor` component captures the current value of `profileForm`. Use `EventEmitter` to keep the form encapsulated and to provide the form value outside the component. The following example uses `console.warn` to log a message to the browser console.
|
||||
The `onSubmit()` method in the `ProfileEditor` component captures the current value of `profileForm`. Use `output()` to keep the form encapsulated and to provide the form value outside the component. The following example uses `console.warn` to log a message to the browser console.
|
||||
|
||||
<docs-code header="profile-editor.component.ts (submit method)" path="adev/src/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.ts" region="on-submit"/>
|
||||
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
# Field metadata
|
||||
|
||||
Field metadata is reactive data you can attach to an individual field. Angular's built-in constraint validators like `required()` and `min()` use this system internally. In other words, every time you call a validator, you're contributing to a metadata key for that particular field.
|
||||
|
||||
This guide covers the metadata system in depth: how reducers combine contributions from multiple schema rules, how to write custom reducers, how to read metadata values from fields, and how managed metadata ties lifecycle-aware objects to individual fields.
|
||||
|
||||
## You have already been using metadata
|
||||
|
||||
When you call `required()` in a schema and read `.required()` on the resulting field in a template, you are using the metadata system. `state.required` is not a special-case property. It is a convenience getter that returns the current value of a built-in `REQUIRED` metadata key.
|
||||
|
||||
```angular-ts
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {form, required, FormField} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
selector: 'app-registration',
|
||||
imports: [FormField],
|
||||
template: `
|
||||
<form>
|
||||
<label>
|
||||
Username
|
||||
@if (registrationForm.username().required()) {
|
||||
<span class="required-marker" aria-hidden="true">*</span>
|
||||
}
|
||||
<input [formField]="registrationForm.username" />
|
||||
</label>
|
||||
</form>
|
||||
`,
|
||||
})
|
||||
export class Registration {
|
||||
registrationModel = signal({username: ''});
|
||||
|
||||
registrationForm = form(this.registrationModel, (path) => {
|
||||
required(path.username);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Calling `required(path.username)` contributes a value to the `REQUIRED` metadata key on that field. Reading `registrationForm.username().required()` returns the accumulated value. The metadata key is the bridge connecting the two.
|
||||
|
||||
Several built-in constraint validators follow this pattern:
|
||||
|
||||
| Validator | Metadata key | Type | `FieldState` getter |
|
||||
| ------------- | ------------ | --------------------- | ------------------- |
|
||||
| `required()` | `REQUIRED` | `boolean` | `required` |
|
||||
| `min()` | `MIN` | `number \| undefined` | `min` |
|
||||
| `max()` | `MAX` | `number \| undefined` | `max` |
|
||||
| `minLength()` | `MIN_LENGTH` | `number \| undefined` | `minLength` |
|
||||
| `maxLength()` | `MAX_LENGTH` | `number \| undefined` | `maxLength` |
|
||||
| `pattern()` | `PATTERN` | `RegExp[]` | `pattern` |
|
||||
|
||||
Non-constraint validators like `email()` and `validate()` do not contribute to metadata. They run their check and surface a validation error, but they do not publish a reactive value for templates to read.
|
||||
|
||||
## When to use custom metadata
|
||||
|
||||
When you need reactive data attached to a specific field that built-in state signals like `valid()`, `disabled()`, and `touched()` do not cover, use **custom metadata**.
|
||||
|
||||
Some examples might include:
|
||||
|
||||
- **Configuration attached to reusable field schemas.** A currency symbol on a price field, so any template or custom control rendering the field can display it. Or `MIN_DATE` and `MAX_DATE` on a date field, read by a reusable range picker.
|
||||
- **Parsed values shared between rules on one field.** A phone number parsed once into E.164 format, so a format validator and a uniqueness check both read the same canonical form without reparsing.
|
||||
- **Display hints assembled from the field's state.** A severity level (`'info' | 'warning' | 'error'`) that the UI maps to badges and icons, or a context-aware help message that changes based on what the user has typed and which other fields are filled in.
|
||||
|
||||
If you find yourself keeping a parallel `Map<fieldKey, value>` alongside your form to track something per field, that is a sign metadata is the right tool. Metadata stays colocated with the schema, stays reactive, and participates in the field's lifecycle.
|
||||
|
||||
## Creating a metadata key
|
||||
|
||||
When you want to create a custom key, call `createMetadataKey<TWrite>()`. The type parameter describes the value your schema rules will contribute.
|
||||
|
||||
```ts
|
||||
import {createMetadataKey} from '@angular/forms/signals';
|
||||
|
||||
export const USERNAME_HELP = createMetadataKey<string>();
|
||||
```
|
||||
|
||||
Every `createMetadataKey()` call creates a new unique key. Two calls with matching type parameters are still two distinct keys, so define each key once at module scope and import it wherever it's needed.
|
||||
|
||||
NOTE: A key created without a reducer uses "override" semantics by default: the last contribution wins if multiple rules set the key.
|
||||
|
||||
## Setting values from a schema
|
||||
|
||||
When you need to register a value for the key on a specific field, use `metadata(path, key, logic)` inside a schema function.
|
||||
|
||||
```angular-ts
|
||||
import {Component, computed, signal} from '@angular/core';
|
||||
import {form, metadata, FormField} from '@angular/forms/signals';
|
||||
import {USERNAME_HELP} from './metadata-keys';
|
||||
|
||||
@Component({
|
||||
selector: 'app-registration',
|
||||
imports: [FormField],
|
||||
template: `
|
||||
<form>
|
||||
<label>
|
||||
Username
|
||||
<input [formField]="registrationForm.username" />
|
||||
</label>
|
||||
<p class="help">{{ usernameHelp() }}</p>
|
||||
</form>
|
||||
`,
|
||||
})
|
||||
export class Registration {
|
||||
registrationModel = signal({username: ''});
|
||||
|
||||
registrationForm = form(this.registrationModel, (path) => {
|
||||
metadata(path.username, USERNAME_HELP, ({value}) => {
|
||||
const username = value();
|
||||
if (username.length === 0) {
|
||||
return 'Choose a unique username between 3 and 20 characters.';
|
||||
}
|
||||
if (username.length < 3) {
|
||||
return 'Keep typing, usernames are at least 3 characters.';
|
||||
}
|
||||
if (username.length > 20) {
|
||||
return 'Usernames are at most 20 characters.';
|
||||
}
|
||||
return 'Looks good.';
|
||||
});
|
||||
});
|
||||
|
||||
usernameHelp = computed(() => this.registrationForm.username().metadata(USERNAME_HELP)?.() ?? '');
|
||||
}
|
||||
```
|
||||
|
||||
The logic function receives the field's context, which exposes `value` as a signal of the field's current value, `state` as the field's `FieldState`, and methods like `valueOf(path)` and `stateOf(path)` for reading other fields in the same form. Any signal the function reads becomes a reactive dependency: when `value()` changes, the metadata recomputes, and any template reading the key updates.
|
||||
|
||||
## Reading metadata from a field
|
||||
|
||||
`state.metadata(key)` returns `undefined` when no rule has registered the key, and a signal of the current reduced value otherwise.
|
||||
|
||||
```ts
|
||||
const usernameHelp = registrationForm.username().metadata(USERNAME_HELP);
|
||||
```
|
||||
|
||||
The shape of that inner value (whether it can itself be `undefined`, what type it holds) depends on the key's reducer. Reducers are covered in the next section.
|
||||
|
||||
When the key may not be registered, use optional chaining:
|
||||
|
||||
```ts
|
||||
const message = registrationForm.username().metadata(USERNAME_HELP)?.();
|
||||
// message: string | undefined
|
||||
```
|
||||
|
||||
Or, when the rule is guaranteed to have registered, drop the optional chain and assert:
|
||||
|
||||
```ts
|
||||
const message = registrationForm.username().metadata(USERNAME_HELP)!();
|
||||
// message: string | undefined (still, because the inner value may be undefined)
|
||||
```
|
||||
|
||||
The component example above uses optional chaining inside a `computed()` so the template binds to a plain `string`, with an empty fallback for the initial frame.
|
||||
|
||||
This is the whole API for a single contributor. The next section covers what happens when more than one schema rule contributes to the same key, and how to combine those contributions with reducers.
|
||||
|
||||
## Combining contributions with reducers
|
||||
|
||||
Override semantics work when only one rule contributes to a key on a given field. As soon as two rules contribute, the first value is silently discarded:
|
||||
|
||||
```ts
|
||||
const HELP = createMetadataKey<string>();
|
||||
|
||||
form(model, (path) => {
|
||||
metadata(path.username, HELP, () => 'Choose something unique across the system.');
|
||||
metadata(path.username, HELP, () => 'Usernames are 3 to 20 characters.');
|
||||
});
|
||||
```
|
||||
|
||||
After both rules run, `state.metadata(HELP)!()` returns only the second message. This is almost never what you want. Contributions often come from different sources: two schemas composed with `apply()` that each attach help text, or multiple validation rules that each contribute a hint.
|
||||
|
||||
To combine contributions, pass a reducer to `createMetadataKey()`. A reducer describes how to fold individual values into an accumulated result:
|
||||
|
||||
```ts
|
||||
import {createMetadataKey, MetadataReducer} from '@angular/forms/signals';
|
||||
|
||||
const HELP = createMetadataKey<string, string[]>(MetadataReducer.list());
|
||||
|
||||
form(model, (path) => {
|
||||
metadata(path.username, HELP, () => 'Choose something unique across the system.');
|
||||
metadata(path.username, HELP, () => 'Usernames are 3 to 20 characters.');
|
||||
});
|
||||
|
||||
// state.metadata(HELP)!() === [
|
||||
// 'Choose something unique across the system.',
|
||||
// 'Usernames are 3 to 20 characters.',
|
||||
// ]
|
||||
```
|
||||
|
||||
Notice the two type parameters on `createMetadataKey<TWrite, TAcc>`: the first is the type each rule contributes, the second is the type the reducer produces. With `list()`, rules contribute a `string` and the field reads back a `string[]`.
|
||||
|
||||
### Built-in reducers
|
||||
|
||||
Angular provides six built-in reducers on the [`MetadataReducer`](api/forms/signals/MetadataReducer) namespace. `override()` has two forms with slightly different semantics, listed separately in the table:
|
||||
|
||||
| Reducer | Accumulator type | What it does | Initial value |
|
||||
| -------------- | --------------------- | ---------------------------------------------------------------------- | ------------- |
|
||||
| `list<T>()` | `T[]` | Accepts `T \| undefined` contributions; appends non-`undefined` values | `[]` |
|
||||
| `or()` | `boolean` | `true` if any contribution is `true` | `false` |
|
||||
| `and()` | `boolean` | `true` only if every contribution is `true` | `true` |
|
||||
| `min()` | `number \| undefined` | Keeps the smallest contributed number | `undefined` |
|
||||
| `max()` | `number \| undefined` | Keeps the largest contributed number | `undefined` |
|
||||
| `override()` | `T \| undefined` | Last contribution replaces previous (the default) | `undefined` |
|
||||
| `override(fn)` | `T` | Same, but with a provided initial value | `fn()` |
|
||||
|
||||
`list()` is the only built-in reducer whose item type is wider than its accumulator's element type. A rule may contribute `undefined` and the reducer will silently drop it. This is how the built-in `PATTERN` key handles dynamic `pattern()` rules whose logic function returns `undefined`: the `undefined` contribution is skipped rather than included in the final regex list.
|
||||
|
||||
### How built-in validator keys use reducers
|
||||
|
||||
While `MetadataReducer.min()` and `MetadataReducer.max()` are reducers, you may be surprised to learn that they are not validators. `MetadataReducer.min()` picks the smallest contribution to a key, while the `min()` validator enforces a lower bound on a field's value. They share a name but solve different problems.
|
||||
|
||||
The built-in constraint keys pick their reducers based on what "strictest" means for the constraint, which is often the opposite of what the key's name suggests:
|
||||
|
||||
| Key | Reducer | Reasoning |
|
||||
| ------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `REQUIRED` | `or()` | If any `required()` rule evaluates to `true`, the field is required. |
|
||||
| `MIN` | `max()` | A minimum-value constraint is strictest when largest. If one rule requires `>= 5` and another `>= 10`, the effective minimum is `10`. |
|
||||
| `MAX` | `min()` | A maximum-value constraint is strictest when smallest. If one rule caps at `100` and another at `50`, the effective maximum is `50`. |
|
||||
| `MIN_LENGTH` | `max()` | Same logic as `MIN`: the longest required length wins. |
|
||||
| `MAX_LENGTH` | `min()` | Same logic as `MAX`: the shortest allowed length wins. |
|
||||
| `PATTERN` | `list<RegExp>()` | Each `pattern()` call contributes a regex; the value must match all of them. |
|
||||
|
||||
This pairing of "strictest wins" is why calling `min(path.age, 18)` and `min(path.age, 21)` in two composed schemas works correctly. Each call registers its own validator that enforces its specific bound (so a value below either bound fails validation). Separately, each call contributes to the public `MIN` key, and `state.metadata(MIN)!()` reports the aggregate (`21`) so UI and custom controls can read the effective minimum.
|
||||
|
||||
### Writing a custom reducer
|
||||
|
||||
When you want to write your own reducer, implement an object matching the `MetadataReducer<TAcc, TItem>` interface:
|
||||
|
||||
```ts
|
||||
interface MetadataReducer<TAcc, TItem> {
|
||||
reduce: (acc: TAcc, item: TItem) => TAcc;
|
||||
getInitial: () => TAcc;
|
||||
}
|
||||
```
|
||||
|
||||
You can define a custom reducer when none of the built-ins match the semantics you need. For example, a `SEVERITY` key that keeps the most severe level contributed by any rule:
|
||||
|
||||
```ts
|
||||
import {createMetadataKey, type MetadataReducer} from '@angular/forms/signals';
|
||||
|
||||
type Severity = 'info' | 'warning' | 'error';
|
||||
|
||||
const SEVERITY_RANK: Record<Severity, number> = {info: 0, warning: 1, error: 2};
|
||||
|
||||
const maxSeverity: MetadataReducer<Severity | undefined, Severity> = {
|
||||
reduce(acc, item) {
|
||||
if (acc === undefined) return item;
|
||||
return SEVERITY_RANK[item] > SEVERITY_RANK[acc] ? item : acc;
|
||||
},
|
||||
getInitial: () => undefined,
|
||||
};
|
||||
|
||||
export const SEVERITY = createMetadataKey<Severity, Severity | undefined>(maxSeverity);
|
||||
```
|
||||
|
||||
Any number of rules can now contribute a severity, and the field reports the highest:
|
||||
|
||||
```ts
|
||||
form(model, (path) => {
|
||||
metadata(path.password, SEVERITY, () => 'info');
|
||||
metadata(path.password, SEVERITY, ({value}) => (value().length < 12 ? 'warning' : 'info'));
|
||||
metadata(path.password, SEVERITY, ({value}) =>
|
||||
/password|1234/i.test(value()) ? 'error' : 'info',
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
The reducer runs whenever any contribution's signals change, so `state.metadata(SEVERITY)!()` stays in sync with the current worst case across all rules.
|
||||
|
||||
TIP: Keep your reducers pure: `reduce()` should depend only on its two arguments, and `getInitial()` should return the same value every time it is called. Reducers run inside a reactive computation that re-executes when any contribution's signals change, so impure reducers produce inconsistent metadata.
|
||||
|
||||
## Attaching lifecycle-aware objects with managed metadata
|
||||
|
||||
Managed metadata stores a lifecycle-aware object on a field instead of a reactive value. Use it for per-field objects like a `resource()` that fetches external data, an `effect()` that syncs to an outside system, or a service handle scoped to a single field.
|
||||
|
||||
### Creating a managed key
|
||||
|
||||
When you want to define a managed key, call `createManagedMetadataKey<TRead, TWrite>(create)`. The `create` function you pass produces the value the key holds.
|
||||
|
||||
```ts
|
||||
import {Signal} from '@angular/core';
|
||||
import {httpResource} from '@angular/common/http';
|
||||
import {createManagedMetadataKey} from '@angular/forms/signals';
|
||||
|
||||
export interface UrlPreview {
|
||||
title: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export const URL_PREVIEW = createManagedMetadataKey((_state, url: Signal<string | undefined>) => {
|
||||
return httpResource<UrlPreview>(() => {
|
||||
const currentUrl = url();
|
||||
return currentUrl ? {url: '/api/url-preview', params: {url: currentUrl}} : undefined;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
The `create` function receives the field's `FieldState` and a `Signal<TAcc>` of data contributed by `metadata()` rules for this key, and returns whatever object should live on the field. The return value is stored as-is: unlike non-managed keys, the framework does not wrap it in a `computed()`.
|
||||
|
||||
`create` runs once when a field is constructed, inside the field's injection context. That lets you call `inject()`, `resource()`, and `effect()` inside `create`, and ties cleanup to the field's lifecycle: when the field is destroyed, Angular destroys the injection context, and any `resource()`, `effect()`, or `DestroyRef` callback you registered there cleans up automatically.
|
||||
|
||||
Because `create` itself is not reactive, any behavior that needs to respond to signal changes has to live inside an `effect()`, `resource()`, or `httpResource()` set up during that initial call. `URL_PREVIEW` demonstrates the pattern: the `httpResource()` reads the URL signal inside its request function, so the request re-runs whenever the signal changes. The schema rule (`metadata(path.url, URL_PREVIEW, ({value}) => value())`) decides what data to feed in; the managed key decides what to do with it.
|
||||
|
||||
### Using a managed key in a form
|
||||
|
||||
When you need to use a managed key in a form, register a `metadata()` rule for the key, and then read the returned object from the field state.
|
||||
|
||||
```angular-ts
|
||||
import {Component, computed, signal} from '@angular/core';
|
||||
import {applyEach, form, metadata, FormField} from '@angular/forms/signals';
|
||||
import {URL_PREVIEW} from './url-preview';
|
||||
|
||||
@Component({
|
||||
selector: 'app-link-editor',
|
||||
imports: [FormField],
|
||||
template: `
|
||||
<form>
|
||||
@for (link of linksForm.links; track link) {
|
||||
<fieldset>
|
||||
<label>
|
||||
URL
|
||||
<input [formField]="link.url" />
|
||||
</label>
|
||||
<!-- Read the URL_PREVIEW key for this link's url field; the result is the resource its create function produced -->
|
||||
@let preview = link.url().metadata(URL_PREVIEW);
|
||||
@if (preview?.isLoading()) {
|
||||
<p>Loading preview...</p>
|
||||
} @else if (preview?.hasValue() && preview.value(); as data) {
|
||||
<article class="preview">
|
||||
<h3>{{ data.title }}</h3>
|
||||
@if (data.description) {
|
||||
<p>{{ data.description }}</p>
|
||||
}
|
||||
</article>
|
||||
} @else if (preview?.error()) {
|
||||
<p class="error">Could not load preview.</p>
|
||||
}
|
||||
</fieldset>
|
||||
}
|
||||
<button type="button" (click)="addLink()">Add link</button>
|
||||
</form>
|
||||
`,
|
||||
})
|
||||
export class LinkEditor {
|
||||
linksModel = signal({links: [{url: ''}]});
|
||||
|
||||
linksForm = form(this.linksModel, (path) => {
|
||||
// Register the URL_PREVIEW key on each link's url field.
|
||||
// applyEach runs the schema per item, so create() runs once per link
|
||||
// and each link gets its own resource.
|
||||
applyEach(path.links, (itemPath) => {
|
||||
metadata(itemPath.url, URL_PREVIEW, ({value}) => value());
|
||||
});
|
||||
});
|
||||
|
||||
addLink() {
|
||||
this.linksForm.links().value.update((links) => [...links, {url: ''}]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each array item gets its own `URL_PREVIEW` resource because `applyEach` registers the schema rules against each item independently. When the user adds a link, `create` runs for the new item's field. When a link is removed (not shown here, but a common pattern), the framework tears down that field's injector along with the resource.
|
||||
|
||||
## Next steps
|
||||
|
||||
Remember that metadata exists so reactive data can travel with the field through schema composition, accumulate across rules, and tear down with the field's lifecycle. It leverages the same system Angular's built-in validators use, and can be tailored to your own use cases.
|
||||
|
||||
For detailed API documentation, see:
|
||||
|
||||
- [`createMetadataKey()`](api/forms/signals/createMetadataKey) - Define a metadata key with optional reducer
|
||||
- [`createManagedMetadataKey()`](api/forms/signals/createManagedMetadataKey) - Define a lifecycle-aware metadata key
|
||||
- [`metadata()`](api/forms/signals/metadata) - Contribute a value to a metadata key in a schema
|
||||
- [`MetadataReducer`](api/forms/signals/MetadataReducer) - Built-in reducers for combining contributions
|
||||
|
||||
For additional related guides on Signal Forms, check out:
|
||||
|
||||
<docs-pill-row>
|
||||
<docs-pill href="guide/forms/signals/form-logic" title="Adding form logic" />
|
||||
<docs-pill href="guide/forms/signals/validation" title="Validation" />
|
||||
<docs-pill href="guide/forms/signals/field-state-management" title="Field state management" />
|
||||
<docs-pill href="guide/forms/signals/async-operations" title="Async operations" />
|
||||
</docs-pill-row>
|
||||
@@ -454,24 +454,20 @@ Don't use debouncing if:
|
||||
|
||||
## Associate data with a field using `metadata()`
|
||||
|
||||
Metadata allows you to attach computed information to fields that can be read by [custom controls](guide/forms/signals/custom-controls) or form logic. Common use cases include HTML input attributes (min, max, maxlength, pattern), custom UI hints (placeholder text, help text), and accessibility information.
|
||||
Metadata attaches reactive data to a field. Validation rules use this system internally, and you can publish your own keys for application-specific information like help text, configuration, or computed display values.
|
||||
|
||||
### Pre-defined metadata keys
|
||||
Signal Forms provides six pre-defined metadata keys that built-in validators populate automatically:
|
||||
|
||||
Signal Forms provides six pre-defined metadata keys that validation rules automatically populate:
|
||||
| Key | Populated by | Read via |
|
||||
| ------------ | ------------- | --------------------- |
|
||||
| `REQUIRED` | `required()` | `field().required()` |
|
||||
| `MIN` | `min()` | `field().min()` |
|
||||
| `MAX` | `max()` | `field().max()` |
|
||||
| `MIN_LENGTH` | `minLength()` | `field().minLength()` |
|
||||
| `MAX_LENGTH` | `maxLength()` | `field().maxLength()` |
|
||||
| `PATTERN` | `pattern()` | `field().pattern()` |
|
||||
|
||||
- `REQUIRED` - Whether the field is required (`boolean`)
|
||||
- `MIN` - Minimum numeric value (`number | undefined`)
|
||||
- `MAX` - Maximum numeric value (`number | undefined`)
|
||||
- `MIN_LENGTH` - Minimum string/array length (`number | undefined`)
|
||||
- `MAX_LENGTH` - Maximum string/array length (`number | undefined`)
|
||||
- `PATTERN` - Regular expression pattern (`RegExp[]` - array to support multiple patterns)
|
||||
|
||||
When you use validation rules like `required()` or `min()`, they automatically set the corresponding metadata. The `metadata()` function provides a way to publish additional data associated with a field.
|
||||
|
||||
### Reading pre-defined metadata
|
||||
|
||||
The `[FormField]` directive automatically binds built-in metadata to HTML attributes. You can also read metadata directly using the built-in accessors on field state:
|
||||
The `[formField]` directive automatically binds five of these (`REQUIRED`, `MIN`, `MAX`, `MIN_LENGTH`, and `MAX_LENGTH`) to the corresponding HTML attribute on a native form control. `PATTERN` is the exception, because Signal Forms supports multiple patterns per field but the HTML `pattern` attribute accepts only a single regular expression.
|
||||
|
||||
```angular-ts
|
||||
import {Component, signal} from '@angular/core';
|
||||
@@ -482,7 +478,7 @@ import {form, FormField, required, min, max} from '@angular/forms/signals';
|
||||
imports: [FormField],
|
||||
template: `
|
||||
<label>
|
||||
Age (between {{ ageForm.age().min() }} and {{ ageForm.age().max() }})
|
||||
Age (between {{ ageForm.age().min?.() }} and {{ ageForm.age().max?.() }})
|
||||
<input type="number" [formField]="ageForm.age" />
|
||||
</label>
|
||||
|
||||
@@ -492,9 +488,7 @@ import {form, FormField, required, min, max} from '@angular/forms/signals';
|
||||
`,
|
||||
})
|
||||
export class Age {
|
||||
ageModel = signal({
|
||||
age: 0,
|
||||
});
|
||||
ageModel = signal({age: 0});
|
||||
|
||||
ageForm = form(this.ageModel, (schemaPath) => {
|
||||
required(schemaPath.age);
|
||||
@@ -504,138 +498,9 @@ export class Age {
|
||||
}
|
||||
```
|
||||
|
||||
The `[formField]` directive automatically binds `required`, `min`, and `max` attributes to the input. You can read these values using `field().required()`, `field().min()`, and `field().max()` for display or logic purposes.
|
||||
|
||||
### Setting metadata manually
|
||||
|
||||
Use the `metadata()` function to set metadata values when validation rules don't automatically set them. For built-in metadata like `MIN` and `MAX`, prefer using the validation rules:
|
||||
|
||||
```angular-ts
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {form, FormField, min, max, validate} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
selector: 'app-custom',
|
||||
imports: [FormField],
|
||||
template: ` <input [formField]="customForm.score" /> `,
|
||||
})
|
||||
export class Custom {
|
||||
customModel = signal({score: 0});
|
||||
|
||||
customForm = form(this.customModel, (schemaPath) => {
|
||||
// Use built-in validation rules - they automatically set metadata
|
||||
min(schemaPath.score, 0);
|
||||
max(schemaPath.score, 100);
|
||||
|
||||
// Add custom validation logic if needed
|
||||
validate(schemaPath.score, ({value}) => {
|
||||
const score = value();
|
||||
// Custom validation beyond min/max (e.g., must be multiple of 5)
|
||||
if (score % 5 !== 0) {
|
||||
return {kind: 'increment', message: 'Score must be a multiple of 5'};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Creating custom metadata keys
|
||||
|
||||
Create your own metadata keys for application-specific information:
|
||||
|
||||
```angular-ts
|
||||
import {createMetadataKey, metadata} from '@angular/forms/signals';
|
||||
|
||||
// Define at module level (not inside components)
|
||||
export const PLACEHOLDER = createMetadataKey<string>();
|
||||
export const HELP_TEXT = createMetadataKey<string>();
|
||||
|
||||
// Use in schema
|
||||
form(model, (schemaPath) => {
|
||||
metadata(schemaPath.email, PLACEHOLDER, () => 'user@example.com');
|
||||
metadata(schemaPath.email, HELP_TEXT, () => 'We will never share your email');
|
||||
});
|
||||
|
||||
// Read in component
|
||||
const placeholderText = myForm.email().metadata(PLACEHOLDER);
|
||||
const helpText = myForm.email().metadata(HELP_TEXT);
|
||||
```
|
||||
|
||||
By default, custom metadata keys use a "last write wins" strategy - if you call `metadata()` multiple times with the same key, only the last value is kept.
|
||||
|
||||
**Important:** Always define metadata keys at module level, never inside components. Metadata keys rely on object identity, and recreating them loses that identity.
|
||||
|
||||
### Accumulating metadata with reducers
|
||||
|
||||
By default, calling `metadata()` multiple times with the same key uses "last write wins" - only the final value is kept. To accumulate values instead, pass a reducer to `createMetadataKey()`:
|
||||
|
||||
```angular-ts
|
||||
import {createMetadataKey, metadata, MetadataReducer} from '@angular/forms/signals';
|
||||
|
||||
// Create a key that accumulates values into an array
|
||||
export const HINTS = createMetadataKey<string, string[]>(MetadataReducer.list());
|
||||
|
||||
// Multiple calls accumulate values
|
||||
form(model, (schemaPath) => {
|
||||
metadata(schemaPath.password, HINTS, () => 'At least 8 characters');
|
||||
metadata(schemaPath.password, HINTS, () => 'Include a number');
|
||||
metadata(schemaPath.password, HINTS, () => 'Include a special character');
|
||||
});
|
||||
|
||||
// Result: Signal containing the accumulated array
|
||||
const passwordHints = passwordForm.password().metadata(HINTS)();
|
||||
// ['At least 8 characters', 'Include a number', 'Include a special character']
|
||||
```
|
||||
|
||||
Angular provides built-in reducers through `MetadataReducer`:
|
||||
|
||||
- `MetadataReducer.list()` - Accumulates values into an array
|
||||
- `MetadataReducer.min()` - Keeps the minimum value
|
||||
- `MetadataReducer.max()` - Keeps the maximum value
|
||||
- `MetadataReducer.or()` - Logical OR of boolean values
|
||||
- `MetadataReducer.and()` - Logical AND of boolean values
|
||||
|
||||
### Managed metadata keys
|
||||
|
||||
Use `createManagedMetadataKey()` when you need to compute a new value from the accumulated result. The transform function receives a signal of the reduced value and returns the computed result:
|
||||
|
||||
```angular-ts
|
||||
import {createManagedMetadataKey, metadata, MetadataReducer} from '@angular/forms/signals';
|
||||
|
||||
// Accumulate hints and compute additional data from the result
|
||||
export const HINTS = createManagedMetadataKey(
|
||||
(signal) =>
|
||||
computed(() => {
|
||||
const hints = signal();
|
||||
return {
|
||||
messages: hints,
|
||||
count: hints?.length ?? 0,
|
||||
};
|
||||
}),
|
||||
MetadataReducer.list(),
|
||||
);
|
||||
|
||||
// Multiple calls accumulate values
|
||||
form(model, (schemaPath) => {
|
||||
metadata(schemaPath.password, HINTS, () => 'At least 8 characters');
|
||||
metadata(schemaPath.password, HINTS, () => 'Include a number');
|
||||
metadata(schemaPath.password, HINTS, () => 'Include a special character');
|
||||
});
|
||||
|
||||
// Result: Signal with transformed value
|
||||
const passwordHints = passwordForm.password().metadata(HINTS)();
|
||||
// { messages: ['At least 8 characters', 'Include a number', 'Include a special character'], count: 3 }
|
||||
```
|
||||
|
||||
The managed metadata key takes two arguments:
|
||||
|
||||
1. **Transform function** - Computes a new value from the accumulated result (receives a signal of the reduced value)
|
||||
2. **Reducer** - Determines how values accumulate (optional - defaults to "last write wins")
|
||||
|
||||
### Reactive metadata
|
||||
|
||||
Make metadata reactive to other field values:
|
||||
Validation rules can derive their constraints from other fields, making the published metadata reactive:
|
||||
|
||||
```angular-ts
|
||||
import {Component, signal} from '@angular/core';
|
||||
@@ -654,12 +519,8 @@ import {form, FormField, max} from '@angular/forms/signals';
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Quantity (max: {{ inventoryForm.quantity().max() }})
|
||||
<input
|
||||
type="number"
|
||||
[formField]="inventoryForm.quantity"
|
||||
[max]="inventoryForm.quantity().max()"
|
||||
/>
|
||||
Quantity (max: {{ inventoryForm.quantity().max?.() }})
|
||||
<input type="number" [formField]="inventoryForm.quantity" />
|
||||
</label>
|
||||
`,
|
||||
})
|
||||
@@ -678,47 +539,9 @@ export class Inventory {
|
||||
}
|
||||
```
|
||||
|
||||
The `max()` validation rule sets the `MAX` metadata reactively based on the selected item. This demonstrates how validation rules can have conditional values that change when other fields update.
|
||||
The `max()` validation rule sets the `MAX` metadata reactively based on the selected item, so any template or control reading `field().max()` updates whenever the item changes.
|
||||
|
||||
### Using metadata in custom controls
|
||||
|
||||
Custom controls can read metadata to configure their HTML attributes and behavior:
|
||||
|
||||
```angular-ts
|
||||
import {Component, input, computed, model} from '@angular/core';
|
||||
import {FormValueControl, Field, PLACEHOLDER} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
selector: 'custom-input',
|
||||
template: `
|
||||
<input
|
||||
type="number"
|
||||
[value]="state().value()"
|
||||
(input)="state().value.set(($event.target as HTMLInputElement).valueAsNumber)"
|
||||
[min]="state().min()"
|
||||
[max]="state().max()"
|
||||
[required]="state().required()"
|
||||
[placeholder]="placeholderText()"
|
||||
/>
|
||||
`,
|
||||
})
|
||||
export class CustomInput implements FormValueControl<number> {
|
||||
// Bind to the form field.
|
||||
formField = input.required<Field<number>>();
|
||||
|
||||
// Compute the current field state.
|
||||
state = computed(() => this.formField()());
|
||||
|
||||
// Required property of the FormValueControl interface.
|
||||
value = model(0);
|
||||
|
||||
placeholderText = computed(() => this.state().metadata(PLACEHOLDER)() ?? '');
|
||||
}
|
||||
```
|
||||
|
||||
This pattern allows custom controls to automatically configure themselves based on the validation rules and metadata defined in the schema.
|
||||
|
||||
TIP: For more information on creating custom controls, see the [Custom Controls guide](guide/forms/signals/custom-controls).
|
||||
For deeper coverage, including how to define custom keys, combine contributions with reducers, and use managed metadata for lifecycle-aware objects, see the [Field metadata guide](guide/forms/signals/field-metadata).
|
||||
|
||||
## Combining rules
|
||||
|
||||
@@ -730,15 +553,8 @@ Apply multiple rules to configure all aspects of a field's behavior:
|
||||
|
||||
```angular-ts
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {
|
||||
form,
|
||||
FormField,
|
||||
disabled,
|
||||
hidden,
|
||||
debounce,
|
||||
metadata,
|
||||
PLACEHOLDER,
|
||||
} from '@angular/forms/signals';
|
||||
import {form, FormField, disabled, hidden, debounce, metadata} from '@angular/forms/signals';
|
||||
import {PLACEHOLDER} from './metadata-keys';
|
||||
|
||||
@Component({
|
||||
selector: 'app-promo',
|
||||
@@ -835,8 +651,9 @@ The conditional rules only run when the condition is true. This is useful for co
|
||||
|
||||
Extract common rule configurations into reusable functions:
|
||||
|
||||
```angular-ts
|
||||
import {SchemaPath, debounce, metadata, maxLength, PLACEHOLDER} from '@angular/forms/signals';
|
||||
```ts
|
||||
import {SchemaPath, debounce, metadata, maxLength} from '@angular/forms/signals';
|
||||
import {PLACEHOLDER} from './metadata-keys';
|
||||
|
||||
function emailFieldConfig(path: SchemaPath<string>) {
|
||||
debounce(path, 300);
|
||||
|
||||
@@ -40,6 +40,40 @@ The [`form()`](api/forms/signals/form) function accepts the model signal and cre
|
||||
|
||||
The `[formField]` directive binds each input element to its corresponding field in the field tree, enabling automatic two-way synchronization between the UI and model.
|
||||
|
||||
### Supported model structures
|
||||
|
||||
Signal Forms builds the field tree by walking your model. The objects and arrays it walks through (the **structural layer**) must be plain JavaScript objects and arrays. The values at the **leaves** (positions with no nested fields) are usually primitives (strings, numbers, booleans) or `null`. Native `date`, `month`, `time`, and `week` inputs also accept `Date`, and custom controls can accept any value type they understand.
|
||||
|
||||
```ts {prefer, header: 'Plain structure'}
|
||||
interface UserFormModel {
|
||||
name: string;
|
||||
birthday: Date | null;
|
||||
preferences: {
|
||||
theme: string;
|
||||
notifications: boolean;
|
||||
};
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const userModel = signal<UserFormModel>({
|
||||
name: '',
|
||||
birthday: null,
|
||||
preferences: {
|
||||
theme: 'dark',
|
||||
notifications: true,
|
||||
},
|
||||
tags: [],
|
||||
});
|
||||
```
|
||||
|
||||
IMPORTANT: Class instances, `Map`, and `Set` are **not supported in the structural layer**, even though TypeScript will accept them. Signal Forms does not validate the model shape at runtime, so the framework accepts these values without throwing, then produces incorrect behavior in different ways depending on shape:
|
||||
|
||||
- **Class instances** lose their prototype on the first write because Signal Forms shallow-copies parent objects on update. Methods, getters, and `instanceof` checks are gone afterward.
|
||||
- **Non-extensible or frozen objects inside arrays** throw when Signal Forms assigns a tracking symbol to preserve item identity across reorders.
|
||||
- **`Map` and `Set`** produce empty field trees, because Signal Forms enumerates children with `Object.keys`.
|
||||
|
||||
If your application uses classes for domain modeling, translate to plain objects at the form boundary. See [Translating between form model and domain model](guide/forms/signals/model-design#translating-between-form-model-and-domain-model).
|
||||
|
||||
### Using TypeScript types
|
||||
|
||||
While TypeScript infers types from object literals, defining explicit types improves code quality and provides better IntelliSense support.
|
||||
|
||||
@@ -247,7 +247,7 @@ Each `HttpEvent` reported in the event stream has a `type` which distinguishes w
|
||||
| `HttpEventType.ResponseHeader` | The head of the response has been received, including status and headers |
|
||||
| `HttpEventType.DownloadProgress` | An `HttpDownloadProgressEvent` reporting progress on downloading the response body |
|
||||
| `HttpEventType.Response` | The entire response has been received, including the response body |
|
||||
| `HttpEventType.User` | A custom event from an Http interceptor. |
|
||||
| `HttpEventType.User` | A custom event from an HTTP interceptor. |
|
||||
|
||||
## Handling request failure
|
||||
|
||||
@@ -614,7 +614,7 @@ IMPORTANT: The `integrity` option requires an exact match between the response c
|
||||
|
||||
TIP: Use subresource integrity when loading critical resources from external sources to ensure they haven't been modified. Generate hashes using tools like `openssl`.
|
||||
|
||||
## Http `Observable`s
|
||||
## HTTP `Observable`s
|
||||
|
||||
Each request method on `HttpClient` constructs and returns an `Observable` of the requested response type. Understanding how these `Observable`s work is important when using `HttpClient`.
|
||||
|
||||
|
||||
@@ -8,15 +8,12 @@ At the end, tests can verify that the app made no unexpected requests.
|
||||
|
||||
## Setup for testing
|
||||
|
||||
To begin testing usage of `HttpClient`, configure `TestBed` and include `provideHttpClient()` and `provideHttpClientTesting()` in your test's setup. This configures `HttpClient` to use a test backend instead of the real network. It also provides `HttpTestingController`, which you'll use to interact with the test backend, set expectations about which requests have been made, and flush responses to those requests. `HttpTestingController` can be injected from `TestBed` once configured.
|
||||
|
||||
IMPORTANT: Keep in mind to provide `provideHttpClient()` **before** `provideHttpClientTesting()`, as `provideHttpClientTesting()` will overwrite parts of `provideHttpClient()`. Doing it the other way around can potentially break your tests.
|
||||
To begin testing usage of `HttpClient`, configure `TestBed` and include `provideHttpClientTesting()` in your test's setup. `HttpClient` is provided by Angular's test environment, and `provideHttpClientTesting()` configures it to use a test backend instead of the real network. It also provides `HttpTestingController`, which you'll use to interact with the test backend, set expectations about which requests have been made, and flush responses to those requests. `HttpTestingController` can be injected from `TestBed` once configured.
|
||||
|
||||
```ts
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
// ... other test providers
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
});
|
||||
@@ -26,13 +23,24 @@ const httpTesting = TestBed.inject(HttpTestingController);
|
||||
|
||||
Now when your tests make requests, they will hit the testing backend instead of the normal one. You can use `httpTesting` to make assertions about those requests.
|
||||
|
||||
### Configuring `HttpClient` in tests
|
||||
|
||||
If a test needs to configure `HttpClient` features, such as interceptors, include `provideHttpClient(...)` before `provideHttpClientTesting()`.
|
||||
IMPORTANT: Keep in mind to provide `provideHttpClient()` **before** `provideHttpClientTesting()`, as `provideHttpClientTesting()` will overwrite parts of `provideHttpClient()`. Doing it the other way around can potentially break your tests.
|
||||
|
||||
```ts
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideHttpClient(withInterceptors([authInterceptor])), provideHttpClientTesting()],
|
||||
});
|
||||
```
|
||||
|
||||
## Expecting and answering requests
|
||||
|
||||
For example, you can write a test that expects a GET request to occur and provides a mock response:
|
||||
|
||||
```ts
|
||||
TestBed.configureTestingModule({
|
||||
providers: [ConfigService, provideHttpClient(), provideHttpClientTesting()],
|
||||
providers: [ConfigService, provideHttpClientTesting()],
|
||||
});
|
||||
|
||||
const httpTesting = TestBed.inject(HttpTestingController);
|
||||
|
||||
@@ -9,7 +9,7 @@ To add the `@angular/localize` package, use the following command to update the
|
||||
It adds `types: ["@angular/localize"]` in the TypeScript configuration files.
|
||||
It also adds line `/// <reference types="@angular/localize" />` at the top of the `main.ts` file which is the reference to the type definition.
|
||||
|
||||
HELPFUL: For more information about `package.json` and `tsconfig.json` files, see [Workspace npm dependencies][GuideNpmPackages] and [TypeScript Configuration][GuideTsConfig]. To learn about Triple-slash Directives visit [Typescript Handbook](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html#-reference-types-).
|
||||
HELPFUL: For more information about `package.json` and `tsconfig.json` files, see [Workspace npm dependencies][GuideNpmPackages] and [TypeScript Configuration][GuideTsConfig]. To learn about Triple-slash Directives visit [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html#-reference-types-).
|
||||
|
||||
If `@angular/localize` is not installed and you try to build a localized version of your project (for example, while using the `i18n` attributes in templates), the [Angular CLI][CliMain] will generate an error, which would contain the steps that you can take to enable i18n for your project.
|
||||
|
||||
|
||||
@@ -31,10 +31,12 @@ Use the `i18n` project option in the [`angular.json`][GuideWorkspaceConfig] work
|
||||
|
||||
The following sub-options identify the source language and tell the compiler where to find supported translations for the project.
|
||||
|
||||
| Suboption | Details |
|
||||
| :------------- | :--------------------------------------------------------------------------- |
|
||||
| `sourceLocale` | The locale you use within the application source code \(`en-US` by default\) |
|
||||
| `locales` | A map of locale identifiers to translation files |
|
||||
| Suboption | Details |
|
||||
| :------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `sourceLocale` | The locale you use within the application source code \(`en-US` by default\). Can also be an object with `code`, `baseHref`, and `subPath` properties. |
|
||||
| `locales` | A map of locale identifiers to translation files. Each entry can also be an object with `translation`, `baseHref`, and `subPath` properties. |
|
||||
|
||||
For the full list of `i18n` properties and their types, see [i18n options][GuideWorkspaceConfigI18n].
|
||||
|
||||
### `angular.json` for `en-US` and `fr` example
|
||||
|
||||
@@ -149,3 +151,4 @@ TLDR: Compile once, then translate for each locale.
|
||||
[GuideI18nCommonMergeGenerateApplicationVariantsForEachLocale]: guide/i18n/merge#generate-application-variants-for-each-locale 'Generate application variants for each locale - Merge translations into the application | Angular'
|
||||
[GuideI18nCommonTranslationFilesChangeTheSourceLanguageFileFormat]: guide/i18n/translation-files#change-the-source-language-file-format 'Change the source language file format - Work with translation files | Angular'
|
||||
[GuideWorkspaceConfig]: reference/configs/workspace-config 'Angular workspace configuration | Angular'
|
||||
[GuideWorkspaceConfigI18n]: reference/configs/workspace-config#i18n-options 'i18n options - Angular workspace configuration | Angular'
|
||||
|
||||
@@ -48,6 +48,26 @@ The following example shows the `<ng-container>` element transformed into a non-
|
||||
|
||||
<docs-code path="adev/src/content/examples/i18n/src/app/app.component.html" region="i18n-ng-container"/>
|
||||
|
||||
### Name the interpolation placeholder
|
||||
|
||||
By default, Angular generates a placeholder name for each interpolation in a translated message. To give it a meaningful name that helps translators understand the context, add an `//i18n(ph="name")` comment inside the interpolation.
|
||||
|
||||
```html
|
||||
<element i18n>{{ expression //i18n(ph="placeholder_name") }}</element>
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```html
|
||||
<p i18n>Hello, {{ username //i18n(ph="name") }}!</p>
|
||||
```
|
||||
|
||||
This is the template equivalent of naming a placeholder in component code with [`$localize`][ApiLocalizeInitLocalize]:
|
||||
|
||||
```ts
|
||||
$localize`Hello, ${username}:name:!`;
|
||||
```
|
||||
|
||||
## Mark element attributes for translations
|
||||
|
||||
In a component template, the i18n metadata is the value of the `i18n-{attribute_name}` attribute.
|
||||
|
||||
@@ -468,7 +468,7 @@ 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?
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
Signals are useful because they notify interested consumers when they change. An **effect** is an operation that runs whenever one or more signal values change. You can create an effect with the `effect` function:
|
||||
|
||||
```ts
|
||||
import {effect} from '@angular/core';
|
||||
|
||||
effect(() => {
|
||||
console.log(`The current count is: ${count()}`);
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ const firstName = computed(() => {
|
||||
if (userResource.hasValue()) {
|
||||
// `hasValue` serves 2 purposes:
|
||||
// - It acts as type guard to strip `undefined` from the type
|
||||
// - If protects against reading a throwing `value` when the resource is in error state
|
||||
// - It protects against reading a throwing `value` when the resource is in error state
|
||||
return userResource.value().firstName;
|
||||
}
|
||||
|
||||
@@ -116,11 +116,11 @@ The `status` signal provides a specific `ResourceStatus` that describes the stat
|
||||
| `'idle'` | `undefined` | The resource has no valid request and the loader has not run. |
|
||||
| `'error'` | `undefined` | The loader has encountered an error. |
|
||||
| `'loading'` | `undefined` | The loader is running as a result of the `params` value changing. |
|
||||
| `'reloading'` | Previous value | The loader is running as a result calling of the resource's `reload` method. |
|
||||
| `'reloading'` | Previous value | The loader is running as a result of calling the resource's `reload` method. |
|
||||
| `'resolved'` | Resolved value | The loader has completed. |
|
||||
| `'local'` | Locally set value | The resource's value has been set locally via `.set()` or `.update()` |
|
||||
|
||||
You can use this status information to conditionally display user interface elements, such loading indicators and error messages.
|
||||
You can use this status information to conditionally display user interface elements, such as loading indicators and error messages.
|
||||
|
||||
## Reactive data fetching with `httpResource`
|
||||
|
||||
|
||||
@@ -432,7 +432,7 @@ To configure this, update your `angular.json` file as follows:
|
||||
You can customize how Angular caches HTTP responses during server‑side rendering (SSR) and reuses them during hydration by configuring `HttpTransferCacheOptions`.
|
||||
This configuration is provided globally using `withHttpTransferCacheOptions` inside `provideClientHydration()`.
|
||||
|
||||
By default, `HttpClient` caches all `HEAD` and `GET` requests which don't contain `Authorization` or `Proxy-Authorization` headers. You can override those settings by using `withHttpTransferCacheOptions` to the hydration configuration.
|
||||
By default, `HttpClient` caches all `HEAD` and `GET` requests which don't contain `Authorization`, `Proxy-Authorization`, or `Cookie` headers and are not sent with `withCredentials` or Fetch API `credentials` modes that can send credentials. Angular also skips transfer cache when a request or response includes `Cache-Control` directives that forbid caching (`no-store`, `no-cache`, or `private`), or when the Fetch API `cache` option is set to `no-store` or `no-cache`. Responses that carry a `Set-Cookie` header are also skipped. You can override the request filtering settings by using `withHttpTransferCacheOptions` in the hydration configuration.
|
||||
|
||||
```ts
|
||||
import {bootstrapApplication} from '@angular/platform-browser';
|
||||
@@ -467,6 +467,8 @@ withHttpTransferCacheOptions({
|
||||
|
||||
IMPORTANT: Avoid including sensitive headers like authentication tokens. These can leak user‑specific data between requests.
|
||||
|
||||
Including `Cache-Control` in `includeHeaders` only makes that header available on the hydrated response. Angular already evaluates `Cache-Control` headers automatically when deciding whether a request or response is eligible for transfer cache.
|
||||
|
||||
---
|
||||
|
||||
### `includePostRequests`
|
||||
@@ -486,8 +488,8 @@ Use this only when `POST` requests are **idempotent** and safe to reuse between
|
||||
|
||||
### `includeRequestsWithAuthHeaders`
|
||||
|
||||
Determines whether requests containing `Authorization` or `Proxy‑Authorization` headers are eligible for caching.
|
||||
By default, these are excluded to prevent caching user‑specific responses.
|
||||
Determines whether requests containing `Authorization`, `Proxy‑Authorization`, or `Cookie` headers are eligible for caching.
|
||||
By default, these are excluded to prevent caching user‑specific responses. Requests sent with `withCredentials` or Fetch API `credentials` set to `include` or `same-origin` are also excluded by default.
|
||||
|
||||
```ts
|
||||
withHttpTransferCacheOptions({
|
||||
@@ -558,6 +560,8 @@ To disable caching for an individual request, you can specify the [`transferCach
|
||||
httpClient.get('/api/sensitive-data', {transferCache: false});
|
||||
```
|
||||
|
||||
`HttpTransferCache` does not cache requests or responses that explicitly opt out of caching. Angular skips transfer cache entries when a request includes a `Cache-Control` header with `no-store`, `no-cache`, or `private`, or when the request uses the Fetch API `cache` option set to `no-store` or `no-cache`. Responses with `Cache-Control: no-store`, `Cache-Control: no-cache`, or `Cache-Control: private` are also not stored in the transfer cache. Responses that include a `Set-Cookie` header are likewise not stored, as they typically carry user-specific state.
|
||||
|
||||
NOTE: If your application uses different HTTP origins to make API calls on the server and on the client, the `HTTP_TRANSFER_CACHE_ORIGIN_MAP` token allows you to establish a mapping between those origins, so that `HttpTransferCache` feature can recognize those requests as the same ones and reuse the data cached on the server during hydration on the client.
|
||||
|
||||
## Configuring a server
|
||||
|
||||
@@ -349,6 +349,37 @@ By default, when rendering an application on the server (either using SSR or SSG
|
||||
|
||||
To render the main content of `@defer` blocks on the server (both SSR and SSG), you can enable [the Incremental Hydration feature](/guide/incremental-hydration) and configure `hydrate` triggers for the necessary blocks.
|
||||
|
||||
## Barrel files and lazy chunks
|
||||
|
||||
If you're using `@defer` but not seeing a separate lazy chunk in your build output, check how you're importing the deferred component. Importing through a barrel file (`index.ts`) is a common culprit — bundlers see the barrel as a single module and keep all its exports together, so your component ends up in the main bundle regardless of `@defer`.
|
||||
|
||||
```typescript
|
||||
// index.ts
|
||||
export {HeavyComponent} from './heavy.component';
|
||||
export {OtherComponent} from './other.component';
|
||||
```
|
||||
|
||||
```typescript
|
||||
// parent.component.ts
|
||||
import {HeavyComponent} from './index'; // pulls in OtherComponent too
|
||||
|
||||
@Component({
|
||||
imports: [HeavyComponent],
|
||||
template: `@defer {
|
||||
<heavy-component />
|
||||
}`,
|
||||
})
|
||||
export class ParentComponent {}
|
||||
```
|
||||
|
||||
The fix is straightforward — import directly from the component's own file:
|
||||
|
||||
```typescript
|
||||
import {HeavyComponent} from './heavy.component';
|
||||
```
|
||||
|
||||
That's enough for the bundler to split it into its own chunk and load it lazily when the trigger fires.
|
||||
|
||||
## Best practices for deferring views
|
||||
|
||||
### Avoid cascading loads with nested `@defer` blocks
|
||||
|
||||
@@ -96,7 +96,7 @@ Angular also allows you to specify [Code values for keyboard events](https://dev
|
||||
<input type="text" (keydown.code.alt.shiftleft)="updateField($event)" />
|
||||
```
|
||||
|
||||
This can be useful for handling keyboard events consistently across different operating systems. For example, when using the Alt key on MacOS devices, the `key` property reports the key based on the character already modified by the Alt key. This means that a combination like Alt + S reports a `key` value of `'ß'`. The `code` property, however, corresponds to the physical or virtual button pressed rather than the character produced.
|
||||
This can be useful for handling keyboard events consistently across different operating systems. For example, when using the Alt key on macOS devices, the `key` property reports the key based on the character already modified by the Alt key. This means that a combination like Alt + S reports a `key` value of `'ß'`. The `code` property, however, corresponds to the physical or virtual button pressed rather than the character produced.
|
||||
|
||||
## Listening on global targets
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Pipes are a special operator in Angular template expressions that allows you to transform data declaratively in your template. Pipes let you declare a transformation function once and then use that transformation across multiple templates. Angular pipes use the vertical bar character (`|`), inspired by the [Unix pipe](<https://en.wikipedia.org/wiki/Pipeline_(Unix)>).
|
||||
Pipes are special operators in Angular template expressions that allow you to transform data declaratively in your template. Pipes let you declare a transformation function once and then use that transformation across multiple templates. Angular pipes use the vertical bar character (`|`), inspired by the [Unix pipe](<https://en.wikipedia.org/wiki/Pipeline_(Unix)>).
|
||||
|
||||
NOTE: Angular's pipe syntax deviates from standard JavaScript, which uses the vertical bar character for the [bitwise OR operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_OR). Angular template expressions do not support bitwise operators.
|
||||
|
||||
@@ -33,7 +33,7 @@ export class ShoppingCart {
|
||||
}
|
||||
```
|
||||
|
||||
When Angular renders the component, it will ensure that the appropriate date format and currency is based on the locale of the user. If the user is in the United States, it would render:
|
||||
When Angular renders the component, it will ensure that the appropriate date format and currency are based on the locale of the user. If the user is in the United States, it would render:
|
||||
|
||||
```angular-html
|
||||
<main>
|
||||
@@ -162,7 +162,7 @@ Always use parentheses in your expressions when operator precedence may be ambig
|
||||
|
||||
### Change detection with pipes
|
||||
|
||||
By default, all pipes are considered `pure`, which means that it only executes when a primitive input value (such as a `String`, `Number`, `Boolean`, or `Symbol`) or a object reference (such as `Array`, `Object`, `Function`, or `Date`) is changed. Pure pipes offer a performance advantage because Angular can avoid calling the transformation function if the passed value has not changed.
|
||||
By default, all pipes are considered `pure`, which means that they only execute when a primitive input value (such as a `String`, `Number`, `Boolean`, or `Symbol`) or an object reference (such as `Array`, `Object`, `Function`, or `Date`) is changed. Pure pipes offer a performance advantage because Angular can avoid calling the transformation function if the passed value has not changed.
|
||||
|
||||
As a result, this means that mutations to object properties or array items are not detected unless the entire object or array reference is replaced with a different instance. If you want this level of change detection, refer to [detecting changes within arrays or objects](#detecting-change-within-arrays-or-objects).
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ Choose one of the following browser providers based on your needs:
|
||||
|
||||
- **Playwright**: `@vitest/browser-playwright` for Chromium, Firefox, and WebKit.
|
||||
- **WebdriverIO**: `@vitest/browser-webdriverio` for Chrome, Firefox, Safari, and Edge.
|
||||
- **Preview**: `@vitest/browser-preview` for Webcontainer environments (like StackBlitz).
|
||||
- **Preview**: `@vitest/browser-preview` for WebContainer environments (like StackBlitz).
|
||||
|
||||
<docs-code-multifile>
|
||||
<docs-code header="npm" language="shell">
|
||||
@@ -132,8 +132,6 @@ Add the `browsers` option to your `test` target's options. The browser name depe
|
||||
|
||||
Headless mode is enabled automatically if the `CI` environment variable is set or if a browser name includes "Headless" (e.g., `ChromeHeadless`). Otherwise, tests will run in a headed browser.
|
||||
|
||||
NOTE: Debugging with `ng test --debug` is not supported by browser mode.
|
||||
|
||||
## Automated test refactoring with schematics
|
||||
|
||||
IMPORTANT: The `refactor-jasmine-vitest` schematic is experimental and may not cover all possible test patterns. Always review the changes made by the schematic.
|
||||
|
||||
@@ -53,11 +53,10 @@ The `setupFiles` and `providersFile` options are particularly useful for managin
|
||||
For example, you could create a `src/test-providers.ts` file to provide `provideHttpClientTesting` to all your tests:
|
||||
|
||||
```typescript {header: "src/test-providers.ts"}
|
||||
import {Provider} from '@angular/core';
|
||||
import {provideHttpClient} from '@angular/common/http';
|
||||
import {EnvironmentProviders, Provider} from '@angular/core';
|
||||
import {provideHttpClientTesting} from '@angular/common/http/testing';
|
||||
|
||||
const testProviders: Provider[] = [provideHttpClient(), provideHttpClientTesting()];
|
||||
const testProviders: (Provider | EnvironmentProviders)[] = [provideHttpClientTesting()];
|
||||
|
||||
export default testProviders;
|
||||
```
|
||||
@@ -188,7 +187,7 @@ Choose one of the following browser providers based on your needs:
|
||||
|
||||
### Preview
|
||||
|
||||
The `@vitest/browser-preview` provider is designed for Webcontainer environments like StackBlitz and is not intended for use in CI/CD.
|
||||
The `@vitest/browser-preview` provider is designed for WebContainer environments like StackBlitz and is not intended for use in CI/CD.
|
||||
|
||||
<docs-code-multifile>
|
||||
<docs-code header="npm" language="shell">
|
||||
|
||||
@@ -62,7 +62,7 @@ If you are having issues running this command in Windows or Unix, check out the
|
||||
|
||||
#### Create a new project
|
||||
|
||||
In your terminal, run the CLI command `ng new` with the desired project name. In the following examples, we'll be using the example project name of `my-first-angular-app`.
|
||||
In your terminal, run the CLI command [`ng new`](cli/new) with the desired project name. In the following examples, we'll be using the example project name of `my-first-angular-app`.
|
||||
|
||||
```shell
|
||||
ng new <project-name>
|
||||
|
||||
@@ -76,8 +76,8 @@
|
||||
trusted type support, help protect your users from common vulnerabilities like
|
||||
cross-site scripting and cross-site request forgery.
|
||||
</docs-card>
|
||||
<docs-card title="Keep large teams productive with Vite and esbuild" href="tools/cli/build-system-migration" link="ESBuild and Vite" titleIconName="sensors">
|
||||
Angular CLI includes a fast, modern build pipeline using Vite and ESBuild. Developers report
|
||||
<docs-card title="Keep large teams productive with Vite and esbuild" href="tools/cli/build-system-migration" link="Vite and esbuild" titleIconName="sensors">
|
||||
Angular CLI includes a fast, modern build pipeline using Vite and esbuild. Developers report
|
||||
building projects with hundreds of thousands of lines of code in less than a minute.
|
||||
</docs-card>
|
||||
<docs-card title="Proven in some of Google's largest web apps" titleIconName="sensors">
|
||||
@@ -108,7 +108,7 @@
|
||||
<docs-card title="Partnering with other Google technologies" titleIconName="sensors">
|
||||
<p>Angular partners closely with other Google technologies and teams to improve the web.</p>
|
||||
<p>Our ongoing partnership with Chrome’s Aurora actively explores improvements to user experience across the web, developing built-in performance optimizations like <code>NgOptimizedImage</code> and improvements to Angular’s Core Web Vitals.</p>
|
||||
<p>We are also working with <a href="https://firebase.google.com/">Firebase</a>, <a href="https://www.tensorflow.org/">Tensorflow</a>, <a href="https://flutter.dev/">Flutter</a>, <a href="https://m3.material.io/">Material Design</a>, and <a href="https://cloud.google.com/">Google Cloud</a> to ensure we provide meaningful integrations across the developer workflow.</p>
|
||||
<p>We are also working with <a href="https://firebase.google.com/" target="_blank">Firebase</a>, <a href="https://www.tensorflow.org/" target="_blank">TensorFlow</a>, <a href="https://flutter.dev/" target="_blank">Flutter</a>, <a href="https://m3.material.io/" target="_blank">Material Design</a>, and <a href="https://cloud.google.com/" target="_blank">Google Cloud</a> to ensure we provide meaningful integrations across the developer workflow.</p>
|
||||
</docs-card>
|
||||
</docs-card-container>
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ You can create multifile examples by wrapping the examples inside a `<docs-code-
|
||||
|
||||
### Adding `preview` to your code example
|
||||
|
||||
Adding the `preview` flag builds a running example of the code below the code snippet. This also automatically adds a button to open the running example in Stackblitz.
|
||||
Adding the `preview` flag builds a running example of the code below the code snippet. This also automatically adds a button to open the running example in StackBlitz.
|
||||
|
||||
NOTE: `preview` only works with standalone.
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ The default value is `'full'`.
|
||||
|
||||
For most applications, `'full'` is the correct compilation mode.
|
||||
|
||||
Use `'partial'` for independently published libraries, such as NPM packages.
|
||||
Use `'partial'` for independently published libraries, such as npm packages.
|
||||
`'partial'` compilations output a stable, intermediate format which better supports usage by applications built at different Angular versions from the library.
|
||||
Libraries built at "HEAD" alongside their applications and using the same version of Angular such as in a mono-repository can use `'full'` since there is no risk of version skew.
|
||||
|
||||
@@ -222,6 +222,11 @@ When `true`, reports an error if a component, directive, or pipe is not standalo
|
||||
When `true`, prints extra information while compiling templates.
|
||||
Default is `false`.
|
||||
|
||||
### `typeCheckHostBindings`
|
||||
|
||||
When `true`, enables type checking of expressions in the `host` object literal and `@HostBinding`/`@HostListener` decorators of components and directives.
|
||||
Default is `true`.
|
||||
|
||||
## Command line options
|
||||
|
||||
Most of the time, you interact with the Angular Compiler indirectly using [Angular CLI](reference/configs/angular-compiler-options). When debugging certain issues, you might find it useful to invoke the Angular Compiler directly.
|
||||
|
||||
@@ -62,14 +62,46 @@ The following properties are a set of options that customize the Angular CLI.
|
||||
|
||||
The following top-level configuration properties are available for each project, under `projects['project-name']`.
|
||||
|
||||
| Property | Details | Value type | Default value |
|
||||
| :------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | :-------------- |
|
||||
| `root` | The root directory for this project's files, relative to the workspace directory. Empty for the initial application, which resides at the top level of the workspace. | `string` | None (required) |
|
||||
| `projectType` | One of "application" or "library" An application can run independently in a browser, while a library cannot. | `application` \| `library` | None (required) |
|
||||
| `sourceRoot` | The root directory for this project's source files. | `string` | `''` |
|
||||
| `prefix` | A string that Angular prepends to selectors when generating new components, directives, and pipes using `ng generate`. Can be customized to identify an application or feature area. | `string` | `'app'` |
|
||||
| `schematics` | A set of schematics that customize the `ng generate` sub-command option defaults for this project. See the [Generation schematics](#schematics) section. | See [schematics](#schematics) | `{}` |
|
||||
| `architect` | Configuration defaults for Architect builder targets for this project. | See [Configuring builder targets](#configuring-builder-targets) | `{}` |
|
||||
| Property | Details | Value type | Default value |
|
||||
| :------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | :-------------- |
|
||||
| `root` | The root directory for this project's files, relative to the workspace directory. Empty for the initial application, which resides at the top level of the workspace. | `string` | None (required) |
|
||||
| `projectType` | One of "application" or "library" An application can run independently in a browser, while a library cannot. | `application` \| `library` | None (required) |
|
||||
| `sourceRoot` | The root directory for this project's source files. | `string` | `''` |
|
||||
| `prefix` | A string that Angular prepends to selectors when generating new components, directives, and pipes using `ng generate`. Can be customized to identify an application or feature area. | `string` | `'app'` |
|
||||
| `i18n` | Internationalization options for the project. Defines the source locale and additional locales to build. See [Define locales in the build configuration](guide/i18n/merge#define-locales-in-the-build-configuration). | See [i18n options](#i18n-options) | `{}` |
|
||||
| `schematics` | A set of schematics that customize the `ng generate` sub-command option defaults for this project. See the [Generation schematics](#schematics) section. | See [schematics](#schematics) | `{}` |
|
||||
| `architect` | Configuration defaults for Architect builder targets for this project. | See [Configuring builder targets](#configuring-builder-targets) | `{}` |
|
||||
|
||||
## i18n options
|
||||
|
||||
Use the `i18n` project option to define the application's source locale and any additional locales to build.
|
||||
|
||||
| Property | Details | Value type | Default value |
|
||||
| :------------- | :----------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------ | :------------ |
|
||||
| `sourceLocale` | The locale used in the application source code. Can be a locale identifier string or a [configuration object](#sourcelocale-object). | `string` \| [sourceLocale object](#sourcelocale-object) | `"en-US"` |
|
||||
| `locales` | A map of locale identifiers to translation files or [locale configuration objects](#locale-object). | `object` | `{}` |
|
||||
|
||||
### `sourceLocale` object
|
||||
|
||||
Pass an object instead of a string to customize the output directory or base HREF for the source locale:
|
||||
|
||||
| Property | Details | Value type | Default value |
|
||||
| :--------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :--------- | :------------ |
|
||||
| `code` | The source locale identifier. | `string` | `"en-US"` |
|
||||
| `baseHref` | Overrides the HTML `<base href>` for this locale. The output directory name stays as the locale code. Cannot be used together with `subPath`. | `string` | Locale code |
|
||||
| `subPath` | Sets both the output directory name and the HTML `<base href>` for this locale. Cannot be used together with `baseHref`. | `string` | Locale code |
|
||||
|
||||
### Locale object
|
||||
|
||||
Each `locales` entry can be a path string, an array of paths, or an object:
|
||||
|
||||
| Property | Details | Value type | Default value |
|
||||
| :------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------- | :------------ |
|
||||
| `translation` | Path or paths to the translation file(s) for this locale. | `string` \| `string[]` | |
|
||||
| `baseHref` | Overrides the HTML `<base href>` for this locale. The output directory name stays as the locale identifier. Cannot be used together with `subPath`. | `string` | Locale code |
|
||||
| `subPath` | Sets both the output directory name and the HTML `<base href>` for this locale. Cannot be used together with `baseHref`. | `string` | Locale code |
|
||||
|
||||
HELPFUL: Use `subPath` rather than `baseHref` when you also need to rename the output directory — for example, to output to `de-DE/` instead of `de/`.
|
||||
|
||||
## Schematics
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ ng generate @angular/core:output-migration
|
||||
## What does the migration change?
|
||||
|
||||
1. `@Output()` class members are updated to their `output()` equivalent.
|
||||
2. Imports in the file of components or directives, at Typescript module level, are updated as well.
|
||||
2. Imports in the file of components or directives, at TypeScript module level, are updated as well.
|
||||
3. Migrates API calls like `event.next()`, whose use is not recommended, to `event.emit()` and removes `event.complete()` calls.
|
||||
|
||||
**Before**
|
||||
|
||||
@@ -97,9 +97,8 @@ The following table provides the status for Angular versions under support.
|
||||
| :------ | :----- | :--------- | :---------- | :--------- |
|
||||
| ^21.0.0 | Active | 2025-11-19 | 2026-05-19 | 2027-05-19 |
|
||||
| ^20.0.0 | LTS | 2025-05-28 | 2025-11-19 | 2026-11-28 |
|
||||
| ^19.0.0 | LTS | 2024-11-19 | 2025-05-28 | 2026-05-19 |
|
||||
|
||||
Angular versions v2 to v18 are no longer supported.
|
||||
Angular versions v2 to v19 are no longer supported.
|
||||
|
||||
### LTS fixes
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ In Angular v19 we shipped initial support for CSS and template HMR and in v20 we
|
||||
<docs-card title="Zoneless Angular" link="Completed in Q4 2025">
|
||||
In v18 we shipped experimental zoneless support in Angular. It enables developers to use the framework without including zone.js in their bundle, which improves performance, debugging experience, and interoperability. As part of the initial release we also introduced zoneless support to the Angular CDK and Angular Material.
|
||||
|
||||
In v19 we introduced zoneless support in server-side rendering, addressed some edge cases, and created a schematic to scaffold zoneless projects. We transitioned <a href="https://fonts.google.com/">Google Fonts</a> to zoneless which improved performance, developer experience, and allowed us to identify gaps that we need to address before moving this feature to developer preview.
|
||||
In v19 we introduced zoneless support in server-side rendering, addressed some edge cases, and created a schematic to scaffold zoneless projects. We transitioned <a href="https://fonts.google.com/" target="_blank">Google Fonts</a> to zoneless which improved performance, developer experience, and allowed us to identify gaps that we need to address before moving this feature to developer preview.
|
||||
|
||||
As of Angular v20.2, Zoneless Angular is now stable and includes improvements in error handling and server-side rendering.
|
||||
</docs-card>
|
||||
@@ -163,7 +163,7 @@ In v17 we shipped a vite and esbuild-based application builder and enabled it fo
|
||||
<docs-card title="Make Angular.dev the official home for Angular developers" link="Completed in Q2 2024" href="https://goo.gle/angular-dot-dev">
|
||||
Angular.dev is the new site, domain and home for Angular development. The new site contains updated documentation, tutorials and guidance that will help developers build with Angular’s latest features.
|
||||
</docs-card>
|
||||
<docs-card title="Introduce built-in control flow" link="Completed in Q2 2024" href="https://next.angular.dev/essentials/conditionals-and-loops">
|
||||
<docs-card title="Introduce built-in control flow" link="Completed in Q2 2024" href="guide/templates/control-flow">
|
||||
In v17 we shipped a developer preview version of a new control flow. It brings significant performance improvements and better ergonomics for template authoring. We also provided a migration of existing `*ngIf`, `*ngFor`, and `*ngSwitch` which you can run to move your project to the new implementation. As of v18 the built-in control flow is now stable.
|
||||
</docs-card>
|
||||
<docs-card title="Modernize getting started tutorial" link="Completed Q4 2023">
|
||||
@@ -175,7 +175,7 @@ In Angular v16, we released a developer preview of an esbuild-based builder with
|
||||
<docs-card title="Introduce dependency injection debugging APIs" link="Completed Q4 2023" href="tools/devtools">
|
||||
To improve the debugging utilities of Angular and Angular DevTools, we'll work on APIs that provide access to the dependency injection runtime. As part of the project, we'll expose debugging methods that allow us to explore the injector hierarchy and the dependencies across their associated providers. As of v17, we shipped a feature that enables us to plug into the dependency injection life-cycle. We also launched a visualization of the injector tree and inspection of the providers declared inside each individual node,
|
||||
</docs-card>
|
||||
<docs-card title="Improve documentation and schematics for standalone components" link="Completed Q4 2023" href="components">
|
||||
<docs-card title="Improve documentation and schematics for standalone components" link="Completed Q4 2023" href="essentials/components">
|
||||
We released a developer preview of the `ng new --standalone` schematics collection, allowing you to create apps free of NgModules. In v17 we switched the new application authoring format to standalone APIs and changed the documentation to reflect the recommendation. Additionally, we shipped schematics which support updating existing applications to standalone components, directives, and pipes. Even though NgModules will stick around for foreseeable future, we recommend you to explore the benefits of the new APIs to improve developer experience and benefit from the new features we build for them.
|
||||
</docs-card>
|
||||
<docs-card title="Explore hydration and server-side rendering improvements" link="Completed Q4 2023">
|
||||
@@ -231,7 +231,7 @@ We will work on finding a way to implement stricter type checking for reactive f
|
||||
<docs-card title="Improve integration of Angular DevTools with framework" link="Completed Q1 2022" href="tools/devtools">
|
||||
To improve the integration of Angular DevTools with the framework, we are working on moving the codebase to the angular/angular monorepository. This includes transitioning Angular DevTools to Bazel and integrating it into the existing processes and CI pipeline.
|
||||
</docs-card>
|
||||
<docs-card title="Launch advanced compiler diagnostics" link="Completed Q1 2022" href="reference/extended-diagnostics">
|
||||
<docs-card title="Launch advanced compiler diagnostics" link="Completed Q1 2022" href="extended-diagnostics">
|
||||
Extend the diagnostics of the Angular compiler outside type checking. Introduce other correctness and conformance checks to further guarantee correctness and best practices.
|
||||
</docs-card>
|
||||
<docs-card title="Update our e2e testing strategy" link="Completed Q3 2021" href="guide/testing">
|
||||
@@ -270,7 +270,7 @@ As part of the v11 release, we introduced an opt-in preview of webpack 5 in the
|
||||
<docs-card title="Faster apps by inlining critical styles in Universal apps" link="Completed Q1 2021" href="guide/ssr">
|
||||
Loading external stylesheets is a blocking operation, which means that the browser cannot start rendering your app until it loads all the referenced CSS. Having render-blocking resources in the header of a page can significantly impact its load performance, for example, its first contentful paint. To make apps faster, we have been collaborating with the Google Chrome team on inlining critical CSS and loading the rest of the styles asynchronously.
|
||||
</docs-card>
|
||||
<docs-card title="Improve debugging with better Angular error messages" link="Completed Q1 2021" href="reference/errors">
|
||||
<docs-card title="Improve debugging with better Angular error messages" link="Completed Q1 2021" href="errors">
|
||||
Error messages often bring limited actionable information to help developers resolve them. We have been working on making error messages more discoverable by adding associated codes, developing guides, and other materials to ensure a smoother debugging experience.
|
||||
</docs-card>
|
||||
<docs-card title="Improved developer onboarding with refreshed introductory documentation" link="Completed Q1 2021" href="tutorials">
|
||||
|
||||
@@ -56,7 +56,7 @@ To create a builder, use the `createBuilder()` CLI Builder function, and return
|
||||
<docs-code header="src/my-builder.ts (builder skeleton)" path="adev/src/content/examples/cli-builder/src/my-builder.ts" region="builder-skeleton"/>
|
||||
|
||||
Now let's add some logic to it.
|
||||
The following code retrieves the source and destination file paths from user options and copies the file from the source to the destination \(using the [Promise version of the built-in NodeJS `copyFile()` function](https://nodejs.org/api/fs.html#fs_fspromises_copyfile_src_dest_mode)\).
|
||||
The following code retrieves the source and destination file paths from user options and copies the file from the source to the destination \(using the [Promise version of the built-in Node.js `copyFile()` function](https://nodejs.org/api/fs.html#fs_fspromises_copyfile_src_dest_mode)\).
|
||||
If the copy operation fails, it returns an error with a message about the underlying problem.
|
||||
|
||||
<docs-code header="src/my-builder.ts (builder)" path="adev/src/content/examples/cli-builder/src/my-builder.ts" region="builder"/>
|
||||
|
||||
@@ -33,7 +33,7 @@ You can read more by following the links associated with the package names below
|
||||
| [Firebase hosting](https://firebase.google.com/docs/hosting) | [`ng add @angular/fire`](https://npmjs.org/package/@angular/fire) |
|
||||
| [Vercel](https://vercel.com/solutions/angular) | [`vercel init angular`](https://github.com/vercel/vercel/tree/main/examples/angular) |
|
||||
| [Netlify](https://www.netlify.com) | [`ng add @netlify-builder/deploy`](https://npmjs.org/package/@netlify-builder/deploy) |
|
||||
| [GitHub pages](https://pages.github.com) | [`ng add angular-cli-ghpages`](https://npmjs.org/package/angular-cli-ghpages) |
|
||||
| [GitHub Pages](https://pages.github.com) | [`ng add angular-cli-ghpages`](https://npmjs.org/package/angular-cli-ghpages) |
|
||||
| [Amazon Cloud S3](https://aws.amazon.com/s3/?nc2=h_ql_prod_st_s3) | [`ng add @jefiozie/ngx-aws-deploy`](https://www.npmjs.com/package/@jefiozie/ngx-aws-deploy) |
|
||||
|
||||
If you're deploying to a self-managed server or there's no builder for your favorite cloud platform, you can either [create a builder](tools/cli/cli-builder) that allows you to use the `ng deploy` command, or read through this guide to learn how to manually deploy your application.
|
||||
|
||||
@@ -63,7 +63,7 @@ Possible values are:
|
||||
To bundle your schematics together with your library, you must configure the library to build the schematics separately, then add them to the bundle.
|
||||
You must build your schematics _after_ you build your library, so they are placed in the correct directory.
|
||||
|
||||
- Your library needs a custom Typescript configuration file with instructions on how to compile your schematics into your distributed library
|
||||
- Your library needs a custom TypeScript configuration file with instructions on how to compile your schematics into your distributed library
|
||||
- To add the schematics to the library bundle, add scripts to the library's `package.json` file
|
||||
|
||||
Assume you have a library project `my-lib` in your Angular workspace.
|
||||
|
||||
@@ -68,7 +68,7 @@ To install the Angular CLI, open a terminal window and run the following command
|
||||
### Powershell execution policy
|
||||
|
||||
On Windows client computers, the execution of PowerShell scripts is disabled by default, so the above command may fail with an error.
|
||||
To allow the execution of PowerShell scripts, which is needed for npm global binaries, you must set the following <a href="https://docs.microsoft.com/powershell/module/microsoft.powershell.core/about/about_execution_policies">execution policy</a>:
|
||||
To allow the execution of PowerShell scripts, which is needed for npm global binaries, you must set the following <a href="https://docs.microsoft.com/powershell/module/microsoft.powershell.core/about/about_execution_policies" target="_blank">execution policy</a>:
|
||||
|
||||
```sh
|
||||
|
||||
|
||||
@@ -280,7 +280,7 @@ In this section are the definitions of all of them to provide additional clarity
|
||||
|
||||
### Package
|
||||
|
||||
The smallest set of files that are published to NPM and installed together, for example `@angular/core`.
|
||||
The smallest set of files that are published to npm and installed together, for example `@angular/core`.
|
||||
This package includes a manifest called package.json, compiled source code, typescript definition files, source maps, metadata, etc.
|
||||
The package is installed with `npm install @angular/core`.
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ You should be very careful when choosing the name of your library if you want to
|
||||
See [Publishing your library](tools/libraries/creating-libraries#publishing-your-library).
|
||||
|
||||
Avoid using a name that is prefixed with `ng-`, such as `ng-library`.
|
||||
The `ng-` prefix is a reserved keyword used from the Angular framework and its libraries.
|
||||
The `ng-` prefix is a reserved keyword used by the Angular framework and its libraries.
|
||||
The `ngx-` prefix is preferred as a convention used to denote that the library is suitable for use with Angular.
|
||||
It is also an excellent indication to consumers of the registry to differentiate between libraries of different JavaScript frameworks.
|
||||
|
||||
@@ -242,7 +242,7 @@ TypeScript path mappings should _not_ point to the library source `.ts` files.
|
||||
|
||||
This section explains how to use your package manager's local linking feature
|
||||
(such as [`npm link`](https://docs.npmjs.com/cli/v11/commands/npm-link) or [`pnpm link`](https://pnpm.io/cli/link)) to test a standalone Angular library with an external application during
|
||||
local development, without relying on the monorepo workspace structure or publishing to the NPM registry.
|
||||
local development, without relying on the monorepo workspace structure or publishing to the npm registry.
|
||||
|
||||
NOTE: If your library and application are in the same Angular workspace (a monorepo setup), the standard monorepo workflow automatically handles the linking and is generally more efficient. This local linking approach is best when:
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ import * as $ from 'jquery';
|
||||
```
|
||||
|
||||
If you import it using import statements, you have two different copies of the library: one imported as a global library, and one imported as a module.
|
||||
This is especially bad for libraries with plugins, like JQuery, because each copy includes different plugins.
|
||||
This is especially bad for libraries with plugins, like jQuery, because each copy includes different plugins.
|
||||
|
||||
Instead, run the `npm install @types/jquery` Angular CLI command to download typings for your library and then follow the library installation steps.
|
||||
This gives you access to the global variables exposed by that library.
|
||||
@@ -135,7 +135,7 @@ For example:
|
||||
declare var libraryName: any;
|
||||
```
|
||||
|
||||
Some scripts extend other libraries; for instance with JQuery plugins:
|
||||
Some scripts extend other libraries; for instance with jQuery plugins:
|
||||
|
||||
```ts
|
||||
$('.test').myPlugin();
|
||||
|
||||
@@ -23,7 +23,7 @@ The lessons in this tutorial assume that you have experience with the following:
|
||||
|
||||
### Your equipment
|
||||
|
||||
These lessons can be completed using a local installation of the Angular tools or in our embedded editor. Local Angular development can be completed on Windows, MacOS or Linux based systems.
|
||||
These lessons can be completed using a local installation of the Angular tools or in our embedded editor. Local Angular development can be completed on Windows, macOS or Linux based systems.
|
||||
|
||||
NOTE: Look for alerts like this one, which call out steps that may only be for your local editor.
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ The server is now reading data from the HTTP request but the components that rel
|
||||
|
||||
</docs-workflow>
|
||||
|
||||
NOTE: This lesson relies on the `fetch` browser API. For the support of interceptors, please refer to the [Http Client documentation](/guide/http)
|
||||
NOTE: This lesson relies on the `fetch` browser API. For the support of interceptors, please refer to the [HTTP client documentation](/guide/http)
|
||||
|
||||
SUMMARY: In this lesson, you updated your app to use a local web server (`json-server`), and use asynchronous service methods to retrieve data.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Here's an example of how to use the `@for` syntax in a component:
|
||||
`,
|
||||
})
|
||||
export class App {
|
||||
operatingSystems = [{id: 'win', name: 'Windows'}, {id: 'osx', name: 'MacOS'}, {id: 'linux', name: 'Linux'}];
|
||||
operatingSystems = [{id: 'win', name: 'Windows'}, {id: 'osx', name: 'macOS'}, {id: 'linux', name: 'Linux'}];
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -33,11 +33,11 @@ interface LoginData {
|
||||
</label>
|
||||
|
||||
@if (loginForm.password().invalid()) {
|
||||
<div class="error">
|
||||
<ul class="error-list">
|
||||
@for (error of loginForm.password().errors(); track error) {
|
||||
<p>{{ error.message }}</p>
|
||||
<li>{{ error.message }}</li>
|
||||
}
|
||||
</div>
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ It also explains the basic mechanics of using `git`, `node`, and `pnpm`.
|
||||
- [Development in a Container](#development-in-a-container)
|
||||
- [Experimenting in dev-app](#experimenting-in-dev-app)
|
||||
- [Getting the Sources](#getting-the-sources)
|
||||
- [Installing NPM Modules](#installing-npm-modules)
|
||||
- [Installing npm Modules](#installing-npm-modules)
|
||||
- [Building](#building)
|
||||
- [Running Tests Locally](#running-tests-locally)
|
||||
- [Testing changes against a local library/project](#testing-changes-against-a-local-libraryproject)
|
||||
@@ -95,7 +95,7 @@ cd angular
|
||||
git remote add upstream https://github.com/angular/angular.git
|
||||
```
|
||||
|
||||
## Installing NPM Modules
|
||||
## Installing npm Modules
|
||||
|
||||
Next, install the JavaScript modules needed to build and test Angular:
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ If the problem still exists in your application, please [open a new issue](https
|
||||
## Angular: Support Request (v1)
|
||||
|
||||
```
|
||||
Hello, we reviewed this issue and determined that it doesn't fall into the bug report or feature request category. This issue tracker is not suitable for support requests, please repost your issue on [StackOverflow](https://stackoverflow.com/) using tag `angular`.
|
||||
Hello, we reviewed this issue and determined that it doesn't fall into the bug report or feature request category. This issue tracker is not suitable for support requests, please repost your issue on [Stack Overflow](https://stackoverflow.com/) using tag `angular`.
|
||||
|
||||
If you are wondering why we don't resolve support issues via the issue tracker, please [check out this explanation](https://github.com/angular/angular/blob/main/CONTRIBUTING.md#question).
|
||||
```
|
||||
|
||||
@@ -21,5 +21,5 @@ limited resources across all contributors and projects.
|
||||
## Exceptions
|
||||
|
||||
If you plan to undertake more significant work that you anticipate will generate
|
||||
many individual PRs, please reach out to the team in a Github issue first to
|
||||
many individual PRs, please reach out to the team in a GitHub issue first to
|
||||
validate that Angular will be able to support and accept your contributions.
|
||||
|
||||
@@ -25,6 +25,8 @@ export const enum RuntimeErrorCode {
|
||||
// (undocumented)
|
||||
JSONP_HEADERS_NOT_SUPPORTED = 2812,
|
||||
// (undocumented)
|
||||
JSONP_UNSAFE_URL = 2826,
|
||||
// (undocumented)
|
||||
JSONP_WRONG_METHOD = 2810,
|
||||
// (undocumented)
|
||||
JSONP_WRONG_RESPONSE_TYPE = 2811,
|
||||
|
||||
@@ -1739,6 +1739,8 @@ export interface SchemaMetadata {
|
||||
|
||||
// @public
|
||||
export enum SecurityContext {
|
||||
// (undocumented)
|
||||
ATTRIBUTE_NO_BINDING = 6,
|
||||
// (undocumented)
|
||||
HTML = 1,
|
||||
// (undocumented)
|
||||
|
||||
@@ -50,6 +50,7 @@ export function renderApplication(bootstrap: (context: BootstrapContext) => Prom
|
||||
document?: string | Document;
|
||||
url?: string;
|
||||
platformProviders?: Provider[];
|
||||
allowedHosts?: Readonly<string>[];
|
||||
}): Promise<string>;
|
||||
|
||||
// @public
|
||||
@@ -57,8 +58,27 @@ export function renderModule<T>(moduleType: Type<T>, options: {
|
||||
document?: string | Document;
|
||||
url?: string;
|
||||
extraProviders?: StaticProvider[];
|
||||
allowedHosts?: Readonly<string>[];
|
||||
}): Promise<string>;
|
||||
|
||||
// @public
|
||||
export const enum RuntimeErrorCode {
|
||||
// (undocumented)
|
||||
DISABLED_DOM_EMULATION_IN_NON_BROWSER = 5704,
|
||||
// (undocumented)
|
||||
GET_COOKIE_NOT_IMPLEMENTED = 5700,
|
||||
// (undocumented)
|
||||
HOST_NOT_ALLOWED = 5706,
|
||||
// (undocumented)
|
||||
INVALID_URL = 5701,
|
||||
// (undocumented)
|
||||
PROTOCOL_RELATIVE_URL_NOT_ALLOWED = 5702,
|
||||
// (undocumented)
|
||||
SUSPICIOUS_URL_CHANGE_ORIGIN = 5703,
|
||||
// (undocumented)
|
||||
XHR_NOT_LOADED = 5705
|
||||
}
|
||||
|
||||
// @public
|
||||
export class ServerModule {
|
||||
// (undocumented)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"dist/main.js": 135813,
|
||||
"dist/main.js": 144843,
|
||||
"dist/polyfills.js": 35883
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"zone.js": "0.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/core": "7.29.7",
|
||||
"@rollup/plugin-babel": "^6.0.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||
"@types/jasmine": "^6.0.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dist/browser/main-[hash].js": 227093,
|
||||
"dist/browser/polyfills-[hash].js": 34544,
|
||||
"dist/browser/main-[hash].js": 233303,
|
||||
"dist/browser/polyfills-[hash].js": 35677,
|
||||
"dist/browser/event-dispatch-contract.min.js": 476
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ app.use((req, res) => {
|
||||
|
||||
renderApplication(bootstrap, {
|
||||
document: indexHtml,
|
||||
allowedHosts: ['localhost'],
|
||||
url: `${protocol}://${headers.host}${originalUrl}`,
|
||||
platformProviders: [{provide: APP_BASE_HREF, useValue: baseUrl}],
|
||||
}).then((response: string) => {
|
||||
|
||||
@@ -42,6 +42,7 @@ app.use((req, res) => {
|
||||
|
||||
renderModule(AppServerModule, {
|
||||
document: indexHtml,
|
||||
allowedHosts: ['localhost'],
|
||||
url: `${protocol}://${headers.host}${originalUrl}`,
|
||||
extraProviders: [{provide: APP_BASE_HREF, useValue: baseUrl}],
|
||||
}).then((response: string) => {
|
||||
|
||||
@@ -42,6 +42,7 @@ app.use((req, res) => {
|
||||
|
||||
renderApplication(bootstrap, {
|
||||
document: indexHtml,
|
||||
allowedHosts: ['localhost'],
|
||||
url: `${protocol}://${headers.host}${originalUrl}`,
|
||||
platformProviders: [{provide: APP_BASE_HREF, useValue: baseUrl}],
|
||||
}).then((response: string) => {
|
||||
|
||||
@@ -27,7 +27,9 @@ export class JsonpCmp {
|
||||
people: Person[] = [];
|
||||
|
||||
constructor(http: HttpClient) {
|
||||
http.jsonp('./people.json', 'callback').subscribe((res: unknown) => {
|
||||
const peopleUrl = new URL('./people.json', window.location.href).toString();
|
||||
|
||||
http.jsonp(peopleUrl, 'callback').subscribe((res: unknown) => {
|
||||
this.people = res as Person[];
|
||||
});
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "21.2.12",
|
||||
"version": "21.2.18",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
@@ -76,9 +76,9 @@
|
||||
"@angular/service-worker": "workspace:*",
|
||||
"@angular/ssr": "21.2.9",
|
||||
"@angular/upgrade": "workspace: *",
|
||||
"@babel/cli": "7.28.6",
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/generator": "7.29.1",
|
||||
"@babel/cli": "7.29.7",
|
||||
"@babel/core": "7.29.7",
|
||||
"@babel/generator": "7.29.7",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||
"@microsoft/api-extractor": "^7.24.2",
|
||||
"@rollup/plugin-babel": "^6.0.0",
|
||||
@@ -116,7 +116,7 @@
|
||||
"d3": "^7.0.0",
|
||||
"dagre-d3-es": "^7.0.14",
|
||||
"diff": "^8.0.0",
|
||||
"domino": "https://github.com/angular/domino.git#928dffb9d9431b2cd2a73d7b940d1575f221e072",
|
||||
"domino": "https://github.com/angular/domino.git#f74cccd496283b6141fbdbeb25b168f4b1c9836a",
|
||||
"esbuild": "0.27.2",
|
||||
"esbuild-plugin-umd-wrapper": "^3.0.0",
|
||||
"http-server": "^14.0.0",
|
||||
@@ -163,7 +163,7 @@
|
||||
"@actions/github": "^9.0.0",
|
||||
"@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#4de8a14a1682d0f07e0b14a3b26498757c195904",
|
||||
"@babel/plugin-proposal-async-generator-functions": "7.20.7",
|
||||
"@babel/plugin-transform-async-generator-functions": "^7.27.1",
|
||||
"@babel/plugin-transform-async-generator-functions": "^7.29.7",
|
||||
"@bazel/bazelisk": "^1.7.5",
|
||||
"@bazel/buildifier": "^8.0.0",
|
||||
"@bazel/ibazel": "0.28.0",
|
||||
|
||||
@@ -35,4 +35,6 @@ export const enum RuntimeErrorCode {
|
||||
REFERRER_NOT_SUPPORTED_WITH_XHR = 2821,
|
||||
INVALID_TIMEOUT_VALUE = 2822,
|
||||
REFERRER_POLICY_NOT_SUPPORTED_WITH_XHR = 2823,
|
||||
|
||||
JSONP_UNSAFE_URL = 2826,
|
||||
}
|
||||
|
||||
@@ -55,6 +55,10 @@ export const JSONP_ERR_WRONG_RESPONSE_TYPE = 'JSONP requests must use Json respo
|
||||
// headers set
|
||||
export const JSONP_ERR_HEADERS_NOT_SUPPORTED = 'JSONP requests do not support headers.';
|
||||
|
||||
// Error text given when a JSONP request URL is not absolute HTTP(S).
|
||||
export const JSONP_ERR_UNSAFE_URL =
|
||||
'JSONP requests only support absolute URLs with HTTP(S) protocols.';
|
||||
|
||||
/**
|
||||
* DI token/abstract type representing a map of JSONP callbacks.
|
||||
*
|
||||
@@ -139,6 +143,10 @@ export class JsonpClientBackend implements HttpBackend {
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.isAllowedJsonpUrl(req.urlWithParams)) {
|
||||
throw new RuntimeError(RuntimeErrorCode.JSONP_UNSAFE_URL, ngDevMode && JSONP_ERR_UNSAFE_URL);
|
||||
}
|
||||
|
||||
// Everything else happens inside the Observable boundary.
|
||||
return new Observable<HttpEvent<any>>((observer: Observer<HttpEvent<any>>) => {
|
||||
// The first step to make a request is to generate the callback name, and replace the
|
||||
@@ -282,6 +290,10 @@ export class JsonpClientBackend implements HttpBackend {
|
||||
|
||||
foreignDocument.adoptNode(script);
|
||||
}
|
||||
|
||||
private isAllowedJsonpUrl(url: string): boolean {
|
||||
return /^https?:\/\//i.test(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -523,7 +523,7 @@ export class HttpRequest<T> {
|
||||
this.integrity = options.integrity;
|
||||
}
|
||||
|
||||
if (options.referrer) {
|
||||
if (options.referrer !== undefined) {
|
||||
this.referrer = options.referrer;
|
||||
}
|
||||
|
||||
@@ -734,7 +734,8 @@ export class HttpRequest<T> {
|
||||
const mode = update.mode || this.mode;
|
||||
const redirect = update.redirect || this.redirect;
|
||||
const credentials = update.credentials || this.credentials;
|
||||
const referrer = update.referrer || this.referrer;
|
||||
|
||||
const referrer = update.referrer ?? this.referrer;
|
||||
const integrity = update.integrity || this.integrity;
|
||||
const referrerPolicy = update.referrerPolicy || this.referrerPolicy;
|
||||
// Carefully handle the transferCache to differentiate between
|
||||
|
||||
@@ -40,8 +40,10 @@ import {HttpParams} from './params';
|
||||
* @param includePostRequests Enables caching for POST requests. By default, only GET and HEAD
|
||||
* requests are cached. This option can be enabled if POST requests are used to retrieve data
|
||||
* (for example using GraphQL).
|
||||
* @param includeRequestsWithAuthHeaders Enables caching of requests containing either `Authorization`
|
||||
* or `Proxy-Authorization` headers. By default, these requests are excluded from caching.
|
||||
* @param includeRequestsWithAuthHeaders Enables caching of requests containing `Authorization`,
|
||||
* `Proxy-Authorization`, or `Cookie` headers. By default, these requests are excluded from
|
||||
* caching. Requests sent using `withCredentials` or Fetch API `credentials` modes that can send
|
||||
* credentials are also excluded by default.
|
||||
*
|
||||
* @see [Configuring the caching options](guide/ssr#configuring-the-caching-options)
|
||||
*
|
||||
@@ -101,13 +103,13 @@ interface TransferHttpResponse {
|
||||
/** headers */
|
||||
[HEADERS]: Record<string, string[]>;
|
||||
/** status */
|
||||
[STATUS]?: number;
|
||||
[STATUS]: number;
|
||||
/** statusText */
|
||||
[STATUS_TEXT]?: string;
|
||||
[STATUS_TEXT]: string;
|
||||
/** url */
|
||||
[REQ_URL]?: string;
|
||||
[REQ_URL]: string;
|
||||
/** responseType */
|
||||
[RESPONSE_TYPE]?: HttpRequest<unknown>['responseType'];
|
||||
[RESPONSE_TYPE]: HttpRequest<unknown>['responseType'];
|
||||
}
|
||||
|
||||
interface CacheOptions extends HttpTransferCacheOptions {
|
||||
@@ -123,18 +125,24 @@ export const CACHE_OPTIONS = new InjectionToken<CacheOptions>(
|
||||
*/
|
||||
const ALLOWED_METHODS = ['GET', 'HEAD'];
|
||||
|
||||
function shouldCacheRequest(req: HttpRequest<unknown>, options: CacheOptions): boolean {
|
||||
function canUseOrCacheRequest(req: HttpRequest<unknown>, options: CacheOptions): boolean {
|
||||
const {isCacheActive, ...globalOptions} = options;
|
||||
const {transferCache: requestOptions, method: requestMethod} = req;
|
||||
|
||||
if (
|
||||
!isCacheActive ||
|
||||
requestOptions === false ||
|
||||
// Do not cache requests sent with credentials.
|
||||
hasOutgoingCredentials(req) ||
|
||||
// POST requests are allowed either globally or at request level
|
||||
(requestMethod === 'POST' && !globalOptions.includePostRequests && !requestOptions) ||
|
||||
(requestMethod !== 'POST' && !ALLOWED_METHODS.includes(requestMethod)) ||
|
||||
// Do not cache request that require authorization when includeRequestsWithAuthHeaders is falsey
|
||||
// Do not cache requests with authentication or cookie headers unless explicitly enabled.
|
||||
(!globalOptions.includeRequestsWithAuthHeaders && hasAuthHeaders(req)) ||
|
||||
// Do not cache requests that explicitly forbid caching via Cache-Control
|
||||
// or Fetch API cache mode.
|
||||
hasUncacheableCacheControl(req.headers) ||
|
||||
isNonCacheableRequest(req.cache) ||
|
||||
globalOptions.filter?.(req) === false
|
||||
) {
|
||||
return false;
|
||||
@@ -147,25 +155,30 @@ function getHeadersToInclude(
|
||||
options: CacheOptions,
|
||||
requestOptions: HttpTransferCacheOptions | boolean | undefined,
|
||||
): string[] | undefined {
|
||||
const {includeHeaders: globalHeaders} = options;
|
||||
let headersToInclude = globalHeaders;
|
||||
if (typeof requestOptions === 'object' && requestOptions.includeHeaders) {
|
||||
// Request-specific config takes precedence over the global config.
|
||||
headersToInclude = requestOptions.includeHeaders;
|
||||
}
|
||||
return headersToInclude;
|
||||
// Request-specific config takes precedence over the global config.
|
||||
return typeof requestOptions === 'object' && requestOptions.includeHeaders
|
||||
? requestOptions.includeHeaders
|
||||
: options.includeHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cached response for a given request.
|
||||
* @param req The request to retrieve the cached response for.
|
||||
* @param options The caching options.
|
||||
* @param transferState The transfer state to retrieve the cached response from.
|
||||
* @param originMap The origin map to map the request URL to the origin. (Not needed when `storeKey` is provided).
|
||||
* @param storeKey The key to use to store the cached response in the transfer state. (If not provided, it will be computed from the request and originMap).
|
||||
* @param skipUseCacheChecks Whether to skip the use cache checks. (Only disable when the checks have been performed beforehand).
|
||||
*/
|
||||
export function retrieveStateFromCache(
|
||||
req: HttpRequest<unknown>,
|
||||
options: CacheOptions,
|
||||
transferState: TransferState,
|
||||
originMap: Record<string, string> | null,
|
||||
storeKey?: StateKey<TransferHttpResponse>,
|
||||
skipUseCacheChecks = false,
|
||||
): HttpResponse<unknown> | null {
|
||||
const {transferCache: requestOptions} = req;
|
||||
|
||||
// In the following situations we do not want to cache the request
|
||||
if (!shouldCacheRequest(req, options)) {
|
||||
if (!skipUseCacheChecks && !canUseOrCacheRequest(req, options)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -179,58 +192,61 @@ export function retrieveStateFromCache(
|
||||
);
|
||||
}
|
||||
|
||||
const requestUrl =
|
||||
typeof ngServerMode !== 'undefined' && ngServerMode && originMap
|
||||
? mapRequestOriginUrl(req.url, originMap)
|
||||
: req.url;
|
||||
if (!storeKey) {
|
||||
const requestUrl =
|
||||
typeof ngServerMode !== 'undefined' && ngServerMode && originMap
|
||||
? mapRequestOriginUrl(req.url, originMap)
|
||||
: req.url;
|
||||
|
||||
const storeKey = makeCacheKey(req, requestUrl);
|
||||
const response = transferState.get(storeKey, null);
|
||||
|
||||
const headersToInclude = getHeadersToInclude(options, requestOptions);
|
||||
|
||||
if (response) {
|
||||
const {
|
||||
[BODY]: undecodedBody,
|
||||
[RESPONSE_TYPE]: responseType,
|
||||
[HEADERS]: httpHeaders,
|
||||
[STATUS]: status,
|
||||
[STATUS_TEXT]: statusText,
|
||||
[REQ_URL]: url,
|
||||
} = response;
|
||||
// Request found in cache. Respond using it.
|
||||
let body: ArrayBuffer | Blob | string | undefined = undecodedBody;
|
||||
|
||||
switch (responseType) {
|
||||
case 'arraybuffer':
|
||||
body = fromBase64(undecodedBody);
|
||||
break;
|
||||
case 'blob':
|
||||
body = new Blob([fromBase64(undecodedBody)]);
|
||||
break;
|
||||
}
|
||||
|
||||
// We want to warn users accessing a header provided from the cache
|
||||
// That HttpTransferCache alters the headers
|
||||
// The warning will be logged a single time by HttpHeaders instance
|
||||
let headers = new HttpHeaders(httpHeaders);
|
||||
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
||||
// Append extra logic in dev mode to produce a warning when a header
|
||||
// that was not transferred to the client is accessed in the code via `get`
|
||||
// and `has` calls.
|
||||
headers = appendMissingHeadersDetection(req.url, headers, headersToInclude ?? []);
|
||||
}
|
||||
|
||||
return new HttpResponse({
|
||||
body,
|
||||
headers,
|
||||
status,
|
||||
statusText,
|
||||
url,
|
||||
});
|
||||
storeKey = makeCacheKey(req, requestUrl);
|
||||
}
|
||||
|
||||
return null;
|
||||
const response = transferState.get(storeKey, null);
|
||||
|
||||
if (!response) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
[BODY]: undecodedBody,
|
||||
[RESPONSE_TYPE]: responseType,
|
||||
[HEADERS]: httpHeaders,
|
||||
[STATUS]: status,
|
||||
[STATUS_TEXT]: statusText,
|
||||
[REQ_URL]: url,
|
||||
} = response;
|
||||
// Request found in cache. Respond using it.
|
||||
let body: ArrayBuffer | Blob | string | undefined = undecodedBody;
|
||||
|
||||
switch (responseType) {
|
||||
case 'arraybuffer':
|
||||
body = fromBase64(undecodedBody);
|
||||
break;
|
||||
case 'blob':
|
||||
body = new Blob([fromBase64(undecodedBody)]);
|
||||
break;
|
||||
}
|
||||
|
||||
// We want to warn users accessing a header provided from the cache
|
||||
// That HttpTransferCache alters the headers
|
||||
// The warning will be logged a single time by HttpHeaders instance
|
||||
let headers = new HttpHeaders(httpHeaders);
|
||||
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
||||
// Append extra logic in dev mode to produce a warning when a header
|
||||
// that was not transferred to the client is accessed in the code via `get`
|
||||
// and `has` calls.
|
||||
const {transferCache: requestOptions} = req;
|
||||
const headersToInclude = getHeadersToInclude(options, requestOptions);
|
||||
headers = appendMissingHeadersDetection(req.url, headers, headersToInclude ?? []);
|
||||
}
|
||||
|
||||
return new HttpResponse({
|
||||
body,
|
||||
headers,
|
||||
status,
|
||||
statusText,
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
export function transferCacheInterceptorFn(
|
||||
@@ -238,47 +254,58 @@ export function transferCacheInterceptorFn(
|
||||
next: HttpHandlerFn,
|
||||
): Observable<HttpEvent<unknown>> {
|
||||
const options = inject(CACHE_OPTIONS);
|
||||
const transferState = inject(TransferState);
|
||||
|
||||
const originMap = inject(HTTP_TRANSFER_CACHE_ORIGIN_MAP, {optional: true});
|
||||
|
||||
const cachedResponse = retrieveStateFromCache(req, options, transferState, originMap);
|
||||
if (cachedResponse) {
|
||||
return of(cachedResponse);
|
||||
if (!canUseOrCacheRequest(req, options)) {
|
||||
return next(req);
|
||||
}
|
||||
|
||||
const {transferCache: requestOptions} = req;
|
||||
const headersToInclude = getHeadersToInclude(options, requestOptions);
|
||||
|
||||
const transferState = inject(TransferState);
|
||||
const originMap = inject(HTTP_TRANSFER_CACHE_ORIGIN_MAP, {optional: true});
|
||||
const requestUrl =
|
||||
typeof ngServerMode !== 'undefined' && ngServerMode && originMap
|
||||
? mapRequestOriginUrl(req.url, originMap)
|
||||
: req.url;
|
||||
const storeKey = makeCacheKey(req, requestUrl);
|
||||
|
||||
// In the following situations we do not want to cache the request
|
||||
if (!shouldCacheRequest(req, options)) {
|
||||
return next(req);
|
||||
const cachedResponse = retrieveStateFromCache(
|
||||
req,
|
||||
options,
|
||||
transferState,
|
||||
/** originMap */ null,
|
||||
storeKey,
|
||||
/** skipUseCacheChecks */ true,
|
||||
);
|
||||
|
||||
if (cachedResponse) {
|
||||
return of(cachedResponse);
|
||||
}
|
||||
|
||||
const event$ = next(req);
|
||||
|
||||
if (typeof ngServerMode !== 'undefined' && ngServerMode) {
|
||||
// Request not found in cache. Make the request and cache it if on the server.
|
||||
return event$.pipe(
|
||||
tap((event: HttpEvent<unknown>) => {
|
||||
// Only cache successful HTTP responses.
|
||||
if (event instanceof HttpResponse) {
|
||||
const {headers, body, status, statusText} = event;
|
||||
|
||||
// Only cache successful HTTP responses that do not have Cache-Control
|
||||
// directives that forbid shared caching (no-store or private) and do not
|
||||
// carry a Set-Cookie header. A Set-Cookie header marks the response as
|
||||
// user-specific.
|
||||
if (hasUncacheableCacheControl(headers) || hasSetCookieHeader(headers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {transferCache: requestOptions, responseType} = req;
|
||||
const headersToInclude = getHeadersToInclude(options, requestOptions);
|
||||
|
||||
transferState.set<TransferHttpResponse>(storeKey, {
|
||||
[BODY]:
|
||||
req.responseType === 'arraybuffer' || req.responseType === 'blob'
|
||||
? toBase64(event.body)
|
||||
: event.body,
|
||||
[HEADERS]: getFilteredHeaders(event.headers, headersToInclude),
|
||||
[STATUS]: event.status,
|
||||
[STATUS_TEXT]: event.statusText,
|
||||
responseType === 'arraybuffer' || responseType === 'blob' ? toBase64(body) : body,
|
||||
[HEADERS]: getFilteredHeaders(headers, headersToInclude),
|
||||
[STATUS]: status,
|
||||
[STATUS_TEXT]: statusText,
|
||||
[REQ_URL]: requestUrl,
|
||||
[RESPONSE_TYPE]: req.responseType,
|
||||
[RESPONSE_TYPE]: responseType,
|
||||
});
|
||||
}
|
||||
}),
|
||||
@@ -288,9 +315,43 @@ export function transferCacheInterceptorFn(
|
||||
return event$;
|
||||
}
|
||||
|
||||
/** @returns true when the requests contains autorization related headers. */
|
||||
/** @returns true when the request contains authentication or cookie headers. */
|
||||
function hasAuthHeaders(req: HttpRequest<unknown>): boolean {
|
||||
return req.headers.has('authorization') || req.headers.has('proxy-authorization');
|
||||
const headers = req.headers;
|
||||
|
||||
return (
|
||||
headers.has('authorization') || headers.has('proxy-authorization') || headers.has('cookie')
|
||||
);
|
||||
}
|
||||
|
||||
function hasOutgoingCredentials(req: HttpRequest<unknown>): boolean {
|
||||
const {withCredentials, credentials} = req;
|
||||
|
||||
return withCredentials || credentials === 'include' || credentials === 'same-origin';
|
||||
}
|
||||
|
||||
const UNCACHEABLE_CACHE_CONTROL_DIRECTIVES = new Set(['no-store', 'private', 'no-cache']);
|
||||
|
||||
function hasUncacheableCacheControl(headers: HttpHeaders): boolean {
|
||||
const cacheControl = headers.get('cache-control');
|
||||
|
||||
if (!cacheControl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return cacheControl.split(',').some((directive) => {
|
||||
const directiveName = directive.split('=', 1)[0].trim().toLowerCase();
|
||||
|
||||
return UNCACHEABLE_CACHE_CONTROL_DIRECTIVES.has(directiveName);
|
||||
});
|
||||
}
|
||||
|
||||
function hasSetCookieHeader(headers: HttpHeaders): boolean {
|
||||
return headers.has('set-cookie');
|
||||
}
|
||||
|
||||
function isNonCacheableRequest(cache: RequestCache): boolean {
|
||||
return cache === 'no-cache' || cache === 'no-store';
|
||||
}
|
||||
|
||||
function getFilteredHeaders(
|
||||
@@ -340,26 +401,6 @@ function makeCacheKey(
|
||||
return makeStateKey(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* A method that returns a hash representation of a string using a variant of DJB2 hash
|
||||
* algorithm.
|
||||
*
|
||||
* This is the same hashing logic that is used to generate component ids.
|
||||
*/
|
||||
function generateHash(value: string): string {
|
||||
let hash = 0;
|
||||
|
||||
for (const char of value) {
|
||||
hash = (Math.imul(31, hash) + char.charCodeAt(0)) << 0;
|
||||
}
|
||||
|
||||
// Force positive number hash.
|
||||
// 2147483647 = equivalent of Integer.MAX_VALUE.
|
||||
hash += 2147483647 + 1;
|
||||
|
||||
return hash.toString();
|
||||
}
|
||||
|
||||
function toBase64(buffer: unknown): string {
|
||||
//TODO: replace with when is Baseline widely available
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toBase64
|
||||
@@ -497,3 +538,163 @@ function verifyMappedOrigin(url: string): void {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA-256 Constants (first 32 bits of the fractional parts of the cube roots of the first 64 primes 2..311):
|
||||
*/
|
||||
const SHA256_ROUND_CONSTANTS = /* @__PURE__ */ new Uint32Array([
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
||||
]);
|
||||
|
||||
let textEncoder: TextEncoder | undefined;
|
||||
|
||||
/**
|
||||
* Generates a SHA-256 hash representation of a string.
|
||||
*
|
||||
* Note: A custom synchronous SHA-256 implementation is used here because the Web Crypto API
|
||||
* (`crypto.subtle.digest`) is strictly asynchronous (Promise-based), whereas the transfer cache
|
||||
* state lookup and interceptor flow must operate synchronously due to the HttpResource API.
|
||||
*
|
||||
* The previous DJB2 hashing logic was vulnerable to pre-image and second-preimage attacks due to
|
||||
* its small 64-bit keyspace and mathematical simplicity. An attacker could craft colliding request
|
||||
* inputs to poison the cache, potentially causing a CDN or the application to serve the wrong
|
||||
* cached response to legitimate users. SHA-256 provides strong cryptographic collision resistance,
|
||||
* preventing cache key collision attacks.
|
||||
*/
|
||||
export function generateHash(value: string): string {
|
||||
textEncoder ??= new TextEncoder();
|
||||
const inputBytes = textEncoder.encode(value);
|
||||
|
||||
// Initial hash values (first 32 bits of the fractional parts of the square roots of the first 8 primes 2..19):
|
||||
let hashState0 = 0x6a09e667;
|
||||
let hashState1 = 0xbb67ae85;
|
||||
let hashState2 = 0x3c6ef372;
|
||||
let hashState3 = 0xa54ff53a;
|
||||
let hashState4 = 0x510e527f;
|
||||
let hashState5 = 0x9b05688c;
|
||||
let hashState6 = 0x1f83d9ab;
|
||||
let hashState7 = 0x5be0cd19;
|
||||
|
||||
// Pre-processing (Padding):
|
||||
const messageLengthInBits = inputBytes.length * 8;
|
||||
|
||||
// The total length of the padded message must be a multiple of 64 bytes (512 bits)
|
||||
const paddedLengthInBytes = (((inputBytes.length + 8) >> 6) + 1) << 6;
|
||||
const paddedBytes = new Uint8Array(paddedLengthInBytes);
|
||||
paddedBytes.set(inputBytes);
|
||||
paddedBytes[inputBytes.length] = 0x80; // Append a single '1' bit (0x80 byte)
|
||||
|
||||
const paddedBytesView = new DataView(paddedBytes.buffer);
|
||||
const lowBits = messageLengthInBits >>> 0;
|
||||
const highBits = (messageLengthInBits / 0x100000000) >>> 0;
|
||||
paddedBytesView.setUint32(paddedLengthInBytes - 8, highBits, false);
|
||||
paddedBytesView.setUint32(paddedLengthInBytes - 4, lowBits, false);
|
||||
|
||||
// Process the message in successive 64-byte chunks:
|
||||
const messageSchedule = new Uint32Array(64);
|
||||
for (let chunkOffset = 0; chunkOffset < paddedLengthInBytes; chunkOffset += 64) {
|
||||
// Initialize first 16 words of the message schedule:
|
||||
for (let i = 0; i < 16; i++) {
|
||||
messageSchedule[i] = paddedBytesView.getUint32(chunkOffset + i * 4, false);
|
||||
}
|
||||
|
||||
// Extend to 64 words:
|
||||
for (let i = 16; i < 64; i++) {
|
||||
const prevWord15 = messageSchedule[i - 15];
|
||||
const sigma0 =
|
||||
(((prevWord15 >>> 7) | (prevWord15 << 25)) ^
|
||||
((prevWord15 >>> 18) | (prevWord15 << 14)) ^
|
||||
(prevWord15 >>> 3)) >>>
|
||||
0;
|
||||
|
||||
const prevWord2 = messageSchedule[i - 2];
|
||||
const sigma1 =
|
||||
(((prevWord2 >>> 17) | (prevWord2 << 15)) ^
|
||||
((prevWord2 >>> 19) | (prevWord2 << 13)) ^
|
||||
(prevWord2 >>> 10)) >>>
|
||||
0;
|
||||
|
||||
messageSchedule[i] =
|
||||
(messageSchedule[i - 16] + sigma0 + messageSchedule[i - 7] + sigma1) >>> 0;
|
||||
}
|
||||
|
||||
// Initialize working variables to current hash values:
|
||||
let workingStateA = hashState0;
|
||||
let workingStateB = hashState1;
|
||||
let workingStateC = hashState2;
|
||||
let workingStateD = hashState3;
|
||||
let workingStateE = hashState4;
|
||||
let workingStateF = hashState5;
|
||||
let workingStateG = hashState6;
|
||||
let workingStateH = hashState7;
|
||||
|
||||
// Compression function main loop:
|
||||
for (let i = 0; i < 64; i++) {
|
||||
const capitalSigma1 =
|
||||
(((workingStateE >>> 6) | (workingStateE << 26)) ^
|
||||
((workingStateE >>> 11) | (workingStateE << 21)) ^
|
||||
((workingStateE >>> 25) | (workingStateE << 7))) >>>
|
||||
0;
|
||||
const chFunction = ((workingStateE & workingStateF) ^ (~workingStateE & workingStateG)) >>> 0;
|
||||
const temp1 =
|
||||
(workingStateH +
|
||||
capitalSigma1 +
|
||||
chFunction +
|
||||
SHA256_ROUND_CONSTANTS[i] +
|
||||
messageSchedule[i]) >>>
|
||||
0;
|
||||
|
||||
const capitalSigma0 =
|
||||
(((workingStateA >>> 2) | (workingStateA << 30)) ^
|
||||
((workingStateA >>> 13) | (workingStateA << 19)) ^
|
||||
((workingStateA >>> 22) | (workingStateA << 10))) >>>
|
||||
0;
|
||||
const majFunction =
|
||||
((workingStateA & workingStateB) ^
|
||||
(workingStateA & workingStateC) ^
|
||||
(workingStateB & workingStateC)) >>>
|
||||
0;
|
||||
const temp2 = (capitalSigma0 + majFunction) >>> 0;
|
||||
|
||||
workingStateH = workingStateG;
|
||||
workingStateG = workingStateF;
|
||||
workingStateF = workingStateE;
|
||||
workingStateE = (workingStateD + temp1) >>> 0;
|
||||
workingStateD = workingStateC;
|
||||
workingStateC = workingStateB;
|
||||
workingStateB = workingStateA;
|
||||
workingStateA = (temp1 + temp2) >>> 0;
|
||||
}
|
||||
|
||||
// Update intermediate hash state:
|
||||
hashState0 = (hashState0 + workingStateA) >>> 0;
|
||||
hashState1 = (hashState1 + workingStateB) >>> 0;
|
||||
hashState2 = (hashState2 + workingStateC) >>> 0;
|
||||
hashState3 = (hashState3 + workingStateD) >>> 0;
|
||||
hashState4 = (hashState4 + workingStateE) >>> 0;
|
||||
hashState5 = (hashState5 + workingStateF) >>> 0;
|
||||
hashState6 = (hashState6 + workingStateG) >>> 0;
|
||||
hashState7 = (hashState7 + workingStateH) >>> 0;
|
||||
}
|
||||
|
||||
// Produce the final 64-character hexadecimal hash:
|
||||
return [
|
||||
hashState0,
|
||||
hashState1,
|
||||
hashState2,
|
||||
hashState3,
|
||||
hashState4,
|
||||
hashState5,
|
||||
hashState6,
|
||||
hashState7,
|
||||
]
|
||||
.map((x) => x.toString(16).padStart(8, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user