mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| d8239c7836 | |||
| c1579163b7 | |||
| dbb15ebab2 | |||
| a6c49e0ea9 | |||
| 2c12339ab8 | |||
| a3048f231c | |||
| ae2f2497d3 | |||
| 2c7d3b5704 | |||
| 6bd2121817 | |||
| e8c047bd8d | |||
| a0ca5a50b2 | |||
| 1cdb322600 | |||
| c9585b7159 | |||
| 238b07fe90 | |||
| 3b3ab8b626 | |||
| e731bbdd14 | |||
| c7a18924a6 | |||
| 244e0b0b59 | |||
| 9bfe1a6065 | |||
| b658accf1f | |||
| 9be5e2c211 | |||
| 8818095a82 | |||
| 80c85ec8a5 | |||
| fb7a10fb18 | |||
| 1af0db5742 | |||
| a821e06e33 | |||
| a21be36e15 | |||
| 7aaa8b9b39 | |||
| d0481860bf | |||
| 880a57d4b3 | |||
| 1ba89d1128 | |||
| 831746eff1 | |||
| 9182b62b96 | |||
| 4d2a9394c3 | |||
| ad0156e056 | |||
| aa3220a4b5 | |||
| b4ab6ba2e8 | |||
| 73d6b01b47 | |||
| 3838554503 | |||
| 1148c93834 | |||
| 9f92312cde | |||
| 04b16dee8f | |||
| 57ce2b64ef | |||
| 5393d8ea2c | |||
| 675d4fb489 | |||
| 851ef77318 | |||
| 5800f17e19 | |||
| e19150d2b5 | |||
| 0e496c1d85 | |||
| 737777ef72 | |||
| fa26e7271f | |||
| 076d41c3f6 | |||
| a8f80c1f99 | |||
| bf948be4c2 | |||
| 23ea431c4e | |||
| 2fb767b139 | |||
| b10e305ccc | |||
| f0d19b88b1 | |||
| 67e0ba7e03 | |||
| ca328eab8f | |||
| faeeef678a | |||
| 4d786517d3 | |||
| 26c43d14ba | |||
| 1890c3008b | |||
| cbb2266984 | |||
| ea4847aae5 | |||
| 5fa3052ed1 | |||
| 86718fbbe1 | |||
| de5fbb7deb | |||
| f39896da99 | |||
| 21cab20dae | |||
| 2d28e9b392 | |||
| 5b46a63c08 | |||
| 03fb3cd463 | |||
| 67a73192bf | |||
| e5ed276f27 | |||
| 334ae10168 | |||
| 1f2dc72e8a | |||
| e3b0e5a0b3 | |||
| 10b0dd94e9 | |||
| 6f41efc689 | |||
| bfb8b177ac | |||
| 2feced366a | |||
| 5d755be01c | |||
| 8017617fd9 | |||
| ed2d324f9c | |||
| d1dcd16c5b | |||
| abbd8797bb | |||
| c06e3a0bb4 | |||
| 8ba2844da5 | |||
| 74698d58ec | |||
| a7e3d764e5 | |||
| 7907e982ee | |||
| adda6c5c10 |
@@ -1 +0,0 @@
|
||||
../.gemini/skills
|
||||
@@ -0,0 +1,133 @@
|
||||
---
|
||||
name: PR Review
|
||||
description: Guidelines and tools for reviewing pull requests in the Angular repository.
|
||||
---
|
||||
|
||||
# PR Review Guidelines
|
||||
|
||||
When reviewing a pull request for the `angular` repository, follow these essential guidelines to ensure high-quality contributions:
|
||||
|
||||
1. **Context & Ecosystem**:
|
||||
- Keep in mind that this is the core Angular framework. Changes here can impact millions of developers.
|
||||
- Be mindful of backwards compatibility. Breaking changes require strict approval processes and deprecation periods.
|
||||
|
||||
2. **Key Focus Areas**:
|
||||
- **Comprehensive Reviews**: You **MUST always** perform a deep, comprehensive review of the _entire_ pull request. If the user asks you to look into a specific issue, file, or area of concern, you must investigate that specific area _in addition to_ reviewing the rest of the PR's substantive changes. Do not terminate your review after addressing only the user's focal point.
|
||||
- **Package-Specific Guidelines**: Check if there are specific guidelines for the package being modified in the `reference/` directory (e.g., `reference/router.md`). Always prioritize these rules for their respective packages.
|
||||
- **Commit Messages**: Evaluate the quality of commit messages. They should explain the _why_ behind the change, not just the _what_. Someone should be able to look at the commit history years from now and clearly understand the context and reasoning for the change.
|
||||
- **Code Cleanliness**: Ensure the code is readable, maintainable, and follows Angular's project standards.
|
||||
- **Performance**: Look out for code that might negatively impact runtime performance or bundle size, particularly in hot paths like change detection or rendering.
|
||||
- **Testing**: Ensure all new logic has comprehensive tests, including edge cases. **Do NOT run tests locally** as part of your review process. CI handles this automatically, and running tests locally is redundant and inefficient.
|
||||
- **API Design**: Ensure new public APIs are well-designed, consistent with existing APIs, and properly documented.
|
||||
- **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 `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**:
|
||||
- First, create a task list (e.g., in `task.md`) that you can easily reference containing **all** the review requirements from the "Key Focus Areas" section (Commit Messages, Performance, Testing, etc.), along with any specific review notes or requests from the user.
|
||||
- Before doing an in-depth review, expand this list into more detailed items of what you plan to explore and verify in the PR.
|
||||
- As you conduct the review, check off items in this list, adding your assessment or findings underneath each item.
|
||||
- At the end of your review, refer back to the checklist to ensure every single requirement was completely verified.
|
||||
- **Fetch PR Metadata Safely**: When you need to read the PR description or context, do NOT use `gh pr view <PR_NUMBER>` by itself, as its default GraphQL query may fail due to lack of `read:org` and `read:discussion` token scopes. Instead, use `read_url_content` on the PR URL or use `gh pr view <PR_NUMBER> --json title,body,state,author`.
|
||||
- **Check Existing Comments First**: Before formulating feedback, use the GitHub MCP or available scripts to fetch existing comments on the PR. Review this feedback to avoid duplicate comments, and incorporate its insights into your own review process.
|
||||
- **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). 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.
|
||||
- **Resolve Comments**: Once the user confirms the changes are good and should be committed/pushed, respond to the existing comments as 'resolved' using the GitHub MCP or available scripts.
|
||||
|
||||
**B. Remote Code Review (For all other PRs)**
|
||||
- **Batching Comments (MCP Server - Preferred)**: If you have the GitHub MCP Server configured, you **MUST** follow this workflow to avoid spamming the author with multiple notifications:
|
||||
1. Create a pending review using `mcp_github-mcp-server_pull_request_review_write` (method `create`).
|
||||
2. Add your inline comments to the pending review using `mcp_github-mcp-server_add_comment_to_pending_review`.
|
||||
3. Submit the review using `mcp_github-mcp-server_pull_request_review_write` (method `submit_pending`).
|
||||
- **Batching Comments (Scripts - Fallback)**: If you do **NOT** have access to the GitHub MCP Server (e.g., specific MCP tools are missing from your context), fallback to using the provided scripts. Use `post_inline_comment.sh` to stage your comments locally. Once all comments are staged, you **MUST** call `submit_pr_review.sh` to publish them as a single batched review (and send a single notification). Try to keep comments minimal or use a general comment if you have many suggestions.
|
||||
- **Use Suggested Changes**: Whenever appropriate (e.g., for simple code fixes, refactoring suggestions, or typo corrections), prefer using GitHub's **Suggested Changes** syntax (`suggestion ... `) in your inline comments. This allows the author to apply your suggested code improvements with a single click in the GitHub UI.
|
||||
- **Review Type**: Never mark an external PR review as an "approval" unless explicitly instructed by a repo maintainer. Always use "Request Changes" or "Comment". Note that some tools might only support commenting.
|
||||
- **Require User Approval Before Posting**: Prepare your review comments and present them to the user, alongside a summary of your completed checklist. Do NOT post comments to the PR without explicitly asking the user for permission first. Only post the review after the user approves.
|
||||
- **Prefix Agent Comments**: To make it clear when comments are generated and posted by an AI agent rather than a human user, **always** prefix your review comments with `AGENT: `.
|
||||
|
||||
## Available Tools
|
||||
|
||||
The following tools are available for remote interactions. We prefer using standard **GitHub MCP Server** tools when available. If you do not have the MCP server set up, you **MUST** fallback to using the custom bash scripts.
|
||||
|
||||
### GitHub MCP Tools (Preferred)
|
||||
|
||||
- `mcp_github-mcp-server_pull_request_review_write`
|
||||
- `mcp_github-mcp-server_add_comment_to_pending_review`
|
||||
|
||||
### Custom Bash Scripts (Fallback)
|
||||
|
||||
The following scripts are provided as fallbacks if the MCP server is not available. Note that they rely on the `gh` CLI being correctly installed and authenticated in the local environment.
|
||||
|
||||
### `determine_review_type.sh`
|
||||
|
||||
Determines whether to use the Local or Remote review workflow by checking if the currently authenticated GitHub user via the `gh` CLI matches the author of the pull request.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
.agent/skills/pr_review/scripts/determine_review_type.sh <PR_NUMBER>
|
||||
```
|
||||
|
||||
### `get_pr_comments.sh`
|
||||
|
||||
Fetches all existing inline comments on a PR using the GitHub API. This is crucial for reviewing other contributors' feedback and avoiding duplicate comments. It outputs JSON containing the `id`, `path`, `line`, `body`, and `user` for each comment.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
.agent/skills/pr_review/scripts/get_pr_comments.sh <PR_NUMBER>
|
||||
```
|
||||
|
||||
### `reply_pr_comment.sh`
|
||||
|
||||
Replies to an existing PR comment thread. This is useful for marking comments as resolved after addressing them in a local code review. Note that the `COMMENT_ID` must be the ID of the top-level comment in the thread.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
.agent/skills/pr_review/scripts/reply_pr_comment.sh <PR_NUMBER> <COMMENT_ID> <REPLY_BODY>
|
||||
```
|
||||
|
||||
### `post_inline_comment.sh`
|
||||
|
||||
The GitHub CLI `gh pr review` command does not natively support adding inline comments to specific lines of code via its standard flags. This script wraps the GitHub API to stage comments locally. They will not be published until you call `submit_pr_review.sh`.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
.agent/skills/pr_review/scripts/post_inline_comment.sh <PR_NUMBER> <FILE_PATH> <LINE_NUMBER> <COMMENT_BODY>
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
.agent/skills/pr_review/scripts/post_inline_comment.sh 12345 "packages/core/src/render3/instructions/element.ts" 42 "AGENT: Consider the performance implications here."
|
||||
```
|
||||
|
||||
### `submit_pr_review.sh`
|
||||
|
||||
Submits all locally staged inline comments as a single batched review via the GitHub Pull Request Reviews API.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
.agent/skills/pr_review/scripts/submit_pr_review.sh <PR_NUMBER> <EVENT_TYPE> [BODY]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
- `EVENT_TYPE`: Must be `COMMENT`, `APPROVE`, or `REQUEST_CHANGES`. Never use `APPROVE` for external PRs.
|
||||
- `BODY`: (Optional) A general summary comment for the review.
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
.agent/skills/pr_review/scripts/submit_pr_review.sh 12345 COMMENT "AGENT: I have left a few inline suggestions for your consideration."
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
# Router PR Review Guidelines
|
||||
|
||||
When reviewing pull requests that modify the Angular Router (`packages/router`), pay special attention to the following:
|
||||
|
||||
- **Timing Sensitivity**: The router is extremely sensitive to timing changes. Any changes that alter the asynchronous timing of navigations, resolvers, or guards are almost always breaking changes and must be scrutinized carefully.
|
||||
- **Testing Practices**: Tests should usually use the `RouterTestingHarness`. Many existing tests are older and do not use this harness. Do not blindly follow the shape of existing tests when writing or reviewing new ones; encourage the use of modern testing utilities.
|
||||
- **Feature Justification**: Changes to router core code should be well-justified. Consider whether the change is proven to be a core developer ask, such as resolving a highly upvoted GitHub issue or addressing a critical bug.
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# determine_review_type.sh <PR_NUMBER>
|
||||
# Determines if the PR should be reviewed locally or remotely based on author.
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: determine_review_type.sh <PR_NUMBER>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PR_NUMBER=$1
|
||||
|
||||
# Get current authenticated user
|
||||
CURRENT_USER=$(gh api user -q .login 2>/dev/null)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Could not determine current GitHub user. Are you logged in to gh?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get PR author
|
||||
PR_AUTHOR=$(gh pr view "$PR_NUMBER" --json author -q .author.login 2>/dev/null)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Could not retrieve PR information for $PR_NUMBER."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CURRENT_USER" = "$PR_AUTHOR" ]; then
|
||||
echo "local"
|
||||
else
|
||||
echo "remote"
|
||||
fi
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# get_pr_comments.sh
|
||||
# Fetches existing inline comments on a PR to avoid duplicate reviews.
|
||||
# Usage: ./get_pr_comments.sh <PR_NUMBER>
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "Usage: $0 <PR_NUMBER>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PR_NUMBER="$1"
|
||||
|
||||
# Ensure gh cli is installed
|
||||
if ! command -v gh &> /dev/null; then
|
||||
echo "Error: gh CLI could not be found. Please install and authenticate."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the current repository (e.g., angular/angular)
|
||||
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
||||
|
||||
# Fetch comments
|
||||
gh api \
|
||||
--paginate \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/${REPO}/pulls/${PR_NUMBER}/comments" \
|
||||
--jq '.[] | {id: .id, path: .path, line: .line, body: .body, user: .user.login}'
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# post_inline_comment.sh
|
||||
# Adds an inline comment to a specific line in a PR via the GitHub API.
|
||||
# Usage: ./post_inline_comment.sh <PR_NUMBER> <FILE_PATH> <LINE_NUMBER> <COMMENT_BODY>
|
||||
|
||||
if [ "$#" -lt 4 ]; then
|
||||
echo "Usage: $0 <PR_NUMBER> <FILE_PATH> <LINE_NUMBER> <COMMENT_BODY>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PR_NUMBER="$1"
|
||||
FILE_PATH="$2"
|
||||
LINE="$3"
|
||||
BODY="$4"
|
||||
|
||||
# Ensure gh cli is installed
|
||||
if ! command -v gh &> /dev/null; then
|
||||
echo "Error: gh CLI could not be found. Please install and authenticate."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the current repository (e.g., angular/angular)
|
||||
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
||||
|
||||
echo "Staging inline comment for PR #${PR_NUMBER} on ${FILE_PATH}:${LINE}..."
|
||||
|
||||
COMMENT_FILE="/tmp/angular_pr_${PR_NUMBER}_comments.json"
|
||||
if [ ! -f "$COMMENT_FILE" ]; then
|
||||
echo "[]" > "$COMMENT_FILE"
|
||||
fi
|
||||
|
||||
# Append the new comment to the JSON array
|
||||
jq --arg path "${FILE_PATH}" --argjson line "${LINE}" --arg body "${BODY}" \
|
||||
'. += [{"path": $path, "line": $line, "body": $body}]' "$COMMENT_FILE" > "${COMMENT_FILE}.tmp" && mv "${COMMENT_FILE}.tmp" "$COMMENT_FILE"
|
||||
|
||||
echo "Comment successfully staged locally. Remember to call submit_pr_review.sh when finished to publish all comments as a single review!"
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# reply_pr_comment.sh <PR_NUMBER> <COMMENT_ID> <REPLY_BODY>
|
||||
# Replies to an existing PR comment thread. Note: COMMENT_ID must be the ID of the top-level comment in the thread you are replying to.
|
||||
|
||||
if [ "$#" -lt 3 ]; then
|
||||
echo "Usage: reply_pr_comment.sh <PR_NUMBER> <COMMENT_ID> <REPLY_BODY>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PR_NUMBER="$1"
|
||||
COMMENT_ID="$2"
|
||||
BODY="$3"
|
||||
|
||||
# Ensure gh cli is installed
|
||||
if ! command -v gh &> /dev/null; then
|
||||
echo "Error: gh CLI could not be found. Please install and authenticate."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the current repository (e.g., angular/angular)
|
||||
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
||||
|
||||
# Reply to the thread using the provided comment ID
|
||||
gh api \
|
||||
--silent \
|
||||
--method POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/${REPO}/pulls/${PR_NUMBER}/comments/${COMMENT_ID}/replies" \
|
||||
-f body="$BODY"
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# submit_pr_review.sh
|
||||
# Submits a batched PR review using comments previously staged by post_inline_comment.sh
|
||||
# Usage: ./submit_pr_review.sh <PR_NUMBER> <EVENT_TYPE> [BODY]
|
||||
# EVENT_TYPE must be COMMENT, APPROVE, or REQUEST_CHANGES
|
||||
|
||||
if [ "$#" -lt 2 ]; then
|
||||
echo "Usage: $0 <PR_NUMBER> <EVENT_TYPE> [BODY]"
|
||||
echo "EVENT_TYPE must be COMMENT, APPROVE, or REQUEST_CHANGES"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PR_NUMBER="$1"
|
||||
EVENT="$2"
|
||||
BODY="${3:-}"
|
||||
COMMENT_FILE="/tmp/angular_pr_${PR_NUMBER}_comments.json"
|
||||
|
||||
if ! command -v gh &> /dev/null; then
|
||||
echo "Error: gh CLI could not be found. Please install and authenticate."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
||||
|
||||
# Check if there are staged comments
|
||||
COMMENTS="[]"
|
||||
if [ -f "$COMMENT_FILE" ]; then
|
||||
COMMENTS=$(cat "$COMMENT_FILE")
|
||||
fi
|
||||
|
||||
echo "Submitting review for PR #${PR_NUMBER}..."
|
||||
|
||||
# Create the payload
|
||||
PAYLOAD_FILE="/tmp/angular_pr_${PR_NUMBER}_payload.json"
|
||||
jq -n --arg event "$EVENT" --arg body "$BODY" --argjson comments "$COMMENTS" \
|
||||
'{event: $event, body: $body, comments: $comments}' > "$PAYLOAD_FILE"
|
||||
|
||||
# Post the review using the GitHub Pull Request Reviews API
|
||||
gh api \
|
||||
--method POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"/repos/${REPO}/pulls/${PR_NUMBER}/reviews" \
|
||||
--input "$PAYLOAD_FILE"
|
||||
|
||||
echo "Review submitted successfully!"
|
||||
rm -f "$COMMENT_FILE"
|
||||
rm -f "$PAYLOAD_FILE"
|
||||
+2
-1
@@ -7,4 +7,5 @@ code_review:
|
||||
help: false
|
||||
summary: false
|
||||
code_review: false
|
||||
ignore_patterns: []
|
||||
ignore_patterns:
|
||||
- pnpm-lock.yaml
|
||||
|
||||
@@ -10481,7 +10481,7 @@ var JSONStringify = (value, replacer, space) => {
|
||||
const convertedToCustomJSON = originalStringify(
|
||||
value,
|
||||
(key, value2) => {
|
||||
const isNoise = typeof value2 === "string" && Boolean(value2.match(noiseValue));
|
||||
const isNoise = typeof value2 === "string" && noiseValue.test(value2);
|
||||
if (isNoise)
|
||||
return value2.toString() + "n";
|
||||
if (typeof value2 === "bigint")
|
||||
@@ -10501,12 +10501,29 @@ var JSONStringify = (value, replacer, space) => {
|
||||
const denoisedJSON = processedJSON.replace(noiseStringify, "$1$2$3");
|
||||
return denoisedJSON;
|
||||
};
|
||||
var isContextSourceSupported = () => JSON.parse("1", (_, __, context3) => !!context3 && context3.source === "1");
|
||||
var featureCache = /* @__PURE__ */ new Map();
|
||||
var isContextSourceSupported = () => {
|
||||
const parseFingerprint = JSON.parse.toString();
|
||||
if (featureCache.has(parseFingerprint)) {
|
||||
return featureCache.get(parseFingerprint);
|
||||
}
|
||||
try {
|
||||
const result = JSON.parse(
|
||||
"1",
|
||||
(_, __, context3) => !!context3?.source && context3.source === "1"
|
||||
);
|
||||
featureCache.set(parseFingerprint, result);
|
||||
return result;
|
||||
} catch {
|
||||
featureCache.set(parseFingerprint, false);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
var convertMarkedBigIntsReviver = (key, value, context3, userReviver) => {
|
||||
const isCustomFormatBigInt = typeof value === "string" && value.match(customFormat);
|
||||
const isCustomFormatBigInt = typeof value === "string" && customFormat.test(value);
|
||||
if (isCustomFormatBigInt)
|
||||
return BigInt(value.slice(0, -1));
|
||||
const isNoiseValue = typeof value === "string" && value.match(noiseValue);
|
||||
const isNoiseValue = typeof value === "string" && noiseValue.test(value);
|
||||
if (isNoiseValue)
|
||||
return value.slice(0, -1);
|
||||
if (typeof userReviver !== "function")
|
||||
@@ -10538,7 +10555,7 @@ var JSONParse = (text, reviver) => {
|
||||
stringsOrLargeNumbers,
|
||||
(text2, digits, fractional, exponential) => {
|
||||
const isString = text2[0] === '"';
|
||||
const isNoise = isString && Boolean(text2.match(noiseValueWithQuotes));
|
||||
const isNoise = isString && noiseValueWithQuotes.test(text2);
|
||||
if (isNoise)
|
||||
return text2.substring(0, text2.length - 1) + 'n"';
|
||||
const isFractionalOrExponential = fractional || exponential;
|
||||
@@ -22961,6 +22978,7 @@ var require_stringify = __commonJS2({
|
||||
nullStr: "null",
|
||||
simpleKeys: false,
|
||||
singleQuote: null,
|
||||
trailingComma: false,
|
||||
trueStr: "true",
|
||||
verifyAliasOrder: true
|
||||
}, doc.schema.toStringOptions, options);
|
||||
@@ -23466,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;
|
||||
}
|
||||
@@ -26401,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")) {
|
||||
@@ -29176,7 +29206,7 @@ var JSONStringify2 = (value, replacer, space) => {
|
||||
const convertedToCustomJSON = originalStringify2(
|
||||
value,
|
||||
(key, value2) => {
|
||||
const isNoise = typeof value2 === "string" && Boolean(value2.match(noiseValue2));
|
||||
const isNoise = typeof value2 === "string" && noiseValue2.test(value2);
|
||||
if (isNoise)
|
||||
return value2.toString() + "n";
|
||||
if (typeof value2 === "bigint")
|
||||
@@ -29196,12 +29226,29 @@ var JSONStringify2 = (value, replacer, space) => {
|
||||
const denoisedJSON = processedJSON.replace(noiseStringify2, "$1$2$3");
|
||||
return denoisedJSON;
|
||||
};
|
||||
var isContextSourceSupported2 = () => JSON.parse("1", (_, __, context3) => !!context3 && context3.source === "1");
|
||||
var featureCache2 = /* @__PURE__ */ new Map();
|
||||
var isContextSourceSupported2 = () => {
|
||||
const parseFingerprint = JSON.parse.toString();
|
||||
if (featureCache2.has(parseFingerprint)) {
|
||||
return featureCache2.get(parseFingerprint);
|
||||
}
|
||||
try {
|
||||
const result = JSON.parse(
|
||||
"1",
|
||||
(_, __, context3) => !!context3?.source && context3.source === "1"
|
||||
);
|
||||
featureCache2.set(parseFingerprint, result);
|
||||
return result;
|
||||
} catch {
|
||||
featureCache2.set(parseFingerprint, false);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
var convertMarkedBigIntsReviver2 = (key, value, context3, userReviver) => {
|
||||
const isCustomFormatBigInt = typeof value === "string" && value.match(customFormat2);
|
||||
const isCustomFormatBigInt = typeof value === "string" && customFormat2.test(value);
|
||||
if (isCustomFormatBigInt)
|
||||
return BigInt(value.slice(0, -1));
|
||||
const isNoiseValue = typeof value === "string" && value.match(noiseValue2);
|
||||
const isNoiseValue = typeof value === "string" && noiseValue2.test(value);
|
||||
if (isNoiseValue)
|
||||
return value.slice(0, -1);
|
||||
if (typeof userReviver !== "function")
|
||||
@@ -29233,7 +29280,7 @@ var JSONParse2 = (text, reviver) => {
|
||||
stringsOrLargeNumbers2,
|
||||
(text2, digits, fractional, exponential) => {
|
||||
const isString = text2[0] === '"';
|
||||
const isNoise = isString && Boolean(text2.match(noiseValueWithQuotes2));
|
||||
const isNoise = isString && noiseValueWithQuotes2.test(text2);
|
||||
if (isNoise)
|
||||
return text2.substring(0, text2.length - 1) + 'n"';
|
||||
const isFractionalOrExponential = fractional || exponential;
|
||||
@@ -33536,7 +33583,7 @@ tmp/lib/tmp.js:
|
||||
*)
|
||||
*)
|
||||
|
||||
@angular/ng-dev/bundles/chunk-34WCNHIG.mjs:
|
||||
@angular/ng-dev/bundles/chunk-U2HAQOXA.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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Saucelabs Variables
|
||||
uses: angular/dev-infra/github-actions/saucelabs@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/saucelabs@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- 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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- 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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
with:
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
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}}
|
||||
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- 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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
with:
|
||||
disable-package-manager-cache: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
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@bc22e01685c56e89e7813fd8e26f33dc47f87e15 # v7.1.5
|
||||
uses: cypress-io/github-action@4c06c48f3ffea349b7189aa06dfcda47a9fa7b92 # v7.1.8
|
||||
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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- 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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build adev
|
||||
|
||||
@@ -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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- uses: angular/dev-infra/github-actions/labeling/pull-request@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- uses: angular/dev-infra/github-actions/labeling/issue@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Saucelabs Variables
|
||||
uses: angular/dev-infra/github-actions/saucelabs@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/saucelabs@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- 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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- uses: angular/dev-infra/github-actions/unified-status-check@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- 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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- 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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- 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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/linting/licenses@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
with:
|
||||
disable-package-manager-cache: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- 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@bc22e01685c56e89e7813fd8e26f33dc47f87e15 # v7.1.5
|
||||
uses: cypress-io/github-action@4c06c48f3ffea349b7189aa06dfcda47a9fa7b92 # v7.1.8
|
||||
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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- 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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- 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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- 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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- 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@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@616a50d0b747031b7ea052733adf3771fa6cace9
|
||||
- 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@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||
uses: github/codeql-action/upload-sarif@b8bb9f28b8d3f992092362369c57161b755dea45 # v4.35.0
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
+2
-2
@@ -434,7 +434,7 @@ groups:
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- marktechson
|
||||
- MarkTechson
|
||||
- kirjs
|
||||
- ~JeanMeche
|
||||
- ~dgp1130
|
||||
@@ -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,93 @@
|
||||
<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
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [b4ab6ba2e8](https://github.com/angular/angular/commit/b4ab6ba2e84a18309b0bb5dd68311ff1776b1cb4) | fix | avoid redundant image fetch on destroy with auto sizes |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [880a57d4b3](https://github.com/angular/angular/commit/880a57d4b34af5aa27cd5bee11fa218ade6444bb) | fix | prevent shimCssText from adding extra blank lines per CSS comment |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [ad0156e056](https://github.com/angular/angular/commit/ad0156e056e60ffebfeb804fda70dce88d9475a8) | fix | fixes a regression with animate.leave and reordering |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [73d6b01b47](https://github.com/angular/angular/commit/73d6b01b47bb6762d182f1cd891f8ad4d7f688e1) | fix | inject migration not work in multi-project workspace with option path |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.5"></a>
|
||||
# 21.2.5 (2026-03-18)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [334ae10168](https://github.com/angular/angular/commit/334ae10168fdad15cd1390180e2994b4eb65349b) | fix | ensure generated code compiles |
|
||||
| [23ea431c4e](https://github.com/angular/angular/commit/23ea431c4ec45cbb4a7db9839969e7cb23b07f58) | fix | parse named HTML entities containing digits |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [26c43d14ba](https://github.com/angular/angular/commit/26c43d14baad1a6b3629a77825e702a97a4f8482) | fix | escape template literal in TCB |
|
||||
| [67e0ba7e03](https://github.com/angular/angular/commit/67e0ba7e03bb940639f0eafb3af45015e9727eac) | fix | generic types not filled out correctly in type check block |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1890c3008b](https://github.com/angular/angular/commit/1890c3008bbb41b7143b7ede09bed1f7704744fb) | fix | clean up dehydrated views during HMR component replacement |
|
||||
| [bf948be4c2](https://github.com/angular/angular/commit/bf948be4c2c88c604e428cba35e3b9e532bfe5b0) | fix | run linked signal equality check without reactive consumer |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [076d41c3f6](https://github.com/angular/angular/commit/076d41c3f6496eb6c6f84b54e2d2ca85c1b35e64) | fix | prevent trailing comma syntax errors after removing NgStyle |
|
||||
### service-worker
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e19150d2b5](https://github.com/angular/angular/commit/e19150d2b596e87c69bee61f478c3e9c7cbc8f67) | fix | preserve redirect policy on reconstructed asset requests |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.4"></a>
|
||||
# 21.2.4 (2026-03-12)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [ed2d324f9c](https://github.com/angular/angular/commit/ed2d324f9cc12aab6cfa0569ef10b73243a62c65) | fix | disallow translations of iframe src |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [abbd8797bb](https://github.com/angular/angular/commit/abbd8797bbd3ae53a10033c39bd895b5b85a4fae) | fix | reverts "feat(core): add support for nested animations" |
|
||||
| [d1dcd16c5b](https://github.com/angular/angular/commit/d1dcd16c5b40291aa3fa2dc84d22842cd657b201) | fix | sanitize translated form attributes |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.3"></a>
|
||||
# 21.2.3 (2026-03-11)
|
||||
### core
|
||||
|
||||
+19
-19
@@ -6,40 +6,40 @@ 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.5")
|
||||
bazel_dep(name = "aspect_rules_js", version = "2.9.2")
|
||||
bazel_dep(name = "aspect_rules_esbuild", version = "0.25.0")
|
||||
bazel_dep(name = "aspect_rules_ts", version = "3.8.7")
|
||||
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 = "aspect_bazel_lib", version = "2.22.5")
|
||||
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 = "rules_angular")
|
||||
git_override(
|
||||
module_name = "rules_angular",
|
||||
commit = "d746c4f75e42cffe389d1ab077f4639be2bc78d1",
|
||||
commit = "32ce54318d9ec5d84269d4acecbc39944cd8b5e7",
|
||||
remote = "https://github.com/angular/rules_angular.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "devinfra")
|
||||
git_override(
|
||||
module_name = "devinfra",
|
||||
commit = "e4dcda13e469f50a6bd92667e2ded8b98173bf2c",
|
||||
commit = "616a50d0b747031b7ea052733adf3771fa6cace9",
|
||||
remote = "https://github.com/angular/dev-infra.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_sass")
|
||||
git_override(
|
||||
module_name = "rules_sass",
|
||||
commit = "1184a80751a21af8348f308abc5b38a41f26850e",
|
||||
commit = "13918bec49cd183a591e3781d1d08044b4aa9f61",
|
||||
remote = "https://github.com/angular/rules_sass.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_browsers")
|
||||
git_override(
|
||||
module_name = "rules_browsers",
|
||||
commit = "8231142fc5516323b040883bf774ba8362645387",
|
||||
commit = "afdc95c1ce8ed9ff0cb94f829b7fc988c43dd783",
|
||||
remote = "https://github.com/angular/rules_browsers.git",
|
||||
)
|
||||
|
||||
@@ -49,15 +49,15 @@ use_repo(yq, "yq_toolchains")
|
||||
node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
|
||||
node.toolchain(
|
||||
node_repositories = {
|
||||
"22.22.1-darwin_arm64": ("node-v22.22.1-darwin-arm64.tar.gz", "node-v22.22.1-darwin-arm64", "679ad4966339e4ef4900f57996714864e4211b898825bb840c3086c419fbcef2"),
|
||||
"22.22.1-darwin_amd64": ("node-v22.22.1-darwin-x64.tar.gz", "node-v22.22.1-darwin-x64", "07b13722d558790fca20bb1ecf61bde24b7a4863111f7be77fc57251a407359a"),
|
||||
"22.22.1-linux_arm64": ("node-v22.22.1-linux-arm64.tar.xz", "node-v22.22.1-linux-arm64", "0f3550d58d45e5d3cf7103d9e3f69937f09fe82fb5dd474c66a5d816fa58c9ee"),
|
||||
"22.22.1-linux_ppc64le": ("node-v22.22.1-linux-ppc64le.tar.xz", "node-v22.22.1-linux-ppc64le", "a65a44cf0224505f052b90357b763dbc1ea9148f4f5f2284f0596cf2000f819b"),
|
||||
"22.22.1-linux_s390x": ("node-v22.22.1-linux-s390x.tar.xz", "node-v22.22.1-linux-s390x", "e08b3a73d0bd840e008f589e4be4a2ef3d4a0c59015f4f20a04ed7fc968042a2"),
|
||||
"22.22.1-linux_amd64": ("node-v22.22.1-linux-x64.tar.xz", "node-v22.22.1-linux-x64", "9a6bc82f9b491279147219f6a18add1e18424dce90d41d2a5fcd69d4924ba3aa"),
|
||||
"22.22.1-windows_amd64": ("node-v22.22.1-win-x64.zip", "node-v22.22.1-win-x64", "877cb93829e14fffbbc7903e7d8037336c9a79f3ea43c5d0b8c2379b79da56de"),
|
||||
"22.22.2-darwin_arm64": ("node-v22.22.2-darwin-arm64.tar.gz", "node-v22.22.2-darwin-arm64", "db4b275b83736df67533529a18cc55de2549a8329ace6c7bcc68f8d22d3c9000"),
|
||||
"22.22.2-darwin_amd64": ("node-v22.22.2-darwin-x64.tar.gz", "node-v22.22.2-darwin-x64", "12a6abb9c2902cf48a21120da13f87fde1ed1b71a13330712949e8db818708ba"),
|
||||
"22.22.2-linux_arm64": ("node-v22.22.2-linux-arm64.tar.xz", "node-v22.22.2-linux-arm64", "e9e1930fd321a470e29bb68f30318bf58e3ecb4acb4f1533fb19c58328a091fe"),
|
||||
"22.22.2-linux_ppc64le": ("node-v22.22.2-linux-ppc64le.tar.xz", "node-v22.22.2-linux-ppc64le", "14045b5a5030d35ca0030fb7e870bd11a651eb9b57323ebc0021e8d78ac6bac9"),
|
||||
"22.22.2-linux_s390x": ("node-v22.22.2-linux-s390x.tar.xz", "node-v22.22.2-linux-s390x", "9e4a07c291b8949289c6ea8ee61b1d14666a4810feae776a8d1eb1f57e03a2fb"),
|
||||
"22.22.2-linux_amd64": ("node-v22.22.2-linux-x64.tar.xz", "node-v22.22.2-linux-x64", "88fd1ce767091fd8d4a99fdb2356e98c819f93f3b1f8663853a2dee9b438068a"),
|
||||
"22.22.2-windows_amd64": ("node-v22.22.2-win-x64.zip", "node-v22.22.2-win-x64", "7c93e9d92bf68c07182b471aa187e35ee6cd08ef0f24ab060dfff605fcc1c57c"),
|
||||
},
|
||||
node_version = "22.22.1",
|
||||
node_version = "22.22.2",
|
||||
)
|
||||
use_repo(node, "nodejs_toolchains")
|
||||
use_repo(node, "nodejs_darwin_amd64")
|
||||
@@ -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.0",
|
||||
pnpm_version_integrity = "sha512-myY0uz/tVgHDNjPy2SWT9QYnCjljuMUdKy1qgo2mFc5One6++WFMzrvBOsjTwPnJzM61g8achXhDb6R327INcA==",
|
||||
pnpm_version = "10.33.0",
|
||||
pnpm_version_integrity = "sha512-EFaLtKavtYyes2MNqQzJUWQXq+vT+rvmc58K55VyjaFJHp21pUTHatjrdXD1xLs9bGN7LLQb/c20f6gjyGSTGQ==",
|
||||
)
|
||||
use_repo(pnpm, "pnpm")
|
||||
|
||||
@@ -117,7 +117,7 @@ use_repo(npm, "npm")
|
||||
rules_ts_ext = use_extension("@aspect_rules_ts//ts:extensions.bzl", "ext")
|
||||
rules_ts_ext.deps(
|
||||
name = "angular_npm_typescript",
|
||||
# Obtained by: curl --silent https://registry.npmjs.org/typescript/5.9.3 | jq -r '.dist.integrity'
|
||||
# Obtained by: curl --silent https://registry.npmjs.org/typescript/6.0.1-rc | jq -r '.dist.integrity'
|
||||
ts_integrity = "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
ts_version = "5.9.3",
|
||||
)
|
||||
|
||||
Generated
+298
-445
File diff suppressed because it is too large
Load Diff
+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.0",
|
||||
"@angular/build": "21.2.0",
|
||||
"@angular/cdk": "21.2.0",
|
||||
"@angular/cli": "21.2.0",
|
||||
"@angular/aria": "21.2.4",
|
||||
"@angular/build": "21.2.6",
|
||||
"@angular/cdk": "21.2.4",
|
||||
"@angular/cli": "21.2.6",
|
||||
"@angular/common": "workspace:*",
|
||||
"@angular/compiler": "workspace:*",
|
||||
"@angular/compiler-cli": "workspace:*",
|
||||
"@angular/core": "workspace:*",
|
||||
"@angular/docs": "workspace:*",
|
||||
"@angular/forms": "workspace:*",
|
||||
"@angular/material": "21.2.0",
|
||||
"@angular/material": "21.2.4",
|
||||
"@angular/platform-browser": "workspace:*",
|
||||
"@angular/platform-server": "workspace:*",
|
||||
"@angular/router": "workspace:*",
|
||||
"@angular/ssr": "21.2.0",
|
||||
"@angular/ssr": "21.2.6",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.1",
|
||||
"@codemirror/lang-angular": "0.1.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
load("@aspect_bazel_lib//lib:copy_to_bin.bzl", _copy_to_bin = "copy_to_bin")
|
||||
load("@aspect_bazel_lib//lib:copy_to_directory.bzl", _copy_to_directory = "copy_to_directory")
|
||||
load("@aspect_rules_esbuild//esbuild:defs.bzl", _esbuild = "esbuild")
|
||||
load("@bazel_lib//lib:copy_to_bin.bzl", _copy_to_bin = "copy_to_bin")
|
||||
load("@bazel_lib//lib:copy_to_directory.bzl", _copy_to_directory = "copy_to_directory")
|
||||
load("@devinfra//bazel/private:path_relative_to_label.bzl", _path_relative_to_label = "path_relative_to_label")
|
||||
load(
|
||||
"//tools:defaults.bzl",
|
||||
|
||||
@@ -43,15 +43,6 @@ describe('ClickOutside', () => {
|
||||
|
||||
expect(clickedOutsideSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not docsClickOutside be emitted when user click inside `content` element', () => {
|
||||
const clickedOutsideSpy = spyOn(component, 'clickedOutside');
|
||||
const button = fixture.debugElement.query(By.css('button[id="ignoreThisButton"]'));
|
||||
|
||||
button.nativeElement.click();
|
||||
|
||||
expect(clickedOutsideSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -27,14 +27,14 @@ export function CliCommandReference(entry: CliCommandRenderable) {
|
||||
<div className={'shiki line cli'}>
|
||||
ng {commandName(entry, command)}
|
||||
{entry.argumentsLabel && (
|
||||
<button member-id={'Arguments'} className="shiki-ln-line-argument">
|
||||
<span member-id={'Arguments'} className="shiki-ln-line-argument">
|
||||
{entry.argumentsLabel}
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{entry.optionsLabel && (
|
||||
<button member-id={'Options'} className="shiki-ln-line-option">
|
||||
<span member-id={'Options'} className="shiki-ln-line-option">
|
||||
{entry.optionsLabel}
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</code>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {chromium} from 'playwright-core';
|
||||
import {Mermaid, MermaidConfig} from 'mermaid';
|
||||
import {resolve, join} from 'path';
|
||||
|
||||
const runfilesDir = process.env['RUNFILES']!;
|
||||
const runfilesDir = process.env['JS_BINARY__RUNFILES']!;
|
||||
// Declare mermarid in the context of this file so that typescript doesn't get upset when we
|
||||
// access it within the `page.evaluate` function. At runtime the context in with the method
|
||||
// is run difference than this file, but this makes typescript happy.
|
||||
|
||||
@@ -135,4 +135,29 @@ describe('markdown to html', () => {
|
||||
// We ensure that we still style the heading content
|
||||
expect(markdownDocument.querySelector('strong')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should strip all HTML tags from the aria-label attribute', () => {
|
||||
const markdownDocument = JSDOM.fragment(
|
||||
parseMarkdown(
|
||||
'## **Phase 5: Experimental Signal Forms (⚠️ WARNING: Subject to Change)**',
|
||||
rendererContext,
|
||||
),
|
||||
);
|
||||
const h2Anchor = markdownDocument.querySelector('h2 > a');
|
||||
|
||||
expect(h2Anchor!.innerHTML).toBe(
|
||||
'<strong><span class="docs-emoji">Phase 5: Experimental Signal Forms (⚠️ WARNING: Subject to Change)</span></strong>',
|
||||
);
|
||||
expect(h2Anchor!.getAttribute('aria-label')).toBe(
|
||||
'Link to Phase 5: Experimental Signal Forms (⚠️ WARNING: Subject to Change)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should escape double quotes from the aria-label attribute', () => {
|
||||
const markdownDocument = JSDOM.fragment(parseMarkdown('## "Special" heading', rendererContext));
|
||||
const h2Anchor = markdownDocument.querySelector('h2 > a');
|
||||
|
||||
expect(h2Anchor!.innerHTML).toBe('"Special" heading');
|
||||
expect(h2Anchor!.getAttribute('aria-label')).toBe('Link to "Special" heading');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ export function headingRender(
|
||||
// Replace code backticks and remove custom ID syntax from the displayed label
|
||||
let label = parsedText.replace(/`(.*?)`/g, '<code>$1</code>');
|
||||
label = label.replace(/{#\s*[\w-]+\s*}/g, '').trim();
|
||||
const normalizedLabel = label.replace(/<\/?code>/g, '');
|
||||
const normalizedLabel = label.replace(/<[^>]+>/g, '').replace(/"/g, '"');
|
||||
|
||||
return `
|
||||
<h${depth} id="${link}">
|
||||
|
||||
@@ -426,6 +426,14 @@
|
||||
.docs-ref-content {
|
||||
padding: 1rem 0;
|
||||
|
||||
.docs-ref-option-and-description {
|
||||
.docs-reference-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
border-block-start: 1px solid var(--senary-contrast);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,15 @@ $code-font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.shiki.line.cli {
|
||||
span {
|
||||
color: var(--quaternary-contrast);
|
||||
font-family: var(--inter-font);
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.docs-light-mode .shiki {
|
||||
color: var(--shiki-light);
|
||||
background-color: var(--shiki-light-bg);
|
||||
@@ -46,7 +55,6 @@ $code-font-size: 0.875rem;
|
||||
/* Optional, if you also want font styles */
|
||||
font-style: var(--shiki-light-font-style);
|
||||
font-weight: var(--shiki-light-font-weight);
|
||||
text-decoration: var(--shiki-light-text-decoration);
|
||||
}
|
||||
|
||||
.shiki-ln-line-highlighted,
|
||||
|
||||
@@ -41,3 +41,6 @@ export const setCookieConsent = (state: 'denied' | 'granted'): void => {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// This fixes the RollupError: Exported variable "global" is not defined.
|
||||
export {};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
border-block-end: 1px solid var(--senary-contrast);
|
||||
padding-block-end: 2rem;
|
||||
margin-block-end: 1rem;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
.adev-reference-list-type-filter-label {
|
||||
margin-block: 2.5rem 1rem;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="page docs-viewer">
|
||||
<main class="page docs-viewer">
|
||||
<h1 class="page-header" tabindex="-1">Update Guide</h1>
|
||||
<div class="wizard">
|
||||
<div>
|
||||
@@ -205,4 +205,4 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -875,7 +875,6 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
},
|
||||
{
|
||||
label: 'Build with AI',
|
||||
status: 'new',
|
||||
children: [
|
||||
{
|
||||
label: 'Get Started',
|
||||
@@ -888,9 +887,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 +902,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',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -128,13 +128,8 @@ export const REDIRECT_ROUTES: Route[] = [
|
||||
redirectTo: '/tools/cli/build-system-migration#hot-module-replacement',
|
||||
},
|
||||
{
|
||||
path: 'guide',
|
||||
children: [
|
||||
{
|
||||
path: 'pipes',
|
||||
redirectTo: '/guide/templates/pipes',
|
||||
},
|
||||
],
|
||||
path: 'guide/pipes',
|
||||
redirectTo: '/guide/templates/pipes',
|
||||
},
|
||||
{
|
||||
path: 'guide/experimental/zoneless',
|
||||
|
||||
@@ -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": "a076ff7f05b678c4cdc6b85dbfe474a09f56022a"
|
||||
"sha": "0707c69d63e9962bab077dbc45770c25c8d26b7c"
|
||||
}
|
||||
@@ -44,16 +44,6 @@
|
||||
"description": "A global unique identifier for the panel.",
|
||||
"jsdocTags": []
|
||||
},
|
||||
{
|
||||
"name": "panelId",
|
||||
"type": "InputSignal<string>",
|
||||
"memberType": "property",
|
||||
"memberTags": [
|
||||
"readonly"
|
||||
],
|
||||
"description": "A local unique identifier for the panel, used to match with its trigger's `panelId`.",
|
||||
"jsdocTags": []
|
||||
},
|
||||
{
|
||||
"name": "visible",
|
||||
"type": "Signal<boolean>",
|
||||
@@ -165,7 +155,7 @@
|
||||
}
|
||||
],
|
||||
"generics": [],
|
||||
"description": "The content panel of an accordion item that is conditionally visible.\n\nThis directive is a container for the content that is shown or hidden. It requires\na `panelId` that must match the `panelId` of its corresponding `ngAccordionTrigger`.\nThe content within the panel should be provided using an `ng-template` with the\n`ngAccordionContent` directive so that the content is not rendered on the page until the trigger\nis expanded. It applies `role=\"region\"` for accessibility and uses the `inert` attribute to hide\nits content from assistive technologies when not visible.\n\n```html\n<div ngAccordionPanel panelId=\"unique-id-1\">\n <ng-template ngAccordionContent>\n <p>This content is lazily rendered and will be shown when the panel is expanded.</p>\n </ng-template>\n</div>\n```",
|
||||
"description": "The content panel of an accordion item that is conditionally visible.\n\nThis directive is a container for the content that is shown or hidden. It should\nexpose a template reference that will be used by the corresponding `ngAccordionTrigger`.\nThe content within the panel should be provided using an `ng-template` with the\n`ngAccordionContent` directive so that the content is not rendered on the page until the trigger\nis expanded. It applies `role=\"region\"` for accessibility and uses the `inert` attribute to hide\nits content from assistive technologies when not visible.\n\n```html\n<div ngAccordionPanel #panel=\"ngAccordionPanel\">\n <ng-template ngAccordionContent>\n <p>This content is lazily rendered and will be shown when the panel is expanded.</p>\n </ng-template>\n</div>\n```",
|
||||
"jsdocTags": [
|
||||
{
|
||||
"name": "developerPreview",
|
||||
@@ -176,12 +166,12 @@
|
||||
"comment": "[Accordion](guide/aria/accordion)"
|
||||
}
|
||||
],
|
||||
"rawComment": "/**\n * The content panel of an accordion item that is conditionally visible.\n *\n * This directive is a container for the content that is shown or hidden. It requires\n * a `panelId` that must match the `panelId` of its corresponding `ngAccordionTrigger`.\n * The content within the panel should be provided using an `ng-template` with the\n * `ngAccordionContent` directive so that the content is not rendered on the page until the trigger\n * is expanded. It applies `role=\"region\"` for accessibility and uses the `inert` attribute to hide\n * its content from assistive technologies when not visible.\n *\n * ```html\n * <div ngAccordionPanel panelId=\"unique-id-1\">\n * <ng-template ngAccordionContent>\n * <p>This content is lazily rendered and will be shown when the panel is expanded.</p>\n * </ng-template>\n * </div>\n * ```\n *\n * @developerPreview 21.0\n * @see [Accordion](guide/aria/accordion)\n */",
|
||||
"rawComment": "/**\n * The content panel of an accordion item that is conditionally visible.\n *\n * This directive is a container for the content that is shown or hidden. It should\n * expose a template reference that will be used by the corresponding `ngAccordionTrigger`.\n * The content within the panel should be provided using an `ng-template` with the\n * `ngAccordionContent` directive so that the content is not rendered on the page until the trigger\n * is expanded. It applies `role=\"region\"` for accessibility and uses the `inert` attribute to hide\n * its content from assistive technologies when not visible.\n *\n * ```html\n * <div ngAccordionPanel #panel=\"ngAccordionPanel\">\n * <ng-template ngAccordionContent>\n * <p>This content is lazily rendered and will be shown when the panel is expanded.</p>\n * </ng-template>\n * </div>\n * ```\n *\n * @developerPreview 21.0\n * @see [Accordion](guide/aria/accordion)\n */",
|
||||
"implements": [],
|
||||
"source": {
|
||||
"filePath": "/src/aria/accordion/accordion-panel.ts",
|
||||
"startLine": 42,
|
||||
"endLine": 103
|
||||
"startLine": 34,
|
||||
"endLine": 88
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -199,6 +189,19 @@
|
||||
"description": "A reference to the trigger element.",
|
||||
"jsdocTags": []
|
||||
},
|
||||
{
|
||||
"name": "panel",
|
||||
"type": "InputSignal<AccordionPanel>",
|
||||
"memberType": "property",
|
||||
"memberTags": [
|
||||
"readonly",
|
||||
"input"
|
||||
],
|
||||
"description": "The associated AccordionPanel.",
|
||||
"jsdocTags": [],
|
||||
"inputAlias": "panel",
|
||||
"isRequiredInput": true
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"type": "InputSignal<any>",
|
||||
@@ -207,23 +210,20 @@
|
||||
"readonly",
|
||||
"input"
|
||||
],
|
||||
"description": "A unique identifier for the widget.",
|
||||
"description": "The unique identifier for the trigger.",
|
||||
"jsdocTags": [],
|
||||
"inputAlias": "id",
|
||||
"isRequiredInput": false
|
||||
},
|
||||
{
|
||||
"name": "panelId",
|
||||
"type": "InputSignal<string>",
|
||||
"type": "Signal<any>",
|
||||
"memberType": "property",
|
||||
"memberTags": [
|
||||
"readonly",
|
||||
"input"
|
||||
"readonly"
|
||||
],
|
||||
"description": "A local unique identifier for the trigger, used to match with its panel's `panelId`.",
|
||||
"jsdocTags": [],
|
||||
"inputAlias": "panelId",
|
||||
"isRequiredInput": true
|
||||
"description": "The unique identifier for the correspondingtrigger panel.",
|
||||
"jsdocTags": []
|
||||
},
|
||||
{
|
||||
"name": "disabled",
|
||||
@@ -263,6 +263,39 @@
|
||||
"description": "Whether the trigger is active.",
|
||||
"jsdocTags": []
|
||||
},
|
||||
{
|
||||
"name": "ngOnInit",
|
||||
"signatures": [
|
||||
{
|
||||
"name": "ngOnInit",
|
||||
"entryType": "function",
|
||||
"description": "",
|
||||
"generics": [],
|
||||
"isNewType": false,
|
||||
"jsdocTags": [],
|
||||
"params": [],
|
||||
"rawComment": "",
|
||||
"returnType": "void"
|
||||
}
|
||||
],
|
||||
"implementation": {
|
||||
"params": [],
|
||||
"isNewType": false,
|
||||
"returnType": "void",
|
||||
"generics": [],
|
||||
"name": "ngOnInit",
|
||||
"description": "",
|
||||
"entryType": "function",
|
||||
"jsdocTags": [],
|
||||
"rawComment": ""
|
||||
},
|
||||
"entryType": "function",
|
||||
"description": "",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "",
|
||||
"memberType": "method",
|
||||
"memberTags": []
|
||||
},
|
||||
{
|
||||
"name": "expand",
|
||||
"signatures": [
|
||||
@@ -364,7 +397,7 @@
|
||||
}
|
||||
],
|
||||
"generics": [],
|
||||
"description": "The trigger that toggles the visibility of its associated `ngAccordionPanel`.\n\nThis directive requires a `panelId` that must match the `panelId` of the `ngAccordionPanel` it\ncontrols. When clicked, it will expand or collapse the panel. It also handles keyboard\ninteractions for navigation within the `ngAccordionGroup`. It applies `role=\"button\"` and manages\n`aria-expanded`, `aria-controls`, and `aria-disabled` attributes for accessibility.\nThe `disabled` input can be used to disable the trigger.\n\n```html\n<button ngAccordionTrigger panelId=\"unique-id-1\">\n Accordion Trigger Text\n</button>\n```",
|
||||
"description": "The trigger that toggles the visibility of its associated `ngAccordionPanel`.\n\nThis directive requires the `panel` input be set to the template reference of the `ngAccordionPanel`\nit controls. When clicked, it will expand or collapse the panel. It also handles keyboard\ninteractions for navigation within the `ngAccordionGroup`. It applies `role=\"button\"` and manages\n`aria-expanded`, `aria-controls`, and `aria-disabled` attributes for accessibility.\nThe `disabled` input can be used to disable the trigger.\n\n```html\n<button ngAccordionTrigger [panel]=\"panel\">\n Accordion Trigger Text\n</button>\n```",
|
||||
"jsdocTags": [
|
||||
{
|
||||
"name": "developerPreview",
|
||||
@@ -375,8 +408,10 @@
|
||||
"comment": "[Accordion](guide/aria/accordion)"
|
||||
}
|
||||
],
|
||||
"rawComment": "/**\n * The trigger that toggles the visibility of its associated `ngAccordionPanel`.\n *\n * This directive requires a `panelId` that must match the `panelId` of the `ngAccordionPanel` it\n * controls. When clicked, it will expand or collapse the panel. It also handles keyboard\n * interactions for navigation within the `ngAccordionGroup`. It applies `role=\"button\"` and manages\n * `aria-expanded`, `aria-controls`, and `aria-disabled` attributes for accessibility.\n * The `disabled` input can be used to disable the trigger.\n *\n * ```html\n * <button ngAccordionTrigger panelId=\"unique-id-1\">\n * Accordion Trigger Text\n * </button>\n * ```\n *\n * @developerPreview 21.0\n * @see [Accordion](guide/aria/accordion)\n */",
|
||||
"implements": [],
|
||||
"rawComment": "/**\n * The trigger that toggles the visibility of its associated `ngAccordionPanel`.\n *\n * This directive requires the `panel` input be set to the template reference of the `ngAccordionPanel`\n * it controls. When clicked, it will expand or collapse the panel. It also handles keyboard\n * interactions for navigation within the `ngAccordionGroup`. It applies `role=\"button\"` and manages\n * `aria-expanded`, `aria-controls`, and `aria-disabled` attributes for accessibility.\n * The `disabled` input can be used to disable the trigger.\n *\n * ```html\n * <button ngAccordionTrigger [panel]=\"panel\">\n * Accordion Trigger Text\n * </button>\n * ```\n *\n * @developerPreview 21.0\n * @see [Accordion](guide/aria/accordion)\n */",
|
||||
"implements": [
|
||||
"OnInit"
|
||||
],
|
||||
"isStandalone": true,
|
||||
"selector": "[ngAccordionTrigger]",
|
||||
"exportAs": [
|
||||
@@ -388,7 +423,7 @@
|
||||
"source": {
|
||||
"filePath": "/src/aria/accordion/accordion-trigger.ts",
|
||||
"startLine": 42,
|
||||
"endLine": 107
|
||||
"endLine": 112
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -536,7 +571,7 @@
|
||||
}
|
||||
],
|
||||
"generics": [],
|
||||
"description": "A container for a group of accordion items. It manages the overall state and\ninteractions of the accordion, such as keyboard navigation and expansion mode.\n\nThe `ngAccordionGroup` serves as the root of a group of accordion triggers and panels,\ncoordinating the behavior of the `ngAccordionTrigger` and `ngAccordionPanel` elements within it.\nIt supports both single and multiple expansion modes.\n\n```html\n<div ngAccordionGroup [multiExpandable]=\"true\" [(expandedPanels)]=\"expandedItems\">\n <div class=\"accordion-item\">\n <h3>\n <button ngAccordionTrigger panelId=\"item-1\">Item 1</button>\n </h3>\n <div ngAccordionPanel panelId=\"item-1\">\n <ng-template ngAccordionContent>\n <p>Content for Item 1.</p>\n </ng-template>\n </div>\n </div>\n <div class=\"accordion-item\">\n <h3>\n <button ngAccordionTrigger panelId=\"item-2\">Item 2</button>\n </h3>\n <div ngAccordionPanel panelId=\"item-2\">\n <ng-template ngAccordionContent>\n <p>Content for Item 2.</p>\n </ng-template>\n </div>\n </div>\n</div>\n```",
|
||||
"description": "A container for a group of accordion items. It manages the overall state and\ninteractions of the accordion, such as keyboard navigation and expansion mode.\n\nThe `ngAccordionGroup` serves as the root of a group of accordion triggers and panels,\ncoordinating the behavior of the `ngAccordionTrigger` and `ngAccordionPanel` elements within it.\nIt supports both single and multiple expansion modes.\n\n```html\n<div ngAccordionGroup [multiExpandable]=\"true\">\n <div class=\"accordion-item\">\n <h3>\n <button ngAccordionTrigger [panel]=\"panel1\">Item 1</button>\n </h3>\n <div ngAccordionPanel #panel1=\"ngAccordionPanel\">\n <ng-template ngAccordionContent>\n <p>Content for Item 1.</p>\n </ng-template>\n </div>\n </div>\n <div class=\"accordion-item\">\n <h3>\n <button ngAccordionTrigger [panel]=\"panel2\">Item 2</button>\n </h3>\n <div ngAccordionPanel #panel2=\"ngAccordionPanel\">\n <ng-template ngAccordionContent>\n <p>Content for Item 2.</p>\n </ng-template>\n </div>\n </div>\n</div>\n```",
|
||||
"jsdocTags": [
|
||||
{
|
||||
"name": "developerPreview",
|
||||
@@ -547,7 +582,7 @@
|
||||
"comment": "[Accordion](guide/aria/accordion)"
|
||||
}
|
||||
],
|
||||
"rawComment": "/**\n * A container for a group of accordion items. It manages the overall state and\n * interactions of the accordion, such as keyboard navigation and expansion mode.\n *\n * The `ngAccordionGroup` serves as the root of a group of accordion triggers and panels,\n * coordinating the behavior of the `ngAccordionTrigger` and `ngAccordionPanel` elements within it.\n * It supports both single and multiple expansion modes.\n *\n * ```html\n * <div ngAccordionGroup [multiExpandable]=\"true\" [(expandedPanels)]=\"expandedItems\">\n * <div class=\"accordion-item\">\n * <h3>\n * <button ngAccordionTrigger panelId=\"item-1\">Item 1</button>\n * </h3>\n * <div ngAccordionPanel panelId=\"item-1\">\n * <ng-template ngAccordionContent>\n * <p>Content for Item 1.</p>\n * </ng-template>\n * </div>\n * </div>\n * <div class=\"accordion-item\">\n * <h3>\n * <button ngAccordionTrigger panelId=\"item-2\">Item 2</button>\n * </h3>\n * <div ngAccordionPanel panelId=\"item-2\">\n * <ng-template ngAccordionContent>\n * <p>Content for Item 2.</p>\n * </ng-template>\n * </div>\n * </div>\n * </div>\n * ```\n *\n * @developerPreview 21.0\n * @see [Accordion](guide/aria/accordion)\n */",
|
||||
"rawComment": "/**\n * A container for a group of accordion items. It manages the overall state and\n * interactions of the accordion, such as keyboard navigation and expansion mode.\n *\n * The `ngAccordionGroup` serves as the root of a group of accordion triggers and panels,\n * coordinating the behavior of the `ngAccordionTrigger` and `ngAccordionPanel` elements within it.\n * It supports both single and multiple expansion modes.\n *\n * ```html\n * <div ngAccordionGroup [multiExpandable]=\"true\">\n * <div class=\"accordion-item\">\n * <h3>\n * <button ngAccordionTrigger [panel]=\"panel1\">Item 1</button>\n * </h3>\n * <div ngAccordionPanel #panel1=\"ngAccordionPanel\">\n * <ng-template ngAccordionContent>\n * <p>Content for Item 1.</p>\n * </ng-template>\n * </div>\n * </div>\n * <div class=\"accordion-item\">\n * <h3>\n * <button ngAccordionTrigger [panel]=\"panel2\">Item 2</button>\n * </h3>\n * <div ngAccordionPanel #panel2=\"ngAccordionPanel\">\n * <ng-template ngAccordionContent>\n * <p>Content for Item 2.</p>\n * </ng-template>\n * </div>\n * </div>\n * </div>\n * ```\n *\n * @developerPreview 21.0\n * @see [Accordion](guide/aria/accordion)\n */",
|
||||
"implements": [],
|
||||
"isStandalone": true,
|
||||
"selector": "[ngAccordionGroup]",
|
||||
@@ -559,8 +594,8 @@
|
||||
],
|
||||
"source": {
|
||||
"filePath": "src/aria/accordion/accordion-group.ts",
|
||||
"startLine": 62,
|
||||
"endLine": 158
|
||||
"startLine": 60,
|
||||
"endLine": 120
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -569,28 +604,20 @@
|
||||
"Directive",
|
||||
"@angular/core"
|
||||
],
|
||||
[
|
||||
"input",
|
||||
"@angular/core"
|
||||
],
|
||||
[
|
||||
"inject",
|
||||
"@angular/core"
|
||||
],
|
||||
[
|
||||
"afterRenderEffect",
|
||||
"@angular/core"
|
||||
],
|
||||
[
|
||||
"signal",
|
||||
"@angular/core"
|
||||
],
|
||||
[
|
||||
"computed",
|
||||
"@angular/core"
|
||||
],
|
||||
[
|
||||
"WritableSignal",
|
||||
"inject",
|
||||
"@angular/core"
|
||||
],
|
||||
[
|
||||
"input",
|
||||
"@angular/core"
|
||||
],
|
||||
[
|
||||
@@ -602,17 +629,25 @@
|
||||
"@angular/core"
|
||||
],
|
||||
[
|
||||
"model",
|
||||
"OnInit",
|
||||
"@angular/core"
|
||||
],
|
||||
[
|
||||
"booleanAttribute",
|
||||
"@angular/core"
|
||||
],
|
||||
[
|
||||
"model",
|
||||
"@angular/core"
|
||||
],
|
||||
[
|
||||
"contentChildren",
|
||||
"@angular/core"
|
||||
],
|
||||
[
|
||||
"signal",
|
||||
"@angular/core"
|
||||
],
|
||||
[
|
||||
"Directionality",
|
||||
"@angular/cdk/bidi"
|
||||
@@ -645,10 +680,6 @@
|
||||
"AccordionPanel.id",
|
||||
"@angular/aria/accordion"
|
||||
],
|
||||
[
|
||||
"AccordionPanel.panelId",
|
||||
"@angular/aria/accordion"
|
||||
],
|
||||
[
|
||||
"AccordionPanel.visible",
|
||||
"@angular/aria/accordion"
|
||||
@@ -673,6 +704,10 @@
|
||||
"AccordionTrigger.element",
|
||||
"@angular/aria/accordion"
|
||||
],
|
||||
[
|
||||
"AccordionTrigger.panel",
|
||||
"@angular/aria/accordion"
|
||||
],
|
||||
[
|
||||
"AccordionTrigger.id",
|
||||
"@angular/aria/accordion"
|
||||
@@ -693,6 +728,10 @@
|
||||
"AccordionTrigger.active",
|
||||
"@angular/aria/accordion"
|
||||
],
|
||||
[
|
||||
"AccordionTrigger.ngOnInit",
|
||||
"@angular/aria/accordion"
|
||||
],
|
||||
[
|
||||
"AccordionTrigger.expand",
|
||||
"@angular/aria/accordion"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"branchName": "refs/heads/21.2.x",
|
||||
"sha": "fc8b94d631a5d105a87abc2a008a6e8f76d7f3f5"
|
||||
"sha": "d0d970927ed7daa3a7072ef211d4b85c46c30eef"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"branchName": "refs/heads/21.2.x",
|
||||
"sha": "8e9555870edb3275b35d299613ef3cf9d3bfb73a"
|
||||
"sha": "a8aca91bb6d490f66b6bdc873e3e350b9a1addad"
|
||||
}
|
||||
@@ -109,7 +109,7 @@
|
||||
"aliases": [
|
||||
"s"
|
||||
],
|
||||
"description": "Include the styles for the root component directly within the `app.component.ts` file. Only CSS styles can be included inline. By default, a separate stylesheet file (e.g., `app.component.css`) is created."
|
||||
"description": "Include the styles for the root component directly within the `app.ts` file. Only CSS styles can be included inline. By default, a separate stylesheet file (e.g., `app.css`) is created."
|
||||
},
|
||||
{
|
||||
"name": "inline-template",
|
||||
@@ -117,7 +117,7 @@
|
||||
"aliases": [
|
||||
"t"
|
||||
],
|
||||
"description": "Include the HTML template for the root component directly within the `app.component.ts` file. By default, a separate template file (e.g., `app.component.html`) is created."
|
||||
"description": "Include the HTML template for the root component directly within the `app.ts` file. By default, a separate template file (e.g., `app.html`) is created."
|
||||
},
|
||||
{
|
||||
"name": "minimal",
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"aliases": [
|
||||
"s"
|
||||
],
|
||||
"description": "Include the styles for the initial application's root component directly within the `app.component.ts` file. By default, a separate stylesheet file (e.g., `app.component.css`) is created."
|
||||
"description": "Include the styles for the initial application's root component directly within the `app.ts` file. By default, a separate stylesheet file (e.g., `app.css`) is created."
|
||||
},
|
||||
{
|
||||
"name": "inline-template",
|
||||
@@ -99,7 +99,7 @@
|
||||
"aliases": [
|
||||
"t"
|
||||
],
|
||||
"description": "Include the HTML template for the initial application's root component directly within the `app.component.ts` file. By default, a separate template file (e.g., `app.component.html`) is created."
|
||||
"description": "Include the HTML template for the initial application's root component directly within the `app.ts` file. By default, a separate template file (e.g., `app.html`) is created."
|
||||
},
|
||||
{
|
||||
"name": "interactive",
|
||||
|
||||
@@ -35,8 +35,7 @@
|
||||
{
|
||||
"name": "coverage",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Enables coverage reporting for tests."
|
||||
"description": "Enables coverage reporting for tests. If not specified, the coverage configuration from a runner configuration file will be used if present. Otherwise, coverage is disabled by default."
|
||||
},
|
||||
{
|
||||
"name": "coverage-exclude",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"command": "ng update [packages..]",
|
||||
"shortDescription": "Updates your workspace and its dependencies. See https://update.angular.dev/.",
|
||||
"longDescriptionRelativePath": "@angular/cli/src/commands/update/long-description.md",
|
||||
"longDescription": "Perform a basic update to the current stable release of the core framework and CLI by running the following command.\n\n```\nng update @angular/cli @angular/core\n```\n\nTo update to the next beta or pre-release version, use the `--next` option.\n\nTo update from one major version to another, use the format\n\n```\nng update @angular/cli@^<major_version> @angular/core@^<major_version>\n```\n\nWe recommend that you always update to the latest patch version, as it contains fixes we released since the initial major release.\nFor example, use the following command to take the latest 10.x.x version and use that to update.\n\n```\nng update @angular/cli@^10 @angular/core@^10\n```\n\nFor detailed information and guidance on updating your application, see the interactive [Angular Update Guide](https://update.angular.dev/).\n",
|
||||
"longDescription": "Perform a basic update to the current stable release of the core framework and CLI by running the following command.\n\n```\nng update @angular/cli @angular/core\n```\n\nTo update to the next beta or pre-release version, use the `--next` option.\n\nTo update from one major version to another, use the format\n\n```\nng update @angular/cli@^<major_version> @angular/core@^<major_version>\n```\n\nWe recommend that you always update to the latest patch version, as it contains fixes we released since the initial major release.\nFor example, use the following command to take the latest 21.x.x version and use that to update.\n\n```\nng update @angular/cli@^21 @angular/core@^21\n```\n\nFor detailed information and guidance on updating your application, see the interactive [Angular Update Guide](/update-guide).\n",
|
||||
"aliases": [],
|
||||
"deprecated": false,
|
||||
"options": [
|
||||
|
||||
@@ -10,7 +10,7 @@ There are some niche use cases when you may want to maintain a custom build pipe
|
||||
|
||||
- You have an existing app using a different toolchain and you’d like to add Angular to it
|
||||
- You’re strongly coupled to [module federation](https://module-federation.io/) and unable to adopt bundler-agnostic [native federation](https://www.npmjs.com/package/@angular-architects/native-federation)
|
||||
- You’d like to create an short-lived experiment using your favorite build tool
|
||||
- You’d like to create a short-lived experiment using your favorite build tool
|
||||
|
||||
## What are the options?
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div ngAccordionGroup class="basic-accordion">
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q1" #trigger1="ngAccordionTrigger" [expanded]="true">
|
||||
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger" [expanded]="true">
|
||||
Which attribute tells assistive tech whether the panel is open or closed?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -9,7 +9,7 @@
|
||||
></span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q1">
|
||||
<div ngAccordionPanel #panel1="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
Use <code>aria-expanded</code> on the button element. Set it to "true" when the content
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q2" #trigger2="ngAccordionTrigger" disabled>
|
||||
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger" disabled>
|
||||
How do you link the button to the content it controls?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -29,7 +29,7 @@
|
||||
></span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q2">
|
||||
<div ngAccordionPanel #panel2="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
Use the <code>aria-controls</code> attribute on the button, and set its value to match the
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q3" #trigger3="ngAccordionTrigger">
|
||||
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger">
|
||||
What role should the heading element containing the accordion button have?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -49,7 +49,7 @@
|
||||
></span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q3">
|
||||
<div ngAccordionPanel #panel3="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
The element containing the button should typically have <code>role="heading"</code> with an
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
<div ngAccordionGroup class="material-accordion">
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q1" #trigger1="ngAccordionTrigger" [expanded]="true">
|
||||
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger" [expanded]="true">
|
||||
Which attribute tells assistive tech whether the panel is open or closed?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -11,7 +11,7 @@
|
||||
>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q1">
|
||||
<div ngAccordionPanel #panel1="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
Use <code>aria-expanded</code> on the button element. Set it to "true" when the content
|
||||
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q2" #trigger2="ngAccordionTrigger" disabled>
|
||||
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger" disabled>
|
||||
How do you link the button to the content it controls?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -33,7 +33,7 @@
|
||||
>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q2">
|
||||
<div ngAccordionPanel #panel2="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
Use the <code>aria-controls</code> attribute on the button, and set its value to match the
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q3" #trigger3="ngAccordionTrigger">
|
||||
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger">
|
||||
What role should the heading element containing the accordion button have?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -55,7 +55,7 @@
|
||||
>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q3">
|
||||
<div ngAccordionPanel #panel3="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
The element containing the button should typically have <code>role="heading"</code> with an
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
<div ngAccordionGroup class="retro-accordion">
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q1" #trigger1="ngAccordionTrigger">
|
||||
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger">
|
||||
Unlock Treasure Box
|
||||
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
|
||||
trigger1.expanded() ? 'lock_open_right' : 'lock'
|
||||
}}</span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q1">
|
||||
<div ngAccordionPanel #panel1="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<div class="treasure-box">👻</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q2" #trigger2="ngAccordionTrigger" disabled>
|
||||
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger" disabled>
|
||||
Unlock Treasure Box
|
||||
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
|
||||
trigger2.expanded() ? 'lock_open_right' : 'lock'
|
||||
}}</span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q2">
|
||||
<div ngAccordionPanel #panel2="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<div class="treasure-box">💎💎💎</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q3" #trigger3="ngAccordionTrigger" [expanded]="true">
|
||||
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger" [expanded]="true">
|
||||
Unlock Treasure Box
|
||||
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
|
||||
trigger3.expanded() ? 'lock_open_right' : 'lock'
|
||||
}}</span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q3">
|
||||
<div ngAccordionPanel #panel3="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<div class="treasure-box">🐸 ribbit...ribbit...</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div ngAccordionGroup class="basic-accordion">
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q1" #trigger1="ngAccordionTrigger" [expanded]="true">
|
||||
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger" [expanded]="true">
|
||||
Which attribute tells assistive tech whether the panel is open or closed?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -9,7 +9,7 @@
|
||||
></span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q1">
|
||||
<div ngAccordionPanel #panel1="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
Use <code>aria-expanded</code> on the button element. Set it to "true" when the content
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q2" #trigger2="ngAccordionTrigger">
|
||||
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger">
|
||||
How do you link the button to the content it controls?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -29,7 +29,7 @@
|
||||
></span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q2">
|
||||
<div ngAccordionPanel #panel2="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
Use the <code>aria-controls</code> attribute on the button, and set its value to match the
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q3" #trigger3="ngAccordionTrigger" [expanded]="true">
|
||||
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger" [expanded]="true">
|
||||
What role should the heading element containing the accordion button have?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -49,7 +49,7 @@
|
||||
></span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q3">
|
||||
<div ngAccordionPanel #panel3="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
The element containing the button should typically have <code>role="heading"</code> with an
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div ngAccordionGroup class="material-accordion">
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q1" #trigger1="ngAccordionTrigger" [expanded]="true">
|
||||
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger" [expanded]="true">
|
||||
Which attribute tells assistive tech whether the panel is open or closed?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -11,7 +11,7 @@
|
||||
>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q1">
|
||||
<div ngAccordionPanel #panel1="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
Use <code>aria-expanded</code> on the button element. Set it to "true" when the content
|
||||
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q2" #trigger2="ngAccordionTrigger">
|
||||
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger">
|
||||
How do you link the button to the content it controls?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -33,7 +33,7 @@
|
||||
>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q2">
|
||||
<div ngAccordionPanel #panel2="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
Use the <code>aria-controls</code> attribute on the button, and set its value to match the
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q3" #trigger3="ngAccordionTrigger" [expanded]="true">
|
||||
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger" [expanded]="true">
|
||||
What role should the heading element containing the accordion button have?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -55,7 +55,7 @@
|
||||
>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q3">
|
||||
<div ngAccordionPanel #panel3="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
The element containing the button should typically have <code>role="heading"</code> with an
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
<div ngAccordionGroup class="retro-accordion">
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q1" #trigger1="ngAccordionTrigger">
|
||||
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger">
|
||||
Unlock Treasure Box
|
||||
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
|
||||
trigger1.expanded() ? 'lock_open_right' : 'lock'
|
||||
}}</span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q1">
|
||||
<div ngAccordionPanel #panel1="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<div class="treasure-box">👻</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q2" #trigger2="ngAccordionTrigger">
|
||||
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger">
|
||||
Unlock Treasure Box
|
||||
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
|
||||
trigger2.expanded() ? 'lock_open_right' : 'lock'
|
||||
}}</span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q2">
|
||||
<div ngAccordionPanel #panel2="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<div class="treasure-box">💎💎💎</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q3" #trigger3="ngAccordionTrigger">
|
||||
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger">
|
||||
Unlock Treasure Box
|
||||
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
|
||||
trigger3.expanded() ? 'lock_open_right' : 'lock'
|
||||
}}</span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q3">
|
||||
<div ngAccordionPanel #panel3="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<div class="treasure-box">🐸 ribbit...ribbit...</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div ngAccordionGroup class="basic-accordion" [multiExpandable]="false">
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q1" #trigger1="ngAccordionTrigger" [expanded]="true">
|
||||
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger" [expanded]="true">
|
||||
Which attribute tells assistive tech whether the panel is open or closed?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -9,7 +9,7 @@
|
||||
></span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q1">
|
||||
<div ngAccordionPanel #panel1="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
Use <code>aria-expanded</code> on the button element. Set it to "true" when the content
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q2" #trigger2="ngAccordionTrigger">
|
||||
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger">
|
||||
How do you link the button to the content it controls?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -29,7 +29,7 @@
|
||||
></span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q2">
|
||||
<div ngAccordionPanel #panel2="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
Use the <code>aria-controls</code> attribute on the button, and set its value to match the
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q3" #trigger3="ngAccordionTrigger">
|
||||
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger">
|
||||
What role should the heading element containing the accordion button have?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -49,7 +49,7 @@
|
||||
></span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q3">
|
||||
<div ngAccordionPanel #panel3="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
The element containing the button should typically have <code>role="heading"</code> with an
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div ngAccordionGroup class="material-accordion" [multiExpandable]="false">
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q1" #trigger1="ngAccordionTrigger">
|
||||
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger">
|
||||
Which attribute tells assistive tech whether the panel is open or closed?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -11,7 +11,7 @@
|
||||
>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q1">
|
||||
<div ngAccordionPanel #panel1="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
Use <code>aria-expanded</code> on the button element. Set it to "true" when the content
|
||||
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q2" #trigger2="ngAccordionTrigger" [expanded]="true">
|
||||
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger" [expanded]="true">
|
||||
How do you link the button to the content it controls?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -33,7 +33,7 @@
|
||||
>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q2">
|
||||
<div ngAccordionPanel #panel2="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
Use the <code>aria-controls</code> attribute on the button, and set its value to match the
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q3" #trigger3="ngAccordionTrigger">
|
||||
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger">
|
||||
What role should the heading element containing the accordion button have?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -55,7 +55,7 @@
|
||||
>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q3">
|
||||
<div ngAccordionPanel #panel3="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<p>
|
||||
The element containing the button should typically have <code>role="heading"</code> with an
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
<div ngAccordionGroup class="retro-accordion" [multiExpandable]="false">
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q1" #trigger1="ngAccordionTrigger">
|
||||
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger">
|
||||
Unlock Treasure Box
|
||||
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
|
||||
trigger1.expanded() ? 'lock_open_right' : 'lock'
|
||||
}}</span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q1">
|
||||
<div ngAccordionPanel #panel1="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<div class="treasure-box">👻</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q2" #trigger2="ngAccordionTrigger">
|
||||
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger">
|
||||
Unlock Treasure Box
|
||||
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
|
||||
trigger2.expanded() ? 'lock_open_right' : 'lock'
|
||||
}}</span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q2">
|
||||
<div ngAccordionPanel #panel2="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<div class="treasure-box">💎💎💎</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<span ngAccordionTrigger panelId="q3" #trigger3="ngAccordionTrigger">
|
||||
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger">
|
||||
Unlock Treasure Box
|
||||
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
|
||||
trigger3.expanded() ? 'lock_open_right' : 'lock'
|
||||
}}</span>
|
||||
</span>
|
||||
</h3>
|
||||
<div ngAccordionPanel panelId="q3">
|
||||
<div ngAccordionPanel #panel3="ngAccordionPanel">
|
||||
<ng-template ngAccordionContent>
|
||||
<div class="treasure-box">🐸 ribbit...ribbit...</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -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 {}
|
||||
@@ -76,7 +76,7 @@ export class UserProfile {}
|
||||
|
||||
By default, Angular components are _standalone_, meaning that you can directly add them to the `imports` array of other components. Components created with an earlier version of Angular may instead specify `standalone: false` in their `@Component` decorator. For these components, you instead import the `NgModule` in which the component is defined. See the full [`NgModule` guide](guide/ngmodules/overview) for details.
|
||||
|
||||
Important: In Angular versions before 19.0.0, the `standalone` option defaults to `false`.
|
||||
IMPORTANT: In Angular versions before 19.0.0, the `standalone` option defaults to `false`.
|
||||
|
||||
### Showing components in a template
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ The second piece of syntax is a key-expression pair, `from source`. `from` is a
|
||||
|
||||
## One structural directive per element
|
||||
|
||||
You can only apply one structural directive per element when using the shorthand syntax. This is because there is only one `<ng-template>` element onto which that directive gets unwrapped. Multiple directives would require multiple nested `<ng-template>`, and it's unclear which directive should be first. `<ng-container>` can be used when to create wrapper layers when multiple structural directives need to be applied around the same physical DOM element or component, which allows the user to define the nested structure.
|
||||
You can only apply one structural directive per element when using the shorthand syntax. This is because there is only one `<ng-template>` element onto which that directive gets unwrapped. Multiple directives would require multiple nested `<ng-template>`, and it's unclear which directive should be first. `<ng-container>` can be used to create wrapper layers when multiple structural directives need to be applied around the same physical DOM element or component, which allows the user to define the nested structure.
|
||||
|
||||
## Creating a structural directive
|
||||
|
||||
|
||||
@@ -329,7 +329,7 @@ Combining `cdkDropListHasAnchor` with `cdkDropListSortingDisabled` makes it poss
|
||||
|
||||
Drag and drop supports animations for both:
|
||||
|
||||
- Sorting an draggable element inside a list
|
||||
- Sorting a draggable element inside a list
|
||||
- Moving the draggable element from the position that the user dropped it to the final position inside the list
|
||||
|
||||
To set up your animations, define a CSS transition that targets the transform property. The following classes can be used for animations:
|
||||
|
||||
@@ -339,6 +339,70 @@ With reactive forms, set the property in the `FormControl` instance.
|
||||
new FormControl('', {updateOn: 'blur'});
|
||||
```
|
||||
|
||||
## Managing validators dynamically in reactive forms
|
||||
|
||||
In complex reactive forms, you may need to add, remove, or modify validators based on user input or application state.
|
||||
Angular provides several methods on `AbstractControl` to manage validators at runtime without recreating form controls.
|
||||
|
||||
### Adding and removing validators
|
||||
|
||||
The [`addValidators`](api/forms/AbstractControl#addValidators) and [`removeValidators`](api/forms/AbstractControl#removeValidators) methods allow you to modify a control's validators after initialization.
|
||||
|
||||
```ts
|
||||
onCountryChange(country: string) {
|
||||
const postalCodeControl = this.profileForm.get('postalCode');
|
||||
|
||||
if (country === 'US') {
|
||||
// Add validators for US postal codes
|
||||
postalCodeControl.addValidators([Validators.required, Validators.pattern(/^\d{5}$/)]);
|
||||
} else {
|
||||
// Remove validators when not US
|
||||
postalCodeControl.removeValidators([Validators.required]);
|
||||
}
|
||||
|
||||
postalCodeControl.updateValueAndValidity();
|
||||
}
|
||||
```
|
||||
|
||||
### Replacing all validators
|
||||
|
||||
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) {
|
||||
const nameControl = this.profileForm.get('name');
|
||||
|
||||
if (enable) {
|
||||
// Set strict validation rules
|
||||
nameControl.setValidators([
|
||||
Validators.required,
|
||||
Validators.minLength(3),
|
||||
Validators.pattern(/^[a-zA-Z]+$/),
|
||||
]);
|
||||
} else {
|
||||
// Clear all validators
|
||||
nameControl.clearValidators();
|
||||
}
|
||||
|
||||
nameControl.updateValueAndValidity();
|
||||
}
|
||||
```
|
||||
|
||||
The same pattern applies to async validators using [`addAsyncValidators`](api/forms/AbstractControl#addAsyncValidators), [`removeAsyncValidators`](api/forms/AbstractControl#removeAsyncValidators), [`setAsyncValidators`](api/forms/AbstractControl#setAsyncValidators), and [`clearAsyncValidators`](api/forms/AbstractControl#clearAsyncValidators).
|
||||
|
||||
### Triggering validation updates
|
||||
|
||||
After modifying validators, call [`updateValueAndValidity`](api/forms/AbstractControl#updateValueAndValidity) to recalculate the control's validation status.
|
||||
This method accepts options to control update behavior.
|
||||
|
||||
```ts
|
||||
// Update control and notify parent
|
||||
control.updateValueAndValidity();
|
||||
|
||||
// Update control only, don't notify parent or emit events
|
||||
control.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
||||
```
|
||||
|
||||
## Interaction with native HTML form validation
|
||||
|
||||
By default, Angular disables [native HTML form validation](https://developer.mozilla.org/docs/Web/Guide/HTML/Constraint_validation) by adding the `novalidate` attribute on the enclosing `<form>` and uses directives to match these attributes with validator functions in the framework.
|
||||
|
||||
@@ -379,6 +379,26 @@ The `@for` block iterates over each form control instance provided by the aliase
|
||||
|
||||
Each time a new alias instance is added, the new form array instance is provided its control based on the index. This lets you track each individual control when calculating the status and value of the root control.
|
||||
|
||||
NOTE: In zoneless applications, mutating a reactive forms model (for example calling `FormArray.push()`) does not automatically schedule component change detection. If your template depends on structural model changes such as `aliases.controls`, make sure the component notifies Angular to run change detection, for example by bridging a forms observable to `ChangeDetectorRef.markForCheck()`:
|
||||
|
||||
```ts
|
||||
import {ChangeDetectorRef, Component, inject} from '@angular/core';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
/* ... */
|
||||
})
|
||||
export class ProfileEditor {
|
||||
private readonly cdr = inject(ChangeDetectorRef);
|
||||
|
||||
constructor() {
|
||||
this.profileForm.valueChanges
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe(() => this.cdr.markForCheck());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</docs-step>
|
||||
|
||||
### Using `FormArrayDirective` for top-level form arrays
|
||||
@@ -637,6 +657,8 @@ updatePostalCodeValidator(country: string) {
|
||||
}
|
||||
```
|
||||
|
||||
HELPFUL: For dynamically managing validators at runtime, see the [Managing validators dynamically in reactive forms](guide/forms/form-validation#managing-validators-dynamically-in-reactive-forms) section in the Form Validation guide.
|
||||
|
||||
## Utility functions for narrowing form control types
|
||||
|
||||
Angular provides four utility functions that help determine the concrete type of an `AbstractControl`. These functions act as **type guards** and narrow the control type when they return `true`, which lets you safely access subtype-specific properties inside the same block.
|
||||
|
||||
@@ -328,7 +328,7 @@ export class Registration {
|
||||
|
||||
private createUsernameResource = (usernameSignal: Signal<string | undefined>) => {
|
||||
return rxResource({
|
||||
request: () => usernameSignal(),
|
||||
params: () => usernameSignal(),
|
||||
stream: ({request: username}) => this.usernameService.checkUsername(username),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -211,7 +211,7 @@ const routes: Routes = [
|
||||
];
|
||||
```
|
||||
|
||||
The page `title` property can be set dynamincally to a resolver function using [`ResolveFn`](/api/router/ResolveFn).
|
||||
The page `title` property can be set dynamically to a resolver function using [`ResolveFn`](/api/router/ResolveFn).
|
||||
|
||||
```ts
|
||||
const titleResolver: ResolveFn<string> = (route) => route.queryParams['id'];
|
||||
|
||||
@@ -376,7 +376,7 @@ The validation rules are:
|
||||
- `Host` and `X-Forwarded-Host` headers are validated against a strict allowlist and cannot contain path separators.
|
||||
- `X-Forwarded-Port` header must be numeric.
|
||||
- `X-Forwarded-Proto` header must be `http` or `https`.
|
||||
- `X-Forwarded-Prefix` header must not start with multiple `/` or `\` or contain `.`, `..` path segments.
|
||||
- `X-Forwarded-Prefix` header must not start with `\` or multiple `/` or contain `.`, `..` path segments.
|
||||
|
||||
Invalid or disallowed headers now trigger an error log. Requests with unrecognized hostnames will result in a Client-Side Rendered (CSR) page if `allowedHosts` is defined; if not, a `400 Bad Request` is issued. Note that in a future major release, all unrecognized hostnames will default to a `400 Bad Request` regardless of `allowedHosts` settings.
|
||||
|
||||
@@ -431,6 +431,8 @@ For the Node.js variant `AngularNodeAppEngine`, you can also provide `NG_ALLOWED
|
||||
export NG_ALLOWED_HOSTS="example.com,*.trusted-example.com"
|
||||
```
|
||||
|
||||
IMPORTANT: You can use `*` as a value in `allowedHosts` to allow all hostnames, though this is generally discouraged and presents a security risk. Accepting any host header can expose your application to host header injection and [Server-Side Request Forgery (SSRF)](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/SSRF) attacks. This configuration should only be used when validation for `Host` and `X-Forwarded-Host` headers is performed in another layer, such as a load balancer or reverse proxy. For better security, we recommend using an explicit list of allowed hosts whenever possible. See [GHSA-x288-3778-4hhx](https://github.com/angular/angular-cli/security/advisories/GHSA-x288-3778-4hhx) for more details.
|
||||
|
||||
## Auditing Angular applications
|
||||
|
||||
Angular applications must follow the same security principles as regular web applications, and must be audited as such.
|
||||
|
||||
@@ -237,9 +237,9 @@ When you read a signal within an `OnPush` component's template, Angular tracks t
|
||||
When creating a signal, you can optionally provide an equality function, which will be used to check whether the new value is actually different than the previous one.
|
||||
|
||||
```ts
|
||||
import _ from 'lodash';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
const data = signal(['test'], {equal: _.isEqual});
|
||||
const data = signal(['test'], {equal: isEqual});
|
||||
|
||||
// Even though this is a different array instance, the deep equality
|
||||
// function will consider the values to be equal, and the signal won't
|
||||
|
||||
@@ -60,7 +60,7 @@ All expression values are converted to a string. Objects and arrays are converte
|
||||
|
||||
Angular supports binding dynamic values into object properties and HTML attributes with square brackets.
|
||||
|
||||
You can bind to properties on an HTML element's DOM instance, a [component](guide/components) instance, or a [directive](guide/directives) instance.
|
||||
You can bind to properties on an HTML element's DOM instance, a [component](/guide/components) instance, or a [directive](/guide/directives) instance.
|
||||
|
||||
### Native element properties
|
||||
|
||||
@@ -260,4 +260,4 @@ Angular supports binding string values to ARIA attributes.
|
||||
|
||||
Angular writes the string value to the element’s `aria-label` attribute and removes it when the bound value is `null`.
|
||||
|
||||
Some ARIA features expose DOM properties or directive inputs that accept structured values (such as element references). Use standard property bindings for those cases. See the [accessibility guide](best-practices/a11y#aria-attributes-and-properties) for examples and additional guidance.
|
||||
Some ARIA features expose DOM properties or directive inputs that accept structured values (such as element references). Use standard property bindings for those cases. See the [accessibility guide](/best-practices/a11y#aria-attributes-and-properties) for examples and additional guidance.
|
||||
|
||||
@@ -91,7 +91,7 @@ You can also apply structural directives to `<ng-container>` elements. Common ex
|
||||
|
||||
## Using `<ng-container>` for injection
|
||||
|
||||
See the Dependency Injection guide for more information on Angular's dependency injection system.
|
||||
See the [Dependency Injection guide](/guide/di) for more information on Angular's dependency injection system.
|
||||
|
||||
When you apply a directive to `<ng-container>`, descendant elements can inject the directive or anything that the directive provides. Use this when you want to declaratively provide a value to a specific part of your template.
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
Here is an example of a `BaseButton` component that accepts any markup from its parent.
|
||||
|
||||
```angular-ts
|
||||
// ./base-button/base-button.ts
|
||||
```angular-ts {header:'base-button/base-button.ts'}
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
@@ -15,8 +14,7 @@ import {Component} from '@angular/core';
|
||||
export class BaseButton {}
|
||||
```
|
||||
|
||||
```angular-ts
|
||||
// ./app.ts
|
||||
```angular-ts {header:'app.ts'}
|
||||
import {Component} from '@angular/core';
|
||||
import {BaseButton} from './base-button';
|
||||
|
||||
|
||||
@@ -46,8 +46,7 @@ Leveraging two-way binding between a parent and child component requires more co
|
||||
|
||||
Here is an example where the `App` is responsible for setting the initial count state, but the logic for updating and rendering the UI for the counter primarily resides inside its child `Counter`.
|
||||
|
||||
```angular-ts
|
||||
// ./app.ts
|
||||
```angular-ts {header: 'app.ts'}
|
||||
import {Component} from '@angular/core';
|
||||
import {Counter} from './counter';
|
||||
|
||||
@@ -66,8 +65,7 @@ export class App {
|
||||
}
|
||||
```
|
||||
|
||||
```angular-ts
|
||||
// './counter.ts';
|
||||
```angular-ts {header: 'counter.ts'}
|
||||
import {Component, model} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
@@ -95,8 +93,7 @@ The child component must contain a `model` property.
|
||||
|
||||
Here is a simplified example:
|
||||
|
||||
```angular-ts
|
||||
// './counter.ts';
|
||||
```angular-ts {header: 'counter.ts'}
|
||||
import {Component, model} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
@@ -118,8 +115,7 @@ The parent component must:
|
||||
|
||||
Here is a simplified example:
|
||||
|
||||
```angular-ts
|
||||
// ./app.ts
|
||||
```angular-ts {header: 'app.ts'}
|
||||
import {Component} from '@angular/core';
|
||||
import {Counter} from './counter';
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ Now, if your coverage drops below 80% when you run your tests, the command will
|
||||
You can configure several other coverage options in your `angular.json` file:
|
||||
|
||||
- `coverageInclude`: Glob patterns of files to include in the coverage report.
|
||||
- `coverageExclude`: Glob patterns of files to exclude in the coverage report.
|
||||
- `coverageReporters`: An array of reporters to use (e.g., `html`, `lcov`, `json`).
|
||||
- `coverageWatermarks`: An object specifying `[low, high]` watermarks for the HTML reporter, which can affect the color-coding of the report.
|
||||
|
||||
|
||||
@@ -863,7 +863,7 @@ beforeEach(() => {
|
||||
});
|
||||
```
|
||||
|
||||
HELPFUL: The `set` key in this example replaces all the exisiting imports on your component, make sure to imports all dependencies, not only the stubs. Alternatively you can use the `remove`/`add` keys to selectively remove and add imports.
|
||||
HELPFUL: The `set` key in this example replaces all the existing imports on your component, make sure to import all dependencies, not only the stubs. Alternatively you can use the `remove`/`add` keys to selectively remove and add imports.
|
||||
|
||||
### `NO_ERRORS_SCHEMA`
|
||||
|
||||
|
||||
@@ -69,16 +69,16 @@ You can now delete `karma.conf.js` and `src/test.ts` from your project and unins
|
||||
|
||||
<docs-code-multifile>
|
||||
<docs-code header="npm" language="shell">
|
||||
npm uninstall karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter
|
||||
npm uninstall karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter jasmine-core
|
||||
</docs-code>
|
||||
<docs-code header="yarn" language="shell">
|
||||
yarn remove karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter
|
||||
yarn remove karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter jasmine-core
|
||||
</docs-code>
|
||||
<docs-code header="pnpm" language="shell">
|
||||
pnpm remove karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter
|
||||
pnpm remove karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter jasmine-core
|
||||
</docs-code>
|
||||
<docs-code header="bun" language="shell">
|
||||
bun remove karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter
|
||||
bun remove karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter jasmine-core
|
||||
</docs-code>
|
||||
</docs-code-multifile>
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ Additional testing environments require custom bindings. See the [adding harness
|
||||
|
||||
### Using the loader from `TestbedHarnessEnvironment` for unit tests
|
||||
|
||||
For unit tests you can create a harness loader from [TestbedHarnessEnvironment](/api/cdk/testing/TestbedHarnessEnvironment). This environment uses a [component fixture](api/core/testing/ComponentFixture) created by Angular's `TestBed`.
|
||||
For unit tests you can create a harness loader from [TestbedHarnessEnvironment](/api/cdk/testing/testbed/TestbedHarnessEnvironment). This environment uses a [component fixture](api/core/testing/ComponentFixture) created by Angular's `TestBed`.
|
||||
|
||||
To create a harness loader rooted at the fixture's root element, use the `loader()` method:
|
||||
|
||||
@@ -68,7 +68,7 @@ To get `ComponentHarness` for the first instance of the element, use the `getHar
|
||||
const myComponentHarness = await loader.getHarness(MyComponent);
|
||||
|
||||
// Get harnesses for all instances of the element
|
||||
const myComponentHarnesses = await loader.getHarnesses(MyComponent);
|
||||
const myComponentHarnesses = await loader.getAllHarnesses(MyComponent);
|
||||
```
|
||||
|
||||
In addition to `getHarness` and `getAllHarnesses`, `HarnessLoader` has several other useful methods for querying for harnesses:
|
||||
|
||||
@@ -60,7 +60,7 @@ use [ChangeDetectionStrategy.OnPush](/best-practices/skipping-subtrees#using-onp
|
||||
|
||||
The `OnPush` change detection strategy is not required, but it is a recommended step towards zoneless compatibility for application components. It is not always possible for library components to use `ChangeDetectionStrategy.OnPush`.
|
||||
When a library component is a host for user-components which might use `ChangeDetectionStrategy.Eager`/`Default`, it cannot use `OnPush` because that would prevent the child component from being refreshed if it is not `OnPush` compatible and relies on ZoneJS to trigger change detection. Components can use the `Default` strategy as long as they notify Angular when change detection needs to run (calling `markForCheck`, using signals, `AsyncPipe`, etc.).
|
||||
Being a host for a user component means using an API such as `ViewContainerRef.createComponent` and not just hosting a portion of a template from a user component (i.e. content projection or a using a template ref input).
|
||||
Being a host for a user component means using an API such as `ViewContainerRef.createComponent` and not just hosting a portion of a template from a user component (i.e. content projection or using a template ref input).
|
||||
|
||||
### Remove `NgZone.onMicrotaskEmpty`, `NgZone.onUnstable`, `NgZone.isStable`, or `NgZone.onStable`
|
||||
|
||||
@@ -123,6 +123,12 @@ readonly myObservableState = someObservable.pipe(pendingUntilEvent());
|
||||
The framework uses this service internally as well to prevent serialization until asynchronous tasks are complete. These include, but are not limited to,
|
||||
an ongoing Router navigation and an incomplete `HttpClient` request.
|
||||
|
||||
### Reactive forms in zoneless applications
|
||||
|
||||
Reactive forms model updates (`setValue`, `patchValue`, `FormArray.push`, and similar APIs) update form state and emit forms observables, but they do not automatically schedule component change detection.
|
||||
|
||||
If a template depends on reactive forms state, connect forms observables to a change-detection notification (for example `ChangeDetectorRef.markForCheck()`), or reflect the data through signals consumed by the template.
|
||||
|
||||
## Testing and Debugging
|
||||
|
||||
### Using Zoneless in `TestBed`
|
||||
|
||||
@@ -138,7 +138,7 @@ You can repeat part of a template multiple times with Angular's `@for` block:
|
||||
</ul>
|
||||
```
|
||||
|
||||
Angular's uses the `track` keyword, shown in the example above, to associate data with the DOM elements created by `@for`. See [_Why is track in @for blocks important?_](guide/templates/control-flow#why-is-track-in-for-blocks-important) for more info.
|
||||
Angular uses the `track` keyword, shown in the example above, to associate data with the DOM elements created by `@for`. See [_Why is track in @for blocks important?_](guide/templates/control-flow#why-is-track-in-for-blocks-important) for more info.
|
||||
|
||||
TIP: Want to know more about Angular templates? See the [In-depth Templates guide](guide/templates) for the full details.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This diagnostic detects unreachable or redundant triggers in `@defer` blocks.
|
||||
|
||||
```typescript
|
||||
```angular-ts
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
@@ -31,7 +31,7 @@ This diagnostic flags the following problematic patterns:
|
||||
|
||||
**Bad — prefetch never runs**
|
||||
|
||||
```typescript
|
||||
```angular-ts
|
||||
@Component({
|
||||
template: `
|
||||
@defer (on immediate; prefetch on idle) {
|
||||
@@ -44,7 +44,7 @@ class MyComponent {}
|
||||
|
||||
**Good — remove redundant prefetch**
|
||||
|
||||
```typescript
|
||||
```angular-ts
|
||||
@Component({
|
||||
template: `
|
||||
@defer (on immediate) {
|
||||
@@ -59,7 +59,7 @@ class MyComponent {}
|
||||
|
||||
**Bad — prefetch is later than main**
|
||||
|
||||
```typescript
|
||||
```angular-ts
|
||||
@Component({
|
||||
template: `
|
||||
@defer (on timer(100ms); prefetch on timer(3000ms)) {
|
||||
@@ -72,7 +72,7 @@ class MyComponent {}
|
||||
|
||||
**Bad — equal timing provides no benefit**
|
||||
|
||||
```typescript
|
||||
```angular-ts
|
||||
@Component({
|
||||
template: `
|
||||
@defer (on timer(500ms); prefetch on timer(500ms)) {
|
||||
@@ -85,7 +85,7 @@ class MyComponent {}
|
||||
|
||||
**Good — prefetch fires earlier**
|
||||
|
||||
```typescript
|
||||
```angular-ts
|
||||
@Component({
|
||||
template: `
|
||||
@defer (on timer(1000ms); prefetch on timer(500ms)) {
|
||||
@@ -100,7 +100,7 @@ class MyComponent {}
|
||||
|
||||
**Bad — identical viewport trigger**
|
||||
|
||||
```typescript
|
||||
```angular-ts
|
||||
@Component({
|
||||
template: `
|
||||
@defer (on viewport; prefetch on viewport) {
|
||||
@@ -113,7 +113,7 @@ class MyComponent {}
|
||||
|
||||
**Bad — identical interaction target**
|
||||
|
||||
```typescript
|
||||
```angular-ts
|
||||
@Component({
|
||||
template: `
|
||||
<button #loadBtn>Load</button>
|
||||
@@ -127,7 +127,7 @@ class MyComponent {}
|
||||
|
||||
**Good — remove redundant prefetch**
|
||||
|
||||
```typescript
|
||||
```angular-ts
|
||||
@Component({
|
||||
template: `
|
||||
<button #loadBtn>Load</button>
|
||||
@@ -141,7 +141,7 @@ class MyComponent {}
|
||||
|
||||
**Good — use different targets for prefetch and main**
|
||||
|
||||
```typescript
|
||||
```angular-ts
|
||||
@Component({
|
||||
template: `
|
||||
<div #hoverArea>Hover to prefetch</div>
|
||||
|
||||
@@ -36,18 +36,21 @@ class MyComponent {}
|
||||
|
||||
If a conditional is necessary, you can wrap the component in an `*ngIf`.
|
||||
|
||||
```html
|
||||
import {Component} from '@angular/core'; @Component({ template: `
|
||||
```angular-ts
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
<div \*ngIf="hasUser; else noUser">
|
||||
<user-viewer ngSkipHydration />
|
||||
</div>
|
||||
@Component({
|
||||
template: `
|
||||
<div *ngIf="hasUser; else noUser">
|
||||
<user-viewer ngSkipHydration />
|
||||
</div>
|
||||
|
||||
<ng-template #noUser>
|
||||
<user-viewer />
|
||||
</ng-template>
|
||||
|
||||
`, }) class MyComponent {}
|
||||
<ng-template #noUser>
|
||||
<user-viewer />
|
||||
</ng-template>
|
||||
`,
|
||||
})
|
||||
class MyComponent {}
|
||||
```
|
||||
|
||||
## Configuration requirements
|
||||
|
||||
@@ -17,7 +17,7 @@ ng generate @angular/core:output-migration
|
||||
|
||||
1. `@Output()` class members are updated to their `output()` equivalent.
|
||||
2. Imports in the file of components or directives, at Typescript module level, are updated as well.
|
||||
3. Migrates the APIs functions like `event.next()`, which use is not recommended, to `event.emit()` and removes `event.complete()` calls.
|
||||
3. Migrates API calls like `event.next()`, whose use is not recommended, to `event.emit()` and removes `event.complete()` calls.
|
||||
|
||||
**Before**
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Migration to lazy-loaded routes
|
||||
|
||||
This schematic helps developers to convert eagerly loaded component routes to lazy loaded routes. This allows the build process to split the production bundle into smaller chunks, to avoid big JS bundle that includes all routes, which negatively affects initial page load of an application.
|
||||
This schematic helps developers to convert eagerly loaded component routes to lazy loaded routes. This allows the build process to split the production bundle into smaller chunks, to avoid a big JS bundle that includes all routes, which negatively affects initial page load of an application.
|
||||
|
||||
Run the schematic using the following command:
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user