mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
142 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 6aafd87038 | |||
| d4c8a9a887 | |||
| 057cc6d09d | |||
| dea76c7a6e | |||
| a07d64b406 | |||
| a6d6842467 | |||
| 33a30e0e03 | |||
| a385743983 | |||
| c9f8f3afb6 | |||
| 910dcb6d6a | |||
| de12bc7e02 | |||
| a4f312060c | |||
| de533fe491 | |||
| ea1e34c4dd | |||
| 304222014a | |||
| 82192deda9 | |||
| e8265f4593 | |||
| c0496c1f39 | |||
| 2d2ba938da | |||
| ff8911fe52 | |||
| 72d7a47dfd | |||
| 2c6781071f | |||
| e40d378f3e | |||
| d715c366d9 | |||
| c91bd074c0 | |||
| 2ec9ff087f | |||
| 8102331f82 | |||
| 44fc79b7f7 | |||
| 39827a08fe | |||
| 04f5841eed | |||
| 958fc6ab8a | |||
| cbfd1ee571 | |||
| b0dc2fbfcb | |||
| ef204bdf7a | |||
| 4441a95119 | |||
| d150e2672e | |||
| 59f9912dc1 | |||
| bce82bdc34 | |||
| 2ae0912eea | |||
| e583f4c808 | |||
| 05bcc601ac | |||
| 9f61910923 | |||
| 7797671257 | |||
| 0b08e29f26 | |||
| 50761c8be4 | |||
| 8d56b17b95 | |||
| 4ad6d582d2 | |||
| f363ab41f1 | |||
| 0960592d3d | |||
| d04ddd73df | |||
| b682c62873 | |||
| fea25d1a60 | |||
| f916531ee1 | |||
| 8fd896e99a | |||
| 895c576d49 | |||
| 5fd4f0f854 | |||
| 429513bed0 | |||
| f22d9a7098 | |||
| cf7846bd62 | |||
| 9ea8cb6eea | |||
| 3c41e74fdd | |||
| efbba945bc | |||
| 7d54952e85 | |||
| 94b1b530dc | |||
| 0c9c270552 | |||
| 9b48cb2989 | |||
| bba5ed8e64 | |||
| 56c872dacc | |||
| c8902deb00 | |||
| 4b2008d0f2 | |||
| b453c0ce67 | |||
| a9ca90fc11 | |||
| 807cabd9e1 | |||
| 13f050d220 | |||
| d0cf2995a2 | |||
| a35d572b22 | |||
| 80fd08bcc8 |
@@ -22,7 +22,7 @@ When reviewing a pull request for the `angular` repository, follow these essenti
|
||||
- **Payload Size**: Pay attention to the impact of changes on the final client payload size.
|
||||
|
||||
3. **Execution Workflow**:
|
||||
Determine the appropriate review method. If the user explicitly asks for a `local` or `remote` review in their request, that takes precedence. Otherwise, use the GitHub MCP or available scripts to determine if the review should be `local` or `remote`.
|
||||
Determine the appropriate review method. If the user explicitly asks for a `remote` or `local` review in their request, that takes precedence (e.g. "leave comments on the PR" implies `remote`). Otherwise, use the GitHub MCP or available scripts to determine if the review should be `local` or `remote`.
|
||||
|
||||
**Common Review Practices (Applies to both Local and Remote)**
|
||||
- **Preparation & Checklist**:
|
||||
@@ -35,7 +35,7 @@ When reviewing a pull request for the `angular` repository, follow these essenti
|
||||
- **Constructive Feedback**: Provide clear, actionable, and polite feedback. Explain the _why_ behind your suggestions or edits. Do **NOT** leave inline comments purely to praise, agree with, or acknowledge a correct implementation detail, as this clutters the review. If you want to praise the PR, do so in the single general PR comment.
|
||||
|
||||
**A. Local Code Review (If the PR is owned by the author requesting the review)**
|
||||
- **Checkout**: Check out the PR branch locally (if it doesn't already exist, fetch it).
|
||||
- **Checkout**: Check out the PR branch locally (if it doesn't already exist, fetch it). If checking out the branch fails due to a worktree claim (e.g. "fatal: '<branch>' is already used by worktree at '<path>'"), do the review in that directory.
|
||||
- **Review & Edit**: Execute the review directly on the code. Instead of adding inline PR comments for suggestions, format the codebase or apply the edits directly to the files.
|
||||
- **Feedback**: Summarize the review findings and the concrete changes you made in a message to the user, referencing the completed items from your checklist.
|
||||
- **Do NOT Commit or Push**: Leave the changes uncommitted in the working directory so the user can easily review the pending edits locally. Let the user know the changes are ready for their review, but do not ask for approval to push.
|
||||
|
||||
@@ -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?
|
||||
@@ -22978,6 +22978,7 @@ var require_stringify = __commonJS2({
|
||||
nullStr: "null",
|
||||
simpleKeys: false,
|
||||
singleQuote: null,
|
||||
trailingComma: false,
|
||||
trueStr: "true",
|
||||
verifyAliasOrder: true
|
||||
}, doc.schema.toStringOptions, options);
|
||||
@@ -23483,12 +23484,19 @@ ${indent}${line}` : "\n";
|
||||
if (comment)
|
||||
reqNewline = true;
|
||||
let str = stringify.stringify(item, itemCtx, () => comment = null);
|
||||
if (i < items.length - 1)
|
||||
reqNewline || (reqNewline = lines.length > linesAtValue || str.includes("\n"));
|
||||
if (i < items.length - 1) {
|
||||
str += ",";
|
||||
} else if (ctx.options.trailingComma) {
|
||||
if (ctx.options.lineWidth > 0) {
|
||||
reqNewline || (reqNewline = lines.reduce((sum, line) => sum + line.length + 2, 2) + (str.length + 2) > ctx.options.lineWidth);
|
||||
}
|
||||
if (reqNewline) {
|
||||
str += ",";
|
||||
}
|
||||
}
|
||||
if (comment)
|
||||
str += stringifyComment.lineComment(str, itemIndent, commentString(comment));
|
||||
if (!reqNewline && (lines.length > linesAtValue || str.includes("\n")))
|
||||
reqNewline = true;
|
||||
lines.push(str);
|
||||
linesAtValue = lines.length;
|
||||
}
|
||||
@@ -26418,17 +26426,22 @@ var require_compose_node = __commonJS2({
|
||||
case "block-map":
|
||||
case "block-seq":
|
||||
case "flow-collection":
|
||||
node = composeCollection.composeCollection(CN, ctx, token, props, onError);
|
||||
if (anchor)
|
||||
node.anchor = anchor.source.substring(1);
|
||||
try {
|
||||
node = composeCollection.composeCollection(CN, ctx, token, props, onError);
|
||||
if (anchor)
|
||||
node.anchor = anchor.source.substring(1);
|
||||
} catch (error2) {
|
||||
const message = error2 instanceof Error ? error2.message : String(error2);
|
||||
onError(token, "RESOURCE_EXHAUSTION", message);
|
||||
}
|
||||
break;
|
||||
default: {
|
||||
const message = token.type === "error" ? token.message : `Unsupported token (type: ${token.type})`;
|
||||
onError(token, "UNEXPECTED_TOKEN", message);
|
||||
node = composeEmptyNode(ctx, token.offset, void 0, null, props, onError);
|
||||
isSrcToken = false;
|
||||
}
|
||||
}
|
||||
node ?? (node = composeEmptyNode(ctx, token.offset, void 0, null, props, onError));
|
||||
if (anchor && node.anchor === "")
|
||||
onError(anchor, "BAD_ALIAS", "Anchor cannot be an empty string");
|
||||
if (atKey && ctx.options.stringKeys && (!identity.isScalar(node) || typeof node.value !== "string" || node.tag && node.tag !== "tag:yaml.org,2002:str")) {
|
||||
@@ -32569,6 +32582,54 @@ var types = (
|
||||
return types2;
|
||||
}()
|
||||
);
|
||||
async function invokeWithRetry(fn, retries = 3, delay = 1e3) {
|
||||
let attempt = 0;
|
||||
while (attempt < retries) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
attempt++;
|
||||
if (attempt >= retries) {
|
||||
throw e;
|
||||
}
|
||||
if (isGithubApiError(e) && e.status < 500) {
|
||||
throw e;
|
||||
}
|
||||
if (e instanceof GraphqlResponseError2) {
|
||||
if (!e.errors) {
|
||||
throw e;
|
||||
}
|
||||
if (e.errors.every((err) => ["NOT_FOUND", "FORBIDDEN", "BAD_USER_INPUT", "UNAUTHENTICATED"].includes(err.type))) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
Log.warn(`GitHub API call failed (attempt ${attempt}/${retries}). Retrying in ${delay}ms...`);
|
||||
await new Promise((resolve22) => setTimeout(resolve22, delay));
|
||||
}
|
||||
}
|
||||
throw new Error("Unreachable");
|
||||
}
|
||||
function createRetryProxy(target) {
|
||||
return new Proxy(target, {
|
||||
get(targetObj, prop, receiver) {
|
||||
const value = Reflect.get(targetObj, prop, receiver);
|
||||
if (typeof value === "function") {
|
||||
return new Proxy(value, {
|
||||
apply(targetFn, thisArg, argArray) {
|
||||
return invokeWithRetry(() => targetFn.apply(targetObj, argArray));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (typeof value === "object" && value !== null) {
|
||||
return createRetryProxy(value);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
apply(targetFn, thisArg, argArray) {
|
||||
return invokeWithRetry(() => targetFn.apply(thisArg, argArray));
|
||||
}
|
||||
});
|
||||
}
|
||||
var GithubClient = class {
|
||||
constructor(_octokitOptions) {
|
||||
this._octokitOptions = _octokitOptions;
|
||||
@@ -32581,18 +32642,18 @@ var GithubClient = class {
|
||||
},
|
||||
...this._octokitOptions
|
||||
});
|
||||
this.pulls = this._octokit.pulls;
|
||||
this.orgs = this._octokit.orgs;
|
||||
this.repos = this._octokit.repos;
|
||||
this.issues = this._octokit.issues;
|
||||
this.git = this._octokit.git;
|
||||
this.rateLimit = this._octokit.rateLimit;
|
||||
this.teams = this._octokit.teams;
|
||||
this.search = this._octokit.search;
|
||||
this.rest = this._octokit.rest;
|
||||
this.paginate = this._octokit.paginate;
|
||||
this.checks = this._octokit.checks;
|
||||
this.users = this._octokit.users;
|
||||
this.pulls = createRetryProxy(this._octokit.pulls);
|
||||
this.orgs = createRetryProxy(this._octokit.orgs);
|
||||
this.repos = createRetryProxy(this._octokit.repos);
|
||||
this.issues = createRetryProxy(this._octokit.issues);
|
||||
this.git = createRetryProxy(this._octokit.git);
|
||||
this.rateLimit = createRetryProxy(this._octokit.rateLimit);
|
||||
this.teams = createRetryProxy(this._octokit.teams);
|
||||
this.search = createRetryProxy(this._octokit.search);
|
||||
this.rest = createRetryProxy(this._octokit.rest);
|
||||
this.paginate = createRetryProxy(this._octokit.paginate);
|
||||
this.checks = createRetryProxy(this._octokit.checks);
|
||||
this.users = createRetryProxy(this._octokit.users);
|
||||
}
|
||||
};
|
||||
var AuthenticatedGithubClient = class extends GithubClient {
|
||||
@@ -32604,9 +32665,14 @@ var AuthenticatedGithubClient = class extends GithubClient {
|
||||
});
|
||||
}
|
||||
async graphql(queryObject, params2 = {}) {
|
||||
return await this._graphql(query(queryObject).toString(), params2);
|
||||
return invokeWithRetry(async () => {
|
||||
return await this._graphql(query(queryObject).toString(), params2);
|
||||
});
|
||||
}
|
||||
};
|
||||
function isGithubApiError(obj) {
|
||||
return obj instanceof Error && obj.constructor.name === "RequestError" && obj.request !== void 0;
|
||||
}
|
||||
function isDryRun() {
|
||||
return process.env["DRY_RUN"] !== void 0;
|
||||
}
|
||||
@@ -33529,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:
|
||||
@@ -33570,7 +33636,7 @@ tmp/lib/tmp.js:
|
||||
*)
|
||||
*)
|
||||
|
||||
@angular/ng-dev/bundles/chunk-GLEWX2LH.mjs:
|
||||
@angular/ng-dev/bundles/chunk-PTDPQBIK.mjs:
|
||||
(*! Bundled license information:
|
||||
|
||||
@octokit/request-error/dist-src/index.js:
|
||||
|
||||
@@ -5,9 +5,9 @@ runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Saucelabs Variables
|
||||
uses: angular/dev-infra/github-actions/saucelabs@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/saucelabs@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Starting Saucelabs tunnel service
|
||||
shell: bash
|
||||
run: ./tools/saucelabs/sauce-service.sh run &
|
||||
|
||||
@@ -21,17 +21,17 @@ jobs:
|
||||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'adev: preview'))
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build adev
|
||||
# `snapshot-build` config is used to stamp the exact version with sha in the footer.
|
||||
run: pnpm bazel build //adev:build.production --config=snapshot-build
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@b9bca459f29f61177a829e69089730415c95ccde
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
|
||||
@@ -32,15 +32,17 @@ 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@b9bca459f29f61177a829e69089730415c95ccde
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
github-token: '${{secrets.GITHUB_TOKEN}}'
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
|
||||
@@ -17,6 +17,6 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@b9bca459f29f61177a829e69089730415c95ccde
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -34,11 +34,11 @@ jobs:
|
||||
repository: ${{steps.comment-branch.outputs.head_owner}}/${{steps.comment-branch.outputs.head_repo}}
|
||||
# Checkout the pull request and assume it being trusted given we've checked
|
||||
# that the action was triggered by a team member.
|
||||
ref: ${{steps.comment-branch.outputs.head_ref}}
|
||||
ref: ${{steps.comment-branch.outputs.head_sha}}
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
bazelrc: ./.bazelrc.user
|
||||
|
||||
|
||||
+26
-26
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Check code lint
|
||||
@@ -41,13 +41,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
disable-package-manager-cache: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
|
||||
- name: Install node modules
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
- name: Test build
|
||||
run: pnpm devtools:build:chrome
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@ed2d88ccbbf936c5aac087dd43e10425abc3534f # v7.1.7
|
||||
uses: cypress-io/github-action@783cb3f07983868532cabaedaa1e6c00ff4786a8 # v7.1.9
|
||||
with:
|
||||
command: pnpm devtools:test:e2e
|
||||
start: pnpm bazel run //devtools/src:devserver
|
||||
@@ -69,11 +69,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
|
||||
- name: Install node modules
|
||||
@@ -85,11 +85,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
|
||||
- name: Install node modules
|
||||
@@ -102,11 +102,11 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
|
||||
- name: Install node modules
|
||||
@@ -121,11 +121,11 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
|
||||
- name: Install node modules
|
||||
@@ -138,11 +138,11 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- run: echo "https://${{secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN}}:@github.com" > ${HOME}/.git_credentials
|
||||
@@ -154,11 +154,11 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
|
||||
- name: Install node modules
|
||||
@@ -208,11 +208,11 @@ jobs:
|
||||
runs-on: ubuntu-latest-8core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build adev
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: angular/dev-infra/github-actions/labeling/pull-request@b9bca459f29f61177a829e69089730415c95ccde
|
||||
- uses: angular/dev-infra/github-actions/labeling/pull-request@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
labels: '{"requires: TGP": ["packages/core/primitives/**/{*,.*}"]}'
|
||||
@@ -25,14 +25,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@b9bca459f29f61177a829e69089730415c95ccde
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
issue_labels:
|
||||
if: github.event_name == 'issues'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/labeling/issue@b9bca459f29f61177a829e69089730415c95ccde
|
||||
- uses: angular/dev-infra/github-actions/labeling/issue@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
google-generative-ai-key: ${{ secrets.GOOGLE_GENERATIVE_AI_KEY }}
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@b9bca459f29f61177a829e69089730415c95ccde
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
run-tests-guide-url: http://go/angular-g3sync-start
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -13,15 +13,15 @@ jobs:
|
||||
JOBS: 2
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Saucelabs Variables
|
||||
uses: angular/dev-infra/github-actions/saucelabs@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/saucelabs@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Set up Sauce Tunnel Daemon
|
||||
run: pnpm bazel run //tools/saucelabs-daemon/background-service -- $JOBS &
|
||||
env:
|
||||
|
||||
@@ -9,6 +9,6 @@ jobs:
|
||||
status:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/unified-status-check@b9bca459f29f61177a829e69089730415c95ccde
|
||||
- uses: angular/dev-infra/github-actions/unified-status-check@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
workflows: ${{ steps.workflows.outputs.workflows }}
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- id: workflows
|
||||
@@ -36,9 +36,9 @@ jobs:
|
||||
workflow: ${{ fromJSON(needs.list.outputs.workflows) }}
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
# We utilize the google-github-actions/auth action to allow us to get an active credential using workflow
|
||||
|
||||
+21
-21
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Check code lint
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
- name: Check code format
|
||||
run: pnpm ng-dev format changed --check ${{ github.event.pull_request.base.sha }}
|
||||
- name: Check Package Licenses
|
||||
uses: angular/dev-infra/github-actions/linting/licenses@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/linting/licenses@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
allow-dependencies-licenses: 'pkg:npm/google-protobuf@'
|
||||
|
||||
@@ -47,13 +47,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
with:
|
||||
disable-package-manager-cache: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run unit tests
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
- name: Test build
|
||||
run: pnpm devtools:build:chrome
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@ed2d88ccbbf936c5aac087dd43e10425abc3534f # v7.1.7
|
||||
uses: cypress-io/github-action@783cb3f07983868532cabaedaa1e6c00ff4786a8 # v7.1.9
|
||||
with:
|
||||
command: pnpm devtools:test:e2e
|
||||
start: pnpm bazel run //devtools/src:devserver
|
||||
@@ -73,11 +73,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run CI tests for framework
|
||||
@@ -97,11 +97,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run integration CI tests for framework
|
||||
@@ -112,11 +112,11 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run tests
|
||||
@@ -129,11 +129,11 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run tests
|
||||
@@ -144,11 +144,11 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- run: |
|
||||
|
||||
@@ -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@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
|
||||
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
+1
-1
@@ -487,7 +487,7 @@ groups:
|
||||
<<: *defaults
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
contains_any_globs(files.exclude('packages/core/primitives/**/*spec.ts'), [
|
||||
'packages/core/primitives/**/{*,.*}',
|
||||
])
|
||||
reviewers:
|
||||
|
||||
@@ -1,3 +1,91 @@
|
||||
<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
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e40d378f3e](https://github.com/angular/angular/commit/e40d378f3e3e7e57a45c8fbd9565ee06a3a6a13f) | fix | handle nested brackets in host object bindings |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [2c6781071f](https://github.com/angular/angular/commit/2c6781071f52d6378a002fba6611bb283fbb2fde) | fix | error for type parameter declarations |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [82192deda9](https://github.com/angular/angular/commit/82192deda9c07113835e6c85af3f2c8c8218cda0) | fix | handle missing serialized container hydration data |
|
||||
| [057cc6d09d](https://github.com/angular/angular/commit/057cc6d09d234f401a810cfdd3ad14127652b88b) | fix | remove obsolete iOS cursor pointer hack in event delegation |
|
||||
### language-service
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7797671257](https://github.com/angular/angular/commit/7797671257350665e8b3ceb2bc6a0201829dd338) | fix | get quick info at local var location to align with TS semantics and support type narrowing |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.7"></a>
|
||||
# 21.2.7 (2026-04-01)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [fea25d1a60](https://github.com/angular/angular/commit/fea25d1a60ecaba1599d9cd9b8df27109ed195c5) | fix | register SVG animation attributes in URL security context ([#67797](https://github.com/angular/angular/pull/67797)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [bba5ed8e64](https://github.com/angular/angular/commit/bba5ed8e643b9c3f680e7e539c3d744ad6905e59) | fix | prevent recursive scope checks for invalid NgModule imports |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [d04ddd73df](https://github.com/angular/angular/commit/d04ddd73dfc03f420afbdde964c5119f338af135) | fix | prevent binding unsafe attributes on SVG animation elements ([#67797](https://github.com/angular/angular/pull/67797)) |
|
||||
| [8fd896e99a](https://github.com/angular/angular/commit/8fd896e99a13855c6569f29efe7e578c301e13ee) | fix | resolve component import by exact specifier in route lazy-loading schematic |
|
||||
| [b682c62873](https://github.com/angular/angular/commit/b682c628731b86a4884e50abb2f5fa73ac0ad057) | fix | treat `object[data]` as resource URL context ([#67797](https://github.com/angular/angular/pull/67797)) |
|
||||
### localize
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3c41e74fdd](https://github.com/angular/angular/commit/3c41e74fdd279f683156b654699a9312a850add0) | fix | validate locale in getOutputPathFn to prevent path traversal |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0960592d3d](https://github.com/angular/angular/commit/0960592d3d4fad110d5598144fda9f2488520826) | fix | pass outlet context to split to fix empty path named outlets |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.6"></a>
|
||||
# 21.2.6 (2026-03-25)
|
||||
### common
|
||||
|
||||
+2
-2
@@ -71,7 +71,7 @@ You can file new issues by selecting from our [new issue templates](https://gith
|
||||
|
||||
### <a name="pr-quality"></a> Contribution Quality
|
||||
|
||||
We strongly value open source contribution and pull requests from community contributors. Please note that every pull request is reviewed and merged by an actual person on the team, which does take time and effort. That is time and effort that does take away from other valuable work. With that in mind we have an minimum set of expectations that are required of any community contribution pull request that is opened.
|
||||
We strongly value open source contribution and pull requests from community contributors. Please note that every pull request is reviewed and merged by an actual person on the team, which does take time and effort. That is time and effort that does take away from other valuable work. With that in mind we have a minimum set of expectations that are required of any community contribution pull request that is opened.
|
||||
|
||||
1. Search [GitHub](https://github.com/angular/angular/pulls) for an open or closed PR that relates to your submission.
|
||||
- You don't want to duplicate existing efforts.
|
||||
@@ -238,7 +238,7 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise
|
||||
- For corporations, we'll need you to
|
||||
[print, sign and one of scan+email, fax or mail the form][corporate-cla].
|
||||
|
||||
If you have more than one GitHub accounts, or multiple email addresses associated with a single GitHub account, you must sign the CLA using the primary email address of the GitHub account used to author Git commits and send pull requests.
|
||||
If you have more than one GitHub account, or multiple email addresses associated with a single GitHub account, you must sign the CLA using the primary email address of the GitHub account used to author Git commits and send pull requests.
|
||||
|
||||
The following documents can help you sort out issues with GitHub accounts and multiple email addresses:
|
||||
|
||||
|
||||
+15
-17
@@ -5,41 +5,41 @@ module(
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_pkg", version = "1.2.0")
|
||||
bazel_dep(name = "rules_nodejs", version = "6.7.3")
|
||||
bazel_dep(name = "aspect_rules_ts", version = "3.8.7")
|
||||
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")
|
||||
bazel_dep(name = "aspect_rules_jasmine", version = "2.0.4")
|
||||
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.9.0")
|
||||
bazel_dep(name = "yq.bzl", version = "0.3.5")
|
||||
bazel_dep(name = "tar.bzl", version = "0.10.1")
|
||||
bazel_dep(name = "yq.bzl", version = "0.3.6")
|
||||
bazel_dep(name = "rules_angular")
|
||||
git_override(
|
||||
module_name = "rules_angular",
|
||||
commit = "af626f77ad610d1a9c47ee317af88e2c8edd66a4",
|
||||
commit = "03dade2ea0ea355e13ca88c550eaa633191b16ec",
|
||||
remote = "https://github.com/angular/rules_angular.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "devinfra")
|
||||
git_override(
|
||||
module_name = "devinfra",
|
||||
commit = "b9bca459f29f61177a829e69089730415c95ccde",
|
||||
commit = "e04d90adad1a125b29fbc4d97f425798768a8cb1",
|
||||
remote = "https://github.com/angular/dev-infra.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_sass")
|
||||
git_override(
|
||||
module_name = "rules_sass",
|
||||
commit = "13918bec49cd183a591e3781d1d08044b4aa9f61",
|
||||
commit = "dfb751533767caa759a7162a34cfe0852f988976",
|
||||
remote = "https://github.com/angular/rules_sass.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_browsers")
|
||||
git_override(
|
||||
module_name = "rules_browsers",
|
||||
commit = "46029652792eac7bb51b6abd04503af296db70ea",
|
||||
commit = "4de93bbfdbee2cea5162ac8070eb15846b15133d",
|
||||
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.32.1",
|
||||
pnpm_version_integrity = "sha512-pwaTjw6JrBRWtlY+q07fHR+vM2jRGR/FxZeQ6W3JGORFarLmfWE94QQ9LoyB+HMD5rQNT/7KnfFe8a1Wc0jyvg==",
|
||||
pnpm_version = "10.33.0",
|
||||
pnpm_version_integrity = "sha512-EFaLtKavtYyes2MNqQzJUWQXq+vT+rvmc58K55VyjaFJHp21pUTHatjrdXD1xLs9bGN7LLQb/c20f6gjyGSTGQ==",
|
||||
)
|
||||
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
+33
-32
@@ -28,8 +28,8 @@
|
||||
"https://bcr.bazel.build/modules/aspect_rules_js/3.0.3/source.json": "bb8fff9a304452e1042af9522ad1d54d6f1d1fdf71c5127deadb6fd156654193",
|
||||
"https://bcr.bazel.build/modules/aspect_rules_rollup/2.0.1/MODULE.bazel": "296e3a053658c2af989ba9bd62a205e6d1fa84bdd6dd5249196546e6b84770ec",
|
||||
"https://bcr.bazel.build/modules/aspect_rules_rollup/2.0.1/source.json": "2fe8ac1ccb4de74bf884761e070010280b272d94e3997205b361b91c75409726",
|
||||
"https://bcr.bazel.build/modules/aspect_rules_ts/3.8.7/MODULE.bazel": "830f8a53bb9f1139c24006a90ddc0230481326d69fa847eb00daf8eaae118724",
|
||||
"https://bcr.bazel.build/modules/aspect_rules_ts/3.8.7/source.json": "95549d64e28f3e4e3648cc037cefdac01ec3b0f58fced2409c286784e82ad0f0",
|
||||
"https://bcr.bazel.build/modules/aspect_rules_ts/3.8.8/MODULE.bazel": "b52b929a948438665809d49af610f58d1b14f63d6d21ab748f47b6050be4c1f6",
|
||||
"https://bcr.bazel.build/modules/aspect_rules_ts/3.8.8/source.json": "5414530b761a45ab7ca6c49f0a2a9cf8dc0da772f5037cf05ca18aaa64bb1b19",
|
||||
"https://bcr.bazel.build/modules/aspect_tools_telemetry/0.2.6/MODULE.bazel": "cafb8781ad591bc57cc765dca5fefab08cf9f65af363d162b79d49205c7f8af7",
|
||||
"https://bcr.bazel.build/modules/aspect_tools_telemetry/0.2.8/MODULE.bazel": "aa975a83e72bcaac62ee61ab12b788ea324a1d05c4aab28aadb202f647881679",
|
||||
"https://bcr.bazel.build/modules/aspect_tools_telemetry/0.3.3/MODULE.bazel": "37c764292861c2f70314efa9846bb6dbb44fc0308903b3285da6528305450183",
|
||||
@@ -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",
|
||||
@@ -193,16 +194,16 @@
|
||||
"https://bcr.bazel.build/modules/stardoc/0.7.1/MODULE.bazel": "3548faea4ee5dda5580f9af150e79d0f6aea934fc60c1cc50f4efdd9420759e7",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216",
|
||||
"https://bcr.bazel.build/modules/tar.bzl/0.10.1/MODULE.bazel": "bf5fda5b5ccef8c3c4a5f4886144377386e0baa382972f257acb42dcf40ea908",
|
||||
"https://bcr.bazel.build/modules/tar.bzl/0.10.1/source.json": "3f1beb35acf53c270a9de493cdc775a985551d7069cfcf24e136b42f683bbb10",
|
||||
"https://bcr.bazel.build/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468",
|
||||
"https://bcr.bazel.build/modules/tar.bzl/0.5.1/MODULE.bazel": "7c2eb3dcfc53b0f3d6f9acdfd911ca803eaf92aadf54f8ca6e4c1f3aee288351",
|
||||
"https://bcr.bazel.build/modules/tar.bzl/0.6.0/MODULE.bazel": "a3584b4edcfafcabd9b0ef9819808f05b372957bbdff41601429d5fd0aac2e7c",
|
||||
"https://bcr.bazel.build/modules/tar.bzl/0.9.0/MODULE.bazel": "452a22d7f02b1c9d7a22ab25edf20f46f3e1101f0f67dc4bfbf9a474ddf02445",
|
||||
"https://bcr.bazel.build/modules/tar.bzl/0.9.0/source.json": "c732760a374831a2cf5b08839e4be75017196b4d796a5aa55235272ee17cd839",
|
||||
"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",
|
||||
@@ -213,7 +214,7 @@
|
||||
"moduleExtensions": {
|
||||
"@@aspect_rules_esbuild+//esbuild:extensions.bzl%esbuild": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "GnYkDpVOnWnYv+xwyhEi0qjk3Lvp4Wei30PeSIQF5vM=",
|
||||
"bzlTransitiveDigest": "QxNkTCcD6yURsbqznKyC/WCjCyhHZbEipGXl2UQjjzY=",
|
||||
"usagesDigest": "6We6zwGoawD9YXqMI0KPaxEKJTnamXBsuOekhFS2D40=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
@@ -428,8 +429,8 @@
|
||||
},
|
||||
"@@aspect_rules_ts+//ts:extensions.bzl%ext": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "GbfZLeEI0W26fAQ89dUljyLGSAkzkr3CkF+VDQ0IFlI=",
|
||||
"usagesDigest": "CnGVBnDYq2qAfYkDXgJlDcJckUt2NU51jQQX+igoGt8=",
|
||||
"bzlTransitiveDigest": "dhTbv9E6UfT1WJmmu3ORRPO6AKFJvgBjBxu+BO+u1RY=",
|
||||
"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"
|
||||
]
|
||||
@@ -492,7 +493,7 @@
|
||||
"@@aspect_tools_telemetry+//:extension.bzl%telemetry": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "cl5A2O84vDL6Tt+Qga8FCj1DUDGqn+e7ly5rZ+4xvcc=",
|
||||
"usagesDigest": "mS7+2ykIUJH5qIIq/jathl5rcwdqyvYA8HOpHe8d+/Y=",
|
||||
"usagesDigest": "XReaXejedAKAFKyp+2mfnFkPWXBJzM/VxiOYB4nyseY=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
@@ -501,7 +502,7 @@
|
||||
"repoRuleId": "@@aspect_tools_telemetry+//:extension.bzl%tel_repository",
|
||||
"attributes": {
|
||||
"deps": {
|
||||
"aspect_rules_ts": "3.8.7",
|
||||
"aspect_rules_ts": "3.8.8",
|
||||
"aspect_rules_js": "3.0.3",
|
||||
"aspect_rules_esbuild": "0.25.1",
|
||||
"aspect_rules_jasmine": "2.0.4",
|
||||
@@ -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": "n80KYSy9Qdp3Z/R7I+m1NDH4klVgoxFaY608KacpRUQ=",
|
||||
"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": {},
|
||||
|
||||
+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.4",
|
||||
"@angular/build": "21.2.3",
|
||||
"@angular/cdk": "21.2.4",
|
||||
"@angular/cli": "21.2.3",
|
||||
"@angular/aria": "21.2.7",
|
||||
"@angular/build": "21.2.8",
|
||||
"@angular/cdk": "21.2.7",
|
||||
"@angular/cli": "21.2.8",
|
||||
"@angular/common": "workspace:*",
|
||||
"@angular/compiler": "workspace:*",
|
||||
"@angular/compiler-cli": "workspace:*",
|
||||
"@angular/core": "workspace:*",
|
||||
"@angular/docs": "workspace:*",
|
||||
"@angular/forms": "workspace:*",
|
||||
"@angular/material": "21.2.4",
|
||||
"@angular/material": "21.2.7",
|
||||
"@angular/platform-browser": "workspace:*",
|
||||
"@angular/platform-server": "workspace:*",
|
||||
"@angular/router": "workspace:*",
|
||||
"@angular/ssr": "21.2.3",
|
||||
"@angular/ssr": "21.2.8",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.1",
|
||||
"@codemirror/lang-angular": "0.1.4",
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
matrixParams: 'ignored',
|
||||
fragment: 'ignored',
|
||||
}"
|
||||
(click)="emitClickOnLink()"
|
||||
(click)="emitClickOnLink(item)"
|
||||
[matTooltip]="item.label"
|
||||
[matTooltipDisabled]="itemLabel.length < 27"
|
||||
matTooltipPosition="after"
|
||||
|
||||
@@ -153,5 +153,6 @@ describe('NavigationList', () => {
|
||||
class FakeNavigationListState {
|
||||
isOpened = signal(true);
|
||||
activeNavigationItem = signal(navigationItems.at(1));
|
||||
crossCategoryOrigin = signal<NavigationItem | undefined>(undefined);
|
||||
toggleItem(item: NavigationItem) {}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ export class NavigationList {
|
||||
readonly linkClicked = output<void>();
|
||||
|
||||
private readonly navigationState = inject(NavigationState);
|
||||
private readonly crossCategoryOrigin = this.navigationState.crossCategoryOrigin;
|
||||
|
||||
readonly activeItem = this.navigationState.activeNavigationItem;
|
||||
|
||||
@@ -51,10 +52,19 @@ export class NavigationList {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const prevParentItem = this.crossCategoryOrigin();
|
||||
if (prevParentItem) {
|
||||
this.crossCategoryOrigin.set(undefined);
|
||||
this.navigationState.toggleItem(prevParentItem);
|
||||
return;
|
||||
}
|
||||
this.navigationState.toggleItem(item);
|
||||
}
|
||||
|
||||
emitClickOnLink(): void {
|
||||
emitClickOnLink(item: NavigationItem): void {
|
||||
if (item.isCrossReferenced) {
|
||||
this.crossCategoryOrigin.set(item.parent);
|
||||
}
|
||||
this.linkClicked.emit();
|
||||
}
|
||||
|
||||
|
||||
@@ -27,14 +27,16 @@
|
||||
</i>
|
||||
</span>
|
||||
|
||||
<!-- Page title -->
|
||||
<span [innerHtml]="result.labelHtml"></span>
|
||||
@if (result.package) {
|
||||
<span
|
||||
[innerHTML]="result.package"
|
||||
class="docs-search-result__label__package"
|
||||
></span>
|
||||
}
|
||||
<!-- Page title and package badge -->
|
||||
<span class="docs-search-result__label__text">
|
||||
<span [innerHtml]="result.labelHtml"></span>
|
||||
@if (result.package) {
|
||||
<span
|
||||
[innerHTML]="result.package"
|
||||
class="docs-search-result__label__package"
|
||||
></span>
|
||||
}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@if (result.subLabelHtml) {
|
||||
|
||||
@@ -46,6 +46,7 @@ dialog {
|
||||
|
||||
.docs-search-result-icon {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
|
||||
i {
|
||||
display: flex;
|
||||
@@ -91,7 +92,20 @@ dialog {
|
||||
|
||||
&__label {
|
||||
font-weight: 600;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&__text {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 0.25rem 0.5rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
> * {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__package {
|
||||
font-size: 0.75rem;
|
||||
|
||||
@@ -17,5 +17,6 @@ export interface NavigationItem {
|
||||
contentPath?: string;
|
||||
status?: 'new' | 'updated';
|
||||
category?: string;
|
||||
isCrossReferenced?: boolean;
|
||||
preserveOtherCategoryOrder?: boolean; // true by default
|
||||
}
|
||||
|
||||
@@ -59,10 +59,16 @@ export function ClassMember(props: {member: MemberEntryRenderable}) {
|
||||
const memberName = member.name;
|
||||
const displayName = member.displayName;
|
||||
const returnType = getMemberType(member);
|
||||
const label = displayName ?? memberName;
|
||||
|
||||
return (
|
||||
<div id={memberName} className={REFERENCE_MEMBER_CARD}>
|
||||
<header className={REFERENCE_MEMBER_CARD_HEADER}>
|
||||
<h3>{displayName ?? memberName}</h3>
|
||||
<h3>
|
||||
<a class="docs-anchor" href={'#' + memberName}>
|
||||
{label}
|
||||
</a>
|
||||
</h3>
|
||||
{isClassMethodEntry(member) && member.signatures.length > 1 ? (
|
||||
<span>{member.signatures.length} overloads</span>
|
||||
) : returnType ? (
|
||||
|
||||
@@ -13,6 +13,8 @@ import {setSymbols} from '../symbol-context.mjs';
|
||||
import {resolve} from 'path';
|
||||
import {initHighlighter} from '../../../shared/shiki.mjs';
|
||||
import {setHighlighterInstance} from '../shiki/shiki.mjs';
|
||||
import {renderEntry} from '../rendering.mjs';
|
||||
import {JSDOM} from 'jsdom';
|
||||
|
||||
// Note: The tests will probably break if the schema of the api extraction changes.
|
||||
// All entries in the fake-entries are extracted from Angular's api.
|
||||
@@ -60,4 +62,21 @@ describe('renderable', () => {
|
||||
expect(linkedSignal!.experimental).toBe(undefined);
|
||||
expect(linkedSignal!.stable).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should render docs-anchor links in class member card headers', () => {
|
||||
const viewRef = entries.get('ViewRef')!;
|
||||
expect(viewRef).toBeDefined();
|
||||
|
||||
const html = renderEntry(viewRef);
|
||||
const fragment = JSDOM.fragment(html);
|
||||
|
||||
const memberCards = fragment.querySelectorAll('.docs-reference-member-card');
|
||||
expect(memberCards.length).toBeGreaterThan(0);
|
||||
|
||||
for (const card of Array.from(memberCards)) {
|
||||
const id = card.getAttribute('id')!;
|
||||
const anchor = card.querySelector('h3 a.docs-anchor') as HTMLAnchorElement;
|
||||
expect(anchor.getAttribute('href')).toBe(`#${id}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,6 +95,7 @@ export async function addRenderableCodeToc<T extends DocEntry & HasModuleName>(
|
||||
{
|
||||
language: 'typescript',
|
||||
apiEntries: getSymbolsAsApiEntries(),
|
||||
removeWhitespace: false,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ export function codeToHtml(
|
||||
apiEntries?: ApiEntries;
|
||||
language?: string;
|
||||
highlight?: Set<number>;
|
||||
removeWhitespace?: boolean;
|
||||
},
|
||||
): string {
|
||||
const html = highlighter.codeToHtml(code, {
|
||||
@@ -53,7 +54,7 @@ export function codeToHtml(
|
||||
cssVariablePrefix: '--shiki-',
|
||||
defaultColor: false,
|
||||
transformers: [
|
||||
removeWhitespaceTransformer(),
|
||||
...(config.removeWhitespace ? [removeWhitespaceTransformer()] : []),
|
||||
highlightTransformer(config.highlight),
|
||||
linkApiEntriesTransformer(config.apiEntries),
|
||||
],
|
||||
|
||||
@@ -21,6 +21,7 @@ export class NavigationState {
|
||||
private readonly _isMobileNavVisible = signal<boolean>(false);
|
||||
private readonly _level = linkedSignal(() => this._expandedItems().length);
|
||||
|
||||
readonly crossCategoryOrigin = signal<NavigationItem | undefined>(undefined);
|
||||
readonly primaryActiveRouteItem = signal<string | null>(null);
|
||||
activeNavigationItem = this._activeNavigationItem.asReadonly();
|
||||
expandedItems = this._expandedItems.asReadonly();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -252,7 +268,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);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
provideZonelessChangeDetection,
|
||||
provideEnvironmentInitializer,
|
||||
} from '@angular/core';
|
||||
import {UrlSerializer} from '@angular/router';
|
||||
import {
|
||||
DOCS_CONTENT_LOADER,
|
||||
ENVIRONMENT,
|
||||
@@ -33,6 +34,7 @@ import {CustomErrorHandler} from './core/services/errors-handling/error-handler'
|
||||
import {ExampleContentLoader} from './core/services/example-content-loader.service';
|
||||
import {routerProviders} from './routing/router_providers';
|
||||
import {TYPESCRIPT_VFS_WORKER_PROVIDER} from './editor/code-editor/workers/factory-provider';
|
||||
import {AdevUrlSerializer} from './core/services/routing/adev-url-serializer';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
@@ -53,5 +55,9 @@ export const appConfig: ApplicationConfig = {
|
||||
deps: [DOCUMENT],
|
||||
},
|
||||
TYPESCRIPT_VFS_WORKER_PROVIDER,
|
||||
{
|
||||
provide: UrlSerializer,
|
||||
useClass: AdevUrlSerializer,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -436,6 +436,28 @@
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
[href]="ngLinks.STACKOVERFLOW"
|
||||
cdkMenuItem
|
||||
title="Angular Stack Overflow"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<!-- Stack Overflow Icon -->
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 26 31"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m22.18 16.32.05.02-.02.01za19 19 0 0 0-2.45 4.14l-.03.07q-.94 2.19-1.29 4.6v.03a18 18 0 0 0-.05 4.85H.01v-4.88h15.94l.1-.6L.69 20.48l1.28-4.7 15.55 4.1.22-.46-13.96-7.96 2.47-4.22 14.16 8.07.34-.4L9.15 3.47 12.65 0l11.78 11.64 1.25 1.23q-1.97 1.5-3.5 3.44"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {DefaultUrlSerializer, UrlTree} from '@angular/router';
|
||||
|
||||
/**
|
||||
* Custom URL serializer extending the default behavior
|
||||
* with Adev-specific behavior.
|
||||
*/
|
||||
export class AdevUrlSerializer extends DefaultUrlSerializer {
|
||||
override parse(url: string): UrlTree {
|
||||
// Since the app host/server is decoding encoded forward slashes,
|
||||
// we perform this on the client as well in order to maintain
|
||||
// a consistent behavior between the two environments and
|
||||
// avoid opening a different page on client hydration (presumably, 404).
|
||||
url = url.replaceAll(/%2(F|f)/g, '/');
|
||||
|
||||
return super.parse(url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {UrlSerializer} from '@angular/router';
|
||||
import {AdevUrlSerializer} from './adev-url-serializer';
|
||||
|
||||
describe('AdevUrlSerializer', () => {
|
||||
let serializer: UrlSerializer;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: UrlSerializer,
|
||||
useClass: AdevUrlSerializer,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
serializer = TestBed.inject(UrlSerializer);
|
||||
});
|
||||
|
||||
it('should decode encoded forward slash (%2F)', () => {
|
||||
// Uppercase hex
|
||||
expect(serializer.parse('page%2Fabout').toString()).toBe('/page/about');
|
||||
|
||||
// Lowercase hex
|
||||
expect(serializer.parse('page%2fabout').toString()).toBe('/page/about');
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,8 @@ import {NodeRuntimeSandbox} from '../node-runtime-sandbox.service';
|
||||
import {TypingsLoader} from '../typings-loader.service';
|
||||
|
||||
import {FileAndContentRecord} from '@angular/docs';
|
||||
import {DomSanitizer} from '@angular/platform-browser';
|
||||
import {NodeRuntimeState} from '../node-runtime-state.service';
|
||||
import {CODE_EDITOR_EXTENSIONS} from './constants/code-editor-extensions';
|
||||
import {LANGUAGES} from './constants/code-editor-languages';
|
||||
import {getAutocompleteExtension} from './extensions/autocomplete';
|
||||
@@ -26,10 +28,9 @@ import {getDiagnosticsExtension} from './extensions/diagnostics';
|
||||
import {getTooltipExtension} from './extensions/tooltip';
|
||||
import {DiagnosticsState} from './services/diagnostics-state.service';
|
||||
import {TsVfsWorkerActions} from './workers/enums/actions';
|
||||
import {TYPESCRIPT_VFS_WORKER_FACTORY} from './workers/factory-provider';
|
||||
import {CodeChangeRequest} from './workers/interfaces/code-change-request';
|
||||
import {ActionMessage} from './workers/interfaces/message';
|
||||
import {NodeRuntimeState} from '../node-runtime-state.service';
|
||||
import {TYPESCRIPT_VFS_WORKER_FACTORY} from './workers/factory-provider';
|
||||
|
||||
export interface EditorFile {
|
||||
filename: string;
|
||||
@@ -81,6 +82,7 @@ export class CodeMirrorEditor {
|
||||
private readonly typingsLoader = inject(TypingsLoader);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly diagnosticsState = inject(DiagnosticsState);
|
||||
private readonly domSanitizer = inject(DomSanitizer);
|
||||
private readonly tsVfsWorkerFactory = inject(TYPESCRIPT_VFS_WORKER_FACTORY);
|
||||
private tsVfsWorker: Worker | null = null;
|
||||
|
||||
@@ -448,7 +450,12 @@ export class CodeMirrorEditor {
|
||||
this.sendRequestToTsVfs,
|
||||
this.diagnosticsState,
|
||||
),
|
||||
getTooltipExtension(this.eventManager$, this.currentFile, this.sendRequestToTsVfs),
|
||||
getTooltipExtension(
|
||||
this.eventManager$,
|
||||
this.currentFile,
|
||||
this.sendRequestToTsVfs,
|
||||
this.domSanitizer,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {DomSanitizer} from '@angular/platform-browser';
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {getMarkedHtmlFromString, getTagsHtml} from './tooltip';
|
||||
|
||||
describe('getMarkedHtmlFromString', () => {
|
||||
it('sanitizes markdown HTML content before assigning to innerHTML', () => {
|
||||
const markdownContent = 'hello <img src=x onerror="alert(1)" />';
|
||||
const domSanitizer = TestBed.inject(DomSanitizer);
|
||||
|
||||
const result = getMarkedHtmlFromString(markdownContent, domSanitizer);
|
||||
|
||||
expect(result.innerHTML.trim()).toBe('<p>hello <img src="x"></p>');
|
||||
expect(result.innerHTML).not.toContain('onerror');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTagsHtml', () => {
|
||||
it('sanitizes JSDoc tag content before assigning to innerHTML', () => {
|
||||
const tags = [
|
||||
{
|
||||
name: 'example',
|
||||
text: [{text: 'hello <img src=x onerror="alert(1)" />'}],
|
||||
},
|
||||
] as any[];
|
||||
const domSanitizer = TestBed.inject(DomSanitizer);
|
||||
|
||||
const result = getTagsHtml(tags, domSanitizer);
|
||||
|
||||
expect(result.innerHTML).toContain('@example');
|
||||
expect(result.innerHTML).not.toContain('onerror');
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Signal} from '@angular/core';
|
||||
import {SecurityContext, Signal} from '@angular/core';
|
||||
import {DomSanitizer} from '@angular/platform-browser';
|
||||
|
||||
import {Tooltip, hoverTooltip} from '@codemirror/view';
|
||||
import {marked} from 'marked';
|
||||
import {Subject, filter, take} from 'rxjs';
|
||||
@@ -23,6 +25,7 @@ export const getTooltipExtension = (
|
||||
emitter: Subject<ActionMessage<DisplayTooltipResponse>>,
|
||||
currentFile: Signal<EditorFile>,
|
||||
sendRequestToTsVfs: (request: ActionMessage<DisplayTooltipRequest>) => void,
|
||||
domSanitizer: DomSanitizer,
|
||||
) => {
|
||||
return hoverTooltip(
|
||||
async (_, pos: number): Promise<Tooltip | null> => {
|
||||
@@ -58,9 +61,9 @@ export const getTooltipExtension = (
|
||||
|
||||
// use documentation if available as it's more informative than tags
|
||||
if (documentation?.[0]?.text) {
|
||||
tooltip.appendChild(getMarkedHtmlFromString(documentation[0]?.text));
|
||||
tooltip.appendChild(getMarkedHtmlFromString(documentation[0]?.text, domSanitizer));
|
||||
} else if (tags?.length) {
|
||||
tooltip.appendChild(getTagsHtml(tags));
|
||||
tooltip.appendChild(getTagsHtml(tags, domSanitizer));
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -91,9 +94,13 @@ function forceTooltipScrollTop() {
|
||||
}
|
||||
}
|
||||
|
||||
function getMarkedHtmlFromString(content: string): HTMLDivElement {
|
||||
export function getMarkedHtmlFromString(
|
||||
content: string,
|
||||
domSanitizer: DomSanitizer,
|
||||
): HTMLDivElement {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = marked(content) as string;
|
||||
const sanitizedHtml = renderAndSanitizeMarkdownToHtml(content, domSanitizer);
|
||||
wrapper.innerHTML = sanitizedHtml;
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
@@ -123,7 +130,7 @@ function getHtmlFromDisplayParts(displayParts: ts.SymbolDisplayPart[]): HTMLDivE
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function getTagsHtml(tags: ts.JSDocTagInfo[]): HTMLDivElement {
|
||||
export function getTagsHtml(tags: ts.JSDocTagInfo[], domSanitizer: DomSanitizer): HTMLDivElement {
|
||||
const tagsWrapper = document.createElement('div');
|
||||
|
||||
let contentString = '';
|
||||
@@ -138,7 +145,14 @@ function getTagsHtml(tags: ts.JSDocTagInfo[]): HTMLDivElement {
|
||||
}
|
||||
}
|
||||
|
||||
tagsWrapper.innerHTML = marked(contentString) as string;
|
||||
const sanitizedHtml = renderAndSanitizeMarkdownToHtml(contentString, domSanitizer);
|
||||
tagsWrapper.innerHTML = sanitizedHtml;
|
||||
|
||||
return tagsWrapper;
|
||||
}
|
||||
|
||||
function renderAndSanitizeMarkdownToHtml(content: string, domSanitizer: DomSanitizer): string {
|
||||
const markedHtml = marked(content) as string;
|
||||
const sanitizedHtml = domSanitizer.sanitize(SecurityContext.HTML, markedHtml) ?? '';
|
||||
return sanitizedHtml;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
@use '@angular/docs/styles/media-queries' as mq;
|
||||
|
||||
$ver-dropdown-width: 200px;
|
||||
$ver-dropdown-width: clamp(165px, 20vw, 200px);
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
padding: var(--layout-padding) 0px;
|
||||
padding: var(--layout-padding);
|
||||
container: update-guide-page / inline-size;
|
||||
|
||||
.docs-viewer {
|
||||
|
||||
@@ -108,7 +108,7 @@ export default class UpdateComponent {
|
||||
];
|
||||
protected from = this.versions.find((version) => version.name === '20.0')!;
|
||||
protected to = this.versions.find((version) => version.name === '21.0')!;
|
||||
protected futureVersion = 2100;
|
||||
protected futureVersion = 2200;
|
||||
|
||||
protected readonly steps: Step[] = RECOMMENDATIONS;
|
||||
|
||||
|
||||
@@ -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',
|
||||
@@ -660,6 +681,7 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
path: 'guide/routing/testing',
|
||||
contentPath: 'guide/routing/testing',
|
||||
status: 'new',
|
||||
isCrossReferenced: true,
|
||||
},
|
||||
{
|
||||
label: 'Debugging tests',
|
||||
@@ -863,6 +885,7 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
label: 'Route transition animations',
|
||||
path: 'guide/routing/route-transition-animations',
|
||||
contentPath: 'guide/routing/route-transition-animations',
|
||||
isCrossReferenced: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -875,7 +898,6 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
},
|
||||
{
|
||||
label: 'Build with AI',
|
||||
status: 'new',
|
||||
children: [
|
||||
{
|
||||
label: 'Get Started',
|
||||
@@ -888,9 +910,10 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
contentPath: 'ai/develop-with-ai',
|
||||
},
|
||||
{
|
||||
label: 'Design Patterns',
|
||||
path: 'ai/design-patterns',
|
||||
contentPath: 'ai/design-patterns',
|
||||
label: 'Agent Skills',
|
||||
path: 'ai/agent-skills',
|
||||
contentPath: 'ai/agent-skills',
|
||||
status: 'new',
|
||||
},
|
||||
{
|
||||
label: 'Angular CLI MCP Server setup',
|
||||
@@ -902,6 +925,11 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
path: 'ai/ai-tutor',
|
||||
contentPath: 'ai/ai-tutor',
|
||||
},
|
||||
{
|
||||
label: 'Design Patterns',
|
||||
path: 'ai/design-patterns',
|
||||
contentPath: 'ai/design-patterns',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -1037,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',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,24 @@
|
||||
# Agent Skills
|
||||
|
||||
Agent Skills are specialized, domain-specific instructions and capabilities designed for AI agents like Gemini CLI. These skills provide architectural guidance, generate idiomatic Angular code, and help scaffold new projects using modern best practices.
|
||||
|
||||
By using Agent Skills, you can ensure that the AI agent you are working with has the most up-to-date information about Angular's conventions, reactivity models (like Signals), and project structure.
|
||||
|
||||
## Available Skills
|
||||
|
||||
The Angular team maintains a collection of official skills that are regularly updated to stay in sync with the latest framework improvements.
|
||||
|
||||
| Skill | Description |
|
||||
| :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`angular-developer`** | Generates Angular code and provides architectural guidance. Useful for creating components, services, or obtaining best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling, testing, or CLI tooling. |
|
||||
| **`angular-new-app`** | Creates a new Angular app using the Angular CLI. Provides important guidelines for effectively setting up and structuring a modern Angular application. |
|
||||
|
||||
## Using Agent Skills
|
||||
|
||||
Agent Skills are designed to be used with agentic coding tools like [Gemini CLI](https://geminicli.com/docs/cli/skills/), [Antigravity](https://antigravity.google/docs/skills) and more. Activating a skill loads the specific instructions and resources needed for that task.
|
||||
|
||||
To use these skills in your own environment you may follow the instructions for your specific tool or use a community tool like [skills.sh](https://skills.sh/).
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/angular/skills
|
||||
```
|
||||
@@ -115,4 +115,5 @@ To learn about LLM prompts and AI IDE setup, see the following guides:
|
||||
|
||||
<docs-pill-row>
|
||||
<docs-pill href="ai/develop-with-ai" title="LLM prompts and IDE setup"/>
|
||||
<docs-pill href="ai/agent-skills" title="Agent Skills"/>
|
||||
</docs-pill-row>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"branchName": "refs/heads/21.2.x",
|
||||
"sha": "fc8b94d631a5d105a87abc2a008a6e8f76d7f3f5"
|
||||
"sha": "4e2c69bca091e2218fd456afe797826f033a76f2"
|
||||
}
|
||||
@@ -482,19 +482,6 @@
|
||||
"endLine": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "CDK_DROP_LIST_GROUP",
|
||||
"type": "any",
|
||||
"entryType": "constant",
|
||||
"rawComment": "/**\n * Injection token that can be used to reference instances of `CdkDropListGroup`. It serves as\n * alternative token to the actual `CdkDropListGroup` class which could cause unnecessary\n * retention of the class and its directive metadata.\n */",
|
||||
"description": "Injection token that can be used to reference instances of `CdkDropListGroup`. It serves as\nalternative token to the actual `CdkDropListGroup` class which could cause unnecessary\nretention of the class and its directive metadata.",
|
||||
"jsdocTags": [],
|
||||
"source": {
|
||||
"filePath": "/src/cdk/drag-drop/directives/drop-list-group.ts",
|
||||
"startLine": 16,
|
||||
"endLine": 18
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "CdkDragRelease",
|
||||
"entryType": "interface",
|
||||
@@ -533,6 +520,19 @@
|
||||
"endLine": 26
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "CDK_DROP_LIST_GROUP",
|
||||
"type": "any",
|
||||
"entryType": "constant",
|
||||
"rawComment": "/**\n * Injection token that can be used to reference instances of `CdkDropListGroup`. It serves as\n * alternative token to the actual `CdkDropListGroup` class which could cause unnecessary\n * retention of the class and its directive metadata.\n */",
|
||||
"description": "Injection token that can be used to reference instances of `CdkDropListGroup`. It serves as\nalternative token to the actual `CdkDropListGroup` class which could cause unnecessary\nretention of the class and its directive metadata.",
|
||||
"jsdocTags": [],
|
||||
"source": {
|
||||
"filePath": "/src/cdk/drag-drop/directives/drop-list-group.ts",
|
||||
"startLine": 17,
|
||||
"endLine": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "CDK_DRAG_PLACEHOLDER",
|
||||
"type": "any",
|
||||
@@ -559,70 +559,6 @@
|
||||
"endLine": 25
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "CdkDropListGroup",
|
||||
"isAbstract": false,
|
||||
"entryType": "undecorated_class",
|
||||
"members": [
|
||||
{
|
||||
"name": "disabled",
|
||||
"type": "boolean",
|
||||
"memberType": "property",
|
||||
"memberTags": [],
|
||||
"description": "Whether starting a dragging sequence from inside this group is disabled.",
|
||||
"jsdocTags": []
|
||||
},
|
||||
{
|
||||
"name": "ngOnDestroy",
|
||||
"signatures": [
|
||||
{
|
||||
"name": "ngOnDestroy",
|
||||
"entryType": "function",
|
||||
"description": "",
|
||||
"generics": [],
|
||||
"isNewType": false,
|
||||
"jsdocTags": [],
|
||||
"params": [],
|
||||
"rawComment": "",
|
||||
"returnType": "void"
|
||||
}
|
||||
],
|
||||
"implementation": {
|
||||
"params": [],
|
||||
"isNewType": false,
|
||||
"returnType": "void",
|
||||
"generics": [],
|
||||
"name": "ngOnDestroy",
|
||||
"description": "",
|
||||
"entryType": "function",
|
||||
"jsdocTags": [],
|
||||
"rawComment": ""
|
||||
},
|
||||
"entryType": "function",
|
||||
"description": "",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "",
|
||||
"memberType": "method",
|
||||
"memberTags": []
|
||||
}
|
||||
],
|
||||
"generics": [
|
||||
{
|
||||
"name": "T"
|
||||
}
|
||||
],
|
||||
"description": "Declaratively connects sibling `cdkDropList` instances together. All of the `cdkDropList`\nelements that are placed inside a `cdkDropListGroup` will be connected to each other\nautomatically. Can be used as an alternative to the `cdkDropListConnectedTo` input\nfrom `cdkDropList`.",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "/**\n * Declaratively connects sibling `cdkDropList` instances together. All of the `cdkDropList`\n * elements that are placed inside a `cdkDropListGroup` will be connected to each other\n * automatically. Can be used as an alternative to the `cdkDropListConnectedTo` input\n * from `cdkDropList`.\n */",
|
||||
"implements": [
|
||||
"OnDestroy"
|
||||
],
|
||||
"source": {
|
||||
"filePath": "/src/cdk/drag-drop/directives/drop-list-group.ts",
|
||||
"startLine": 26,
|
||||
"endLine": 42
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "CdkDragPlaceholder",
|
||||
"isAbstract": false,
|
||||
@@ -706,6 +642,70 @@
|
||||
"endLine": 44
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "CdkDropListGroup",
|
||||
"isAbstract": false,
|
||||
"entryType": "undecorated_class",
|
||||
"members": [
|
||||
{
|
||||
"name": "disabled",
|
||||
"type": "boolean",
|
||||
"memberType": "property",
|
||||
"memberTags": [],
|
||||
"description": "Whether starting a dragging sequence from inside this group is disabled.",
|
||||
"jsdocTags": []
|
||||
},
|
||||
{
|
||||
"name": "ngOnDestroy",
|
||||
"signatures": [
|
||||
{
|
||||
"name": "ngOnDestroy",
|
||||
"entryType": "function",
|
||||
"description": "",
|
||||
"generics": [],
|
||||
"isNewType": false,
|
||||
"jsdocTags": [],
|
||||
"params": [],
|
||||
"rawComment": "",
|
||||
"returnType": "void"
|
||||
}
|
||||
],
|
||||
"implementation": {
|
||||
"params": [],
|
||||
"isNewType": false,
|
||||
"returnType": "void",
|
||||
"generics": [],
|
||||
"name": "ngOnDestroy",
|
||||
"description": "",
|
||||
"entryType": "function",
|
||||
"jsdocTags": [],
|
||||
"rawComment": ""
|
||||
},
|
||||
"entryType": "function",
|
||||
"description": "",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "",
|
||||
"memberType": "method",
|
||||
"memberTags": []
|
||||
}
|
||||
],
|
||||
"generics": [
|
||||
{
|
||||
"name": "T"
|
||||
}
|
||||
],
|
||||
"description": "Declaratively connects sibling `cdkDropList` instances together. All of the `cdkDropList`\nelements that are placed inside a `cdkDropListGroup` will be connected to each other\nautomatically. Can be used as an alternative to the `cdkDropListConnectedTo` input\nfrom `cdkDropList`.",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "/**\n * Declaratively connects sibling `cdkDropList` instances together. All of the `cdkDropList`\n * elements that are placed inside a `cdkDropListGroup` will be connected to each other\n * automatically. Can be used as an alternative to the `cdkDropListConnectedTo` input\n * from `cdkDropList`.\n */",
|
||||
"implements": [
|
||||
"OnDestroy"
|
||||
],
|
||||
"source": {
|
||||
"filePath": "/src/cdk/drag-drop/directives/drop-list-group.ts",
|
||||
"startLine": 27,
|
||||
"endLine": 43
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "CdkDragPreview",
|
||||
"isAbstract": false,
|
||||
@@ -6294,11 +6294,11 @@
|
||||
"@angular/core"
|
||||
],
|
||||
[
|
||||
"OnDestroy",
|
||||
"Input",
|
||||
"@angular/core"
|
||||
],
|
||||
[
|
||||
"Input",
|
||||
"OnDestroy",
|
||||
"@angular/core"
|
||||
],
|
||||
[
|
||||
@@ -6422,11 +6422,11 @@
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
"CDK_DROP_LIST_GROUP",
|
||||
"CdkDragRelease",
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
"CdkDragRelease",
|
||||
"CDK_DROP_LIST_GROUP",
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
@@ -6438,11 +6438,11 @@
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
"CdkDropListGroup",
|
||||
"CdkDragPlaceholder",
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
"CdkDragPlaceholder",
|
||||
"CdkDropListGroup",
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
@@ -6593,10 +6593,6 @@
|
||||
"DropListOrientation",
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
"CDK_DROP_LIST_GROUP",
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
"CdkDragRelease",
|
||||
"@angular/cdk/drag-drop"
|
||||
@@ -6609,6 +6605,10 @@
|
||||
"CdkDragRelease.event",
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
"CDK_DROP_LIST_GROUP",
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
"CDK_DRAG_PLACEHOLDER",
|
||||
"@angular/cdk/drag-drop"
|
||||
@@ -6617,18 +6617,6 @@
|
||||
"CDK_DRAG_PREVIEW",
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
"CdkDropListGroup",
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
"CdkDropListGroup.disabled",
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
"CdkDropListGroup.ngOnDestroy",
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
"CdkDragPlaceholder",
|
||||
"@angular/cdk/drag-drop"
|
||||
@@ -6645,6 +6633,18 @@
|
||||
"CdkDragPlaceholder.ngOnDestroy",
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
"CdkDropListGroup",
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
"CdkDropListGroup.disabled",
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
"CdkDropListGroup.ngOnDestroy",
|
||||
"@angular/cdk/drag-drop"
|
||||
],
|
||||
[
|
||||
"CdkDragPreview",
|
||||
"@angular/cdk/drag-drop"
|
||||
|
||||
@@ -1142,8 +1142,8 @@
|
||||
"implements": [],
|
||||
"source": {
|
||||
"filePath": "/src/cdk/testing/selenium-webdriver/selenium-web-driver-harness-environment.ts",
|
||||
"startLine": 35,
|
||||
"endLine": 38
|
||||
"startLine": 34,
|
||||
"endLine": 37
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1194,8 +1194,8 @@
|
||||
"rawComment": "/** Waits for angular to be ready after the page load. */",
|
||||
"source": {
|
||||
"filePath": "/src/cdk/testing/selenium-webdriver/selenium-web-driver-harness-environment.ts",
|
||||
"startLine": 65,
|
||||
"endLine": 68
|
||||
"startLine": 66,
|
||||
"endLine": 69
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1635,8 +1635,8 @@
|
||||
"implements": [],
|
||||
"source": {
|
||||
"filePath": "/src/cdk/testing/selenium-webdriver/selenium-web-driver-harness-environment.ts",
|
||||
"startLine": 71,
|
||||
"endLine": 150
|
||||
"startLine": 72,
|
||||
"endLine": 151
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -22,16 +22,4 @@ Angular v21 is being delivered to you as a brand new release adventure. With mod
|
||||
- Your first look at Signal Forms, our new streamlined, signal-based approach to forms in Angular
|
||||
- Exciting new details about the Angular Aria package
|
||||
|
||||
<div style="display: block; width: 80%; margin: 0 auto; margin-top: 20px;">
|
||||
<iframe
|
||||
credentialless
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.youtube.com/embed/DDAHORVzQ5g?si=B9Uv5vXzIKJfcLGr"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
<docs-video src="https://www.youtube.com/embed/DDAHORVzQ5g?si=B9Uv5vXzIKJfcLGr" title="Angular v21 Developer Event"/>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<!-- #docregion unsupported -->
|
||||
<p app:Highlight>This is invalid</p>
|
||||
<!-- #enddocregion unsupported -->
|
||||
@@ -1,7 +0,0 @@
|
||||
// #docregion
|
||||
import {Directive} from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[appHighlight]',
|
||||
})
|
||||
export class HighlightDirective {}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ You can alternatively choose to write your template and styles in separate files
|
||||
export class ProfilePhoto {}
|
||||
```
|
||||
|
||||
This can help separate the concerns of _presentation_ from _behavior_ in your project. You can choose one approach for your entire project, or you decide which to use for each component.
|
||||
This can help separate the concerns of _presentation_ from _behavior_ in your project. You can choose one approach for your entire project, or you can decide which to use for each component.
|
||||
|
||||
Both `templateUrl` and `styleUrl` are relative to the directory in which the component resides.
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ For example:
|
||||
|
||||
So, in effect, the `<a>` element may be in the component's template, or in any of its projected or child content.
|
||||
|
||||
- With `:host ::ng-deep p a`, both the `<a>` and `<p>` elements must be decendants of the component's host element.
|
||||
- With `:host ::ng-deep p a`, both the `<a>` and `<p>` elements must be descendants of the component's host element.
|
||||
|
||||
They can come from the component's template or the views of its child components, but not elsewhere in the app.
|
||||
|
||||
|
||||
@@ -14,7 +14,14 @@ This section walks you through creating a highlight directive that sets the back
|
||||
|
||||
The CLI creates `src/app/highlight.directive.ts`, a corresponding test file `src/app/highlight.directive.spec.ts`.
|
||||
|
||||
<docs-code header="highlight.directive.ts" path="adev/src/content/examples/attribute-directives/src/app/highlight.directive.0.ts"/>
|
||||
```angular-ts
|
||||
import {Directive} from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[appHighlight]',
|
||||
})
|
||||
export class HighlightDirective {}
|
||||
```
|
||||
|
||||
The `@Directive()` decorator's configuration property specifies the directive's CSS attribute selector, `[appHighlight]`.
|
||||
|
||||
@@ -25,11 +32,13 @@ This section walks you through creating a highlight directive that sets the back
|
||||
|
||||
1. Add logic to the `HighlightDirective` class that sets the background to yellow.
|
||||
|
||||
<docs-code header="highlight.directive.ts" path="adev/src/content/examples/attribute-directives/src/app/highlight.directive.1.ts"/>
|
||||
<docs-code header="highlight.directive.ts" path="adev/src/content/examples/attribute-directives/src/app/highlight.directive.1.ts"/>
|
||||
|
||||
HELPFUL: Directives _do not_ support namespaces.
|
||||
IMPORTANT: Directives _do not_ support namespaces.
|
||||
|
||||
<docs-code header="app.component.avoid.html (unsupported)" path="adev/src/content/examples/attribute-directives/src/app/app.component.avoid.html" region="unsupported"/>
|
||||
```angular-html {avoid}
|
||||
<p app:Highlight>This is invalid</p>
|
||||
```
|
||||
|
||||
## Applying an attribute directive
|
||||
|
||||
|
||||
@@ -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'},
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ Providing a custom or a "noop" Zone.js implementation may lead to a different ti
|
||||
|
||||
## Errors
|
||||
|
||||
There are several hydration related errors you may encounter ranging from node mismatches to cases when the `ngSkipHydration` was used on an invalid host node. The most common error case that may occur is due to direct DOM manipulation using native APIs that results in hydration being unable to find or match the expected DOM tree structure on the client that was rendered by the server. The other case you may encounter this type of error was mentioned in the [Valid HTML structure](#valid-html-structure) section earlier. So, make sure the HTML in your templates are using valid structure, and you'll avoid that error case.
|
||||
There are several hydration related errors you may encounter ranging from node mismatches to cases when the `ngSkipHydration` was used on an invalid host node. The most common error case that may occur is due to direct DOM manipulation using native APIs that results in hydration being unable to find or match the expected DOM tree structure on the client that was rendered by the server. The other case you may encounter this type of error was mentioned in the [Valid HTML structure](#valid-html-structure) section earlier. So, make sure the HTML in your templates is using valid structure, and you'll avoid that error case.
|
||||
|
||||
For a full reference on hydration related errors, visit the [Errors Reference Guide](/errors).
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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']);
|
||||
```
|
||||
@@ -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,6 +36,7 @@
|
||||
| `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) |
|
||||
| `NG02200` | [Missing Iterable Differ](errors/NG02200) |
|
||||
|
||||
@@ -38,7 +38,7 @@ Start developing with the latest Angular features from our roadmap. This list re
|
||||
### Bringing the best of AI to Angular
|
||||
|
||||
<docs-card-container>
|
||||
<docs-card title="AI Powered Angular" href="">
|
||||
<docs-card title="AI Powered Angular">
|
||||
AI continues to shape the development landscape. It has changed how we develop apps and the types of user experiences that are possible. We plan to best support the developer community in AI-assisted coding and integrating AI in their applications.
|
||||
</docs-card>
|
||||
<docs-card title="AI Development">
|
||||
@@ -73,11 +73,11 @@ As part of this project, we'll explore the requirement space of cross framework
|
||||
<docs-card title="Signal Forms" href="/guide/forms/signals/overview">
|
||||
In Angular v21, we landed an experimental version of Signal Forms. This new approach allows developers to manage form state using signals, providing an ergonomic forms creation experience. Next, our plans include promoting Signal Forms to stable and enhancing interoperability with reactive forms - enabling teams to progressively migrate large forms at their own pace.
|
||||
</docs-card>
|
||||
<docs-card title="Reactivity" href="">
|
||||
<docs-card title="Reactivity">
|
||||
We introduced experimental signal APIs, resource() and httpResource(), for flexible asynchronous data handling. We plan to promote these APIs to developer preview/stable based on community feedback.
|
||||
We are also evaluating new APIs for unhandled use cases, considering community benefits and tradeoffs before implementation after careful consideration.
|
||||
</docs-card>
|
||||
<docs-card title="Change Detection" href="">
|
||||
<docs-card title="Change Detection">
|
||||
With Zoneless being stable and default, we are also planning to shift the default change detection strategy to OnPush, to follow current best practices. [See the RFC discussion for details](https://github.com/angular/angular/discussions/66779).
|
||||
</docs-card>
|
||||
<docs-card title="Components">
|
||||
@@ -90,7 +90,7 @@ For accessibility, we are continuously evaluating the components and patterns ag
|
||||
### Improve tooling
|
||||
|
||||
<docs-card-container>
|
||||
<docs-card title="Modernize unit testing tooling with ng test" href="">
|
||||
<docs-card title="Modernize unit testing tooling with ng test">
|
||||
Following the stable release of Vitest in Angular v21, it is now our primary test runner. We are now focused on promoting our experimental Karma to Vitest migration tool to stable as well as investigating new features to further refine and improve the developer testing workflow.
|
||||
</docs-card>
|
||||
</docs-card-container>
|
||||
@@ -98,7 +98,7 @@ For accessibility, we are continuously evaluating the components and patterns ag
|
||||
## Completed projects
|
||||
|
||||
<docs-card-container>
|
||||
<docs-card title="Signal debugging in Angular DevTools" href="" link="Completed in 2025">
|
||||
<docs-card title="Signal debugging in Angular DevTools" link="Completed in 2025">
|
||||
With the evolution of Signals in Angular, we are working on a better tooling for debugging them. High on the priority list is a UI for inspecting and debugging signals.
|
||||
</docs-card>
|
||||
<docs-card title="Improve HMR (Hot Module Reload)" href="https://github.com/angular/angular/issues/39367#issuecomment-1439537306" link="Completed in 2025">
|
||||
@@ -106,19 +106,20 @@ For accessibility, we are continuously evaluating the components and patterns ag
|
||||
|
||||
In Angular v19 we shipped initial support for CSS and template HMR and in v20 we graduated template HMR to stable. We'll continue collecting feedback to make sure we're addressing developers' needs before we mark this project as complete.
|
||||
</docs-card>
|
||||
<docs-card title="Zoneless Angular" href="" link="Completed in Q4 2025">
|
||||
<docs-card title="Zoneless Angular" link="Completed in Q4 2025">
|
||||
In v18 we shipped experimental zoneless support in Angular. It enables developers to use the framework without including zone.js in their bundle, which improves performance, debugging experience, and interoperability. As part of the initial release we also introduced zoneless support to the Angular CDK and Angular Material.
|
||||
|
||||
In v19 we introduced zoneless support in server-side rendering, addressed some edge cases, and created a schematic to scaffold zoneless projects. We transitioned <a href="https://fonts.google.com/">Google Fonts</a> to zoneless which improved performance, developer experience, and allowed us to identify gaps that we need to address before moving this feature to developer preview.
|
||||
In v19 we introduced zoneless support in server-side rendering, addressed some edge cases, and created a schematic to scaffold zoneless projects. We transitioned <a href="https://fonts.google.com/">Google Fonts</a> to zoneless which improved performance, developer experience, and allowed us to identify gaps that we need to address before moving this feature to developer preview.
|
||||
|
||||
As of Angular v20.2, Zoneless Angular is now stable and includes improvements in error handling and server-side rendering.
|
||||
</docs-card>
|
||||
<docs-card title="Server route configuration" link="Completed in Q2 2025" href="">
|
||||
We're working towards enabling a more ergonomic route configuration on the server. We want to make it trivial to declare which routes should be server-side rendered, prerendered or client-side rendered.
|
||||
As of Angular v20.2, Zoneless Angular is now stable and includes improvements in error handling and server-side rendering.
|
||||
</docs-card>
|
||||
|
||||
<docs-card title="Server route configuration" link="Completed in Q2 2025" >
|
||||
We're working towards enabling a more ergonomic route configuration on the server. We want to make it trivial to declare which routes should be server-side rendered, prerendered or client-side rendered.
|
||||
|
||||
In Angular v19 we shipped developer preview of route-level render mode which allows you to granularly configure which routes you want Angular to prerender, server-side render or client-side render. In Angular v20 we graduated it to stable.
|
||||
</docs-card>
|
||||
<docs-card title="Enable incremental hydration" link="Completed in Q2 2025" href="">
|
||||
<docs-card title="Enable incremental hydration" link="Completed in Q2 2025">
|
||||
In v17 we graduated hydration from developer preview and we've been consistently observing 40-50% improvements in LCP. Since then we started prototyping incremental hydration and shared a demo on stage at ng-conf.
|
||||
|
||||
In v19 we shipped the incremental hydration in developer preview mode, powered by `@defer` blocks. In Angular v20 we graduated it to stable!
|
||||
@@ -131,15 +132,15 @@ In Angular v20 we graduated all the fundamental reactivity primitives to stable
|
||||
<docs-card title="Support two-dimensional drag-and-drop" link="Completed in Q2 2024" href="https://github.com/angular/components/issues/13372">
|
||||
As part of this project, we implemented mixed orientation support for the Angular CDK drag and drop. This is one of the repository's most highly requested features.
|
||||
</docs-card>
|
||||
<docs-card title="Event replay with SSR and prerendering" link="Completed in Q4 2024" href="https://angular.dev/api/platform-browser/withEventReplay">
|
||||
<docs-card title="Event replay with SSR and prerendering" link="Completed in Q4 2024" href="api/platform-browser/withEventReplay">
|
||||
In v18 we introduced an event replay functionality when using server-side rendering or prerendering. For this feature we depend on the event dispatch primitive (previously known as jsaction) that is running on Google.com.
|
||||
|
||||
In Angular v19 we graduated event replay to stable and enabled it by default for all new projects.
|
||||
</docs-card>
|
||||
<docs-card title="Integrate Angular Language Service with Schematics" link="Completed in Q4 2024" href="">
|
||||
<docs-card title="Integrate Angular Language Service with Schematics" link="Completed in Q4 2024">
|
||||
To make it easier for developers to use modern Angular APIs, we enabled integration between the Angular language service and schematics which allows you to refactor your app with a single click.
|
||||
</docs-card>
|
||||
<docs-card title="Streamline standalone imports with Language Service" link="Completed in Q4 2024" href="">
|
||||
<docs-card title="Streamline standalone imports with Language Service" link="Completed in Q4 2024">
|
||||
As part of this initiative, the language service automatically imports components and pipes in standalone and NgModule-based apps. Additionally, we've added a template diagnostic to highlight unused imports in standalone components, which should help make application bundles smaller.
|
||||
</docs-card>
|
||||
<docs-card title="Local template variables" link="Completed in Q3 2024">
|
||||
@@ -153,7 +154,7 @@ In v17.2 we shared experimental support for Angular Material 3 and in v18 we gra
|
||||
<docs-card title="Introduce deferred loading" link="Completed in Q2 2024" href="https://next.angular.dev/guide/templates/defer">
|
||||
In v17 we shipped deferrable views in developer preview, which provide an ergonomic API for deferred code loading. In v18 we enabled deferrable views for library developers and graduated the API to stable.
|
||||
</docs-card>
|
||||
<docs-card title="iframe support in Angular DevTools" link="Completed in Q2 2024" href="">
|
||||
<docs-card title="iframe support in Angular DevTools" link="Completed in Q2 2024">
|
||||
We enabled debugging and profiling of Angular apps embedded within an iframe on the page.
|
||||
</docs-card>
|
||||
<docs-card title="Automation for transition of existing hybrid rendering projects to esbuild and vite" link="Completed in Q2 2024" href="tools/cli/build-system-migration">
|
||||
@@ -165,7 +166,7 @@ Angular.dev is the new site, domain and home for Angular development. The new si
|
||||
<docs-card title="Introduce built-in control flow" link="Completed in Q2 2024" href="https://next.angular.dev/essentials/conditionals-and-loops">
|
||||
In v17 we shipped a developer preview version of a new control flow. It brings significant performance improvements and better ergonomics for template authoring. We also provided a migration of existing `*ngIf`, `*ngFor`, and `*ngSwitch` which you can run to move your project to the new implementation. As of v18 the built-in control flow is now stable.
|
||||
</docs-card>
|
||||
<docs-card title="Modernize getting started tutorial" link="Completed Q4 2023" href="">
|
||||
<docs-card title="Modernize getting started tutorial" link="Completed Q4 2023">
|
||||
Over the past two quarters, we developed a new [video](https://www.youtube.com/watch?v=xAT0lHYhHMY&list=PL1w1q3fL4pmj9k1FrJ3Pe91EPub2_h4jF) and [textual](/tutorials/learn-angular) tutorial based on standalone components.
|
||||
</docs-card>
|
||||
<docs-card title="Investigate modern bundlers" link="Completed Q4 2023" href="guide/hydration">
|
||||
|
||||
@@ -130,7 +130,7 @@ The following list discusses all the `browser` builder options that will need to
|
||||
- `resourcesOutputPath` should be removed, this is now always `media`.
|
||||
- `vendorChunk` should be removed, as this was a performance optimization which is no longer needed.
|
||||
- `commonChunk` should be removed, as this was a performance optimization which is no longer needed.
|
||||
- `deployUrl` should be removed and is not supported. Prefer [`<base href>`](guide/routing/common-router-tasks) instead. See [deployment documentation](tools/cli/deployment#--deploy-url) for more information.
|
||||
- `deployUrl` should be removed and is not supported. Prefer [`<base href>`](guide/routing/router-reference#base-href) instead. See [deployment documentation](tools/cli/deployment#--deploy-url) for more information.
|
||||
- `ngswConfigPath` should be renamed to `serviceWorker`.
|
||||
|
||||
If the application is not using SSR currently, this should be the final step to allow `ng build` to function.
|
||||
|
||||
@@ -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.
|
||||
@@ -128,7 +128,7 @@ ng build --deploy-url /my/assets
|
||||
|
||||
```
|
||||
|
||||
The effect and purpose of `--deploy-url` overlaps with [`<base href>`](guide/routing/common-router-tasks). Both can be used for initial scripts, stylesheets, lazy scripts, and css resources.
|
||||
The effect and purpose of `--deploy-url` overlaps with [`<base href>`](guide/routing/router-reference#base-href). Both can be used for initial scripts, stylesheets, lazy scripts, and css resources.
|
||||
|
||||
Unlike `<base href>` which can be defined in a single place at runtime, the `--deploy-url` needs to be hard-coded into an application at build time.
|
||||
Prefer `<base href>` where possible.
|
||||
|
||||
@@ -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"]
|
||||
|
||||
+388
-293
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user