mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef9669c1f8 | |||
| 6c6db5b604 | |||
| 185862ef76 | |||
| 631e303dd9 | |||
| 41e4851928 | |||
| a24dcfd1ec | |||
| 1dde3827e9 | |||
| b40b67cdc1 | |||
| 4900e453e1 | |||
| 42d57c3578 | |||
| 10ad3c0692 | |||
| d07f502946 | |||
| 600da64ba4 | |||
| a40e2cebc8 | |||
| 9ed1b6c045 | |||
| 2b9b27e882 | |||
| 85f1c0a268 | |||
| fb7e67ca9d | |||
| 316d49a8ae | |||
| 9bcbf37641 | |||
| a3d14fc11b | |||
| 0612c8c53b | |||
| 4f5d8a2c0b | |||
| be1f80a253 | |||
| 76cf531002 | |||
| 885a1a1d97 | |||
| 7a64aff9b5 | |||
| cbaba8afe6 | |||
| fbe080f829 | |||
| 1be52ad880 | |||
| fa4eff36cb | |||
| 27da56ee9d | |||
| 7b8faccc34 | |||
| 6662724c99 | |||
| 8ce8b9342a | |||
| 131c422da2 | |||
| fa33854d24 | |||
| f3308dc1f7 | |||
| f9b70a0887 | |||
| a8dd801856 | |||
| 2ec753cb77 | |||
| 90cc755a56 | |||
| 4fd1a08699 | |||
| 750af5b123 | |||
| 5533ab4f56 | |||
| 2b9954fd3d | |||
| 4dc7bf5a75 | |||
| 0d5ee9ae1b | |||
| 6a02320575 | |||
| 751e4af80b | |||
| da346bf696 | |||
| 580212c995 | |||
| a9ddc5d40a | |||
| 0d08d9cc82 | |||
| 4c7ec66807 | |||
| 62266eee8b | |||
| 2a50dceef5 | |||
| c9215b3539 | |||
| d88d6ed69e | |||
| e5b93ea4ca | |||
| ce883d95ef | |||
| be7490964a | |||
| 810fb7382f | |||
| b24ead5571 | |||
| 6d10b8bb95 | |||
| c738d45fa5 | |||
| b2fe2c2474 | |||
| 9577b49666 | |||
| 50f88e1887 | |||
| a57a6496fa | |||
| 8d22beb22c | |||
| e14d5eadd5 | |||
| 528a93a5da | |||
| 32a830231e | |||
| 0d1a5b80c2 | |||
| 17cae6ae5f | |||
| 4f65bb34b2 | |||
| eddca4280b | |||
| 175343dfdb | |||
| e0b5078cf2 | |||
| 1e474f7cfa | |||
| 63a857b874 | |||
| c3d69aeaaa | |||
| 684e9fd53d | |||
| ff0af64ced | |||
| bb8cdd9566 | |||
| 17ffa19a2d | |||
| 6c341347b2 | |||
| 8c32f577f1 | |||
| aa5d23799b | |||
| 540536c386 | |||
| f603d4714f | |||
| b72b6b4710 | |||
| 3ed14c6354 | |||
| 236b80b6f9 | |||
| ca5b3c4d3e | |||
| 245bcdd607 | |||
| e2e7211530 | |||
| b351d493ea | |||
| 096a5c2105 | |||
| b1407e1add | |||
| b4a747a94c | |||
| 3ae69406cc | |||
| b52a6264ac | |||
| dc9581469f | |||
| 05d9b97cf9 |
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: adev-writing-guide
|
||||
description: Comprehensive writing guide for Angular documentation (adev). Covers Google Technical Writing standards, Angular-specific markdown extensions, code blocks, and components. Use when authoring or reviewing content in adev/src/content.
|
||||
description: Comprehensive writing guide for Angular documentation (adev). Covers Google Technical Writing standards, Angular-specific markdown extensions, code blocks, and components. You MUST use this skill any time you plan to create, edit, or review documentation files in `adev/` or `adev/src/content`.
|
||||
---
|
||||
|
||||
# Angular Documentation (adev) Writing Guide
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
description: Find and fix flaky tests in the repository
|
||||
---
|
||||
|
||||
Investigate flaky tests in the repo and propose fixes to improve stability.
|
||||
High-level process:
|
||||
|
||||
1. Run tests in the repo to look for flakes.
|
||||
- Consider using Bazel's `--runs_per_test` flag to easily find
|
||||
flakes.
|
||||
- Be cognizant of not exhausting all the resources on the current
|
||||
machine, run a subset of tests at a time such as
|
||||
`bazel test //packages/core/...`.
|
||||
2. Once you find some flakes, focus on one at a time.
|
||||
3. Create a new branch named `flakes/${relevantNameFromTest}`.
|
||||
4. Reproduce the flake to the best of your ability.
|
||||
- Consider using `--test_env JASMINE_RANDOM_SEED=1234` to
|
||||
replicate the broken test ordering.
|
||||
5. Debug the test to understand the failure mode.
|
||||
- Consider temporarily disabling / skipping other tests with `xit`
|
||||
and `fit` to narrow down where the flake might be coming from if
|
||||
multiple tests are influencing each other.
|
||||
- Consider temporarily ignoring Firefox tests with
|
||||
`--test_tag_filters -firefox` if the flake does not appear to be
|
||||
browser specific.
|
||||
- Consider using `--test_sharding_strategy disabled` to run the
|
||||
test in a single shard.
|
||||
- Try to understand why the test was _flaky_, not just why it
|
||||
_failed_. Understanding the inconsistency is important to
|
||||
finding the correct fix.
|
||||
6. Attempt a fix and validate with `--runs_per_test`.
|
||||
- Iterate on the fix until you have something which appears to
|
||||
work.
|
||||
- If you find yourself stuck and not making meaningful progress,
|
||||
note down what you've learned/where you're struggling, commit
|
||||
what you have, look for another flake to fix, and continue. At
|
||||
the end, surface to the user what you failed to fix.
|
||||
- Don't try to make significant changes to Angular's runtime
|
||||
behavior, focus just on making the test pass/fail consistently.
|
||||
7. Commit the change with relevant details in the commit message and
|
||||
move on to the next test.
|
||||
- Be sure to include your theory of why the test was flaky and
|
||||
how this fix eliminates or reduces that flakiness.
|
||||
8. Iterate as many times as the user requests you to (default 5
|
||||
branches if not otherwise specified).
|
||||
9. Once you can't find any flaky tests or have iterated as many times
|
||||
as requested, stop and inform the user what you found and fixed.
|
||||
|
||||
Additional notes:
|
||||
|
||||
- Multiple fixes including the same/related files can go in the same
|
||||
commit or multiple commits on the same branch.
|
||||
- Distinct test fixes should go in different branches, make a new one
|
||||
for each investigation.
|
||||
- You may push these branches to `origin`, but do not create PRs for
|
||||
them.
|
||||
@@ -19248,7 +19248,7 @@ var ChildProcess = class {
|
||||
return new Promise((resolve5, reject) => {
|
||||
const commandText = `${command2} ${args.join(" ")}`;
|
||||
Log.debug(`Executing command: ${commandText}`);
|
||||
const childProcess = _spawn(command2, args, { ...options, shell: true, stdio: "inherit" });
|
||||
const childProcess = _spawn(command2, args, { ...options, stdio: "inherit" });
|
||||
childProcess.on("close", (status) => status === 0 ? resolve5() : reject(status));
|
||||
});
|
||||
}
|
||||
@@ -19256,7 +19256,7 @@ var ChildProcess = class {
|
||||
const commandText = `${command2} ${args.join(" ")}`;
|
||||
const env22 = getEnvironmentForNonInteractiveCommand(options.env);
|
||||
Log.debug(`Executing command: ${commandText}`);
|
||||
const { status: exitCode, signal, stdout, stderr } = _spawnSync(command2, args, { ...options, env: env22, encoding: "utf8", shell: true, stdio: "pipe" });
|
||||
const { status: exitCode, signal, stdout, stderr } = _spawnSync(command2, args, { ...options, env: env22, encoding: "utf8", stdio: "pipe" });
|
||||
const status = statusFromExitCodeAndSignal(exitCode, signal);
|
||||
if (status === 0 || options.suppressErrorOnFailingExitCode) {
|
||||
return { status, stdout, stderr };
|
||||
@@ -19266,7 +19266,7 @@ var ChildProcess = class {
|
||||
static spawn(command2, args, options = {}) {
|
||||
const commandText = `${command2} ${args.join(" ")}`;
|
||||
const env22 = getEnvironmentForNonInteractiveCommand(options.env);
|
||||
return processAsyncCmd(commandText, options, _spawn(command2, args, { ...options, env: env22, shell: true, stdio: "pipe" }));
|
||||
return processAsyncCmd(commandText, options, _spawn(command2, args, { ...options, env: env22, stdio: "pipe" }));
|
||||
}
|
||||
static exec(command2, options = {}) {
|
||||
const env22 = getEnvironmentForNonInteractiveCommand(options.env);
|
||||
@@ -19321,7 +19321,7 @@ ${logOutput}`);
|
||||
});
|
||||
}
|
||||
function determineRepoBaseDirFromCwd() {
|
||||
const { stdout, stderr, status } = ChildProcess.spawnSync("git", ["rev-parse --show-toplevel"]);
|
||||
const { stdout, stderr, status } = ChildProcess.spawnSync("git", ["rev-parse", "--show-toplevel"]);
|
||||
if (status !== 0) {
|
||||
throw Error(`Unable to find the path to the base directory of the repository.
|
||||
Was the command run from inside of the repo?
|
||||
@@ -33595,7 +33595,7 @@ tmp/lib/tmp.js:
|
||||
(* v8 ignore next -- @preserve *)
|
||||
(* v8 ignore else -- @preserve *)
|
||||
|
||||
@angular/ng-dev/bundles/chunk-YN3IWAKJ.mjs:
|
||||
@angular/ng-dev/bundles/chunk-G7GMCCSS.mjs:
|
||||
(*! Bundled license information:
|
||||
|
||||
yargs-parser/build/lib/string-utils.js:
|
||||
@@ -33636,7 +33636,7 @@ tmp/lib/tmp.js:
|
||||
*)
|
||||
*)
|
||||
|
||||
@angular/ng-dev/bundles/chunk-TUTTLTAK.mjs:
|
||||
@angular/ng-dev/bundles/chunk-PTDPQBIK.mjs:
|
||||
(*! Bundled license information:
|
||||
|
||||
@octokit/request-error/dist-src/index.js:
|
||||
|
||||
@@ -32,13 +32,15 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: '${{secrets.GITHUB_TOKEN}}'
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure Firebase deploy target
|
||||
working-directory: ./
|
||||
run: |
|
||||
# We can use `npx` as the Firebase deploy actions uses it too.
|
||||
npx -y firebase-tools@latest target:clear --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs
|
||||
npx -y firebase-tools@latest target:apply --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs ${{env.PREVIEW_SITE}}
|
||||
# Use stable version release
|
||||
npx -y firebase-tools@15.15.0 target:clear --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs
|
||||
npx -y firebase-tools@15.15.0 target:apply --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs ${{env.PREVIEW_SITE}}
|
||||
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
|
||||
@@ -49,7 +49,8 @@ jobs:
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
run: pnpm benchmarks prepare-for-github-action "$COMMENT_BODY"
|
||||
|
||||
- run: pnpm benchmarks run-compare ${{steps.info.outputs.compareSha}} ${{steps.info.outputs.benchmarkTarget}}
|
||||
- run: pnpm benchmarks run-compare ${{steps.info.outputs.compareSha}} "${{steps.info.outputs.benchmarkTarget}}"
|
||||
|
||||
id: benchmark
|
||||
name: Running benchmark
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
ANGULAR_READONLY_GITHUB_TOKEN: ${{ secrets.READONLY_GITHUB_TOKEN }}
|
||||
|
||||
- name: Create a PR (if necessary)
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
token: ${{ secrets.ANGULAR_ROBOT_ACCESS_TOKEN }}
|
||||
push-to-fork: 'angular-robot/angular'
|
||||
|
||||
@@ -47,6 +47,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: 'Upload to code-scanning'
|
||||
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -1,3 +1,65 @@
|
||||
<a name="21.2.11"></a>
|
||||
# 21.2.11 (2026-04-29)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [10ad3c0692](https://github.com/angular/angular/commit/10ad3c06923453ae0ec06b06e664ce05900a4ff6) | fix | prevent focus from scrollToAnchor |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [4f5d8a2c0b](https://github.com/angular/angular/commit/4f5d8a2c0b5e38d4debc4293945270cea4a9590d) | fix | let declaration span not including end character |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [a40e2cebc8](https://github.com/angular/angular/commit/a40e2cebc878965c3e21bfb61658f3f80cbd2ebf) | fix | fix ordering of view queries metadata in JIT mode |
|
||||
| [885a1a1d97](https://github.com/angular/angular/commit/885a1a1d9757adfa8766d9b369c848a277438c31) | fix | guard against non-object events and avoid listener wrapper identity mismatch |
|
||||
| [7a64aff9b5](https://github.com/angular/angular/commit/7a64aff9b59999077ea915486a7fa0b97a286659) | fix | prevent event replay double-invocation when element hydrates before app stability |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [be1f80a253](https://github.com/angular/angular/commit/be1f80a253b8ee27ed7d8de2287d6895c4821909) | fix | ensure origin has a trailing slash when parsing url |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.10"></a>
|
||||
# 21.2.10 (2026-04-22)
|
||||
### docs
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0d5ee9ae1b](https://github.com/angular/angular/commit/0d5ee9ae1ba4b7acd8f27a059a778f0b4bd8a5bd) | fix | link formatting in "Animating your Application with CSS" |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5533ab4f56](https://github.com/angular/angular/commit/5533ab4f56f574bc9365cf0573c4a34a3ab5aaf1) | fix | fix NgClass leaving trailing comma after removal |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [580212c995](https://github.com/angular/angular/commit/580212c995751c4bf4ce8a49df4167498743e0ea) | fix | restore internal URL on popstate when `browserUrl` is used |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.9"></a>
|
||||
# 21.2.9 (2026-04-15)
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f603d4714f](https://github.com/angular/angular/commit/f603d4714fa184aad34a6f7f9ea4e79c8af3afac) | fix | escape forward slashes in transfer state to prevent crawler indexing |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [540536c386](https://github.com/angular/angular/commit/540536c386f2c735a700c2c9e2697a88dcb3d4ec) | fix | add CSP nonce support to JsonpClientBackend |
|
||||
| [63a857b874](https://github.com/angular/angular/commit/63a857b874172766451aa75ed3347ba50f0ee229) | fix | Don't on Passthru outside of reactive context |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e0b5078cf2](https://github.com/angular/angular/commit/e0b5078cf2ebe79a6de85e9123148ae948b3d81d) | fix | prevent SSRF bypasses via protocol-relative and backslash URLs |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [684e9fd53d](https://github.com/angular/angular/commit/684e9fd53daacb9e910f42d98c6017f9e5cb4180) | fix | normalize multiple leading slashes in URL parser |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.8"></a>
|
||||
# 21.2.8 (2026-04-08)
|
||||
### compiler
|
||||
|
||||
+13
-15
@@ -5,7 +5,7 @@ module(
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_pkg", version = "1.2.0")
|
||||
bazel_dep(name = "rules_nodejs", version = "6.7.3")
|
||||
bazel_dep(name = "rules_nodejs", version = "6.7.4")
|
||||
bazel_dep(name = "aspect_rules_ts", version = "3.8.8")
|
||||
bazel_dep(name = "aspect_rules_js", version = "3.0.3")
|
||||
bazel_dep(name = "aspect_rules_esbuild", version = "0.25.1")
|
||||
@@ -14,32 +14,32 @@ bazel_dep(name = "aspect_rules_rollup", version = "2.0.1")
|
||||
bazel_dep(name = "bazel_skylib", version = "1.9.0")
|
||||
bazel_dep(name = "bazel_lib", version = "3.2.2")
|
||||
bazel_dep(name = "tar.bzl", version = "0.10.1")
|
||||
bazel_dep(name = "yq.bzl", version = "0.3.5")
|
||||
bazel_dep(name = "yq.bzl", version = "0.3.6")
|
||||
bazel_dep(name = "rules_angular")
|
||||
git_override(
|
||||
module_name = "rules_angular",
|
||||
commit = "6c36180c2efebc6526ef0e6a55a6d738c7de6909",
|
||||
commit = "c6f7e15470934f7c2fe46ff5bae7d68be0230501",
|
||||
remote = "https://github.com/angular/rules_angular.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "devinfra")
|
||||
git_override(
|
||||
module_name = "devinfra",
|
||||
commit = "ba726e7bca0b08b125ccc6f93c233749e1213c17",
|
||||
commit = "f018eca84d10c594e16e9517ade1254a36dc8733",
|
||||
remote = "https://github.com/angular/dev-infra.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_sass")
|
||||
git_override(
|
||||
module_name = "rules_sass",
|
||||
commit = "b5ddaa8e77509bcce35158ad20009636d3da4fbc",
|
||||
commit = "dfb751533767caa759a7162a34cfe0852f988976",
|
||||
remote = "https://github.com/angular/rules_sass.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_browsers")
|
||||
git_override(
|
||||
module_name = "rules_browsers",
|
||||
commit = "652b57c41218be318f33fc92032696f53d3aa0ef",
|
||||
commit = "af35c89a5a099c4bbc3f1a3495688cf51bd6a8da",
|
||||
remote = "https://github.com/angular/rules_browsers.git",
|
||||
)
|
||||
|
||||
@@ -71,8 +71,8 @@ use_repo(node, "nodejs_windows_amd64")
|
||||
pnpm = use_extension("@aspect_rules_js//npm:extensions.bzl", "pnpm")
|
||||
pnpm.pnpm(
|
||||
name = "pnpm",
|
||||
pnpm_version = "10.33.0",
|
||||
pnpm_version_integrity = "sha512-EFaLtKavtYyes2MNqQzJUWQXq+vT+rvmc58K55VyjaFJHp21pUTHatjrdXD1xLs9bGN7LLQb/c20f6gjyGSTGQ==",
|
||||
pnpm_version = "10.33.2",
|
||||
pnpm_version_integrity = "sha512-qQ+vb+6rca1sblf5Tg/hoS9dzCLNdU20CulZPraj4LaxLjVAIYuzeuCDQEsfLObbKkEh6XmCm0r/lLmfSdoc+A==",
|
||||
)
|
||||
use_repo(pnpm, "pnpm")
|
||||
|
||||
@@ -125,14 +125,12 @@ use_repo(rules_ts_ext, **{"npm_typescript": "angular_npm_typescript"})
|
||||
|
||||
# TODO: Figure out how to make ng_project update whenever the packages/core::pkg target changes.
|
||||
rules_angular = use_extension("@rules_angular//setup:extensions.bzl", "rules_angular")
|
||||
|
||||
use_repo_rule("@rules_angular//setup:repositories.bzl", "configurable_deps_repo")(
|
||||
name = "rules_angular_configurable_deps",
|
||||
angular_compiler_cli = "@angular//:node_modules/@angular/compiler-cli",
|
||||
typescript = "@angular//:node_modules/typescript",
|
||||
rules_angular.setup(
|
||||
name = "angular_rules_angular_configurable_deps",
|
||||
angular_compiler_cli = "//:node_modules/@angular/compiler-cli",
|
||||
typescript = "//:node_modules/typescript",
|
||||
)
|
||||
|
||||
override_repo(rules_angular, "rules_angular_configurable_deps")
|
||||
use_repo(rules_angular, rules_angular_configurable_deps = "angular_rules_angular_configurable_deps")
|
||||
|
||||
register_toolchains(
|
||||
"@devinfra//bazel/git-toolchain:git_linux_toolchain",
|
||||
|
||||
Generated
+25
-24
@@ -161,7 +161,8 @@
|
||||
"https://bcr.bazel.build/modules/rules_nodejs/6.2.0/MODULE.bazel": "ec27907f55eb34705adb4e8257952162a2d4c3ed0f0b3b4c3c1aad1fac7be35e",
|
||||
"https://bcr.bazel.build/modules/rules_nodejs/6.5.0/MODULE.bazel": "546d0cf79f36f9f6e080816045f97234b071c205f4542e3351bd4424282a8810",
|
||||
"https://bcr.bazel.build/modules/rules_nodejs/6.7.3/MODULE.bazel": "c22a48b2a0dbf05a9dc5f83837bbc24c226c1f6e618de3c3a610044c9f336056",
|
||||
"https://bcr.bazel.build/modules/rules_nodejs/6.7.3/source.json": "a3f966f4415a8a6545e560ee5449eac95cc633f96429d08e87c87775c72f5e09",
|
||||
"https://bcr.bazel.build/modules/rules_nodejs/6.7.4/MODULE.bazel": "e6a241a55c82e999145553d2e00a08fc6ebadf62b63d108fb5e984696ffd0bd2",
|
||||
"https://bcr.bazel.build/modules/rules_nodejs/6.7.4/source.json": "34e7a8a3b4c8d630ac0e0492b3fed9dba41fe008a0edf220b7d88fa38ac53698",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/1.2.0/MODULE.bazel": "c7db3c2b407e673c7a39e3625dc05dc9f12d6682cbd82a3a5924a13b491eda7e",
|
||||
@@ -201,8 +202,8 @@
|
||||
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.3.2/MODULE.bazel": "0384efa70e8033d842ea73aa4b7199fa099709e236a7264345c03937166670b6",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.3.5/MODULE.bazel": "130c603e54be717bdf84100210f06598a0d2b4b4e01888fb01b70f50f41767ec",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.3.5/source.json": "1ae7bdc03cb26aaa8bd2bceadf65e90d90f0b2d03008ba9a0564da2e21396c39",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.3.6/MODULE.bazel": "985c2a0cb4ad9994bb0e33cc7fae931c91105eeefe3faa355b8f4c258d0607c0",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.3.6/source.json": "678aaf6e291164f3cd761bb3e872e8a151248f413dbb63c5524a50b82a5bc890",
|
||||
"https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
|
||||
"https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
|
||||
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca",
|
||||
@@ -429,7 +430,7 @@
|
||||
"@@aspect_rules_ts+//ts:extensions.bzl%ext": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "dhTbv9E6UfT1WJmmu3ORRPO6AKFJvgBjBxu+BO+u1RY=",
|
||||
"usagesDigest": "CnGVBnDYq2qAfYkDXgJlDcJckUt2NU51jQQX+igoGt8=",
|
||||
"usagesDigest": "QJswGu07xQeMlf+NomctjP9AY7OKl/U5GqMvEr6U1g8=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
@@ -447,8 +448,8 @@
|
||||
"rules_angular_npm_typescript": {
|
||||
"repoRuleId": "@@aspect_rules_ts+//ts/private:npm_repositories.bzl%http_archive_version",
|
||||
"attributes": {
|
||||
"version": "5.9.3",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"version": "6.0.2",
|
||||
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
|
||||
"urls": [
|
||||
"https://registry.npmjs.org/typescript/-/typescript-{}.tgz"
|
||||
]
|
||||
@@ -457,8 +458,8 @@
|
||||
"npm_typescript": {
|
||||
"repoRuleId": "@@aspect_rules_ts+//ts/private:npm_repositories.bzl%http_archive_version",
|
||||
"attributes": {
|
||||
"version": "5.9.3",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"version": "6.0.2",
|
||||
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
|
||||
"urls": [
|
||||
"https://registry.npmjs.org/typescript/-/typescript-{}.tgz"
|
||||
]
|
||||
@@ -467,8 +468,8 @@
|
||||
"npm_rules_browsers_typescript": {
|
||||
"repoRuleId": "@@aspect_rules_ts+//ts/private:npm_repositories.bzl%http_archive_version",
|
||||
"attributes": {
|
||||
"version": "5.9.3",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"version": "6.0.2",
|
||||
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
|
||||
"urls": [
|
||||
"https://registry.npmjs.org/typescript/-/typescript-{}.tgz"
|
||||
]
|
||||
@@ -560,24 +561,24 @@
|
||||
},
|
||||
"@@rules_angular+//setup:extensions.bzl%rules_angular": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "fkaH7HMicL3g7/NDaFzlq39kcLopMyQ3KdbDn+5CRzA=",
|
||||
"usagesDigest": "Li29KHtfoig0M7RQAoy9uoACa/RcVICtFkFkARsVOAA=",
|
||||
"bzlTransitiveDigest": "aS7Uud1IzoU7PPLzH3s6IfFS4b2fa0SRWDi2/fS4bQU=",
|
||||
"usagesDigest": "PaJB/TvnSzJTbqGUeIfiFAEjGkG4FEW7es6f6MFMtq8=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
"generatedRepoSpecs": {
|
||||
"angular_rules_angular_configurable_deps": {
|
||||
"repoRuleId": "@@rules_angular+//setup:repositories.bzl%configurable_deps_repo",
|
||||
"attributes": {
|
||||
"angular_compiler_cli": "@@//:node_modules/@angular/compiler-cli",
|
||||
"typescript": "@@//:node_modules/typescript"
|
||||
}
|
||||
},
|
||||
"rules_angular_configurable_deps": {
|
||||
"repoRuleId": "@@rules_angular+//setup:repositories.bzl%configurable_deps_repo",
|
||||
"attributes": {
|
||||
"angular_compiler_cli": "@@rules_angular+//:node_modules/@angular/compiler-cli",
|
||||
"typescript": "@@rules_angular+//:node_modules/typescript"
|
||||
}
|
||||
},
|
||||
"dev_infra_rules_angular_configurable_deps": {
|
||||
"repoRuleId": "@@rules_angular+//setup:repositories.bzl%configurable_deps_repo",
|
||||
"attributes": {
|
||||
"angular_compiler_cli": "@@rules_angular+//:node_modules/@angular/compiler-cli",
|
||||
"typescript": "@@rules_angular+//:node_modules/typescript"
|
||||
"angular_compiler_cli": "@@//:node_modules/@angular/compiler-cli",
|
||||
"typescript": "@@//:node_modules/typescript"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -945,8 +946,8 @@
|
||||
},
|
||||
"@@rules_nodejs+//nodejs:extensions.bzl%node": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "4pUxCNc22K4I+6+4Nxu52Hur12tFRfa1JMsN5mdDv60=",
|
||||
"usagesDigest": "o//mYtRPOfTZZh8pDGhbI7WGo7UOnSiSLgWYaJCEB+0=",
|
||||
"bzlTransitiveDigest": "oZFClfRhTTwsYzpxVPkOpOt/r0+OzEfEV37au0jFZ0s=",
|
||||
"usagesDigest": "dp2HPl9Y2BFhrnM0JALIsJHz02rKnRBpISBHQX4qV7E=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
@@ -4162,7 +4163,7 @@
|
||||
"@@yq.bzl+//yq:extensions.bzl%yq": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "UfFMy8CWK4/dVo/tfaSAIYUiDGNAPes5eRllx9O9Q9Q=",
|
||||
"usagesDigest": "263D9xYtKhXWWCVTxT66bpT89HuZpdkB1AqEir45vbY=",
|
||||
"usagesDigest": "5cUmZOEOibp2h65JoFppstHiXcjDFil/AG+HgD3avRk=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
|
||||
@@ -67,7 +67,7 @@ Install the Angular CLI globally:
|
||||
npm install -g @angular/cli
|
||||
```
|
||||
|
||||
Create workspace:
|
||||
Create a workspace:
|
||||
|
||||
```
|
||||
ng new [PROJECT NAME]
|
||||
|
||||
+6
-6
@@ -5,21 +5,21 @@
|
||||
"@algolia/requester-browser-xhr": "5.48.0",
|
||||
"@algolia/requester-node-http": "5.48.0",
|
||||
"@angular/animations": "workspace:*",
|
||||
"@angular/aria": "21.2.5",
|
||||
"@angular/build": "21.2.6",
|
||||
"@angular/cdk": "21.2.5",
|
||||
"@angular/cli": "21.2.6",
|
||||
"@angular/aria": "21.2.9",
|
||||
"@angular/build": "21.2.9",
|
||||
"@angular/cdk": "21.2.9",
|
||||
"@angular/cli": "21.2.9",
|
||||
"@angular/common": "workspace:*",
|
||||
"@angular/compiler": "workspace:*",
|
||||
"@angular/compiler-cli": "workspace:*",
|
||||
"@angular/core": "workspace:*",
|
||||
"@angular/docs": "workspace:*",
|
||||
"@angular/forms": "workspace:*",
|
||||
"@angular/material": "21.2.5",
|
||||
"@angular/material": "21.2.9",
|
||||
"@angular/platform-browser": "workspace:*",
|
||||
"@angular/platform-server": "workspace:*",
|
||||
"@angular/router": "workspace:*",
|
||||
"@angular/ssr": "21.2.6",
|
||||
"@angular/ssr": "21.2.9",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.1",
|
||||
"@codemirror/lang-angular": "0.1.4",
|
||||
|
||||
@@ -199,6 +199,23 @@ describe('jsdoc transforms', () => {
|
||||
|
||||
expect(entryFn).toThrowError(/Forbidden relative link: cli\/build ng build/);
|
||||
});
|
||||
|
||||
it('should throw on a miscased absolute @link to a known API symbol', () => {
|
||||
setSymbols({RouterModule: 'router'});
|
||||
|
||||
const entryFn = () =>
|
||||
addHtmlAdditionalLinks({
|
||||
jsdocTags: [
|
||||
{
|
||||
name: 'see',
|
||||
comment: '{@link /api/router/routerModule#forRoot forRoot}',
|
||||
},
|
||||
],
|
||||
moduleName: 'test',
|
||||
});
|
||||
|
||||
expect(entryFn).toThrowError(/Broken @link.*Did you mean \/api\/router\/RouterModule/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addHtmlDescription', () => {
|
||||
|
||||
@@ -223,6 +223,27 @@ function parseAtLink(link: string): {label: string; url: string} | undefined {
|
||||
);
|
||||
}
|
||||
|
||||
// Validate absolute `/api/...` links against the known symbol registry. This catches
|
||||
// miscased symbol names (e.g. `/api/router/routerModule` instead of
|
||||
// `/api/router/RouterModule`) at build time.
|
||||
if (rawSymbol.startsWith('/api/')) {
|
||||
const [pathPart] = rawSymbol.split('#');
|
||||
const segments = pathPart.split('/').filter((s) => s.length > 0);
|
||||
const symbolName = segments[segments.length - 1];
|
||||
// Case-insensitive lookup: find the canonical symbol name in the registry.
|
||||
const knownSymbols = Object.keys(getSymbolsAsApiEntries());
|
||||
const canonicalSymbol = knownSymbols.find(
|
||||
(s) => s.toLowerCase() === symbolName.toLowerCase(),
|
||||
);
|
||||
if (canonicalSymbol && canonicalSymbol !== symbolName) {
|
||||
const expectedUrl = getSymbolUrl(canonicalSymbol);
|
||||
throw Error(
|
||||
`Broken @link: ${link}. Did you mean ${expectedUrl}? ` +
|
||||
`Symbol names in API URLs are case-sensitive.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url: rawSymbol,
|
||||
label: description ?? rawSymbol.split('/').pop()!,
|
||||
|
||||
@@ -18,6 +18,7 @@ interface DocsCardToken extends Tokens.Generic {
|
||||
href?: string;
|
||||
imgSrc?: string;
|
||||
iconImgSrc?: string; // Need image since icons are custom
|
||||
titleInline?: boolean;
|
||||
tokens: Token[];
|
||||
}
|
||||
|
||||
@@ -30,6 +31,7 @@ const linkRule = /link="([^"]*)"/;
|
||||
const hrefRule = /href="([^"]*)"/;
|
||||
const imgSrcRule = /imgSrc="([^"]*)"/;
|
||||
const iconImgSrcRule = /iconImgSrc="([^"]*)"/;
|
||||
const titleInlineRule = /(?:^|\s)titleInline(?=\s|$|=)/;
|
||||
|
||||
export const docsCardExtension = {
|
||||
name: 'docs-card',
|
||||
@@ -47,6 +49,7 @@ export const docsCardExtension = {
|
||||
const href = hrefRule.exec(attr);
|
||||
const imgSrc = imgSrcRule.exec(attr);
|
||||
const iconImgSrc = iconImgSrcRule.exec(attr);
|
||||
const titleInline = titleInlineRule.test(attr);
|
||||
|
||||
const body = match[2].trim();
|
||||
|
||||
@@ -59,6 +62,7 @@ export const docsCardExtension = {
|
||||
link: link ? link[1] : undefined,
|
||||
imgSrc: imgSrc ? imgSrc[1] : undefined,
|
||||
iconImgSrc: iconImgSrc ? iconImgSrc[1] : undefined,
|
||||
titleInline,
|
||||
tokens: [],
|
||||
};
|
||||
this.lexer.blockTokens(token.body, token.tokens);
|
||||
@@ -79,12 +83,14 @@ function getStandardCard(renderer: AdevDocsRenderer, token: DocsCardToken) {
|
||||
// We need to read svg content, instead of renering svg with `img`,
|
||||
// cause we would like to use CSS variables to support dark and light mode.
|
||||
const icon = loadWorkspaceRelativeFile(token.iconImgSrc);
|
||||
const header = token.titleInline
|
||||
? `<div class="docs-card-header-inline">${icon}<h3>${token.title}</h3></div>`
|
||||
: `${icon}<h3>${token.title}</h3>`;
|
||||
|
||||
return `
|
||||
<a href="${token.href}" ${anchorTarget(token.href)} class="docs-card">
|
||||
<div>
|
||||
${icon}
|
||||
<h3>${token.title}</h3>
|
||||
${header}
|
||||
${renderer.parser.parse(token.tokens)}
|
||||
</div>
|
||||
<span>${token.link ? token.link : 'Learn more'}</span>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Card Grid
|
||||
|
||||
@mixin docs-card() {
|
||||
$compact-breakpoint: 550px;
|
||||
|
||||
.docs-card-container-wrapper {
|
||||
border: 1px solid var(--senary-contrast);
|
||||
border-radius: 0.25rem;
|
||||
@@ -15,6 +17,11 @@
|
||||
h2 {
|
||||
padding: 2.5rem 1rem 2.5rem 2.5rem;
|
||||
min-width: 350px;
|
||||
|
||||
@container docs-content (max-width: $compact-breakpoint) {
|
||||
min-width: auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-fill-bg {
|
||||
@@ -57,7 +64,11 @@
|
||||
svg {
|
||||
fill-opacity: 1;
|
||||
|
||||
@container header (max-width: 550px) {
|
||||
@container header (max-width: $compact-breakpoint) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@container docs-content (max-width: $compact-breakpoint) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -71,6 +82,11 @@
|
||||
margin: 1rem;
|
||||
padding: 1.5rem;
|
||||
|
||||
@container docs-content (max-width: $compact-breakpoint) {
|
||||
margin: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.docs-card {
|
||||
margin: 0;
|
||||
|
||||
@@ -143,6 +159,21 @@
|
||||
margin-block: 1.5rem;
|
||||
}
|
||||
|
||||
.docs-card-header-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
svg {
|
||||
margin-block-end: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-block: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.docs-card-with-svg {
|
||||
padding: 0;
|
||||
|
||||
@@ -252,7 +283,7 @@
|
||||
.docs-nav-card-svg {
|
||||
width: 350px;
|
||||
|
||||
@container header (max-width: 550px) {
|
||||
@container header (max-width: $compact-breakpoint) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
vertical-align: top;
|
||||
min-width: 10ch;
|
||||
}
|
||||
td:has(code) {
|
||||
min-width: 8ch;
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-block-end: 1px solid var(--senary-contrast);
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
width: 100%;
|
||||
padding-inline: calc(var(--layout-padding) - 1.25rem);
|
||||
height: auto;
|
||||
scrollbar-width: none;
|
||||
padding-block: 0;
|
||||
}
|
||||
|
||||
@@ -169,6 +170,7 @@
|
||||
|
||||
@include mq.for-tablet {
|
||||
flex-direction: row;
|
||||
padding-block-start: 0;
|
||||
}
|
||||
|
||||
// version dropdown button
|
||||
@@ -233,7 +235,9 @@
|
||||
|
||||
@include mq.for-tablet {
|
||||
flex-direction: row !important;
|
||||
align-items: center;
|
||||
margin-inline-end: 1.25rem;
|
||||
margin-top: 0.25rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@@ -289,6 +293,8 @@
|
||||
|
||||
@include mq.for-phone-only {
|
||||
display: block;
|
||||
position: relative;
|
||||
top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
|
||||
<section class="explore-section" id="learn-more">
|
||||
<div class="title">
|
||||
<h2>What to learn more about Angular?</h2>
|
||||
<h2>Want to learn more about Angular?</h2>
|
||||
<div class="pattern"></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -28,8 +28,12 @@
|
||||
left: calc(var(--layout-padding) + var(--primary-nav-width));
|
||||
z-index: 1;
|
||||
|
||||
@include mq.for-tablet-down {
|
||||
justify-content: flex-start;
|
||||
@include mq.for-tablet-landscape-down {
|
||||
justify-content: center;
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
@include mq.for-phone-only {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
@@ -182,7 +186,7 @@ section {
|
||||
}
|
||||
|
||||
.search-field {
|
||||
@include mq.for-tablet-down() {
|
||||
@include mq.for-tablet-landscape-down() {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -269,6 +273,11 @@ section {
|
||||
width: fit-content;
|
||||
margin: 0 auto 2rem;
|
||||
|
||||
@include mq.for-phone-only {
|
||||
width: auto;
|
||||
margin: 0 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.tab-background {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
border-radius: 0.25rem;
|
||||
padding: 0;
|
||||
transform: translateY(-0.7rem);
|
||||
background: var(--page-background);
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
|
||||
@@ -33,6 +33,7 @@ describe('TutorialPlayground', () => {
|
||||
|
||||
class FakeNodeRuntimeSandbox {
|
||||
init() {}
|
||||
reset() {}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
@@ -56,4 +57,22 @@ describe('TutorialPlayground', () => {
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not call reset on the sandbox before it is initialized', async () => {
|
||||
const fakeSandbox = {reset: jasmine.createSpy('reset')} as any;
|
||||
component['nodeRuntimeSandbox'] = fakeSandbox;
|
||||
component['isSandboxReady'].set(false);
|
||||
spyOn<any>(component, 'loadTemplate').and.resolveTo();
|
||||
await component.changeTemplate(component.templates[1]);
|
||||
expect(fakeSandbox.reset).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call reset on the sandbox after it is initialized', async () => {
|
||||
const fakeSandbox = {reset: jasmine.createSpy('reset')} as any;
|
||||
component['nodeRuntimeSandbox'] = fakeSandbox;
|
||||
component['isSandboxReady'].set(true);
|
||||
spyOn<any>(component, 'loadTemplate').and.resolveTo();
|
||||
await component.changeTemplate(component.templates[1]);
|
||||
expect(fakeSandbox.reset).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
DestroyRef,
|
||||
EnvironmentInjector,
|
||||
PLATFORM_ID,
|
||||
signal,
|
||||
Type,
|
||||
effect,
|
||||
inject,
|
||||
@@ -55,6 +56,7 @@ export default class PlaygroundComponent {
|
||||
protected nodeRuntimeSandbox?: NodeRuntimeSandbox;
|
||||
protected embeddedEditorComponent?: Type<unknown>;
|
||||
protected selectedTemplate: PlaygroundTemplate = this.defaultTemplate;
|
||||
private readonly isSandboxReady = signal(false);
|
||||
|
||||
constructor() {
|
||||
if (this.isServer) {
|
||||
@@ -84,6 +86,7 @@ export default class PlaygroundComponent {
|
||||
.subscribe(() => {
|
||||
this.changeDetectorRef.markForCheck();
|
||||
this.nodeRuntimeSandbox?.init();
|
||||
this.isSandboxReady.set(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,7 +102,9 @@ export default class PlaygroundComponent {
|
||||
});
|
||||
this.selectedTemplate = template;
|
||||
await this.loadTemplate(template.path);
|
||||
await this.nodeRuntimeSandbox?.reset();
|
||||
if (this.isSandboxReady()) {
|
||||
await this.nodeRuntimeSandbox?.reset();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadTemplate(tutorialPath: string) {
|
||||
|
||||
+59
-28
@@ -28,10 +28,11 @@
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
.adev-reference-list-type-filter-label {
|
||||
margin-block: 2.5rem 1rem;
|
||||
margin-block: 1rem;
|
||||
}
|
||||
|
||||
.adev-reference-list-type-filter {
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
margin-block: 0;
|
||||
@@ -45,6 +46,7 @@
|
||||
}
|
||||
@container api-ref-page (max-width: 600px) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
max-width: 500px;
|
||||
}
|
||||
@container api-ref-page (max-width: 500px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
@@ -88,16 +90,70 @@
|
||||
|
||||
.adev-reference-list-query-filter {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
docs-text-field,
|
||||
docs-select {
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
@container api-ref-page (max-width: 600px) {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
docs-text-field,
|
||||
docs-select {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adev-reference-list-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
|
||||
::ng-deep .mat-mdc-chip-listbox {
|
||||
@container api-ref-page (max-width: 600px) {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
|
||||
.mdc-evolution-chip-set__chips {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.mat-mdc-chip-option {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@container api-ref-page (max-width: 350px) {
|
||||
.mdc-evolution-chip-set__chips {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adev-reference-list-empty {
|
||||
text-align: center;
|
||||
margin-block-start: 2rem;
|
||||
flex-basis: 100%;
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
@@ -108,28 +164,3 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.adev-reference-list-empty {
|
||||
flex-basis: 100%;
|
||||
p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.docs-api-item-label-full {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.map-chip-option {
|
||||
min-width: 190px;
|
||||
}
|
||||
|
||||
.adev-reference-list-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
|
||||
label {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@ export enum ApplicationComplexity {
|
||||
|
||||
export interface Step {
|
||||
step: string;
|
||||
/**
|
||||
* Action text rendered as Markdown. Use Markdown link syntax `[text](url)`
|
||||
* for any links — raw `<a>` HTML tags bypass the custom link renderer in
|
||||
* `update.component.ts` and will not pick up `target="_blank"` or the
|
||||
* external-link icon.
|
||||
*/
|
||||
action: string;
|
||||
possibleIn: number;
|
||||
necessaryAsOf: number;
|
||||
@@ -155,7 +161,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Advanced,
|
||||
step: 'gendir',
|
||||
action:
|
||||
'Do not rely on `gendir`, instead look at using `skipTemplateCodeGen`. <a href=https://github.com/angular/angular/issues/19339#issuecomment-332607471" target="_blank">Read More</a>',
|
||||
'Do not rely on `gendir`, instead look at using `skipTemplateCodeGen`. [Read More](https://github.com/angular/angular/issues/19339#issuecomment-332607471)',
|
||||
},
|
||||
{
|
||||
possibleIn: 220,
|
||||
@@ -239,7 +245,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'node 8',
|
||||
action:
|
||||
'Make sure you are using <a href="http://www.hostingadvice.com/how-to/update-node-js-latest-version/" target="_blank">Node 8 or later</a>',
|
||||
'Make sure you are using [Node 8 or later](http://www.hostingadvice.com/how-to/update-node-js-latest-version/)',
|
||||
},
|
||||
{
|
||||
possibleIn: 600,
|
||||
@@ -248,7 +254,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
windows: false,
|
||||
step: 'Update to CLI v6',
|
||||
action:
|
||||
'Update your Angular CLI, and migrate the configuration to the <a href="https://github.com/angular/angular-cli/wiki/angular-workspace" target="_blank">new angular.json format</a> by running the following:<br/><br/>`NG_DISABLE_VERSION_CHECK=1 npx @angular/cli@6 update @angular/cli@6`<br/>',
|
||||
'Update your Angular CLI, and migrate the configuration to the [new angular.json format](https://github.com/angular/angular-cli/wiki/angular-workspace) by running the following:<br/><br/>`NG_DISABLE_VERSION_CHECK=1 npx @angular/cli@6 update @angular/cli@6`<br/>',
|
||||
},
|
||||
{
|
||||
possibleIn: 600,
|
||||
@@ -257,7 +263,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
windows: true,
|
||||
step: 'Update to CLI v6',
|
||||
action:
|
||||
'Update your Angular CLI, and migrate the configuration to the <a href="https://github.com/angular/angular-cli/wiki/angular-workspace" target="_blank">new angular.json format</a> by running the following:<br/><br/>`cmd /C "set "NG_DISABLE_VERSION_CHECK=1" && npx @angular/cli@6 update @angular/cli@6 @angular/core@6"`<br/>',
|
||||
'Update your Angular CLI, and migrate the configuration to the [new angular.json format](https://github.com/angular/angular-cli/wiki/angular-workspace) by running the following:<br/><br/>`cmd /C "set "NG_DISABLE_VERSION_CHECK=1" && npx @angular/cli@6 update @angular/cli@6 @angular/core@6"`<br/>',
|
||||
},
|
||||
{
|
||||
possibleIn: 600,
|
||||
@@ -343,7 +349,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Medium,
|
||||
step: 'strictPropertyInitializer',
|
||||
action:
|
||||
'If you have TypeScript configured to be strict (if you have set `strict` to `true` in your `tsconfig.json` file), update your `tsconfig.json` to disable `strictPropertyInitialization` or move property initialization from `ngOnInit` to your constructor. You can learn more about this flag on the <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#strict-class-initialization">TypeScript 2.7 release notes</a>.',
|
||||
'If you have TypeScript configured to be strict (if you have set `strict` to `true` in your `tsconfig.json` file), update your `tsconfig.json` to disable `strictPropertyInitialization` or move property initialization from `ngOnInit` to your constructor. You can learn more about this flag on the [TypeScript 2.7 release notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#strict-class-initialization).',
|
||||
},
|
||||
{
|
||||
possibleIn: 600,
|
||||
@@ -351,7 +357,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'update to RxJS 6',
|
||||
action:
|
||||
'Remove deprecated RxJS 5 features using <a href="https://github.com/ReactiveX/rxjs-tslint" target="_blank">rxjs-tslint auto update rules</a><br/><br/>For most applications this will mean running the following two commands:<br/><br/>`npx rxjs-tslint`<br/>`rxjs-5-to-6-migrate -p src/tsconfig.app.json`',
|
||||
'Remove deprecated RxJS 5 features using [rxjs-tslint auto update rules](https://github.com/ReactiveX/rxjs-tslint)<br/><br/>For most applications this will mean running the following two commands:<br/><br/>`npx rxjs-tslint`<br/>`rxjs-5-to-6-migrate -p src/tsconfig.app.json`',
|
||||
},
|
||||
{
|
||||
possibleIn: 600,
|
||||
@@ -374,7 +380,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'TypeScript 3.1',
|
||||
action:
|
||||
'Angular now uses TypeScript 3.1, read more about any potential breaking changes: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-1.html',
|
||||
'Angular now uses TypeScript 3.1, read more about [any potential breaking changes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-1.html).',
|
||||
},
|
||||
{
|
||||
possibleIn: 700,
|
||||
@@ -382,7 +388,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'Node 10',
|
||||
action:
|
||||
'Angular has now added support for Node 10: https://nodejs.org/en/blog/release/v10.0.0/',
|
||||
'Angular has now added support for [Node 10](https://nodejs.org/en/blog/release/v10.0.0/).',
|
||||
},
|
||||
{
|
||||
possibleIn: 700,
|
||||
@@ -464,7 +470,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'use ::ng-deep instead of /deep/',
|
||||
action:
|
||||
'Replace `/deep/` with `::ng-deep` in your styles, [read more about angular component styles and ::ng-deep](https://angular.io/guide/component-styles#deprecated-deep--and-ng-deep). `/deep/` and `::ng-deep` both are deprecated but using `::ng-deep` is preferred until the shadow-piercing descendant combinator is [removed from browsers and tools](https://www.chromestatus.com/features/6750456638341120) completely.',
|
||||
'Replace `/deep/` with `::ng-deep` in your styles, [read more about angular component styles and ::ng-deep](https://angular.io/guide/component-styles#deprecated-deep--and-ng-deep). `/deep/` and `::ng-deep` both are deprecated but using `::ng-deep` is preferred until the shadow-piercing descendant combinator is [removed from browsers and tools](https://chromestatus.com/feature/5045542597951488) completely.',
|
||||
},
|
||||
{
|
||||
possibleIn: 800,
|
||||
@@ -480,7 +486,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'node 10',
|
||||
action:
|
||||
'Make sure you are using <a href="http://www.hostingadvice.com/how-to/update-node-js-latest-version/" target="_blank">Node 10 or later</a>.',
|
||||
'Make sure you are using [Node 10 or later](http://www.hostingadvice.com/how-to/update-node-js-latest-version/).',
|
||||
},
|
||||
{
|
||||
possibleIn: 800,
|
||||
@@ -573,7 +579,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'node 10.13',
|
||||
action:
|
||||
'Make sure you are using <a href="http://www.hostingadvice.com/how-to/update-node-js-latest-version/" target="_blank">Node 10.13 or later</a>.',
|
||||
'Make sure you are using [Node 10.13 or later](http://www.hostingadvice.com/how-to/update-node-js-latest-version/).',
|
||||
},
|
||||
{
|
||||
possibleIn: 900,
|
||||
@@ -782,8 +788,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
necessaryAsOf: 1000,
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'v10 NodeJS 12',
|
||||
action:
|
||||
'Make sure you are using <a href="https://nodejs.org/dist/latest-v12.x/" target="_blank">Node 12 or later</a>.',
|
||||
action: 'Make sure you are using [Node 12 or later](https://nodejs.org/dist/latest-v12.x/).',
|
||||
},
|
||||
{
|
||||
possibleIn: 1000,
|
||||
@@ -871,7 +876,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Medium,
|
||||
step: 'esm5-bundles',
|
||||
action:
|
||||
"The [Angular Package Format](https://g.co/ng/apf) has been updated to remove `esm5` and `fesm5` formats. These are no longer distributed in our npm packages. If you don't use the CLI, you may need to downlevel Angular code to ES5 yourself.",
|
||||
"The [Angular Package Format](https://angular.dev/tools/libraries/angular-package-format) has been updated to remove `esm5` and `fesm5` formats. These are no longer distributed in our npm packages. If you don't use the CLI, you may need to downlevel Angular code to ES5 yourself.",
|
||||
},
|
||||
{
|
||||
possibleIn: 1000,
|
||||
@@ -1335,7 +1340,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'TypeScript 4.4',
|
||||
action:
|
||||
'Angular now uses TypeScript 4.4, read more about any potential breaking changes: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-4.html',
|
||||
'Angular now uses TypeScript 4.4, read more about [any potential breaking changes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-4.html).',
|
||||
},
|
||||
{
|
||||
possibleIn: 1300,
|
||||
@@ -1343,7 +1348,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'v13 node',
|
||||
action:
|
||||
'Make sure you are using <a href="http://www.hostingadvice.com/how-to/update-node-js-latest-version/" target="_blank">Node 12.20.0 or later</a>',
|
||||
'Make sure you are using [Node 12.20.0 or later](http://www.hostingadvice.com/how-to/update-node-js-latest-version/)',
|
||||
},
|
||||
{
|
||||
possibleIn: 1300,
|
||||
@@ -1457,7 +1462,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'TypeScript 4.6',
|
||||
action:
|
||||
'Angular now uses TypeScript 4.6, read more about any potential breaking changes: https://devblogs.microsoft.com/typescript/announcing-typescript-4-6/',
|
||||
'Angular now uses TypeScript 4.6, read more about [any potential breaking changes](https://devblogs.microsoft.com/typescript/announcing-typescript-4-6/).',
|
||||
},
|
||||
|
||||
{
|
||||
@@ -1466,7 +1471,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'v14 node',
|
||||
action:
|
||||
'Make sure you are using <a href="http://www.hostingadvice.com/how-to/update-node-js-latest-version/" target="_blank">Node 14.15.0 or later</a>',
|
||||
'Make sure you are using [Node 14.15.0 or later](http://www.hostingadvice.com/how-to/update-node-js-latest-version/)',
|
||||
},
|
||||
{
|
||||
possibleIn: 1400,
|
||||
@@ -1669,7 +1674,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'v15 node support',
|
||||
action:
|
||||
'Make sure that you are using a supported version of node.js before you upgrade your application. Angular v15 supports node.js versions: 14.20.x, 16.13.x and 18.10.x. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-01" alt="Link to more information about this change">Read further</a>',
|
||||
'Make sure that you are using a supported version of node.js before you upgrade your application. Angular v15 supports node.js versions: 14.20.x, 16.13.x and 18.10.x. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-01)',
|
||||
},
|
||||
{
|
||||
possibleIn: 1500,
|
||||
@@ -1677,7 +1682,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'v15 ts support',
|
||||
action:
|
||||
'Make sure that you are using a supported version of TypeScript before you upgrade your application. Angular v15 supports TypeScript version 4.8 or later. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-02" alt="Link to more information about this change">Read further</a>',
|
||||
'Make sure that you are using a supported version of TypeScript before you upgrade your application. Angular v15 supports TypeScript version 4.8 or later. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-02)',
|
||||
},
|
||||
{
|
||||
possibleIn: 1500,
|
||||
@@ -1701,7 +1706,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Medium,
|
||||
step: 'v15 keyframe',
|
||||
action:
|
||||
'In v15, the Angular compiler prefixes `@keyframes` in CSS with the component\'s scope. This means that any TypeScript code that relies on `keyframes` names no longer works in v15. Update any such instances to: define keyframes programmatically, use global stylesheets, or change the component\'s view encapsulation. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-03" alt="Link to more information about this change">Read further</a>',
|
||||
"In v15, the Angular compiler prefixes `@keyframes` in CSS with the component's scope. This means that any TypeScript code that relies on `keyframes` names no longer works in v15. Update any such instances to: define keyframes programmatically, use global stylesheets, or change the component's view encapsulation. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-03)",
|
||||
},
|
||||
{
|
||||
possibleIn: 1500,
|
||||
@@ -1717,7 +1722,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Medium,
|
||||
step: 'v15 base-decorators',
|
||||
action:
|
||||
'Make sure to use decorators in base classes with child classes that inherit constructors and use dependency injection. Such base classes should be decorated with either `@Injectable` or `@Directive` or the compiler returns an error. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-05" alt="Link to more information about this change">Read further</a>',
|
||||
'Make sure to use decorators in base classes with child classes that inherit constructors and use dependency injection. Such base classes should be decorated with either `@Injectable` or `@Directive` or the compiler returns an error. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-05)',
|
||||
},
|
||||
{
|
||||
possibleIn: 1500,
|
||||
@@ -1725,7 +1730,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Medium,
|
||||
step: 'v15 setDisabledState',
|
||||
action:
|
||||
'In v15, `setDisabledState` is always called when a `ControlValueAccessor` is attached. To opt-out of this behavior, use `FormsModule.withConfig` or `ReactiveFormsModule.withConfig`. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-06" alt="Link to more information about this change">Read further</a>',
|
||||
'In v15, `setDisabledState` is always called when a `ControlValueAccessor` is attached. To opt-out of this behavior, use `FormsModule.withConfig` or `ReactiveFormsModule.withConfig`. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-06)',
|
||||
},
|
||||
{
|
||||
possibleIn: 1500,
|
||||
@@ -1733,7 +1738,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Advanced,
|
||||
step: 'v15 canParse',
|
||||
action:
|
||||
'Applications that use `canParse` should use `analyze` from `@angular/localize/tools` instead. In v15, the `canParse` method was removed from all translation parsers in `@angular/localize/tools`. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-07" alt="Link to more information about this change">Read further</a>',
|
||||
'Applications that use `canParse` should use `analyze` from `@angular/localize/tools` instead. In v15, the `canParse` method was removed from all translation parsers in `@angular/localize/tools`. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-07)',
|
||||
},
|
||||
{
|
||||
possibleIn: 1500,
|
||||
@@ -1741,7 +1746,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'v15 ActivatedRoutSnapshot',
|
||||
action:
|
||||
'Make sure that all `ActivatedRouteSnapshot` objects have a `title` property. In v15, the `title` property is a required property of `ActivatedRouteSnapshot`. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-08" alt="Link to more information about this change">Read further</a>',
|
||||
'Make sure that all `ActivatedRouteSnapshot` objects have a `title` property. In v15, the `title` property is a required property of `ActivatedRouteSnapshot`. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-08)',
|
||||
},
|
||||
{
|
||||
possibleIn: 1500,
|
||||
@@ -1749,7 +1754,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Advanced,
|
||||
step: 'v15 RouterOutlet',
|
||||
action:
|
||||
'If your tests with `RouterOutlet` break, make sure they don\'t depend on the instantiation order of the corresponding component relative to change detection. In v15, `RouterOutlet` instantiates the component after change detection. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-09" alt="Link to more information about this change">Read further</a>',
|
||||
"If your tests with `RouterOutlet` break, make sure they don't depend on the instantiation order of the corresponding component relative to change detection. In v15, `RouterOutlet` instantiates the component after change detection. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-09)",
|
||||
},
|
||||
{
|
||||
possibleIn: 1500,
|
||||
@@ -1757,7 +1762,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'v15 relativeLinkResolution',
|
||||
action:
|
||||
'In v15, `relativeLinkResolution` is not configurable in the Router. It was used to opt out of an earlier bug fix that is now standard. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-10" alt="Link to more information about this change">Read further</a>',
|
||||
'In v15, `relativeLinkResolution` is not configurable in the Router. It was used to opt out of an earlier bug fix that is now standard. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-10)',
|
||||
},
|
||||
{
|
||||
possibleIn: 1500,
|
||||
@@ -1765,7 +1770,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Medium,
|
||||
step: 'v15 DATE_PIPE_DEFAULT_OPTIONS',
|
||||
action:
|
||||
'Change instances of the `DATE_PIPE_DEFAULT_TIMEZONE` token to use `DATE_PIPE_DEFAULT_OPTIONS` to configure time zones. In v15, the `DATE_PIPE_DEFAULT_TIMEZONE` token is deprecated. <a href="https://v15.angular.io/guide/update-to-version-15#v15-dp-01" alt="Link to more information about this change">Read further</a>',
|
||||
'Change instances of the `DATE_PIPE_DEFAULT_TIMEZONE` token to use `DATE_PIPE_DEFAULT_OPTIONS` to configure time zones. In v15, the `DATE_PIPE_DEFAULT_TIMEZONE` token is deprecated. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-dp-01)',
|
||||
},
|
||||
{
|
||||
possibleIn: 1500,
|
||||
@@ -1781,7 +1786,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Medium,
|
||||
step: 'v15 Injector.get',
|
||||
action:
|
||||
'Update instances of `Injector.get()` that use an `InjectFlags` parameter to use an `InjectOptions` parameter. The `InjectFlags` parameter of `Injector.get()` is deprecated in v15. <a href="https://v15.angular.io/guide/update-to-version-15#v15-dp-02" alt="Link to more information about this change">Read further</a>',
|
||||
'Update instances of `Injector.get()` that use an `InjectFlags` parameter to use an `InjectOptions` parameter. The `InjectFlags` parameter of `Injector.get()` is deprecated in v15. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-dp-02)',
|
||||
},
|
||||
{
|
||||
possibleIn: 1500,
|
||||
@@ -1789,7 +1794,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'v15 TestBed.inject',
|
||||
action:
|
||||
'Update instances of `TestBed.inject()` that use an `InjectFlags` parameter to use an `InjectOptions` parameter. The `InjectFlags` parameter of `TestBed.inject()` is deprecated in v15. <a href="https://v15.angular.io/guide/update-to-version-15#v15-dp-01" alt="Link to more information about this change">Read further</a>',
|
||||
'Update instances of `TestBed.inject()` that use an `InjectFlags` parameter to use an `InjectOptions` parameter. The `InjectFlags` parameter of `TestBed.inject()` is deprecated in v15. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-dp-01)',
|
||||
},
|
||||
{
|
||||
possibleIn: 1500,
|
||||
@@ -1797,7 +1802,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Medium,
|
||||
step: 'v15 ngModule in providedIn',
|
||||
action:
|
||||
'Using `providedIn: ngModule` for an `@Injectable` and `InjectionToken` is deprecated in v15. <a href="https://v15.angular.io/guide/update-to-version-15#v15-dp-04" alt="Link to more information about this change">Read further</a>',
|
||||
'Using `providedIn: ngModule` for an `@Injectable` and `InjectionToken` is deprecated in v15. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-dp-04)',
|
||||
},
|
||||
{
|
||||
possibleIn: 1500,
|
||||
@@ -1805,7 +1810,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'v15 providedIn any',
|
||||
action:
|
||||
'Using `providedIn: \'any\'` for an `@Injectable` or `InjectionToken` is deprecated in v15. <a href="https://v15.angular.io/guide/update-to-version-15#v15-dp-05" alt="Link to more information about this change">Read further</a>',
|
||||
"Using `providedIn: 'any'` for an `@Injectable` or `InjectionToken` is deprecated in v15. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-dp-05)",
|
||||
},
|
||||
{
|
||||
possibleIn: 1500,
|
||||
@@ -1813,7 +1818,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Medium,
|
||||
step: 'v15 RouterLinkWithHref',
|
||||
action:
|
||||
'Update instances of the `RouterLinkWithHref`directive to use the `RouterLink` directive. The `RouterLinkWithHref` directive is deprecated in v15. <a href="https://v15.angular.io/guide/update-to-version-15#v15-dp-06" alt="Link to more information about this change">Read further</a>',
|
||||
'Update instances of the `RouterLinkWithHref`directive to use the `RouterLink` directive. The `RouterLinkWithHref` directive is deprecated in v15. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-dp-06)',
|
||||
},
|
||||
{
|
||||
possibleIn: 1500,
|
||||
@@ -1822,7 +1827,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
material: true,
|
||||
step: 'v15 mat refactor',
|
||||
action:
|
||||
'In Angular Material v15, many of the components have been refactored to be based on the official Material Design Components for Web (MDC). This change affected the DOM and CSS classes of many components. <a href="https://rc.material.angular.dev/guide/mdc-migration" alt="Link to more information about this change">Read further</a>',
|
||||
'In Angular Material v15, many of the components have been refactored to be based on the official Material Design Components for Web (MDC). This change affected the DOM and CSS classes of many components. [Read further](https://rc.material.angular.dev/guide/mdc-migration)',
|
||||
},
|
||||
{
|
||||
possibleIn: 1500,
|
||||
@@ -2669,7 +2674,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: '20.0.0_update_nodejs_version',
|
||||
action:
|
||||
'Ensure your Node.js version is at least 20.11.1 and not v18 or v22.0-v22.10 before upgrading to Angular v20. Check https://angular.dev/reference/versions for the full list of supported Node.js versions.',
|
||||
'Ensure your Node.js version is at least 20.11.1 and not v18 or v22.0-v22.10 before upgrading to Angular v20. Check the [full list of supported Node.js versions](https://angular.dev/reference/versions).',
|
||||
},
|
||||
{
|
||||
possibleIn: 2000,
|
||||
@@ -2703,6 +2708,14 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
action:
|
||||
"Upgrade your project's TypeScript version to at least 5.8 before upgrading to Angular v20 to ensure compatibility.",
|
||||
},
|
||||
{
|
||||
possibleIn: 2000,
|
||||
necessaryAsOf: 2000,
|
||||
level: ApplicationComplexity.Medium,
|
||||
step: '20.0.0_set_moduleResolution_to_bundler',
|
||||
action:
|
||||
"Set `moduleResolution` to `'bundler'` in your `tsconfig.json`. Angular CLI's `ng update` migration applies this change automatically; if you upgrade manually or override the option in a base tsconfig, set it explicitly so imports of secondary entry-points such as `@angular/core/rxjs-interop` continue to resolve correctly.",
|
||||
},
|
||||
{
|
||||
possibleIn: 2000,
|
||||
necessaryAsOf: 2000,
|
||||
@@ -2912,4 +2925,12 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
action:
|
||||
'The `lastSuccessfulNavigation` property on the Router has been converted to a signal. To get its value, you now need to invoke it as a function: `router.lastSuccessfulNavigation()`.',
|
||||
},
|
||||
{
|
||||
possibleIn: 2100,
|
||||
necessaryAsOf: 2100,
|
||||
level: ApplicationComplexity.Medium,
|
||||
step: '21.0.0-configure-commonengine-allowed-hosts',
|
||||
action:
|
||||
"Starting `@angular/ssr` 21.1.5, if your application uses SSR with `CommonEngine`, set the `allowedHosts` option in your `server.ts` (for example, `new CommonEngine({allowedHosts: ['localhost', '*.yourdomain.com']})`). Without it, SSR silently falls back to client-side rendering. This requirement comes from security advisory [GHSA-x288-3778-4hhx](https://github.com/angular/angular-cli/security/advisories/GHSA-x288-3778-4hhx) (also backported to 20.3.17 and 19.2.21).",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@use '@angular/docs/styles/media-queries' as mq;
|
||||
@use '@angular/docs/styles/links' as links;
|
||||
|
||||
$ver-dropdown-width: clamp(165px, 20vw, 200px);
|
||||
|
||||
@@ -172,7 +173,14 @@ h4 {
|
||||
}
|
||||
|
||||
// Code blocks are generable from the markdown, we need to opt-out of the scoping
|
||||
::ng-deep code {
|
||||
cursor: pointer;
|
||||
::ng-deep {
|
||||
code {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// External links (rendered with target="_blank") get an "open in new" icon
|
||||
a[target='_blank'] {
|
||||
@include links.external-link-with-icon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {provideHttpClientTesting} from '@angular/common/http/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
|
||||
import UpdateComponent from './update.component';
|
||||
import {ApplicationComplexity} from './recommendations';
|
||||
import {ApplicationComplexity, RECOMMENDATIONS} from './recommendations';
|
||||
|
||||
describe('UpdateComponent', () => {
|
||||
let component: UpdateComponent;
|
||||
@@ -91,4 +91,14 @@ describe('UpdateComponent', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('RECOMMENDATIONS data', () => {
|
||||
it('should use Markdown links instead of raw <a> HTML tags', () => {
|
||||
for (const step of RECOMMENDATIONS) {
|
||||
expect(step.action)
|
||||
.withContext(`step "${step.step}" should use [text](url), not raw <a> HTML`)
|
||||
.not.toMatch(/<a\s/i);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,20 @@ import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {marked} from 'marked';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
|
||||
/**
|
||||
* Configure marked with a custom link renderer so external links in the
|
||||
* update guide open in a new tab, matching the convention applied elsewhere
|
||||
* in adev via the `ExternalLink` directive.
|
||||
*/
|
||||
marked.use({
|
||||
renderer: {
|
||||
link({href, title, text}) {
|
||||
const titleAttr = title ? ` title="${title}"` : '';
|
||||
return `<a href="${href}"${titleAttr} target="_blank" rel="noopener noreferrer">${text}</a>`;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
interface Option {
|
||||
id: keyof Step;
|
||||
name: string;
|
||||
|
||||
@@ -496,6 +496,27 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
category: 'Signal Forms',
|
||||
status: 'new',
|
||||
},
|
||||
{
|
||||
label: 'Cross-field logic',
|
||||
path: 'guide/forms/signals/cross-field-logic',
|
||||
contentPath: 'guide/forms/signals/cross-field-logic',
|
||||
category: 'Signal Forms',
|
||||
status: 'new',
|
||||
},
|
||||
{
|
||||
label: 'Form submission',
|
||||
path: 'guide/forms/signals/form-submission',
|
||||
contentPath: 'guide/forms/signals/form-submission',
|
||||
category: 'Signal Forms',
|
||||
status: 'new',
|
||||
},
|
||||
{
|
||||
label: 'Schemas',
|
||||
path: 'guide/forms/signals/schemas',
|
||||
contentPath: 'guide/forms/signals/schemas',
|
||||
category: 'Signal Forms',
|
||||
status: 'new',
|
||||
},
|
||||
{
|
||||
label: 'Async operations',
|
||||
path: 'guide/forms/signals/async-operations',
|
||||
@@ -1044,16 +1065,15 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
},
|
||||
// TODO: create those guides
|
||||
// The signal debugging docs should also be added to the signal section
|
||||
// {
|
||||
// label: 'Signals',
|
||||
// path: 'tools/devtools/signals',
|
||||
// contentPath: 'tools/devtools/signals',
|
||||
// },
|
||||
// {
|
||||
// label: 'Router',
|
||||
// path: 'tools/devtools/router',
|
||||
// contentPath: 'tools/devtools/router',
|
||||
// }
|
||||
{
|
||||
label: 'Router Tree',
|
||||
path: 'tools/devtools/router',
|
||||
contentPath: 'tools/devtools/router',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 -960 960 960" fill="#F637E3">
|
||||
<path d="M450.001-611.691v-32.386q-39.385-9.923-64.692-41.897-25.308-31.975-25.308-74.025 0-49.922 35.038-84.96 35.039-35.038 84.961-35.038t84.961 35.038q35.038 35.038 35.038 84.96 0 42.05-25.308 74.025-25.307 31.974-64.692 41.897v32.386l273.846 157.538q17.173 9.912 26.663 26.582 9.491 16.671 9.491 36.495v62.152q0 19.824-9.491 36.495-9.49 16.67-26.663 26.582L516.154-111.771q-17.203 9.846-36.217 9.846t-36.091-9.846L176.155-265.847q-17.173-9.912-26.663-26.582-9.491-16.671-9.491-36.495v-62.152q0-19.824 9.491-36.495 9.49-16.67 26.663-26.582l273.846-157.538Zm-6.155 364.537L200-387.461v58.537q0 3.078 1.539 5.962 1.538 2.885 4.615 4.808l267.692 154.692q3.077 1.923 6.154 1.923t6.154-1.923l267.692-154.692q3.077-1.923 4.615-4.808 1.539-2.884 1.539-5.962v-58.537L516.154-247.154q-17.203 9.847-36.217 9.847t-36.091-9.847Zm6.155-162.847V-542.77L250.46-427.691l223.386 128.846q3.077 1.924 6.154 1.924t6.154-1.924l223.001-128.846L509.999-542.77v132.769h-59.998ZM480-699.999q25 0 42.5-17.5t17.5-42.5q0-25-17.5-42.5t-42.5-17.5q-25 0-42.5 17.5t-17.5 42.5q0 25 17.5 42.5t42.5 17.5Zm-2.308 538.61Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -59,7 +59,7 @@ Here are examples of how to build with Genkit and Angular:
|
||||
|
||||
Here is an example of how to build with Firebase AI Logic and Angular:
|
||||
|
||||
- [Firebase AI Logic x Angular Starter Kit](https://github.com/angular/examples/tree/main/vertex-ai-firebase-angular-example) - Use this starter-kit to build an e-commerce application with a chat agent that can perform tasks. Start here if you do not have experience building with Firebase AI Logic and Angular.
|
||||
- [Firebase AI Logic x Angular Starter Kit](https://github.com/angular/examples/tree/main/firebase-ai-logic-angular-example) - Use this starter-kit to build an e-commerce application with a chat agent that can perform tasks. Start here if you do not have experience building with Firebase AI Logic and Angular.
|
||||
|
||||
This example includes an [in-depth video walkthrough explaining the functionality and demonstrates how to add new features](https://youtube.com/live/4vfDz2al_BI).
|
||||
|
||||
@@ -79,7 +79,7 @@ When connecting to model providers, it is important to keep your API secrets saf
|
||||
|
||||
Your application's architecture determines which AI APIs and tools to choose. Specifically, choose based on whether or not your application is client-side or server-side. Tools such as Firebase AI Logic provide a secure connection to the model APIs for client-side code. If you want to use a different API than Firebase AI Logic or prefer to use a different model provider, consider creating a proxy-server or even [Cloud Functions for Firebase](https://firebase.google.com/docs/functions) to serve as a proxy and not expose your API keys.
|
||||
|
||||
For an example of connecting using a client-side app, see the code: [Firebase AI Logic Angular example repository](https://github.com/angular/examples/tree/main/vertex-ai-firebase-angular-example).
|
||||
For an example of connecting using a client-side app, see the code: [Firebase AI Logic Angular example repository](https://github.com/angular/examples/tree/main/firebase-ai-logic-angular-example).
|
||||
|
||||
For server-side connections to model APIs that require API keys, prefer using a secrets manager or environment variable, not `environments.ts`. You should follow standard best practices for securing API keys and credentials. Firebase now provides a new secrets manager with the latest updates from Firebase App Hosting. To learn more, [check out the official documentation](https://firebase.google.com/docs/app-hosting/configure).
|
||||
|
||||
@@ -91,7 +91,7 @@ If you want to build agentic workflows, where agents are able to act and use too
|
||||
|
||||
Tool calling further enhances your web apps by expanding your AI integration further than a question and answer style chat bot. In fact, you can empower your model to request function calls using the function calling API of your model provider. The available tools can be used to perform more complex actions within the context of your application.
|
||||
|
||||
In the [e-commerce example](https://github.com/angular/examples/blob/main/vertex-ai-firebase-angular-example/src/app/ai.service.ts#L88) of the [Angular examples repository](https://github.com/angular/examples), the LLM requests to make calls to functions for inventory in order to gain the necessary context to perform more complex tasks such as calculating how much a group of items in the store will cost. The scope of the available API is up to you as a developer just as is whether or not to call a function requested by the LLM. You remain in control of the flow of execution. You can expose specific functions of a service for example but not all functions of that service.
|
||||
In the [e-commerce example](https://github.com/angular/examples/blob/main/firebase-ai-logic-angular-example/src/app/ai.service.ts#L88) of the [Angular examples repository](https://github.com/angular/examples), the LLM requests to make calls to functions for inventory in order to gain the necessary context to perform more complex tasks such as calculating how much a group of items in the store will cost. The scope of the available API is up to you as a developer just as is whether or not to call a function requested by the LLM. You remain in control of the flow of execution. You can expose specific functions of a service for example but not all functions of that service.
|
||||
|
||||
### Handling non-deterministic responses
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"branchName": "refs/heads/21.2.x",
|
||||
"sha": "0707c69d63e9962bab077dbc45770c25c8d26b7c"
|
||||
"sha": "faea5e033fd1bc0a5dec7e430434414d1cf6243d"
|
||||
}
|
||||
@@ -792,7 +792,7 @@
|
||||
"source": {
|
||||
"filePath": "/src/aria/menu/menu.ts",
|
||||
"startLine": 58,
|
||||
"endLine": 194
|
||||
"endLine": 201
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -37,7 +37,7 @@ The `outputToObservable` function lets you create an RxJS observable from a comp
|
||||
import {outputToObservable} from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component(/*...*/)
|
||||
class CustomSlider {
|
||||
class CustomSlider {
|
||||
valueChange = output<number>();
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ You can use css-grid to animate to auto height.
|
||||
<docs-code header="auto-height.css" path="adev/src/content/examples/animations/src/app/native-css/auto-height.css" />
|
||||
</docs-code-multifile>
|
||||
|
||||
If you don't have to worry about supporting all browsers, you can also check out `calc-size()`, which is the true solution to animating auto height. See [MDN's docs](https://developer.mozilla.org/en-US/docs/Web/CSS/calc-size) and (this tutorial)[https://frontendmasters.com/blog/one-of-the-boss-battles-of-css-is-almost-won-transitioning-to-auto/] for more information.
|
||||
If you don't have to worry about supporting all browsers, you can also check out `calc-size()`, which is the true solution to animating auto height. See [MDN's docs](https://developer.mozilla.org/en-US/docs/Web/CSS/calc-size) and [this tutorial](https://frontendmasters.com/blog/one-of-the-boss-battles-of-css-is-almost-won-transitioning-to-auto/) for more information.
|
||||
|
||||
### Animate entering and leaving a view
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
The manubar is a horizontal navigation bar that provides persistent access to application menus. Menubars organize commands into logical categories like File, Edit, and View, helping users discover and execute application features through keyboard or mouse interaction.
|
||||
The menubar is a horizontal navigation bar that provides persistent access to application menus. Menubars organize commands into logical categories like File, Edit, and View, helping users discover and execute application features through keyboard or mouse interaction.
|
||||
|
||||
<docs-tab-group>
|
||||
<docs-tab label="Basic">
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
## What is Angular Aria?
|
||||
|
||||
Building accessible components seems straightforward, but implementing them according to the W3C Accessibility Guidelines requires significant effort and accessibility expertise.
|
||||
Building accessible components seems straightforward, but implementing them according to the [W3C Accessibility Guidelines](https://www.w3.org/TR/wcag/) requires significant effort and accessibility expertise.
|
||||
|
||||
Angular Aria is a collection of headless, accessible directives that implement common WAI-ARIA patterns. The directives handle keyboard interactions, ARIA attributes, focus management, and screen reader support. All you have to do is provide the HTML structure, CSS styling, and business logic!
|
||||
Angular Aria is a collection of headless, accessible directives that implement common [WAI-ARIA patterns](https://www.w3.org/WAI/ARIA/apg/patterns/). The directives handle keyboard interactions, ARIA attributes, focus management, and screen reader support. All you have to do is provide the HTML structure, CSS styling, and business logic!
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ In cases like this, the following rules determine which value wins:
|
||||
## Styling with CSS custom properties
|
||||
|
||||
Developers often rely on [CSS Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties) to enable a flexible configuration of their component's styles.
|
||||
You can set such custom properties on a host element with a [style binding][style binding](guide/templates/binding#css-style-properties).
|
||||
You can set such custom properties on a host element with a [style binding](guide/templates/binding#css-style-properties).
|
||||
|
||||
```angular-ts
|
||||
@Component({
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
# Creating and using services
|
||||
|
||||
Services are reusable pieces of code that can be shared across your Angular application. They typically handle data fetching, business logic, or other functionality that multiple components need to access.
|
||||
Services are reusable pieces of code that you can share across your Angular application. You commonly use them to handle data fetching, business logic, or other functionality that multiple components need to access.
|
||||
|
||||
## Creating a service
|
||||
|
||||
You can create a service with the [Angular CLI](tools/cli) with the following command:
|
||||
You can create a service using the [Angular CLI](tools/cli) with the following command:
|
||||
|
||||
```bash
|
||||
ng generate service CUSTOM_NAME
|
||||
```
|
||||
|
||||
This creates a dedicated `CUSTOM_NAME.ts` file in your `src` directory.
|
||||
This command creates a dedicated `CUSTOM_NAME.ts` file in your `src` directory.
|
||||
|
||||
You can also manually create a service by adding the `@Injectable()` decorator to a TypeScript class. This tells Angular that the service can be injected as a dependency.
|
||||
You can also manually create a service by adding the `@Injectable()` decorator to a TypeScript class. This tells Angular that you can use the class as an injectable dependency.
|
||||
|
||||
Here is an example of a service that allows users to add and request data:
|
||||
The following example defines a service that allows users to add and retrieve data:
|
||||
|
||||
```ts
|
||||
// 📄 src/app/basic-data-store.ts
|
||||
@@ -38,9 +38,9 @@ export class BasicDataStore {
|
||||
|
||||
When you use `@Injectable({ providedIn: 'root' })` in your service, Angular:
|
||||
|
||||
- **Creates a single instance** (singleton) for your entire application
|
||||
- **Makes it available everywhere** without any additional configuration
|
||||
- **Enables tree-shaking** so the service is only included in your JavaScript bundle if it's actually used
|
||||
- **Creates a single instance** (a singleton) for the entire application
|
||||
- **Makes it available throughout your application** without additional configuration
|
||||
- **Enables tree-shaking** so Angular only includes the service in your JavaScript bundle if you actually use it
|
||||
|
||||
This is the recommended approach for most services.
|
||||
|
||||
@@ -93,7 +93,7 @@ export class BasicDataStore {
|
||||
|
||||
## Next steps
|
||||
|
||||
While `providedIn: 'root'` covers most use cases, Angular offers additional ways to provide services for specialized scenarios:
|
||||
While `providedIn: 'root'` covers most use cases, Angular also provides additional ways you can configure services for more specialized scenarios:
|
||||
|
||||
- **Component-specific instances** - When components need their own isolated service instances
|
||||
- **Manual configuration** - For services that require runtime configuration
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Creating an injectable service
|
||||
|
||||
Service is a broad category encompassing any value, function, or feature that an application needs.
|
||||
A service is typically a class with a narrow, well-defined purpose.
|
||||
A component is one type of class that can use DI.
|
||||
A service is a broad category that encompasses any value, function, or feature that your application needs.
|
||||
A service is typically a class with a focused and well-defined purpose.
|
||||
A component is one type of class that you can use with dependency injection (DI).
|
||||
|
||||
Angular distinguishes components from services to increase modularity and reusability.
|
||||
By separating a component's view-related features from other kinds of processing, you can make your component classes lean and efficient.
|
||||
Angular distinguishes components from services to improve modularity and reusability.
|
||||
By separating a component's view-related features from other types of processing, you can keep your component classes lean and efficient.
|
||||
|
||||
Ideally, a component's job is to enable the user experience and nothing more.
|
||||
Ideally, your component's responsibility is to enable the user experience and nothing more.
|
||||
A component should present properties and methods for data binding, to mediate between the view (rendered by the template) and the application logic (which often includes some notion of a model).
|
||||
|
||||
A component can delegate certain tasks to services, such as fetching data from the server, validating user input, or logging directly to the console.
|
||||
By defining such processing tasks in an injectable service class, you make those tasks available to any component.
|
||||
You can also make your application more adaptable by configuring different providers of the same kind of service, as appropriate in different circumstances.
|
||||
You can delegate tasks from a component to services, such as fetching data from a server, validating user input, or logging to the console.
|
||||
By defining such tasks in an injectable service class, you make those capabilities available to any component.
|
||||
You can also make your application more adaptable by configuring different providers for the same type of service based on different circumstances.
|
||||
|
||||
Angular does not enforce these principles.
|
||||
Angular helps you follow these principles by making it easy to factor your application logic into services and make those services available to components through DI.
|
||||
Angular does not strictly enforce these principles.
|
||||
Angular helps you follow these principles by making it easy to organize your application logic into services and make those services available to components through DI.
|
||||
|
||||
## Service examples
|
||||
|
||||
@@ -82,7 +82,7 @@ export class HeroService {}
|
||||
```
|
||||
|
||||
The `@Injectable()` decorator specifies that Angular can use this class in the DI system.
|
||||
The metadata, `providedIn: 'root'`, means that the `HeroService` is provided throughout the application.
|
||||
The `providedIn: 'root'` metadata specifies that the `HeroService` is available throughout your application.
|
||||
|
||||
Add a `getHeroes()` method that returns the heroes from `mock.heroes.ts` to get the hero mock data:
|
||||
|
||||
@@ -106,7 +106,7 @@ For clarity and maintainability, it is recommended that you define components an
|
||||
|
||||
## Injecting services
|
||||
|
||||
To inject a service as a dependency into a component, you can declare a class field representing the dependency and use Angular's [`inject`](/api/core/inject) function to initialize it.
|
||||
To inject a service into a component, declare a class field for the dependency and use Angular's [`inject`](/api/core/inject) function to initialize it.
|
||||
|
||||
The following example specifies the `HeroService` in the `HeroList`.
|
||||
The type of `heroService` is `HeroService`.
|
||||
@@ -125,7 +125,7 @@ It is also possible to inject a service into a component using the component's c
|
||||
constructor(private heroService: HeroService)
|
||||
```
|
||||
|
||||
The [`inject`](/api/core/inject) method can be used in both classes and functions, while the constructor method can naturally only be used in a class constructor. However, in either case a dependency may only be injected in a valid [injection context](guide/di/dependency-injection-context), usually in the construction or initialization of a component.
|
||||
The [`inject`](/api/core/inject) method can be used in both classes and functions, while the constructor method can naturally only be used in a class constructor. However, in both cases, you can only inject a dependency within a valid [injection context](guide/di/dependency-injection-context), typically during the construction or initialization of a component.
|
||||
|
||||
## Injecting services in other services
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
# Injection context
|
||||
|
||||
The dependency injection (DI) system relies internally on a runtime context where the current injector is available.
|
||||
The dependency injection (DI) system relies on a runtime context where the current injector is available.
|
||||
|
||||
This means that injectors can only work when code is executed in such a context.
|
||||
This means that injectors only work when you execute code within this context.
|
||||
|
||||
The injection context is available in these situations:
|
||||
You have an injection context available in the following situations:
|
||||
|
||||
- During construction (via the `constructor`) of a class being instantiated by the DI system, such as an `@Injectable` or `@Component`.
|
||||
- In the initializer for fields of such classes.
|
||||
- During construction (via the `constructor`) of a class instantiated by the DI system, such as an `@Injectable` or `@Component`.
|
||||
- In field initializers of such classes.
|
||||
- In the factory function specified for `useFactory` of a `Provider` or an `@Injectable`.
|
||||
- In the `factory` function specified for an `InjectionToken`.
|
||||
- Within a stack frame that runs in an injection context.
|
||||
|
||||
Knowing when you are in an injection context will allow you to use the [`inject`](api/core/inject) function to inject instances.
|
||||
Knowing when you are in an injection context allows you to use the [`inject`](api/core/inject) function to retrieve dependencies.
|
||||
|
||||
NOTE: For basic examples of using `inject()` in class constructors and field initializers, see the [overview guide](/guide/di#where-can-inject-be-used).
|
||||
|
||||
## Stack frame in context
|
||||
|
||||
Some APIs are designed to be run in an injection context. This is the case, for example, with router guards. This allows the use of [`inject`](api/core/inject) within the guard function to access a service.
|
||||
Some APIs are designed to run within an injection context. This is the case, for example, with router guards. This allows you to use [`inject`](api/core/inject) within the guard function to access services.
|
||||
|
||||
Here is an example for `CanActivateFn`
|
||||
|
||||
@@ -33,8 +33,8 @@ const canActivateTeam: CanActivateFn = (
|
||||
|
||||
## Run within an injection context
|
||||
|
||||
When you want to run a given function in an injection context without already being in one, you can do so with `runInInjectionContext`.
|
||||
This requires access to a given injector, like the `EnvironmentInjector`, for example:
|
||||
If you need to run a function within an injection context without already being in one, you can use `runInInjectionContext`.
|
||||
This requires access to an injector, such as the `EnvironmentInjector`:
|
||||
|
||||
```ts {highlight: [9], header"hero.service.ts"}
|
||||
@Injectable({
|
||||
@@ -51,11 +51,11 @@ export class HeroService {
|
||||
}
|
||||
```
|
||||
|
||||
Note that [`inject`](/api/core/inject) will return an instance only if the injector can resolve the required token.
|
||||
Note that [`inject`](/api/core/inject) returns an instance only if the injector can resolve the requested token.
|
||||
|
||||
## Asserts the context
|
||||
|
||||
Angular provides the `assertInInjectionContext` helper function to assert that the current context is an injection context and throws a clear error if not. Pass a reference to the calling function so the error message points to the correct API entry point. This produces a clearer, more actionable message than the default generic injection error.
|
||||
Angular provides the `assertInInjectionContext` helper function to verify that the current context is an injection context and throw a clear error if it is not. Pass a reference to the calling function so the error message points to the correct API entry point. This produces a clearer, more actionable message than the default generic injection error.
|
||||
|
||||
```ts
|
||||
import {ElementRef, assertInInjectionContext, inject} from '@angular/core';
|
||||
@@ -86,4 +86,4 @@ export class PreviewCard {
|
||||
|
||||
## Using DI outside of a context
|
||||
|
||||
Calling [`inject`](api/core/inject) or calling `assertInInjectionContext` outside of an injection context will throw [error NG0203](/errors/NG0203).
|
||||
If you call [`inject`](api/core/inject) or `assertInInjectionContext` outside of an injection context, Angular throws [error NG0203](/errors/NG0203).
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# DI in action
|
||||
|
||||
This guide explores additional features of dependency injection in Angular.
|
||||
This guide explores additional features of dependency injection (DI) in Angular.
|
||||
|
||||
NOTE: For comprehensive coverage of InjectionToken and custom providers, see the [defining dependency providers guide](guide/di/defining-dependency-providers#injection-tokens).
|
||||
|
||||
## Inject the component's DOM element
|
||||
|
||||
Although developers strive to avoid it, some visual effects and third-party tools require direct DOM access.
|
||||
As a result, you might need to access a component's DOM element.
|
||||
Although developers generally avoid it, some visual effects and third-party tools require you to access the DOM directly.
|
||||
In such cases, you may need to access a component's DOM element.
|
||||
|
||||
Angular exposes the underlying element of a `@Component` or `@Directive` via injection using the `ElementRef` injection token:
|
||||
Angular exposes the underlying DOM element of a `@Component` or `@Directive` through injection using the `ElementRef` token:
|
||||
|
||||
```ts {highlight:[7]}
|
||||
import {Directive, ElementRef, inject} from '@angular/core';
|
||||
@@ -28,7 +28,7 @@ export class HighlightDirective {
|
||||
|
||||
## Inject the host element's tag name
|
||||
|
||||
When you need the tag name of a host element, inject it using the `HOST_TAG_NAME` token.
|
||||
To get the tag name of a host element, inject it using the `HOST_TAG_NAME` token.
|
||||
|
||||
```ts
|
||||
import {Directive, HOST_TAG_NAME, inject} from '@angular/core';
|
||||
@@ -59,19 +59,19 @@ NOTE: If the host element might not have a tag name (e.g., `ng-container` or `ng
|
||||
|
||||
## Resolve circular dependencies with a forward reference
|
||||
|
||||
The order of class declaration matters in TypeScript.
|
||||
You can't refer directly to a class until it's been defined.
|
||||
In TypeScript, the order of class declarations matters.
|
||||
You cannot reference a class directly until you define it.
|
||||
|
||||
This isn't usually a problem, especially if you adhere to the recommended _one class per file_ rule.
|
||||
But sometimes circular references are unavoidable.
|
||||
For example, when class 'A' refers to class 'B' and 'B' refers to 'A', one of them has to be defined first.
|
||||
However, in some cases, circular references are unavoidable.
|
||||
For example, if class 'A' refers to class 'B' and class 'B' refers to class 'A', one of them must be defined first.
|
||||
|
||||
The Angular `forwardRef()` function creates an _indirect_ reference that Angular can resolve later.
|
||||
|
||||
You face a similar problem when a class makes _a reference to itself_.
|
||||
For example, in its `providers` array.
|
||||
The `providers` array is a property of the `@Component()` decorator function, which must appear before the class definition.
|
||||
You can break such circular references by using `forwardRef`.
|
||||
Such circular references can be resolved using `forwardRef`.
|
||||
|
||||
```typescript {header: 'app.component.ts', highlight: [4]}
|
||||
providers: [
|
||||
|
||||
@@ -232,7 +232,7 @@ In this case, the injector looks no further than the current `ElementInjector` b
|
||||
providers: [{provide: FlowerService, useValue: {emoji: '🌷'}}],
|
||||
})
|
||||
export class Self {
|
||||
constructor(@Self() public flower: FlowerService) {}
|
||||
public flower = inject(FlowerService, {self: true});
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
<docs-decorative-header title="Dependency injection in Angular" imgSrc="adev/src/assets/images/dependency_injection.svg"> <!-- markdownlint-disable-line -->
|
||||
|
||||
Dependency Injection (DI) is a design pattern used to organize and share code across an application.
|
||||
Dependency Injection (DI) is a design pattern you use to organize and share code across your application by supplying dependencies to a class instead of creating them inside it.
|
||||
</docs-decorative-header>
|
||||
|
||||
TIP: Check out Angular's [Essentials](essentials/dependency-injection) before diving into this comprehensive guide.
|
||||
|
||||
As an application grows, developers often need to reuse and share features across different parts of the codebase. [Dependency Injection (DI)](https://en.wikipedia.org/wiki/Dependency_injection) is a design pattern used to organize and share code across an application by allowing you to "inject" features into different parts.
|
||||
As an application grows, developers often need to reuse and share functionality across different parts of the codebase. [Dependency Injection (DI)](https://en.wikipedia.org/wiki/Dependency_injection) helps you achieve this by allowing you to provide dependencies to a class instead of creating them directly inside it. This makes different parts of the application more reusable and easier to manage.
|
||||
|
||||
Dependency injection is a popular pattern because it allows developers to address common challenges such as:
|
||||
|
||||
- **Improved code maintainability**: Dependency injection allows cleaner separation of concerns which enables easier refactoring and reducing code duplication.
|
||||
- **Scalability**: Modular functionality can be reused across multiple contexts and allows for easier scaling.
|
||||
- **Better testing**: DI allows unit tests to easily use [test doubles](https://en.wikipedia.org/wiki/Test_double) for situations when using a real implementation is not practical.
|
||||
- **Improved code maintainability**: Dependency injection promotes a clear separation of concerns, making code easier to refactor and reducing duplication.
|
||||
- **Scalability**: You can reuse modular functionality across different parts of an application, making it easier to scale.
|
||||
- **Better testing**: DI allows unit tests to use [test doubles](https://en.wikipedia.org/wiki/Test_double) in place of real implementations when needed.
|
||||
|
||||
## How does dependency injection work in Angular?
|
||||
|
||||
A dependency is any object, value, function or service that a class needs to work but does not create itself. In other words, it creates a relationship between different parts of your application since it wouldn't work without the dependency.
|
||||
A dependency is any object, value, function, or service that a class requires to work but does not create itself. Instead, you provide it from the outside, creating a clear relationship between different parts of the application.
|
||||
|
||||
There are two ways that code interacts with any dependency injection system:
|
||||
You interact with a dependency injection system in two main ways:
|
||||
|
||||
- Code can _provide_, or make available, values.
|
||||
- Code can _inject_, or ask for, those values as dependencies.
|
||||
- You can _provide_, or make available, values.
|
||||
- You can _inject_, or ask for, those values as dependencies.
|
||||
|
||||
"Values," in this context, can be any JavaScript value, including objects and functions. Common types of injected dependencies include:
|
||||
In this context, "values" can refer to any JavaScript value, including objects, functions, or class instances. Common types of injected dependencies include:
|
||||
|
||||
- **Configuration values**: Environment-specific constants, API URLs, feature flags, etc.
|
||||
- **Factories**: Functions that create objects or values based on runtime conditions
|
||||
- **Services**: Classes that provide common functionality, business logic, or state
|
||||
|
||||
Angular components and directives automatically participate in DI, meaning that they can inject dependencies _and_ they are available to be injected.
|
||||
Angular components and directives automatically participate in DI, meaning that you can inject dependencies into them and make them available for injection.
|
||||
|
||||
## What are services?
|
||||
|
||||
An Angular _service_ is a TypeScript class decorated with `@Injectable`, which makes an instance of the class available to be injected as a dependency. Services are the most common way of sharing data and functionality across an application.
|
||||
An Angular _service_ is a TypeScript class decorated with `@Injectable`, which allows you to inject an instance of the class as a dependency. Services are the most common way of sharing data and functionality across an application.
|
||||
|
||||
Common types of services include:
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ Angular creates the directive class and specifies the CSS selector, `[select]`,
|
||||
Import `TemplateRef`, and `ViewContainerRef`. Inject `TemplateRef` and `ViewContainerRef` in the directive as private properties.
|
||||
|
||||
```ts
|
||||
import {Directive, TemplateRef, ViewContainerRef} from '@angular/core';
|
||||
import {Directive, TemplateRef, ViewContainerRef, inject} from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[select]',
|
||||
|
||||
@@ -9,12 +9,12 @@ A typical use-case is a questionnaire.
|
||||
You might need to get input from users in different contexts.
|
||||
The format and style of the forms a user sees should remain constant, while the actual questions you need to ask vary with the context.
|
||||
|
||||
In this tutorial you will build a dynamic form that presents a basic questionnaire.
|
||||
In this tutorial, you will build a dynamic form that presents a basic questionnaire.
|
||||
You build an online application for heroes seeking employment.
|
||||
The agency is constantly tinkering with the application process, but by using the dynamic form
|
||||
you can create the new forms on the fly without changing the application code.
|
||||
|
||||
The tutorial walks you through the following steps.
|
||||
The tutorial walks you through the following steps:
|
||||
|
||||
1. Enable reactive forms for a project.
|
||||
1. Establish a data model to represent form controls.
|
||||
@@ -30,7 +30,7 @@ The basic version can evolve to support a richer variety of questions, more grac
|
||||
|
||||
Dynamic forms are based on reactive forms.
|
||||
|
||||
To give the application access reactive forms directives, import `ReactiveFormsModule` from the `@angular/forms` library into the necessary components.
|
||||
To give the application access to reactive form directives, import `ReactiveFormsModule` from the `@angular/forms` package into the necessary components.
|
||||
|
||||
<docs-code-multifile>
|
||||
<docs-code header="dynamic-form.component.ts" path="adev/src/content/examples/dynamic-form/src/app/dynamic-form.component.ts"/>
|
||||
|
||||
@@ -8,7 +8,7 @@ This page shows how to validate user input from the UI and display useful valida
|
||||
To add validation to a template-driven form, you add the same validation attributes as you would with [native HTML form validation](https://developer.mozilla.org/docs/Web/Guide/HTML/HTML5/Constraint_validation).
|
||||
Angular uses directives to match these attributes with validator functions in the framework.
|
||||
|
||||
Every time the value of a form control changes, Angular runs validation and generates either a list of validation errors that results in an `INVALID` status, or null, which results in a VALID status.
|
||||
Every time the value of a form control changes, Angular runs validation and generates either a list of validation errors that results in an `INVALID` status, or `null`, which results in a `VALID` status.
|
||||
|
||||
You can then inspect the control's state by exporting `ngModel` to a local template variable.
|
||||
The following example exports `NgModel` into a variable called `name`:
|
||||
@@ -237,7 +237,7 @@ Asynchronous validators implement the `AsyncValidatorFn` and `AsyncValidator` in
|
||||
These are very similar to their synchronous counterparts, with the following differences.
|
||||
|
||||
- The `validate()` functions must return a Promise or an observable,
|
||||
- The observable returned must be finite, meaning it must complete at some point.
|
||||
- The observable returned must be finite, meaning that it must complete at some point.
|
||||
To convert an infinite observable into a finite one, pipe the observable through a filtering operator such as `first`, `last`, `take`, or `takeUntil`.
|
||||
|
||||
Asynchronous validation happens after the synchronous validation, and is performed only if the synchronous validation is successful.
|
||||
@@ -369,7 +369,7 @@ onCountryChange(country: string) {
|
||||
Use [`setValidators`](api/forms/AbstractControl#setValidators) to replace all existing synchronous validators on a control, or [`clearValidators`](api/forms/AbstractControl#clearValidators) to remove all validators.
|
||||
|
||||
```ts
|
||||
toggleStrictNameValidation(isStenablerict: boolean) {
|
||||
toggleStrictNameValidation(isStrict: boolean) {
|
||||
const nameControl = this.profileForm.get('name');
|
||||
|
||||
if (enable) {
|
||||
|
||||
@@ -6,7 +6,7 @@ Applications use forms to enable users to log in, to update a profile, to enter
|
||||
|
||||
Angular provides two different approaches to handling user input through forms: reactive and template-driven.
|
||||
|
||||
Both capture user input events from the view, validate the user input, create a form model and data model to update, and provide a way to track changes.
|
||||
Both capture user input events from the view, validate the input, create a form and data model, and provide a way to track changes.
|
||||
|
||||
TIP: If you're looking for the new experimental Signal Forms, check out our [essential Signal Forms guide](/essentials/signal-forms)!
|
||||
|
||||
@@ -86,7 +86,7 @@ The following component implements the same input field for a single control, us
|
||||
|
||||
<docs-code language="angular-ts" path="adev/src/content/examples/forms-overview/src/app/template/favorite-color/favorite-color.component.ts"/>
|
||||
|
||||
IMPORTANT: In a template-driven form the source of truth is the template. The `NgModel` directive automatically manages the `FormControl` instance for you.
|
||||
IMPORTANT: In a template-driven form, the source of truth is the template. The `NgModel` directive automatically manages the `FormControl` instance for you.
|
||||
|
||||
## Data flow in forms
|
||||
|
||||
@@ -99,10 +99,10 @@ The following diagrams illustrate both kinds of data flow for each type of form,
|
||||
|
||||
### Data flow in reactive forms
|
||||
|
||||
In reactive forms each form element in the view is directly linked to the form model (a `FormControl` instance).
|
||||
In reactive forms, each form element in the view is directly linked to the form model (a `FormControl` instance).
|
||||
Updates from the view to the model and from the model to the view are synchronous and do not depend on how the UI is rendered.
|
||||
|
||||
The view-to-model diagram shows how data flows when an input field's value is changed from the view through the following steps.
|
||||
The view-to-model diagram shows how data flows when an input field's value is changed from the view through the following steps:
|
||||
|
||||
1. The user types a value into the input element, in this case the favorite color _Blue_.
|
||||
1. The form input element emits an "input" event with the latest value.
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
# Reactive forms
|
||||
|
||||
Reactive forms provide a model-driven approach to handling form inputs whose values change over time.
|
||||
This guide shows you how to create and update a basic form control, progress to using multiple controls in a group, validate form values, and create dynamic forms where you can add or remove controls at run time.
|
||||
This guide shows you how to create and update a basic form control, use multiple controls in a group, validate form values, and create dynamic forms where you can add or remove controls at runtime.
|
||||
|
||||
## Overview of reactive forms
|
||||
|
||||
Reactive forms use an explicit and immutable approach to managing the state of a form at a given point in time.
|
||||
Each change to the form state returns a new state, which maintains the integrity of the model between changes.
|
||||
Reactive forms are built around observable streams, where form inputs and values are provided as streams of input values, which can be accessed synchronously.
|
||||
Reactive forms are built around observable streams, where form inputs and values are provided as streams that can be accessed synchronously.
|
||||
|
||||
Reactive forms also provide a straightforward path to testing because you are assured that your data is consistent and predictable when requested.
|
||||
Any consumers of the streams have access to manipulate that data safely.
|
||||
Any consumers of these streams can safely manipulate the data.
|
||||
|
||||
Reactive forms differ from [template-driven forms](guide/forms/template-driven-forms) in distinct ways.
|
||||
Reactive forms provide synchronous access to the data model, immutability with observable operators, and change tracking through observable streams.
|
||||
|
||||
Template-driven forms let direct access modify data in your template, but are less explicit than reactive forms because they rely on directives embedded in the template, along with mutable data to track changes asynchronously.
|
||||
Template-driven forms allow direct access to modify data in your template, but are less explicit than reactive forms because they rely on directives embedded in the template, along with mutable data to track changes asynchronously.
|
||||
See the [Forms Overview](guide/forms) for detailed comparisons between the two paradigms.
|
||||
|
||||
## Adding a basic form control
|
||||
|
||||
There are three steps to using form controls.
|
||||
|
||||
1. Generate a new component and register the reactive forms module. This module declares the reactive-form directives that you need to use reactive forms.
|
||||
1. Generate a new component and register the reactive forms module. This module declares the reactive-form directives required to use reactive forms.
|
||||
1. Instantiate a new `FormControl`.
|
||||
1. Register the `FormControl` in the template.
|
||||
|
||||
@@ -62,7 +62,7 @@ The `FormControl` assigned to the `name` property is displayed when the `<app-na
|
||||
|
||||
### Displaying a form control value
|
||||
|
||||
You can display the value in the following ways.
|
||||
You can display the value in the following ways:
|
||||
|
||||
- Through the `valueChanges` observable where you can listen for changes in the form's value in the template using `AsyncPipe` or in the component class using the `subscribe()` method
|
||||
- With the `value` property, which gives you a snapshot of the current value
|
||||
@@ -231,7 +231,7 @@ Simulate an update by adding a button to the template to update the user profile
|
||||
|
||||
When a user clicks the button, the `profileForm` model is updated with new values for `firstName` and `street`. Notice that `street` is provided in an object inside the `address` property.
|
||||
This is necessary because the `patchValue()` method applies the update against the model structure.
|
||||
`PatchValue()` only updates properties that the form model defines.
|
||||
`patchValue()` only updates properties that the form model defines.
|
||||
|
||||
## Using the FormBuilder service to generate controls
|
||||
|
||||
@@ -437,7 +437,7 @@ Initially, the form contains one `Alias` field. To add another field, click the
|
||||
## Unified control state change events
|
||||
|
||||
All form controls expose a single unified stream of **control state change events** through the `events` observable on `AbstractControl` (`FormControl`, `FormGroup`, `FormArray`, and `FormRecord`).
|
||||
This unified stream lets you react to **value**, **status**, **pristine**, **touched** and **reset** state changes and also for **form-level actions** such as **submit** , allowing you to handle all updates with a one subscription instead of wiring multiple observables.
|
||||
This unified stream lets you react to **value**, **status**, **pristine**, **touched**, and **reset** state changes, as well as **form-level actions** such as **submit**, allowing you to handle all updates with a single subscription instead of wiring multiple observables.
|
||||
|
||||
### Event types
|
||||
|
||||
|
||||
@@ -329,13 +329,13 @@ export class Registration {
|
||||
private createUsernameResource = (usernameSignal: Signal<string | undefined>) => {
|
||||
return rxResource({
|
||||
params: () => usernameSignal(),
|
||||
stream: ({request: username}) => this.usernameService.checkUsername(username),
|
||||
stream: ({params: username}) => this.usernameService.checkUsername(username),
|
||||
});
|
||||
};
|
||||
|
||||
registrationForm = form(this.registrationModel, (schemaPath) => {
|
||||
validateAsync(schemaPath.username, {
|
||||
params: ({value}) => value() || undefined,
|
||||
params: ({value}) => value(),
|
||||
factory: this.createUsernameResource,
|
||||
onSuccess: (result) =>
|
||||
result?.available ? null : {kind: 'usernameTaken', message: 'Username taken'},
|
||||
@@ -350,6 +350,229 @@ export class Registration {
|
||||
|
||||
The `rxResource` function works directly with Observables and handles subscription cleanup automatically when the field value changes.
|
||||
|
||||
## Debouncing
|
||||
|
||||
The `debounce` rule delays when a user's input is committed to the form model. You can think of it as the rule holding back values until the user pauses typing. This is useful when downstream behavior shouldn't react to every keystroke, such as expensive derived computations, validation that flashes errors mid-word, or search filters that reapply on each character.
|
||||
|
||||
Add the `debounce` rule inside a schema to delay how a form field's UI changes reach the form model. In its simplest form, `debounce(path, ms)` holds each UI change for the given number of milliseconds before writing it to the model. A new change within that window resets the timer.
|
||||
|
||||
The following example applies `debounce` and `validateHttp` to the username field to delay the username availability check in a registration form until the user pauses typing:
|
||||
|
||||
```angular-ts
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {form, debounce, validateHttp, FormField} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
selector: 'app-registration',
|
||||
imports: [FormField],
|
||||
template: `
|
||||
<label>
|
||||
Username:
|
||||
<input [formField]="registrationForm.username" />
|
||||
</label>
|
||||
|
||||
@if (registrationForm.username().pending()) {
|
||||
<span class="checking">Checking availability...</span>
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class Registration {
|
||||
registrationModel = signal({username: ''});
|
||||
|
||||
registrationForm = form(this.registrationModel, (schemaPath) => {
|
||||
// Hold UI updates for 300 ms before writing to the model
|
||||
debounce(schemaPath.username, 300);
|
||||
|
||||
// Runs against the debounced model value, not every keystroke
|
||||
validateHttp(schemaPath.username, {
|
||||
request: ({value}) => {
|
||||
const username = value();
|
||||
// Skip the request for blank values
|
||||
return username ? `/api/users/check?username=${username}` : undefined;
|
||||
},
|
||||
onSuccess: (response) =>
|
||||
response.available ? null : {kind: 'usernameTaken', message: 'Username is already taken'},
|
||||
onError: () => ({
|
||||
kind: 'serverError',
|
||||
message: 'Could not verify username availability',
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
With a 300 ms debounce, the model updates and validates only after the user pauses typing longer than the configured duration. For example, typing "signal forms" in a quick burst fires one validation request instead of twelve.
|
||||
|
||||
### Touch flushes the model
|
||||
|
||||
Regardless of the debounce duration, the framework writes the field's `controlValue()` to the model immediately when the field becomes touched. Native inputs become touched on blur, so a user who finishes typing and tabs away doesn't have to wait for the debounce timer to expire. Custom controls can mark the field as touched in response to any event they choose.
|
||||
|
||||
In the typical case, this matters for form submission. When the user clicks a submit button, the focused input blurs, which touches that field and flushes its pending debounce before the submission handler runs.
|
||||
|
||||
### Commit only on blur
|
||||
|
||||
Some fields shouldn't update mid-typing at all, and instead should only update after the user has finished entering a value. For example, if you have a search filter that reapplies on every change or a form that triggers expensive derived state, it is often better for the model to wait until the user finishes typing.
|
||||
|
||||
In these scenarios, pass `'blur'` instead of a duration to defer all updates until the field becomes touched:
|
||||
|
||||
```ts
|
||||
form(this.registrationModel, (schemaPath) => {
|
||||
debounce(schemaPath.username, 'blur');
|
||||
});
|
||||
```
|
||||
|
||||
With `'blur'`, the model keeps its previous value while the user is typing. Sync and async validation, derived signals, and any reactive rules reading the field all see the previous value until the field becomes touched. This commonly occurs when the user blurs a native input, or when a custom control signals touch on its own.
|
||||
|
||||
### Custom timing logic
|
||||
|
||||
For timing logic that a duration or `'blur'` can't express, pass a `Debouncer` function. The function receives the field context and an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal), and returns a `Promise<void>` that resolves when the model should update:
|
||||
|
||||
```ts
|
||||
import {debounce, type Debouncer} from '@angular/forms/signals';
|
||||
|
||||
const shorterWhenLonger: Debouncer<string> = ({value}, abortSignal) => {
|
||||
// Shorter queries get a longer delay since the user is likely still typing.
|
||||
const ms = value().length < 3 ? 500 : 200;
|
||||
return new Promise((resolve) => {
|
||||
const timeoutId = setTimeout(resolve, ms);
|
||||
// Abort fires when this field is touched or its value changes, so the pending timer is cleared
|
||||
abortSignal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve();
|
||||
},
|
||||
{once: true},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
form(this.registrationModel, (schemaPath) => {
|
||||
debounce(schemaPath.username, shorterWhenLonger);
|
||||
});
|
||||
```
|
||||
|
||||
The `abortSignal` fires when the field is touched, or when its value changes before the debounce resolves. Resolve the promise on abort so your debouncer releases any pending timers. The framework writes the pending value to the model on touch, and discards it when a newer value arrives. See the [`debounce` API reference](api/forms/signals/debounce) for the full `Debouncer` signature.
|
||||
|
||||
### Debouncing a single async validator
|
||||
|
||||
The `debounce` rule holds back every reaction to the field, from sync validation to derived signals to async validation. However, there are times when you want the opposite: cheap sync validators like `required` or `email` running immediately for instant feedback, while only the expensive async call waits for the user to settle. Both `validateHttp()` and `validateAsync()` accept their own [`debounce` option](api/forms/signals/validateAsync) that throttles just that validator:
|
||||
|
||||
```ts
|
||||
form(this.registrationModel, (schemaPath) => {
|
||||
validateHttp(schemaPath.username, {
|
||||
// Throttles only this HTTP call
|
||||
debounce: 300,
|
||||
request: ({value}) => {
|
||||
const username = value();
|
||||
// Skip the request for blank values
|
||||
return username ? `/api/users/check?username=${username}` : undefined;
|
||||
},
|
||||
onSuccess: (response) =>
|
||||
response.available ? null : {kind: 'usernameTaken', message: 'Username is already taken'},
|
||||
onError: () => ({
|
||||
kind: 'serverError',
|
||||
message: 'Could not verify username availability',
|
||||
}),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
The model still updates on every keystroke, and any other rules attached to the field still react immediately. Only the HTTP request is debounced: each change waits 300 ms of quiet before firing, so a request only goes out once the user has paused typing.
|
||||
|
||||
Choose between the two layers based on scope:
|
||||
|
||||
| Option | When to use |
|
||||
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `debounce()` rule | Sync validation, derived state, and submission should all wait until the field commits. The whole field shouldn't react mid-typing. |
|
||||
| `validateHttp({ debounce })` or `validateAsync({ debounce })` | Cheap sync validators should give immediate feedback, but expensive async calls should wait for the user to pause. |
|
||||
|
||||
Both options accept a duration in milliseconds. Their custom-timing callbacks differ: the form-level rule takes a `Debouncer`, and the validator-level option takes a `DebounceTimer` from `@angular/core`. The two signatures are not interchangeable.
|
||||
|
||||
## Composing resources in async validation with a factory
|
||||
|
||||
The built-in [`debounce` option](api/forms/signals/validateAsync) covers throttling, but `validateAsync()` exposes a deeper composition point: the `factory` function. The factory receives the params as a signal and returns a resource. Between those two points, you're free to compose whatever you need.
|
||||
|
||||
In its simplest form, a factory wraps a single resource. A username-availability check can live as a method on the component class, and then be wired into `validateAsync` by reference:
|
||||
|
||||
```ts
|
||||
export class Registration {
|
||||
registrationModel = signal({username: ''});
|
||||
private usernameValidator = inject(UsernameValidator);
|
||||
|
||||
// Factory function
|
||||
checkUsernameAvailable = (username: Signal<string | undefined>) =>
|
||||
resource({
|
||||
params: () => username(),
|
||||
loader: async ({params: name}) => this.usernameValidator.checkAvailability(name),
|
||||
});
|
||||
|
||||
registrationForm = form(this.registrationModel, (schemaPath) => {
|
||||
validateAsync(schemaPath.username, {
|
||||
params: ({value}) => {
|
||||
const username = value();
|
||||
// Skip validation for short usernames
|
||||
return username.length >= 3 ? username : undefined;
|
||||
},
|
||||
debounce: 300,
|
||||
// Reference to the factory defined above
|
||||
factory: this.checkUsernameAvailable,
|
||||
onSuccess: (result) =>
|
||||
result?.available ? null : {kind: 'usernameTaken', message: 'Username taken'},
|
||||
onError: () => ({kind: 'serverError', message: 'Could not verify'}),
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The `params` callback returns `undefined` for short usernames, signaling that validation should skip. With `debounce: 300` applied, the resource waits until the user pauses typing for 300 ms before acting on each change. It then runs the loader for valid usernames and stays idle once the debounced value settles to `undefined`.
|
||||
|
||||
### Combining debounce with additional logic
|
||||
|
||||
When you need logic beyond a plain duration debounce, use a custom factory to combine debouncing with that logic. A common case is caching validated responses. For example, once the server has confirmed a username, you don't need to ask again on subsequent keystrokes that revisit the same value.
|
||||
|
||||
```ts
|
||||
export class Registration {
|
||||
registrationModel = signal({username: ''});
|
||||
private usernameValidator = inject(UsernameValidator);
|
||||
|
||||
registrationForm = form(this.registrationModel, (schemaPath) => {
|
||||
validateAsync(schemaPath.username, {
|
||||
params: ({value}) => {
|
||||
const username = value();
|
||||
return username.length >= 3 ? username : undefined;
|
||||
},
|
||||
factory: (username) => {
|
||||
// Core primitive: settles 300 ms after the source stops changing
|
||||
const debouncedUsername = debounced(username, 300);
|
||||
// Cache lives in the factory's closure and persists for the field's lifetime
|
||||
const cache = new Map<string, {available: boolean}>();
|
||||
return resource({
|
||||
// Read from the debounced signal, not the raw one
|
||||
params: () => debouncedUsername.value(),
|
||||
loader: async ({params: name}) => {
|
||||
const cached = cache.get(name);
|
||||
if (cached) return cached;
|
||||
|
||||
const result = await this.usernameValidator.checkAvailability(name);
|
||||
cache.set(name, result);
|
||||
return result;
|
||||
},
|
||||
});
|
||||
},
|
||||
onSuccess: (result) =>
|
||||
result?.available ? null : {kind: 'usernameTaken', message: 'Username taken'},
|
||||
onError: () => ({
|
||||
kind: 'serverError',
|
||||
message: 'Could not verify username',
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The `cache` lives in the factory's closure, so it persists for the field's lifetime. Once the user has typed a username the server has already checked, the loader reads from the cache instead of making a new network request.
|
||||
|
||||
## Understanding pending state
|
||||
|
||||
When async validation runs, the field's `pending()` signal returns `true`. During this time:
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
# Cross-field logic
|
||||
|
||||
**Cross-field logic** is necessary when any rule, validation, or behavior of one field depends on another field's value or state.
|
||||
|
||||
Signal forms provide a **field context** to every rule function. The field context provides access to the current field's value and state, and lets you read other fields in the form using `valueOf()`, `stateOf()`, and `fieldTreeOf()`.
|
||||
|
||||
This guide covers the field context API in depth and shows common cross-field patterns. For single-field validation, see the [Validation guide](/guide/forms/signals/validation).
|
||||
|
||||
## Understanding the field context
|
||||
|
||||
Every rule function in signal forms receives a **field context** parameter, which is an object that describes the current field and provides access to the rest of the form.
|
||||
|
||||
There are three properties you can access for the current field:
|
||||
|
||||
| Property | Type | Description |
|
||||
| ----------- | -------------------- | -------------------------------------------------------------------- |
|
||||
| `value` | `Signal<TValue>` | The current field's value as a signal |
|
||||
| `state` | `FieldState<TValue>` | The current field's state (such as validity, errors, touched, dirty) |
|
||||
| `fieldTree` | `FieldTree<TValue>` | The current field's tree, for programmatic access to child fields |
|
||||
|
||||
For cross-field logic, the following three properties allow you to access other parts of the form:
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `valueOf()` | `(path) => PValue` | Most common. Use when you need another field's raw value for comparisons or calculations. |
|
||||
| `stateOf()` | `(path) => FieldState<PValue>` | Use when your logic depends on another field's state, such as whether it's valid, touched, or dirty. |
|
||||
| `fieldTreeOf()` | `(path) => FieldTree<PModel>` | Use when you need programmatic access to another field's tree, such as pushing errors to a specific child field with validateTree. |
|
||||
|
||||
Here is an example of using `value` and `valueOf()` to validate that the current field (end date) comes after the start date in the form:
|
||||
|
||||
```ts
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {form, validate} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
/* ... */
|
||||
})
|
||||
export class EventForm {
|
||||
eventModel = signal({
|
||||
startDate: new Date('2026-06-01'),
|
||||
endDate: new Date('2026-06-05'),
|
||||
});
|
||||
|
||||
eventForm = form(this.eventModel, (schemaPath) => {
|
||||
validate(schemaPath.endDate, ({value, valueOf}) => {
|
||||
if (value() <= valueOf(schemaPath.startDate)) {
|
||||
return {
|
||||
kind: 'invalidDateRange',
|
||||
message: 'End date must be after start date',
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
NOTE: The `fieldContext` parameter is typically destructured to pull out only what the rule needs. The remaining examples in this guide use this pattern.
|
||||
|
||||
## Cross-field validation patterns
|
||||
|
||||
The date range example from the previous section validates the end date against the start date. Because the rule reads `valueOf(schemaPath.startDate)`, it re-evaluates automatically whenever either date changes. In other words, a single validator is enough to keep the error state correct.
|
||||
|
||||
However, that single validator only places the error on the end date field. If you want both fields to show an error when the range is invalid, add a matching validation rule to each field:
|
||||
|
||||
```ts
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {form, validate} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
/* ... */
|
||||
})
|
||||
export class EventForm {
|
||||
eventModel = signal({
|
||||
startDate: new Date('2026-06-01'),
|
||||
endDate: new Date('2026-06-05'),
|
||||
});
|
||||
|
||||
eventForm = form(this.eventModel, (schemaPath) => {
|
||||
validate(schemaPath.startDate, ({value, valueOf}) => {
|
||||
if (value() >= valueOf(schemaPath.endDate)) {
|
||||
return {
|
||||
kind: 'invalidDateRange',
|
||||
message: 'Start date must be before end date',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
validate(schemaPath.endDate, ({value, valueOf}) => {
|
||||
if (value() <= valueOf(schemaPath.startDate)) {
|
||||
return {
|
||||
kind: 'invalidDateRange',
|
||||
message: 'End date must be after start date',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Both rules make use of `valueOf()` to read the other field. Because each rule is reactive, changing either date re-evaluates both validations automatically.
|
||||
|
||||
NOTE: When a rule involves multiple fields, you need to decide where the error belongs: on a specific field, on multiple fields, or on the parent. In general, place the error where the user would most likely go to fix the problem.
|
||||
|
||||
### Conditional requirements
|
||||
|
||||
In some forms, certain fields are only required under certain conditions. For example, a registration form might require a company name only when the user selects a business account type:
|
||||
|
||||
```ts
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {form, required} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
/* ... */
|
||||
})
|
||||
export class RegistrationForm {
|
||||
registrationModel = signal({
|
||||
accountType: 'personal' as 'personal' | 'business',
|
||||
companyName: '',
|
||||
});
|
||||
|
||||
registrationForm = form(this.registrationModel, (schemaPath) => {
|
||||
required(schemaPath.companyName, {
|
||||
when: ({valueOf}) => valueOf(schemaPath.accountType) === 'business',
|
||||
message: 'Company name is required for business accounts',
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The `when` option receives the same field context as any other rule function, so `valueOf` works the same way. When the user switches back to `'personal'`, the condition re-evaluates and the requirement — along with its error — clears automatically.
|
||||
|
||||
Using `required()` with `when` instead of a manual `validate()` check also adds proper required metadata to the field, which enables accessibility features like marking the field as required for screen readers.
|
||||
|
||||
### Validating based on another field's state
|
||||
|
||||
The examples so far use `valueOf()` to read another field's value. Sometimes your logic depends on another field's _state_ instead — whether it's valid, touched, or dirty. Use `stateOf()` for this.
|
||||
|
||||
For example, a confirm-password field should only check for a match once the user has interacted with the password field. If the user hasn't touched the password yet, flagging a mismatch on the confirmation is premature:
|
||||
|
||||
```ts
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {form, validate} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
/* ... */
|
||||
})
|
||||
export class PasswordForm {
|
||||
passwordModel = signal({
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
|
||||
passwordForm = form(this.passwordModel, (schemaPath) => {
|
||||
validate(schemaPath.confirmPassword, ({value, valueOf, stateOf}) => {
|
||||
if (!stateOf(schemaPath.password).touched()) {
|
||||
return null;
|
||||
}
|
||||
if (value() !== valueOf(schemaPath.password)) {
|
||||
return {
|
||||
kind: 'passwordMismatch',
|
||||
message: 'Passwords do not match',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The `stateOf()` call returns the other field's [field state](api/forms/signals/FieldState), giving you access to signals like `invalid()`, `touched()`, and `dirty()`. Because these are signals, the rule re-evaluates whenever the password field's validity changes.
|
||||
|
||||
WARNING: Be careful not to read state which depends on your field's validation, as that creates a circular loop. For example, a validator which checks whether the parent field is valid will create an infinite loop because the parent's validity depends on its children's validity (which includes your validator).
|
||||
|
||||
## Using validateTree
|
||||
|
||||
The examples so far use `validate()` to check individual fields. Sometimes you need to validate a group of fields where the logic is inherently about multiple fields in a group, and direct errors to specific children within it. `validateTree` handles is ideal for these kinds of scenarios.
|
||||
|
||||
For example, in a Sudoku puzzle, each row must contain unique numbers. This is a group-level rule: you check the entire row, then flag the specific cells that violate it. This kind of validation can't be expressed cleanly with `validate` on individual fields, because each cell would need to know about every other cell.
|
||||
|
||||
```ts
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {form, validateTree} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
/* ... */
|
||||
})
|
||||
export class SudokuRow {
|
||||
rowModel = signal({
|
||||
cell1: 1,
|
||||
cell2: 3,
|
||||
cell3: 1,
|
||||
cell4: 4,
|
||||
});
|
||||
|
||||
rowForm = form(this.rowModel, (schemaPath) => {
|
||||
validateTree(schemaPath, ({value, fieldTreeOf}) => {
|
||||
const row = value();
|
||||
const entries = [
|
||||
{val: row.cell1, fieldTree: fieldTreeOf(schemaPath.cell1)},
|
||||
{val: row.cell2, fieldTree: fieldTreeOf(schemaPath.cell2)},
|
||||
{val: row.cell3, fieldTree: fieldTreeOf(schemaPath.cell3)},
|
||||
{val: row.cell4, fieldTree: fieldTreeOf(schemaPath.cell4)},
|
||||
];
|
||||
|
||||
const counts = new Map<number, number>();
|
||||
for (const {val} of entries) {
|
||||
if (val !== 0) {
|
||||
counts.set(val, (counts.get(val) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const errors = entries
|
||||
.filter(({val}) => val !== 0 && (counts.get(val) ?? 0) > 1)
|
||||
.map(({val, fieldTree}) => ({
|
||||
kind: 'duplicateInRow',
|
||||
message: `${val} already appears in this row`,
|
||||
fieldTree,
|
||||
}));
|
||||
|
||||
return errors.length > 0 ? errors : null;
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The validator runs on the parent field (the row), reads all cell values, counts duplicates, and returns an error for each cell that contains a repeated number. The `fieldTree` property on each error tells Angular exactly which cell should show the error. Without `fieldTree`, the errors would apply to the row itself — not where the user needs to see them.
|
||||
|
||||
Because `validateTree` can return an array of errors, a single validator can flag multiple cells at once. Each error includes a `fieldTree` pointing to its target, so Angular routes the errors to the correct fields.
|
||||
|
||||
### When to use validateTree vs validate
|
||||
|
||||
Prefer `validate()` with `valueOf()` when the error belongs on the field being validated — even if the rule reads from other fields. Reach for `validateTree` when:
|
||||
|
||||
- The validation logic is inherently about a group of fields, not any single field
|
||||
- The validator needs to return errors targeting different child fields
|
||||
|
||||
TIP: For an introduction to `validateTree` and its return type, see the [Validation guide](/guide/forms/signals/validation).
|
||||
|
||||
## Next steps
|
||||
|
||||
This guide covered the field context API and common cross-field patterns. To learn more about related Signal Forms guide, check out:
|
||||
|
||||
<docs-pill-row>
|
||||
<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/custom-controls" title="Custom controls" />
|
||||
</docs-pill-row>
|
||||
@@ -516,7 +516,7 @@ import {form, FormField, min, max, validate} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
selector: 'app-custom',
|
||||
imports: [formField],
|
||||
imports: [FormField],
|
||||
template: ` <input [formField]="customForm.score" /> `,
|
||||
})
|
||||
export class Custom {
|
||||
@@ -643,7 +643,7 @@ import {form, FormField, max} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
selector: 'app-inventory',
|
||||
imports: [formField],
|
||||
imports: [FormField],
|
||||
template: `
|
||||
<label>
|
||||
Item
|
||||
@@ -742,7 +742,7 @@ import {
|
||||
|
||||
@Component({
|
||||
selector: 'app-promo',
|
||||
imports: [formField],
|
||||
imports: [FormField],
|
||||
template: `
|
||||
@if (!promoForm.promoCode().hidden()) {
|
||||
<label>
|
||||
@@ -787,7 +787,7 @@ import {form, FormField, applyWhen, required, pattern} from '@angular/forms/sign
|
||||
|
||||
@Component({
|
||||
selector: 'app-address',
|
||||
imports: [formField],
|
||||
imports: [FormField],
|
||||
template: `
|
||||
<label>
|
||||
Country
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
# Form submission
|
||||
|
||||
When a user submits a form, your application typically needs to handle multiple concerns at once: surfacing validation errors, preventing duplicate submission, sending data to a server, and much more. Handling each of these manually can be tedious and prone to error.
|
||||
|
||||
Signal Forms provides a `submit()` function that helps you manage the form submission lifecycle. This guide walks through how to use it.
|
||||
|
||||
## What does `submit()` do?
|
||||
|
||||
The `submit()` function runs through a specific sequence:
|
||||
|
||||
1. **Mark interactive fields as touched** — Fields that display errors only after being touched will now show their validation errors. Hidden, disabled, and readonly fields are skipped.
|
||||
1. **Check validation** — If any validation rules have failed, submission stops and the `action` function does not run.
|
||||
1. **Run the action** — The `action` function executes with the form's current value. While it runs, `submitting()` returns `true`.
|
||||
1. **Handle the result** — If the action returns errors, they are routed to their target fields. If it returns nothing, the submission is treated as successful.
|
||||
|
||||
The `submit()` function returns a `Promise<boolean>` that resolves to `true` when the action completes without errors, and `false` when validation fails or the action returns errors.
|
||||
|
||||
## Setting up form submission with `FormRoot`
|
||||
|
||||
The most common way to use the `submit()` function is through the `FormRoot` directive.
|
||||
|
||||
The `FormRoot` directive handles three things automatically when bound to a `<form>` element:
|
||||
|
||||
1. **Sets [`novalidate`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/form#novalidate)** — Disables the browser's built-in validation so Signal Forms manages validation instead
|
||||
1. **Prevents default** — Stops the browser from navigating on form submission
|
||||
1. **Calls `submit()`** — Triggers the submission flow when the user submits the form
|
||||
|
||||
NOTE: The `FormRoot` directive sets the `novalidate` attribute on the `form` element automatically. You do not need to add it manually when using `FormRoot`.
|
||||
|
||||
`FormRoot` handles the submission event, but you still need to tell it _what to do_ with the form data. That requires three things:
|
||||
|
||||
1. Bind your form to the `FormRoot` directive
|
||||
1. Pass a `submission` option to the `form()` function
|
||||
1. Define an `action` function within the `submission` option that manages the submitted data
|
||||
|
||||
```angular-ts
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {form, FormField, FormRoot, required} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
selector: 'app-contact',
|
||||
imports: [FormField, FormRoot],
|
||||
template: `
|
||||
<form [formRoot]="contactForm">
|
||||
<label>
|
||||
Name
|
||||
<input [formField]="contactForm.name" />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Email
|
||||
<input type="email" [formField]="contactForm.email" />
|
||||
</label>
|
||||
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
`,
|
||||
})
|
||||
export class Contact {
|
||||
contactModel = signal({
|
||||
name: '',
|
||||
email: '',
|
||||
});
|
||||
|
||||
contactForm = form(
|
||||
this.contactModel,
|
||||
(schemaPath) => {
|
||||
required(schemaPath.name);
|
||||
required(schemaPath.email);
|
||||
},
|
||||
{
|
||||
submission: {
|
||||
action: async (field) => {
|
||||
const result = await saveContact(field().value());
|
||||
if (result.ok) return;
|
||||
|
||||
return {kind: 'serverError', message: 'Failed to submit form'};
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The `action` function runs only when no validation rules have failed. By default, pending async validators do not block submission (see [Controlling validation gating](#controlling-validation-gating-with-ignorevalidators) for more details). The action receives the field tree and a `detail` object with `root` and `submitted` field trees, which is useful when submitting a sub-form.
|
||||
|
||||
After validation passes, the action itself may still fail due to scenarios such as a network error or duplicate entry. In those cases, you can surface the failure by returning the error(s). On the other hand, to indicate success, you only need to return `null` or `undefined`, or call an empty `return`.
|
||||
|
||||
## Showing submission state with `submitting()`
|
||||
|
||||
When you need to track whether the form is in the process of submitting, Signal Forms provides a `submitting()` signal that returns `true` while the `action` function is running. Use it to show loading indicators or disable the submit button to prevent duplicate submissions.
|
||||
|
||||
```angular-html
|
||||
<button type="submit" [disabled]="contactForm().submitting()">
|
||||
@if (contactForm().submitting()) {
|
||||
Sending...
|
||||
} @else {
|
||||
Send
|
||||
}
|
||||
</button>
|
||||
```
|
||||
|
||||
Once the `action` function succeeds or returns an error, the `submitting()` signal automatically resets back to `false`.
|
||||
|
||||
## Managing submission errors
|
||||
|
||||
### Server errors
|
||||
|
||||
When your `action` function communicates with a server, the server may return errors that need to appear on specific fields. Return these errors from the `action` to route them to their target fields.
|
||||
|
||||
#### Errors on the submitted field
|
||||
|
||||
By default, errors returned from the `action` are assigned to the submitted field (the field tree you passed to `submit()`):
|
||||
|
||||
```ts
|
||||
action: async (field) => {
|
||||
const result = await saveContact(field().value());
|
||||
if (result.ok) return;
|
||||
|
||||
return {kind: 'serverError', message: 'Failed to submit form'};
|
||||
};
|
||||
```
|
||||
|
||||
#### Errors on specific fields
|
||||
|
||||
When you want to route an error to a specific field, include a `fieldTree` property pointing to that field:
|
||||
|
||||
```ts
|
||||
action: async (field) => {
|
||||
const result = await saveContact(field().value());
|
||||
if (result.ok) return;
|
||||
|
||||
return {kind: 'taken', message: result.message, fieldTree: field.email};
|
||||
};
|
||||
```
|
||||
|
||||
#### Multiple errors
|
||||
|
||||
When you want to report errors on multiple fields, return an array:
|
||||
|
||||
```ts
|
||||
action: async (field) => {
|
||||
const result = await registerUser(field().value());
|
||||
if (result.ok) return;
|
||||
|
||||
return result.errors.map((err: {field: string; message: string}) => ({
|
||||
kind: 'serverError',
|
||||
message: err.message,
|
||||
fieldTree: field[err.field as keyof typeof field],
|
||||
}));
|
||||
};
|
||||
```
|
||||
|
||||
### Auto-clearing submission errors
|
||||
|
||||
Submission errors clear automatically when the user edits the field. If the `action` returns an error on the email field, that error disappears as soon as the user changes the email value.
|
||||
|
||||
This differs from validation errors, which recompute reactively. Validation rules run again on each change and may produce the same error. Submission errors are one-time results from the server — once cleared, they do not reappear unless the form is submitted again.
|
||||
|
||||
TIP: Submission errors appear alongside validation errors in the field's `errors()` signal. For guidance on displaying errors in your template, see the [Field State Management guide](guide/forms/signals/field-state-management).
|
||||
|
||||
## Handling invalid submissions with `onInvalid`
|
||||
|
||||
When validation fails, the `action` function does not run. If you need to respond to a failed submission attempt — such as scrolling to the first error, showing a toast, or focusing an invalid field — use the `onInvalid` callback.
|
||||
|
||||
```ts
|
||||
contactForm = form(
|
||||
this.contactModel,
|
||||
(schemaPath) => {
|
||||
required(schemaPath.name);
|
||||
required(schemaPath.email);
|
||||
},
|
||||
{
|
||||
submission: {
|
||||
action: async (field) => {
|
||||
await saveContact(field().value());
|
||||
},
|
||||
onInvalid: (field) => {
|
||||
const firstError = field().errorSummary()[0];
|
||||
firstError?.fieldTree().focusBoundControl();
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
The `onInvalid` callback receives the same `(field, detail)` parameters as `action`. It runs after all interactive fields are marked as touched, so validation errors are already visible in the UI when it executes.
|
||||
|
||||
## Controlling validation gating with `ignoreValidators`
|
||||
|
||||
By default, `submit()` ignores pending validators. If no validators have failed, the action runs even if some async validators are still in progress. The `ignoreValidators` option gives you control over this behavior.
|
||||
|
||||
| Value | Behavior |
|
||||
| ----------- | ------------------------------------------------------------------------ |
|
||||
| `'pending'` | Submit if no validators have failed, even if some are pending (default) |
|
||||
| `'none'` | Submit only if all validators pass — pending validators block submission |
|
||||
| `'all'` | Always submit regardless of validation state |
|
||||
|
||||
```ts
|
||||
contactForm = form(
|
||||
this.contactModel,
|
||||
(schemaPath) => {
|
||||
required(schemaPath.name);
|
||||
required(schemaPath.email);
|
||||
},
|
||||
{
|
||||
submission: {
|
||||
action: async (field) => {
|
||||
await saveContact(field().value());
|
||||
},
|
||||
ignoreValidators: 'none',
|
||||
},
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
Use `'none'` when your form has async validators (such as checking username availability) and you need all validation to complete before submitting. Use `'all'` for draft-saving scenarios where you want to persist data regardless of validation state.
|
||||
|
||||
## Manual submission with `submit()`
|
||||
|
||||
The `FormRoot` directive is the most common way to trigger submission, but you can also call `submit()` directly. This is useful for multi-step wizards, auto-save, or triggering submission from outside the form element.
|
||||
|
||||
```angular-ts
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {form, FormField, required, submit} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
selector: 'app-contact',
|
||||
imports: [FormField],
|
||||
template: `
|
||||
<label>
|
||||
Name
|
||||
<input [formField]="contactForm.name" />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Email
|
||||
<input type="email" [formField]="contactForm.email" />
|
||||
</label>
|
||||
|
||||
<button (click)="onSave()">Save</button>
|
||||
`,
|
||||
})
|
||||
export class Contact {
|
||||
contactModel = signal({
|
||||
name: '',
|
||||
email: '',
|
||||
});
|
||||
|
||||
contactForm = form(this.contactModel, (schemaPath) => {
|
||||
required(schemaPath.name);
|
||||
required(schemaPath.email);
|
||||
});
|
||||
|
||||
async onSave() {
|
||||
// When calling `submit()` directly, you pass the action as the second argument
|
||||
// instead of configuring it in `FormOptions`.
|
||||
const success = await submit(this.contactForm, async (field) => {
|
||||
const result = await saveContact(field().value());
|
||||
if (result.ok) return;
|
||||
|
||||
return {kind: 'serverError', message: 'Failed to save'};
|
||||
});
|
||||
|
||||
if (success) {
|
||||
// Handle success — navigate, show confirmation, etc.
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Handling side effects
|
||||
|
||||
The `submit()` function returns a `Promise<boolean>` — `true` when the action completes without errors, `false` when validation fails or the action returns errors. Use this to trigger side effects like navigation or notifications.
|
||||
|
||||
```ts
|
||||
async onSave() {
|
||||
const success = await submit(this.contactForm, async (field) => {
|
||||
await saveContact(field().value());
|
||||
});
|
||||
|
||||
if (success) {
|
||||
await this.router.navigate(['/confirmation']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When the action produces data that a side effect needs, such as a server-generated ID, handle the side effect inside the action:
|
||||
|
||||
```ts
|
||||
async onSave() {
|
||||
await submit(this.contactForm, async (field) => {
|
||||
const contact = await createContact(field().value());
|
||||
await this.router.navigate(['/confirmation', contact.id]);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
When using `FormRoot`, side effects also go inside the `action` since `FormRoot` calls `submit()` internally:
|
||||
|
||||
```ts
|
||||
submission: {
|
||||
action: async (field) => {
|
||||
const result = await saveContact(field().value());
|
||||
if (result.ok) {
|
||||
await this.router.navigate(['/confirmation']);
|
||||
return;
|
||||
}
|
||||
|
||||
return {kind: 'serverError', message: 'Failed to submit form'};
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
This guide covered submitting forms and handling form submission errors. Related guides explore other aspects of Signal Forms:
|
||||
|
||||
<docs-pill-row>
|
||||
<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/form-logic" title="Adding form logic" />
|
||||
</docs-pill-row>
|
||||
@@ -235,13 +235,79 @@ export class UserProfile {
|
||||
}
|
||||
```
|
||||
|
||||
The `SignalFormControl` synchronizes values and validation status bi-directionally:
|
||||
The `SignalFormControl` synchronizes values bi-directionally between the **Signal Forms** system and the **Reactive Forms** system:
|
||||
|
||||
- **Signal -> Control**: Changing `email.set(...)` updates `emailControl.value` and the parent `form.value`.
|
||||
- **Control -> Signal**: Typing in the input (updating `emailControl`) updates the `email` signal.
|
||||
- **Validation**: Schema validators (like `required`) propagate errors to `emailControl.errors`.
|
||||
- **Signal -> Reactive**: Updating the value via Signal Forms updates the Reactive Form control immediately.
|
||||
|
||||
### Disabling/Enabling control.
|
||||
```typescript
|
||||
// Signal Forms update
|
||||
this.emailControl.fieldTree().value.set('new@example.com');
|
||||
|
||||
// Reactive Forms reflects the change
|
||||
console.log(this.form.value); // {email: 'new@example.com'}
|
||||
```
|
||||
|
||||
- **Reactive -> Signal**: Updating the value via the parent `FormGroup` updates the Signal Forms state.
|
||||
|
||||
```typescript
|
||||
// Reactive Forms update
|
||||
this.form.patchValue({email: 'other@example.com'});
|
||||
|
||||
// Signal Forms reflects the change
|
||||
console.log(this.emailControl.fieldTree().value()); // 'other@example.com'
|
||||
```
|
||||
|
||||
### Binding `SignalFormControl`
|
||||
|
||||
To use `SignalFormControl` in a `FormGroup`, pass it as a control and bind it in the template using `.fieldTree`:
|
||||
|
||||
```typescript
|
||||
readonly emailControl = new SignalFormControl('', (p) => { required(p); });
|
||||
|
||||
readonly form = new FormGroup({
|
||||
name: new FormControl('Alice'),
|
||||
email: this.emailControl,
|
||||
});
|
||||
```
|
||||
|
||||
```angular-html {prefer}
|
||||
<form [formGroup]="form">
|
||||
<!-- Standard control -->
|
||||
<input formControlName="name" />
|
||||
|
||||
<!-- Signal control -->
|
||||
<input [formField]="emailControl.fieldTree" />
|
||||
</form>
|
||||
```
|
||||
|
||||
```angular-html {avoid}
|
||||
<!-- Avoid: Using formControlName or [formControl] for SignalFormControl -->
|
||||
<input formControlName="email" />
|
||||
<input [formControl]="emailControl" />
|
||||
```
|
||||
|
||||
### Why `SignalFormControl` takes a value instead of a signal
|
||||
|
||||
In standard Signal Forms, you create a form by passing a signal: `form(mySignal)`.
|
||||
|
||||
However, `SignalFormControl` takes a **raw value** (like a string or object) as its first argument:
|
||||
|
||||
```typescript
|
||||
// Takes a raw value, not a signal
|
||||
const userControl = new SignalFormControl({
|
||||
email: 'pirojok@example.com',
|
||||
});
|
||||
```
|
||||
|
||||
`SignalFormControl` creates the signal internally to intercept writes and trigger the **synchronous updates** expected by Reactive Forms.
|
||||
|
||||
You can still access the internal signal via `.sourceValue`:
|
||||
|
||||
```typescript
|
||||
const value = userControl.sourceValue();
|
||||
```
|
||||
|
||||
### Disabling/Enabling control
|
||||
|
||||
Imperative APIs for changing the enabled/disabled state (like `enable()`, `disable()`) are intentionally not supported
|
||||
in `SignalFormControl`. This is because the state of the control should be derived from the signal state and rules.
|
||||
@@ -383,6 +449,3 @@ bootstrapApplication(App, {
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
<!-- TODO: include some high level usage comment about how people should mostly interact with this via the signal forms API exposed on .fieldTree, not via the reactive forms methods. -->
|
||||
<!-- TODO: Elaborate on why the value taken is not a signal. -->
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
# Schemas and schema composability
|
||||
|
||||
Signal Forms uses a two-layer architecture to separate _how your form is structured_ from _how it behaves at runtime_.
|
||||
|
||||
When you pass a schema function to `form()`, that function _runs once_ during form creation. Its job is to set up the form's logic tree by declaring which fields have validation, which fields are disabled, and which fields depend on other fields. This is the **structural layer** of your form.
|
||||
|
||||
Inside a schema function, you call rule functions such as `disabled()` and `validate()`. These rule functions accept reactive logic that recomputes whenever the signals they reference change. Other rules like `required()` accept optional configuration, including a `when` function that conditionally activates the rule. Together, these form the **behavioral layer** of your form during runtime.
|
||||
|
||||
```ts
|
||||
contactForm = form(this.contactModel, (schemaPath) => {
|
||||
// Schema function: runs ONCE during form creation
|
||||
required(schemaPath.name);
|
||||
disabled(schemaPath.couponCode, ({valueOf}) => valueOf(schemaPath.total) < 50);
|
||||
// ^^^ Reactive logic: recomputes when total changes
|
||||
});
|
||||
```
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["form(model, schemaFn)"] --> B["Schema function runs ONCE"]
|
||||
B --> C["required(path)"]
|
||||
B --> D["disabled(path, logicFn)"]
|
||||
B --> E["validate(path, logicFn)"]
|
||||
B --> F["apply(path, schema)"]
|
||||
C --> G["Reactive: recomputes on signal change"]
|
||||
D --> G
|
||||
E --> G
|
||||
F --> B2["Nested schema function runs ONCE"]
|
||||
B2 --> G
|
||||
```
|
||||
|
||||
This distinction is important when you compose schemas because functions like `apply()`, `applyWhen()`, and `schema()` all operate at the structural layer. Schemas control _which_ rules exist and _whether_ they're active, while rule functions define _what_ those rules evaluate.
|
||||
|
||||
## Create reusable schemas with `schema()`
|
||||
|
||||
When multiple forms share the same rules for a common data shape, you can use the `schema()` function to extract those rules into a reusable schema.
|
||||
|
||||
```ts
|
||||
import {schema, required, minLength} from '@angular/forms/signals';
|
||||
|
||||
const nameSchema = schema<{first: string; last: string}>((name) => {
|
||||
required(name.first);
|
||||
required(name.last);
|
||||
minLength(name.first, 2);
|
||||
minLength(name.last, 2);
|
||||
});
|
||||
```
|
||||
|
||||
The `schema()` function wraps a function and converts it into a reusable `Schema<T>` object. Like any schema function, it _runs once_ per form, but the object itself can be shared across as many forms as you need.
|
||||
|
||||
TIP: If rules only appear in one place, an inline schema function works just as well. Use `schema()` when you want to reuse the same schema across multiple forms or apply the same schema to multiple paths. Reusable `Schema` objects are cached per form compilation.
|
||||
|
||||
### Using the schema with `apply()`
|
||||
|
||||
You can apply a reusable schema to a specific path in a form by using the `apply()` function. When you call `apply()`, the schema receives a scoped path that only sees the fields within that sub-path:
|
||||
|
||||
```ts
|
||||
import {apply} from '@angular/forms/signals';
|
||||
|
||||
profileForm = form(this.profileModel, (schemaPath) => {
|
||||
apply(schemaPath.name, nameSchema);
|
||||
});
|
||||
|
||||
registrationForm = form(this.registrationModel, (schemaPath) => {
|
||||
apply(schemaPath.name, nameSchema);
|
||||
});
|
||||
```
|
||||
|
||||
## Conditional schemas with `applyWhen()`
|
||||
|
||||
NOTE: The [Adding form logic guide](guide/forms/signals/form-logic) introduced `applyWhen()` for conditional rules with inline logic. This section covers how to compose `applyWhen()` with reusable schemas.
|
||||
|
||||
Some rules should only apply under certain conditions. For example, a zip code field might require validation only when the selected country is the United States.
|
||||
|
||||
The `applyWhen()` function applies a schema conditionally based on reactive state. It accepts three arguments:
|
||||
|
||||
1. A path to apply the schema to
|
||||
1. A reactive logic function that returns `true` when the schema should be active
|
||||
1. A schema or schema function containing the conditional rules
|
||||
|
||||
```ts
|
||||
import {form, applyWhen, required, pattern} from '@angular/forms/signals';
|
||||
|
||||
addressForm = form(this.addressModel, (schemaPath) => {
|
||||
applyWhen(
|
||||
schemaPath,
|
||||
({valueOf}) => valueOf(schemaPath.country) === 'US',
|
||||
(schemaPath) => {
|
||||
required(schemaPath.zipCode);
|
||||
pattern(schemaPath.zipCode, /^\d{5}(-\d{4})?$/);
|
||||
},
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
The logic function receives a `FieldContext`, which provides access to `value`, `valueOf`, `stateOf`, and other reactive helpers. Because it's reactive, the condition is re-evaluated whenever the signals it reads change. When the condition becomes `false`, the rules inside the schema deactivate. When it becomes `true` again, they reactivate.
|
||||
|
||||
The schema itself is still structural — the schema function runs once during form creation. The condition controls whether those rules are _active_, not whether they _exist_.
|
||||
|
||||
Inside the conditional schema, use the scoped path parameter passed to that schema function. Paths from an outer schema are not valid inside a nested schema.
|
||||
|
||||
### Combining `applyWhen()` with reusable schemas
|
||||
|
||||
Since `applyWhen()` accepts a `Schema` object, you can pair it with `schema()` to conditionally apply reusable schemas:
|
||||
|
||||
```ts
|
||||
const usZipCodeSchema = schema<{zipCode: string}>((address) => {
|
||||
required(address.zipCode);
|
||||
pattern(address.zipCode, /^\d{5}(-\d{4})?$/);
|
||||
});
|
||||
|
||||
const caPostalCodeSchema = schema<{postalCode: string}>((address) => {
|
||||
required(address.postalCode);
|
||||
pattern(address.postalCode, /^[A-Z]\d[A-Z] \d[A-Z]\d$/);
|
||||
});
|
||||
|
||||
shippingForm = form(this.shippingModel, (schemaPath) => {
|
||||
applyWhen(
|
||||
schemaPath.address,
|
||||
({valueOf}) => valueOf(schemaPath.country) === 'US',
|
||||
usZipCodeSchema,
|
||||
);
|
||||
applyWhen(
|
||||
schemaPath.address,
|
||||
({valueOf}) => valueOf(schemaPath.country) === 'CA',
|
||||
caPostalCodeSchema,
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
NOTE: The logic function accesses `valueOf(schemaPath.country)` even though the path argument is `schemaPath.address`. This is because the `valueOf` helper can access any field in the form, not just fields within the scoped path.
|
||||
|
||||
This pattern keeps validation logic modular — each country's address rules live in their own schema, and the form selects which one to activate based on the user's choice.
|
||||
|
||||
## Type-narrowing with `applyWhenValue()`
|
||||
|
||||
The `applyWhenValue()` function simplifies conditions that only need to check the field's value. Instead of receiving a `FieldContext`, the condition function receives the field's raw value directly.
|
||||
|
||||
```ts {header: "applyWhen — logic function receives FieldContext"}
|
||||
applyWhen(schemaPath.payment, ({value}) => value().type === 'credit-card', creditCardSchema);
|
||||
```
|
||||
|
||||
```ts {header: "applyWhenValue — condition receives the value directly"}
|
||||
applyWhenValue(schemaPath.payment, (payment) => payment.type === 'credit-card', creditCardSchema);
|
||||
```
|
||||
|
||||
The main advantage of `applyWhenValue()` is TypeScript type guard support. When the condition function is a type guard, the schema's type parameter narrows to the guarded type. This is especially useful for discriminated unions, where each variant has different fields that need different rules.
|
||||
|
||||
```ts
|
||||
import {form, applyWhenValue, required} from '@angular/forms/signals';
|
||||
|
||||
interface CreditCard {
|
||||
type: 'credit-card';
|
||||
cardNumber: string;
|
||||
expiry: string;
|
||||
cvv: string;
|
||||
}
|
||||
|
||||
interface BankTransfer {
|
||||
type: 'bank-transfer';
|
||||
accountNumber: string;
|
||||
routingNumber: string;
|
||||
}
|
||||
|
||||
type PaymentMethod = CreditCard | BankTransfer;
|
||||
|
||||
function isCreditCard(value: PaymentMethod): value is CreditCard {
|
||||
return value.type === 'credit-card';
|
||||
}
|
||||
|
||||
function isBankTransfer(value: PaymentMethod): value is BankTransfer {
|
||||
return value.type === 'bank-transfer';
|
||||
}
|
||||
|
||||
paymentForm = form(this.paymentModel, (schemaPath) => {
|
||||
applyWhenValue(schemaPath.payment, isCreditCard, (payment) => {
|
||||
// TypeScript knows payment is scoped to CreditCard
|
||||
required(payment.cardNumber);
|
||||
required(payment.expiry);
|
||||
required(payment.cvv);
|
||||
});
|
||||
|
||||
applyWhenValue(schemaPath.payment, isBankTransfer, (payment) => {
|
||||
// TypeScript knows payment is scoped to BankTransfer
|
||||
required(payment.accountNumber);
|
||||
required(payment.routingNumber);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Without the type guard, TypeScript would not know which fields are available inside each schema function. The type narrowing ensures that accessing `payment.cardNumber` is type-safe in the credit card branch and `payment.accountNumber` is type-safe in the bank transfer branch.
|
||||
|
||||
## Array items with `applyEach()`
|
||||
|
||||
When a form contains an array of objects, you often need the same rules applied to every item. The `applyEach()` function applies a schema to each item in an array field, regardless of how many items exist.
|
||||
|
||||
```ts
|
||||
import {form, applyEach, required, min} from '@angular/forms/signals';
|
||||
|
||||
type LineItem = {name: string; quantity: number};
|
||||
|
||||
orderForm = form(this.orderModel, (schemaPath) => {
|
||||
required(schemaPath.title);
|
||||
|
||||
applyEach(schemaPath.items, (item) => {
|
||||
required(item.name);
|
||||
min(item.quantity, 1);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
The schema function passed to `applyEach()` receives a `SchemaPathTree` scoped to a single array item. Rules declared inside apply to every item in the array, including items added after form creation.
|
||||
|
||||
### Combining `applyEach()` with reusable schemas
|
||||
|
||||
Since `applyEach()` accepts a `Schema` object, you can extract item-level rules into a reusable schema and share them across forms:
|
||||
|
||||
```ts
|
||||
const lineItemSchema = schema<LineItem>((item) => {
|
||||
required(item.name);
|
||||
min(item.quantity, 1);
|
||||
});
|
||||
|
||||
orderForm = form(this.orderModel, (schemaPath) => {
|
||||
required(schemaPath.title);
|
||||
applyEach(schemaPath.items, lineItemSchema);
|
||||
});
|
||||
|
||||
invoiceForm = form(this.invoiceModel, (schemaPath) => {
|
||||
required(schemaPath.invoiceNumber);
|
||||
applyEach(schemaPath.lineItems, lineItemSchema);
|
||||
});
|
||||
```
|
||||
|
||||
TIP: For more on validating array items, including custom error messages per field, see the [Validation guide](guide/forms/signals/validation).
|
||||
|
||||
## Next steps
|
||||
|
||||
To learn more about Signal Forms, check out these related guides:
|
||||
|
||||
- [Adding form logic](guide/forms/signals/form-logic) - Learn how to add conditional logic, dynamic behavior, and metadata to your forms
|
||||
- [Validation](guide/forms/signals/validation) - Learn about validation rules and error handling
|
||||
- [Async operations](guide/forms/signals/async-operations) - Learn how to handle form submission and async validation
|
||||
@@ -10,7 +10,7 @@ Angular supports two design approaches for interactive forms. Template-driven fo
|
||||
Template-driven forms are a great choice for small or simple forms, while reactive forms are more scalable and suitable for complex forms. For a comparison of the two approaches, see [Choosing an approach](guide/forms#choosing-an-approach)
|
||||
</docs-callout>
|
||||
|
||||
You can build almost any kind of form with an Angular template —login forms, contact forms, and pretty much any business form.
|
||||
You can build almost any kind of form with an Angular template — login forms, contact forms, and pretty much any business form.
|
||||
You can lay out the controls creatively and bind them to the data in your object model.
|
||||
You can specify validation rules and display validation errors, conditionally allow input from specific controls, trigger built-in visual feedback, and much more.
|
||||
|
||||
@@ -182,8 +182,7 @@ The following table describes the class names that Angular applies based on the
|
||||
| The control's value has changed. | `ng-dirty` | `ng-pristine` |
|
||||
| The control's value is valid. | `ng-valid` | `ng-invalid` |
|
||||
|
||||
Angular also applies the `ng-submitted` class to `form` elements upon submission,
|
||||
but not to the controls inside the `form` element.
|
||||
Angular also applies the `ng-submitted` class to `form` elements upon submission, but not to the controls inside the `form` element.
|
||||
|
||||
You use these CSS classes to define the styles for your control based on its status.
|
||||
|
||||
@@ -336,7 +335,7 @@ You will bind the form property that indicates its overall validity to the **Sub
|
||||
</docs-step>
|
||||
|
||||
<docs-step title="Run the application">
|
||||
Notice that the button is enabled —although it doesn't do anything useful yet.
|
||||
Notice that the button is enabled — although it doesn't do anything useful yet.
|
||||
</docs-step>
|
||||
|
||||
<docs-step title="Delete the Name value">
|
||||
|
||||
@@ -27,7 +27,7 @@ const emailDomain = login.value.email.domain;
|
||||
|
||||
With strictly typed reactive forms, the above code does not compile, because there is no `domain` property on `email`.
|
||||
|
||||
In addition to the added safety, the types enable a variety of other improvements, such as better autocomplete in IDEs, and an explicit way to specify form structure.
|
||||
In addition to the added safety, the types enable a variety of other improvements, such as better autocomplete in IDEs and an explicit way to specify form structure.
|
||||
|
||||
These improvements currently apply only to _reactive_ forms (not [_template-driven_ forms](guide/forms/template-driven-forms)).
|
||||
|
||||
@@ -42,7 +42,7 @@ const login = new UntypedFormGroup({
|
||||
});
|
||||
```
|
||||
|
||||
Each `Untyped` symbol has exactly the same semantics as in previous Angular version. By removing the `Untyped` prefixes, you can incrementally enable the types.
|
||||
Each `Untyped` symbol has exactly the same semantics as in previous Angular versions. By removing the `Untyped` prefixes, you can incrementally enable the types.
|
||||
|
||||
## `FormControl`: Getting Started
|
||||
|
||||
@@ -52,7 +52,7 @@ The simplest possible form consists of a single control:
|
||||
const email = new FormControl('angularrox@gmail.com');
|
||||
```
|
||||
|
||||
This control will be automatically inferred to have the type `FormControl<string|null>`. TypeScript will automatically enforce this type throughout the [`FormControl` API](api/forms/FormControl), such as `email.value`, `email.valueChanges`, `email.setValue(...)`, etc.
|
||||
This control will be automatically inferred to have the type `FormControl<string|null>`. TypeScript will automatically enforce this type throughout the [`FormControl` API](api/forms/FormControl), such as `email.value`, `email.valueChanges`, and `email.setValue(...)`.
|
||||
|
||||
### Nullability
|
||||
|
||||
|
||||
@@ -147,6 +147,29 @@ export class UserDetail {
|
||||
}
|
||||
```
|
||||
|
||||
When navigating multiple levels up, all `..` segments must be in the **first element** of the commands array. The router only parses `..` from the first command string — subsequent array elements are treated as literal path segments.
|
||||
|
||||
```angular-ts {prefer}
|
||||
// From: /team/123/users/456
|
||||
// Result: /team/123/settings
|
||||
this.router.navigate(['../../settings'], {relativeTo: this.route});
|
||||
```
|
||||
|
||||
When using `relativeTo`, never prefix the first command with `/`. A leading `/` makes the navigation absolute and ignores `relativeTo` entirely.
|
||||
|
||||
```angular-ts {prefer}
|
||||
// From: /team/123/users/456
|
||||
// Result: /team/123/users/456/edit
|
||||
this.router.navigate(['edit'], {relativeTo: this.route});
|
||||
```
|
||||
|
||||
```angular-ts {avoid}
|
||||
// From: /team/123/users/456
|
||||
// Leading '/' causes absolute navigation — relativeTo is ignored
|
||||
// Result: /edit
|
||||
this.router.navigate(['/edit'], {relativeTo: this.route});
|
||||
```
|
||||
|
||||
### `router.navigateByUrl()`
|
||||
|
||||
The `router.navigateByUrl()` method provides a direct way to programmatically navigate using URL path strings rather than array segments. This method is ideal when you have a full URL path and need to perform absolute navigation, especially when working with externally provided URLs or deep linking scenarios.
|
||||
|
||||
@@ -376,9 +376,10 @@ The validation rules are:
|
||||
- `Host` and `X-Forwarded-Host` headers are validated against a strict allowlist and cannot contain path separators.
|
||||
- `X-Forwarded-Port` header must be numeric.
|
||||
- `X-Forwarded-Proto` header must be `http` or `https`.
|
||||
- `X-Forwarded-Prefix` header must not start with `\` or multiple `/` or contain `.`, `..` path segments.
|
||||
- `X-Forwarded-Prefix` header must start with `/` and contain only alphanumeric characters, hyphens, and underscores, separated by single slashes.
|
||||
- By default, all `X-Forwarded-*` headers are treated as untrusted and are removed from the request. To retain them, they must be explicitly allowed by configuring `trustProxyHeaders`.
|
||||
|
||||
Invalid or disallowed headers now trigger an error log. Requests with unrecognized hostnames will result in a Client-Side Rendered (CSR) page if `allowedHosts` is defined; if not, a `400 Bad Request` is issued. Note that in a future major release, all unrecognized hostnames will default to a `400 Bad Request` regardless of `allowedHosts` settings.
|
||||
Invalid headers trigger an error log, and unallowed proxy headers are removed from the request. Requests with unrecognized hostnames will result in a `400 Bad Request` is issued.
|
||||
|
||||
NOTE: Most cloud providers and CDN providers perform automatic validation of these headers before a request ever reaches the application origin. This inherent filtering significantly reduces the practical attack surface.
|
||||
|
||||
@@ -433,6 +434,24 @@ export NG_ALLOWED_HOSTS="example.com,*.trusted-example.com"
|
||||
|
||||
IMPORTANT: You can use `*` as a value in `allowedHosts` to allow all hostnames, though this is generally discouraged and presents a security risk. Accepting any host header can expose your application to host header injection and [Server-Side Request Forgery (SSRF)](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/SSRF) attacks. This configuration should only be used when validation for `Host` and `X-Forwarded-Host` headers is performed in another layer, such as a load balancer or reverse proxy. For better security, we recommend using an explicit list of allowed hosts whenever possible. See [GHSA-x288-3778-4hhx](https://github.com/angular/angular-cli/security/advisories/GHSA-x288-3778-4hhx) for more details.
|
||||
|
||||
### Configuring trusted proxy headers
|
||||
|
||||
By default, Angular ignores all `X-Forwarded-*` headers. If your application is behind a trusted reverse proxy (like a load balancer) that sets these headers, you can configure Angular to trust them.
|
||||
|
||||
You can configure `trustProxyHeaders` when initializing the application engine:
|
||||
|
||||
```typescript
|
||||
const appEngine = new AngularAppEngine({
|
||||
trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'], // Trust specific headers
|
||||
});
|
||||
|
||||
const nodeAppEngine = new AngularNodeAppEngine({
|
||||
trustProxyHeaders: true, // Trust all X-Forwarded-* headers
|
||||
});
|
||||
```
|
||||
|
||||
IMPORTANT: Only enable `trustProxyHeaders` if your application is behind a trusted proxy that strictly validates or overrides these headers. Otherwise, attackers can spoof these headers to cause [Server-Side Request Forgery (SSRF)](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/SSRF) attacks.
|
||||
|
||||
## Auditing Angular applications
|
||||
|
||||
Angular applications must follow the same security principles as regular web applications, and must be audited as such.
|
||||
|
||||
@@ -133,7 +133,7 @@ class MyMenuItem {}
|
||||
class MyMenu {
|
||||
triggerText = input('');
|
||||
|
||||
@ContentChildren(MyMenuItem) items: QueryList<MyMenuItem>;
|
||||
items = contentChildren(MyMenuItem);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ generate_guides(
|
||||
"//adev/src/assets/icons:forms.svg",
|
||||
"//adev/src/assets/icons:language-service.svg",
|
||||
"//adev/src/assets/icons:ng-update.svg",
|
||||
"//adev/src/assets/icons:playground.svg",
|
||||
"//adev/src/assets/icons:routing.svg",
|
||||
"//adev/src/assets/icons:signals.svg",
|
||||
"//adev/src/assets/icons:ssr.svg",
|
||||
|
||||
@@ -269,3 +269,7 @@ To learn more about Signal Forms and how it works, check out the in-depth guides
|
||||
- [Form models](guide/forms/signals/models) - Creating and managing form data with signals
|
||||
- [Field state management](guide/forms/signals/field-state-management) - Working with validation state, interaction tracking, and field visibility
|
||||
- [Validation](guide/forms/signals/validation) - Built-in validators, custom validation rules, and async validation
|
||||
|
||||
<docs-pill-row>
|
||||
<docs-pill title="Modular design with dependency injection" href="essentials/dependency-injection" />
|
||||
</docs-pill-row>
|
||||
|
||||
@@ -147,6 +147,6 @@ TIP: Want to know more about Angular templates? See the [In-depth Templates guid
|
||||
Now that you have dynamic data and templates in the application, it's time to learn how to enhance templates by conditionally hiding or showing certain elements, looping over elements, and more.
|
||||
|
||||
<docs-pill-row>
|
||||
<docs-pill title="Modular design with dependency injection" href="essentials/dependency-injection" />
|
||||
<docs-pill title="Forms with Signals" href="essentials/signal-forms" />
|
||||
<docs-pill title="In-depth template guide" href="guide/templates" />
|
||||
</docs-pill-row>
|
||||
|
||||
@@ -7,11 +7,9 @@ Get started with Angular quickly with online starters or locally with your termi
|
||||
|
||||
If you just want to play around with Angular in your browser without setting up a project, you can use our online sandbox:
|
||||
|
||||
<docs-card-container>
|
||||
<docs-card title="" href="/playground" link="Open on Playground">
|
||||
The fastest way to play with an Angular app. No setup required.
|
||||
</docs-card>
|
||||
</docs-card-container>
|
||||
<docs-card title="Playground" href="/playground" link="Open on Playground" iconImgSrc="adev/src/assets/icons/playground.svg" titleInline>
|
||||
The fastest way to play with an Angular app. No setup required.
|
||||
</docs-card>
|
||||
|
||||
## Set up a new project locally
|
||||
|
||||
@@ -21,7 +19,7 @@ If you're starting a new project, you'll most likely want to create a local proj
|
||||
|
||||
- **Node.js** - [v20.19.0 or newer](/reference/versions)
|
||||
- **Text editor** - We recommend [Visual Studio Code](https://code.visualstudio.com/)
|
||||
- **Terminal** - Required for running Angular CLI commands
|
||||
- **Terminal** - Required for running [Angular CLI](/tools/cli) commands
|
||||
- **Development Tool** - To improve your development workflow, we recommend the [Angular Language Service](/tools/language-service)
|
||||
|
||||
### Instructions
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
Angular CLI's `ng update` runs automated code transformations that automatically handle routine breaking changes, dramatically simplifying major version updates. Keeping up with the latest version keeps your app as fast and secure as possible.
|
||||
</docs-card>
|
||||
<docs-card title="Language Service" href="tools/language-service" link="Language Service" iconImgSrc="adev/src/assets/icons/language-service.svg">
|
||||
Angular's IDE language services powers code completion, navigation, refactoring, and real-time diagnostics in your favorite editor.
|
||||
Angular's IDE language service powers code completion, navigation, refactoring, and real-time diagnostics in your favorite editor.
|
||||
</docs-card>
|
||||
</docs-card-container>
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Missing Control Value
|
||||
|
||||
This error occurs when you call `setValue` on a `FormGroup` or `FormArray` but the value you pass is missing an entry for one of the registered controls.
|
||||
|
||||
`setValue` is strict — it expects a value for every control. If you want to update only some controls, use `patchValue` instead.
|
||||
|
||||
## Debugging the error
|
||||
|
||||
Check which control is named in the error message, then make sure your value object includes it.
|
||||
|
||||
A common source of this error is spreading an object that doesn't have all the keys:
|
||||
|
||||
```typescript
|
||||
const someValue = {first: 'Nancy'}; // 'last' is missing
|
||||
|
||||
form.setValue({...someValue}); // throws NG01002
|
||||
```
|
||||
|
||||
This can happen if `someValue` comes from an API response, a partial state update, or a type that doesn't fully match the form structure. In those cases, either fill in the missing keys explicitly or switch to `patchValue`.
|
||||
|
||||
### FormGroup
|
||||
|
||||
```typescript
|
||||
const form = new FormGroup({
|
||||
first: new FormControl(''),
|
||||
last: new FormControl(''),
|
||||
});
|
||||
|
||||
// 'last' is not in the value — throws NG01002
|
||||
form.setValue({first: 'Nancy'});
|
||||
|
||||
// both controls are covered — works fine
|
||||
form.setValue({first: 'Nancy', last: 'Drew'});
|
||||
```
|
||||
|
||||
### FormArray
|
||||
|
||||
```typescript
|
||||
const formArray = new FormArray([new FormControl(''), new FormControl('')]);
|
||||
|
||||
// only one value for two controls — throws NG01002
|
||||
formArray.setValue(['Nancy']);
|
||||
|
||||
// one value per control — works fine
|
||||
formArray.setValue(['Nancy', 'Drew']);
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
# Orphan field in signal forms
|
||||
|
||||
This error indicates that a `Field` instance is no longer connected to the current model structure.
|
||||
Angular signal forms throw `NG01902` when a field path that previously existed can no longer be resolved as the same property in the parent object.
|
||||
|
||||
A common trigger is unintentionally setting a field's value to `undefined`.
|
||||
In signal forms, `undefined` means "field does not exist", so an existing field can become orphaned.
|
||||
|
||||
## Why this happens
|
||||
|
||||
Signal forms keep field instances in sync with your model shape.
|
||||
If code still references an older field instance after the model shape changed, Angular detects the mismatch and throws this error.
|
||||
|
||||
This can happen when:
|
||||
|
||||
- a property is removed from the model object
|
||||
- a property becomes `undefined` instead of remaining present
|
||||
- code holds a stale field reference across structural updates
|
||||
|
||||
## How to fix it
|
||||
|
||||
- Avoid writing `undefined` to model fields. Use `null` or another explicit value when the field should still exist.
|
||||
- Keep the model shape stable while a field is in use.
|
||||
- Re-read fields from the current form tree after structural updates instead of reusing stale references.
|
||||
|
||||
For model design guidance, see [Avoid `undefined`](/guide/forms/signals/model-design#avoid-undefined).
|
||||
@@ -17,7 +17,7 @@ class Test {
|
||||
}
|
||||
```
|
||||
|
||||
In the provided example the `item.key` tracking expression will find two duplicate keys `a` (at index 0 and 2).
|
||||
In the provided example the `item.value` tracking expression will find two duplicate keys `a` (at index 0 and 2).
|
||||
|
||||
Duplicate keys are problematic from the correctness point of view: since the `@for` loop can't uniquely identify items it might choose DOM nodes corresponding to _another_ item (with the same key) when performing DOM moves or destroy.
|
||||
|
||||
|
||||
@@ -36,8 +36,10 @@
|
||||
| `NG0951` | [Child query result is required but no value is available](errors/NG0951) |
|
||||
| `NG0955` | [Track expression resulted in duplicated keys for a given collection](errors/NG0955) |
|
||||
| `NG0956` | [Tracking expression caused re-creation of the DOM structure](errors/NG0956) |
|
||||
| `NG01002` | [Missing Control Value](errors/NG01002) |
|
||||
| `NG01101` | [Wrong Async Validator Return Type](errors/NG01101) |
|
||||
| `NG01203` | [Missing value accessor](errors/NG01203) |
|
||||
| `NG01902` | [Orphan field in signal forms](errors/NG01902) |
|
||||
| `NG02200` | [Missing Iterable Differ](errors/NG02200) |
|
||||
| `NG02800` | [JSONP support in HttpClient configuration](errors/NG02800) |
|
||||
| `NG02802` | [Headers not transferred by HttpTransferCache](errors/NG02802) |
|
||||
|
||||
@@ -8,13 +8,13 @@ Angular CLI includes four builders typically used as `build` targets:
|
||||
|
||||
| Builder | Purpose |
|
||||
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `@angular-devkit/build-angular:application` | Builds an application with a client-side bundle, a Node server, and build-time prerendered routes with [esbuild](https://esbuild.github.io/). |
|
||||
| `@angular/build:application` | Builds an application with a client-side bundle, a Node server, and build-time prerendered routes with [esbuild](https://esbuild.github.io/). |
|
||||
| `@angular-devkit/build-angular:browser-esbuild` | Bundles a client-side application for use in a browser with [esbuild](https://esbuild.github.io/). See [`browser-esbuild` documentation](tools/cli/build-system-migration#manual-migration-to-the-compatibility-builder) for more information. |
|
||||
| `@angular-devkit/build-angular:browser` | Bundles a client-side application for use in a browser with [webpack](https://webpack.js.org/). |
|
||||
| `@angular-devkit/build-angular:ng-packagr` | Builds an Angular library adhering to [Angular Package Format](tools/libraries/angular-package-format). |
|
||||
| `@angular/build:ng-packagr` | Builds an Angular library adhering to [Angular Package Format](tools/libraries/angular-package-format). |
|
||||
|
||||
Applications generated by `ng new` use `@angular-devkit/build-angular:application` by default.
|
||||
Libraries generated by `ng generate library` use `@angular-devkit/build-angular:ng-packagr` by default.
|
||||
Applications generated by `ng new` use `@angular/build:application` by default.
|
||||
Libraries generated by `ng generate library` use `@angular/build:ng-packagr` by default.
|
||||
|
||||
You can determine which builder is being used for a particular project by looking up the `build` target for that project.
|
||||
|
||||
@@ -25,7 +25,7 @@ You can determine which builder is being used for a particular project by lookin
|
||||
"architect": {
|
||||
// `ng build` invokes the Architect target named `build`.
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"builder": "@angular/build:application",
|
||||
…
|
||||
},
|
||||
"serve": { … }
|
||||
@@ -37,7 +37,7 @@ You can determine which builder is being used for a particular project by lookin
|
||||
}
|
||||
```
|
||||
|
||||
This page discusses usage and options of `@angular-devkit/build-angular:application`.
|
||||
This page discusses usage and options of `@angular/build:application`.
|
||||
|
||||
## Output directory
|
||||
|
||||
@@ -142,7 +142,7 @@ If the best option is to use a CommonJS dependency, you can disable these warnin
|
||||
|
||||
```json
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"allowedCommonJsDependencies": [
|
||||
"lodash"
|
||||
|
||||
@@ -147,8 +147,7 @@ In the `package.json` file, add a `builders` key that tells the Architect tool w
|
||||
"description": "Builder for copying files",
|
||||
"builders": "builders.json",
|
||||
"dependencies": {
|
||||
"@angular-devkit/architect": "~0.1200.0",
|
||||
"@angular-devkit/core": "^12.0.0"
|
||||
"@angular/build": "^21.2.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -169,7 +168,7 @@ A target specifies the builder to use, its default options configuration, and na
|
||||
Architect in the Angular CLI uses the target definition to resolve input options for a given run.
|
||||
|
||||
The `angular.json` file has a section for each project, and the "architect" section of each project configures targets for builders used by CLI commands such as 'build', 'test', and 'serve'.
|
||||
By default, for example, the `ng build` command runs the builder `@angular-devkit/build-angular:browser` to perform the build task, and passes in default option values as specified for the `build` target in `angular.json`.
|
||||
By default, for example, the `ng build` command runs the builder `@angular/build:application` to perform the build task, and passes in default option values as specified for the `build` target in `angular.json`.
|
||||
|
||||
```json {header: "angular.json"}
|
||||
{
|
||||
@@ -177,7 +176,7 @@ By default, for example, the `ng build` command runs the builder `@angular-devki
|
||||
"...": "...",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": "dist/myApp",
|
||||
"index": "src/index.html",
|
||||
@@ -267,7 +266,7 @@ If you create a new project with `ng new builder-test`, the generated `angular.j
|
||||
"builder-test": {
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": "dist/builder-test",
|
||||
"index": "src/index.html",
|
||||
@@ -278,8 +277,7 @@ If you create a new project with `ng new builder-test`, the generated `angular.j
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"aot": true,
|
||||
"buildOptimizer": true
|
||||
"aot": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ To manually deploy your application, create a production build and copy the outp
|
||||
By default, `ng build` uses the `production` configuration.
|
||||
If you have customized your build configurations, you may want to confirm [production optimizations](tools/cli/deployment#production-optimizations) are being applied before deploying.
|
||||
|
||||
`ng build` outputs the built artifacts to `dist/my-app/` by default, however this path can be configured with the `outputPath` option in the `@angular-devkit/build-angular:browser` builder.
|
||||
`ng build` outputs the built artifacts to `dist/my-app/` by default, however this path can be configured with the `outputPath` option in the `@angular/build:application` builder.
|
||||
Copy this directory to the server and configure it to serve the directory.
|
||||
|
||||
While this is a minimal deployment solution, there are a few requirements for the server to serve your Angular application correctly.
|
||||
|
||||
@@ -16,7 +16,7 @@ Angular CLI builders support a `configurations` object, which allows overwriting
|
||||
"my-app": {
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
// By default, disable source map generation.
|
||||
"sourceMap": false
|
||||
@@ -54,7 +54,7 @@ ng build --configuration debug,production,customer-facing
|
||||
|
||||
## Configure environment-specific defaults
|
||||
|
||||
`@angular-devkit/build-angular:browser` supports file replacements, an option for substituting source files before executing a build.
|
||||
`@angular/build:application` supports file replacements, an option for substituting source files before executing a build.
|
||||
Using this in combination with `--configuration` provides a mechanism for configuring environment-specific data in your application.
|
||||
|
||||
Start by [generating environments](cli/generate/environments) to create the `src/environments/` directory and configure the project to use file replacements.
|
||||
@@ -99,6 +99,8 @@ export const environment = {
|
||||
};
|
||||
```
|
||||
|
||||
CRITICAL: Files in `src/environments/` are bundled into your client-side application and visible to anyone who loads the page. Never store secrets such as API keys here. Use a server-side proxy or a secrets manager instead.
|
||||
|
||||
You can add target-specific configuration files, such as `environment.development.ts`.
|
||||
The following content sets default values for the development build target:
|
||||
|
||||
@@ -186,7 +188,7 @@ You can also configure `ng serve` to use the targeted build configuration if you
|
||||
```json
|
||||
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular/build:dev-server",
|
||||
"options": { … },
|
||||
"configurations": {
|
||||
"development": {
|
||||
|
||||
@@ -15,15 +15,16 @@ HELPFUL: Chrome's new tab page does not run installed extensions, so the Angular
|
||||
|
||||
## Open your application
|
||||
|
||||
When you open the extension, you'll see three additional tabs:
|
||||
When you open the extension, you'll see four additional tabs:
|
||||
|
||||
| Tabs | Details |
|
||||
| :---------------------------------------- | :--------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Components](tools/devtools/component) | Lets you explore the components and directives in your application and preview or edit their state. |
|
||||
| [Profiler](tools/devtools/profiler) | Lets you profile your application and understand what the performance bottleneck is during change detection execution. |
|
||||
| [Injector Tree](tools/devtools/injectors) | Lets you visualize the Environment and Element Injector hierarchy |
|
||||
| [Router Tree](tools/devtools/router) | Lets you visualize the routing tree of your application. |
|
||||
|
||||
Other tabs like `Router Tree` or `Transfer State` are experimental and can be enabled via the devtools settings and are not documented yet.
|
||||
Other tabs like `Transfer State` are experimental and can be enabled via the devtools settings and are not documented yet.
|
||||
|
||||
HELPFUL: For users of Chromium based browsers, you might be interested in the [Performance panel integration](/best-practices/profiling-with-chrome-devtools).
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Inspect the Router Tree
|
||||
|
||||
The **Router Tree** tab lets you visualize the routing tree of your application. You can explore how routes are nested and view details about specific routes.
|
||||
|
||||
<img src="assets/images/guide/devtools/router-tree.png" alt="A screenshot of the 'Router Tree' tab in Angular DevTools showing a tree of configured routes. The active routes are highlighted in green, while inactive ones are white.">
|
||||
|
||||
### View route details
|
||||
|
||||
When you select a specific route in the tree, Angular DevTools displays its properties in the sidebar on the right. This information includes:
|
||||
|
||||
- **Path**: The URL path for the route. If the route uses a custom URL matcher, DevTools displays the **Matcher** instead.
|
||||
- **Component**: The component rendered for this route. If the route is a redirect, DevTools displays the **Redirect to** target instead.
|
||||
- **Path Match**: The path matching strategy (`prefix` or `full`), if configured.
|
||||
- **Data**: Static data associated with the route, displayed as a JSON tree.
|
||||
- **Resolvers**: Route resolvers, displayed as key-value pairs.
|
||||
- **Guards**: Any guards configured on the route, grouped by type — `canActivate`, `canActivateChild`, `canDeactivate`, and `canMatch`.
|
||||
- **Providers**: Route-level providers, if configured.
|
||||
- **Title**: The route title, if configured.
|
||||
- **RunGuardsAndResolvers**: The re-run strategy for guards and resolvers, if configured.
|
||||
- **Active**: Whether this route is currently active.
|
||||
- **Auxiliary**: Indicates if the route is an auxiliary route (e.g., in a named outlet).
|
||||
- **Lazy**: Indicates if the route is lazily loaded.
|
||||
|
||||
Note: Properties like Path Match, Data, Resolvers, Guards, Providers, Title, and RunGuardsAndResolvers only appear in the sidebar when they are configured on the selected route.
|
||||
|
||||
### Navigate to a specific route
|
||||
|
||||
You can easily trigger navigation directly from the DevTools. While inspecting a route's details in the right sidebar, click on the **Navigate** icon next to the path string. This triggers the Angular router to navigate to that URL in your application.
|
||||
|
||||
<img src="assets/images/guide/devtools/router-tree-navigate.png" alt="A screenshot showing the 'Navigate to' tooltip on the route path in the 'Routes Details' sidebar.">
|
||||
@@ -49,7 +49,7 @@ When you generate a new library, the workspace configuration file, `angular.json
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"builder": "@angular/build:ng-packagr",
|
||||
…
|
||||
|
||||
```
|
||||
@@ -228,7 +228,7 @@ ng build my-lib --watch
|
||||
|
||||
IMPORTANT: The CLI `build` command uses a different builder and invokes a different build tool for libraries than it does for applications.
|
||||
|
||||
- The build system for applications, `@angular-devkit/build-angular`, is based on `webpack`, and is included in all new Angular CLI projects
|
||||
- The build system for applications, `@angular/build`, is based on `esbuild`, and is included in all new Angular CLI projects
|
||||
- The build system for libraries is based on `ng-packagr`.
|
||||
It is only added to your dependencies when you add a library using `ng generate library my-lib`.
|
||||
|
||||
@@ -274,7 +274,7 @@ To use linked libraries, you need to configure your application's `angular.json`
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular/build:dev-server",
|
||||
"options": {
|
||||
"prebundle": {
|
||||
"exclude": ["my-lib"]
|
||||
|
||||
+283
-263
File diff suppressed because it is too large
Load Diff
+294
-274
File diff suppressed because it is too large
Load Diff
+277
-257
File diff suppressed because it is too large
Load Diff
+283
-263
File diff suppressed because it is too large
Load Diff
+288
-268
File diff suppressed because it is too large
Load Diff
+283
-263
File diff suppressed because it is too large
Load Diff
+283
-263
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ It also explains the basic mechanics of using `git`, `node`, and `pnpm`.
|
||||
- [Building and Testing Angular](#building-and-testing-angular)
|
||||
- [Prerequisite Software](#prerequisite-software)
|
||||
- [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)
|
||||
- [Building](#building)
|
||||
@@ -57,6 +58,10 @@ following on your development machine:
|
||||
|
||||
You can also use the provided [Dev Container](https://containers.dev/) configuration to set up a development environment. This approach uses Docker to create a container with all the necessary tools (Node.js, pnpm, etc.) pre-installed.
|
||||
|
||||
## Experimenting in dev-app
|
||||
|
||||
For experimentation while developing Angular, consider running the [dev-app](../dev-app/README.md) in this repository.
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- [Docker Desktop](https://www.docker.com/products/docker-desktop)
|
||||
|
||||
@@ -5,7 +5,10 @@ particularly low-quality or low-value contributions, may see those contributions
|
||||
automatically closed without consideration.
|
||||
|
||||
Community contributors should limit themselves to no more than 3 open PRs at a
|
||||
single time.
|
||||
single time. If you have more than 3 open PRs, we ask that you wait until some of those
|
||||
PRs are merged or closed before opening more.
|
||||
|
||||
Draft pull requests are considered acceptable beyond the limit if 3, but within a reasonable limit (~10 or more within a short time frame). Beyond that reasonable limit, the team may still consider it spam and close them without consideration.
|
||||
|
||||
## Why?
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
"@angular/platform-server": "workspace:*",
|
||||
"@angular/localize": "workspace:*",
|
||||
"@angular/router": "workspace:*",
|
||||
"@angular/ssr": "21.2.6",
|
||||
"@angular/ssr": "21.2.9",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "21.2.6",
|
||||
"@angular/cli": "21.2.6",
|
||||
"@angular/build": "21.2.9",
|
||||
"@angular/cli": "21.2.9",
|
||||
"@angular/compiler-cli": "workspace:*",
|
||||
"jsdom": "^28.0.0",
|
||||
"typescript": "~5.9.2",
|
||||
|
||||
@@ -21,7 +21,7 @@ export const enum RuntimeErrorCode {
|
||||
// (undocumented)
|
||||
MISSING_CONTROL = 1001,
|
||||
// (undocumented)
|
||||
MISSING_CONTROL_VALUE = 1002,
|
||||
MISSING_CONTROL_VALUE = -1002,
|
||||
// (undocumented)
|
||||
NAME_AND_FORM_CONTROL_NAME_MUST_MATCH = 1202,
|
||||
// (undocumented)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cdk": "21.2.5",
|
||||
"@angular/cdk": "21.2.9",
|
||||
"@angular/common": "link:./in-existing-linked-by-bazel",
|
||||
"@angular/compiler": "link:./in-existing-linked-by-bazel",
|
||||
"@angular/core": "link:./in-existing-linked-by-bazel",
|
||||
@@ -19,15 +19,15 @@
|
||||
"@angular/platform-browser": "link:./in-existing-linked-by-bazel",
|
||||
"@angular/platform-browser-dynamic": "link:./in-existing-linked-by-bazel",
|
||||
"@angular/router": "link:./in-existing-linked-by-bazel",
|
||||
"@angular/ssr": "21.2.6",
|
||||
"@angular/ssr": "21.2.9",
|
||||
"rxjs": "^7.0.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "0.16.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "21.2.6",
|
||||
"@angular/build": "21.2.6",
|
||||
"@angular/cli": "21.2.6",
|
||||
"@angular-devkit/build-angular": "21.2.9",
|
||||
"@angular/build": "21.2.9",
|
||||
"@angular/cli": "21.2.9",
|
||||
"@angular/compiler-cli": "link:./in-existing-linked-by-bazel",
|
||||
"@types/jasmine": "^6.0.0",
|
||||
"@types/node": "^20.14.8",
|
||||
@@ -38,5 +38,5 @@
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0"
|
||||
"packageManager": "pnpm@10.33.2"
|
||||
}
|
||||
|
||||
Generated
+530
-556
File diff suppressed because it is too large
Load Diff
@@ -29,9 +29,9 @@
|
||||
"zone.js": "0.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "21.2.6",
|
||||
"@angular/build": "21.2.6",
|
||||
"@angular/cli": "21.2.6",
|
||||
"@angular-devkit/build-angular": "21.2.9",
|
||||
"@angular/build": "21.2.9",
|
||||
"@angular/cli": "21.2.9",
|
||||
"@angular/compiler-cli": "link:./in-existing-linked-by-bazel",
|
||||
"@types/jasmine": "^6.0.0",
|
||||
"@types/jasminewd2": "^2.0.8",
|
||||
@@ -45,5 +45,5 @@
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0"
|
||||
"packageManager": "pnpm@10.33.2"
|
||||
}
|
||||
|
||||
+529
-555
File diff suppressed because it is too large
Load Diff
@@ -13,19 +13,19 @@
|
||||
"@angular/core": "link:./in-existing-linked-by-bazel",
|
||||
"@angular/platform-browser": "link:./in-existing-linked-by-bazel",
|
||||
"@angular/router": "link:./in-existing-linked-by-bazel",
|
||||
"@angular/ssr": "21.2.6",
|
||||
"@angular/ssr": "21.2.9",
|
||||
"rxjs": "^7.0.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "0.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "21.2.6",
|
||||
"@angular/build": "21.2.6",
|
||||
"@angular/cli": "21.2.6",
|
||||
"@angular-devkit/build-angular": "21.2.9",
|
||||
"@angular/build": "21.2.9",
|
||||
"@angular/cli": "21.2.9",
|
||||
"@angular/compiler-cli": "link:./in-existing-linked-by-bazel",
|
||||
"@types/node": "^20.14.8",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0"
|
||||
"packageManager": "pnpm@10.33.2"
|
||||
}
|
||||
|
||||
+490
-516
File diff suppressed because it is too large
Load Diff
@@ -18,19 +18,19 @@
|
||||
"@angular/forms": "link:./in-existing-linked-by-bazel",
|
||||
"@angular/platform-browser": "link:./in-existing-linked-by-bazel",
|
||||
"@angular/router": "link:./in-existing-linked-by-bazel",
|
||||
"@angular/ssr": "21.2.6",
|
||||
"@angular/ssr": "21.2.9",
|
||||
"rxjs": "^7.0.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "0.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "21.2.6",
|
||||
"@angular/build": "21.2.6",
|
||||
"@angular/cli": "21.2.6",
|
||||
"@angular-devkit/build-angular": "21.2.9",
|
||||
"@angular/build": "21.2.9",
|
||||
"@angular/cli": "21.2.9",
|
||||
"@angular/compiler-cli": "link:./in-existing-linked-by-bazel",
|
||||
"@types/node": "^20.14.8",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0"
|
||||
"packageManager": "pnpm@10.33.2"
|
||||
}
|
||||
|
||||
+490
-516
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng build --configuration production && concurrently \"serve dist/browser -l 4210 --no-clipboard --single\" \"protractor e2e/protractor.conf.js --baseUrl=http://localhost:4210\" --kill-others --success first"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"packageManager": "pnpm@10.33.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "link:./in-existing-linked-by-bazel",
|
||||
@@ -25,8 +25,8 @@
|
||||
"zone.js": "0.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "21.2.6",
|
||||
"@angular/cli": "21.2.6",
|
||||
"@angular-devkit/build-angular": "21.2.9",
|
||||
"@angular/cli": "21.2.9",
|
||||
"@angular/compiler-cli": "link:./in-existing-linked-by-bazel",
|
||||
"@types/jasmine": "^6.0.0",
|
||||
"@types/jasminewd2": "^2.0.8",
|
||||
|
||||
+496
-457
File diff suppressed because it is too large
Load Diff
@@ -23,12 +23,12 @@
|
||||
"zone.js": "0.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "21.2.6",
|
||||
"@angular/build": "21.2.6",
|
||||
"@angular/cli": "21.2.6",
|
||||
"@angular-devkit/build-angular": "21.2.9",
|
||||
"@angular/build": "21.2.9",
|
||||
"@angular/cli": "21.2.9",
|
||||
"@angular/compiler-cli": "link:./in-existing-linked-by-bazel",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0"
|
||||
"packageManager": "pnpm@10.33.2"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user