Compare commits

...

142 Commits

Author SHA1 Message Date
Leon Senft 90cc755a56 release: cut the v21.2.10 release 2026-04-22 16:28:45 -07:00
Savio Dsouza 4fd1a08699 build: pin firebase-tools version and disable credential persistence in preview deploy workflow
(cherry picked from commit 8f5e0e09e9)
2026-04-22 14:38:27 -07:00
Angular Robot 750af5b123 build: update cross-repo angular dependencies to v21.2.8
See associated pull request for more information.
2026-04-22 11:03:02 -07:00
Rishabhdeep Singh 5533ab4f56 fix(migrations): fix NgClass leaving trailing comma after removal
This fixes an issue where when removing NgClass from the imports array of a component, an extra trailing comma would be left behind if it was the last element in that component`.

(cherry picked from commit b395173cf2)
2026-04-22 09:59:54 -07:00
Rishabhdeep Singh 2b9954fd3d fix(migrations): fix NgClass leaving trailing comma after removal
This fixes an issue where when removing NgClass from the imports array of a component, an extra trailing comma would be left behind if it was the last element in that component`.

(cherry picked from commit 27f021248d)
2026-04-22 09:59:54 -07:00
Angular Robot 4dc7bf5a75 build: lock file maintenance
See associated pull request for more information.
2026-04-21 10:23:48 -07:00
Joel Kesler 0d5ee9ae1b fix(docs): link formatting in "Animating your Application with CSS"
One of the links in `Animating your Application with CSS` page has a formatting bug for one of it's links.

(cherry picked from commit b24b4cb699)
2026-04-21 10:20:11 -07:00
Nikhil Bachani 6a02320575 docs: fix tracking expression reference in NG0955.md
Corrected the tracking expression reference from 'item.key' to 'item.value' in the explanation of duplicate keys.

(cherry picked from commit ac92a8aae8)
2026-04-21 10:14:53 -07:00
Angular Robot 751e4af80b build: update cross-repo angular dependencies
See associated pull request for more information.
2026-04-21 09:04:23 -07:00
Matthieu Riegler da346bf696 docs: update builder docs
We use `@angular/build` today.

(cherry picked from commit fa6a3d208d)
2026-04-21 09:02:37 -07:00
SkyZeroZx 580212c995 fix(router): restore internal URL on popstate when browserUrl is used
Fixed an issue where back/forward (`popstate`) navigation attempted to match the displayed `browserUrl` instead of the internal route, which could result in `NG04002: Cannot match any routes`.

Fixes #67549

(cherry picked from commit 6eff439546)
2026-04-20 16:46:30 -07:00
Kam a9ddc5d40a docs: warn against storing secrets in environment files
Add a CRITICAL callout warning that files in `src/environments/`
ship to the client and should not hold secrets like API keys.

(cherry picked from commit d27e2c24e1)
2026-04-20 13:29:18 -07:00
Kam 0d08d9cc82 docs(docs-infra): guard sandbox reset before initialization in playground
changeTemplate() was calling reset() on the sandbox before init()
completed, causing a TypeError when spawning processes on an
uninitialized WebContainer. Add isSandboxReady signal to skip
reset until the sandbox is fully initialized.

(cherry picked from commit c04c0b977a)
2026-04-20 13:17:22 -07:00
Kam 4c7ec66807 docs(docs-infra): adjust close button spacing in mobile navigation
Use relative positioning to offset the close button from the top edge without affecting the layout of surrounding elements.

(cherry picked from commit 2dc3ab596b)
2026-04-20 13:14:40 -07:00
Matthieu Riegler 62266eee8b ci: remove disabled side-effects integration tests
This test was disabled 5+ years ago, we probably don't need it anymore.

(cherry picked from commit 13be2961f6)
2026-04-20 13:13:28 -07:00
Bhuvansh855 2a50dceef5 docs: improve wording and consistency in forms documentation
(cherry picked from commit 74a7d6b8f9)
2026-04-20 13:12:35 -07:00
Andrew Scott c9215b3539 Revert "refactor(core): complete removal of deprecated createNgModuleRef alias"
This reverts commit d88d6ed69e.
Depended on a PR that was not merged to 21.2.x
2026-04-20 12:55:55 -07:00
SkyZeroZx d88d6ed69e refactor(core): complete removal of deprecated createNgModuleRef alias
Finalize the cleanup by removing the remaining `createNgModuleRef` alias.

(cherry picked from commit 3ae40e6685)
2026-04-20 12:09:51 -07:00
Bhuvansh855 e5b93ea4ca docs: fix wording in reactive forms guide
(cherry picked from commit c610425310)
2026-04-20 09:51:47 -07:00
Joey Perrott ce883d95ef build: update peer dependencies and bump version
Update peer dependencies to support Angular 21 instead of 22-next and bump the patch version to 0.21.1.
2026-04-20 09:39:10 -07:00
Bhuvansh855 be7490964a docs: improve clarity in dynamic forms guide
(cherry picked from commit a718e188c6)
2026-04-20 09:30:50 -07:00
Angular Robot 810fb7382f build: update cross-repo angular dependencies
See associated pull request for more information.
2026-04-17 14:34:29 -07:00
aparziale b24ead5571 refactor: Improve hydration mismatch errors for third-party scripts
Improves error messages shown during hydration mismatches to better
surface cases where third-party scripts or browser extensions have
modified the DOM outside of Angular's control.

Fixed #59224

(cherry picked from commit d771a65ac0)
2026-04-17 14:33:15 -07:00
pravintargaryen 6d10b8bb95 docs: add inject to structural directive imports
(cherry picked from commit da6c92eccd)
2026-04-17 14:25:09 -07:00
Angular Robot c738d45fa5 build: update all github actions
See associated pull request for more information.
2026-04-17 10:53:19 -07:00
Angular Robot b2fe2c2474 build: update bazel dependencies
See associated pull request for more information.
2026-04-17 10:51:47 -07:00
Michael Small 9577b49666 docs: capitalize FormField in form-logic.md imports: [...]
(cherry picked from commit 0850e20a83)
2026-04-17 10:50:57 -07:00
Michael Small 50f88e1887 docs: fix rxResource example of validateAsync
(cherry picked from commit 4da3f6c432)
2026-04-17 10:49:56 -07:00
Christian Oliff a57a6496fa docs: Fix typo in menubar.md
manubar > menubar

(cherry picked from commit bf4faed626)
2026-04-17 10:49:20 -07:00
Kam 8d22beb22c docs: fix typo in what-is-angular page
Change "language services powers" to "language service powers".

(cherry picked from commit 9c30e74349)
2026-04-17 10:48:35 -07:00
kirjs e14d5eadd5 release: cut the v21.2.9 release 2026-04-16 00:34:03 +03:00
arturovt 528a93a5da docs(router): document .. traversal and relativeTo pitfalls in router.navigate()
Explain two non-obvious behaviors of the commands array in router.navigate():

- Multiple '..' segments must be combined in the first array element
  (e.g. ['../../foo']), not spread across separate elements
  (e.g. ['..', '..', 'foo']), because the router only parses '..'
  from the first command string. Subsequent elements are treated as
  literal path segments, causing a navigation error.
- A leading '/' in the first command makes navigation absolute and
  silently ignores the relativeTo option entirely.

Closes #65657

(cherry picked from commit 79c981840f)
2026-04-15 15:40:47 -04:00
Ben Hong 32a830231e docs: add new signal forms - form submission guide
(cherry picked from commit 50a3b0e1ba)
2026-04-15 12:38:40 -04:00
Angular Robot 0d1a5b80c2 build: update cross-repo angular dependencies
See associated pull request for more information.
2026-04-15 19:37:18 +03:00
Matthieu Riegler 17cae6ae5f docs: fix bootstraping link
fixes #68212

(cherry picked from commit a46c64758e)
2026-04-15 12:25:40 -04:00
Suleiman Yunus 4f65bb34b2 docs: correct "What to learn more about Angular?" to "Want to learn more about Angular?"
(cherry picked from commit e32159b5c5)
2026-04-15 10:33:30 -04:00
arturovt eddca4280b fix(zone.js): allow draining microtasks in Promise.then (through flag)
These changes are essentially the same as those introduced in
angular#45273, but they include backward compatibility
for applications that explicitly rely on the order in which microtasks are drained.

This is critically important for our code and other third-party code, which is
beyond our control, to work properly. If a microtask is scheduled within an event
listener to be executed "later", it should indeed be executed later and not synchronously,
as this would break the expected flow of code execution.

The simple code that reproduces the behavior that exists now:

```ts
Zone.current.fork({name: 'child'}).run(() => {
  const div = document.createElement('div');
  div.style.height = '200px';
  div.style.width = '200px';
  div.style.backgroundColor = 'red';
  document.body.appendChild(div);

  function listener() {
    Promise.resolve().then(() => {
      div.style.height = '400px';
    });
  }

  div.addEventListener('fakeEvent', listener);
  div.dispatchEvent(new Event('fakeEvent'));
  console.log(div.getBoundingClientRect().height); // 400
});
```

The code above logs 400 as the height, but it should actually log 200 because the
height is updated in a microtask within the event listener.

When using Angular with microfrontend applications, especially when other apps might be
using React, zone.js can disrupt the classical order of operations. For example, when using a
`react-component/trigger`, it schedules a microtask within an event listener using
`Promise.resolve().then(...)` to determine whether the event needs to be re-dispatched.
The event is re-dispatched when the layout has changed, which is why a microtask is used.

With this change, we introduce a global configuration flag,
`__zone_symbol__enable_native_microtask_draining`, to allow consumers to enable
microtask draining within a browser microtask.

This flag is necessary to prevent any breaking changes resulting from this modification.
The previous attempt to address this issue caused a significant number of failures in g3.
Therefore, we are hiding that fix behind the configuration flag.

Closes angular#44446
Closes angular#55590
Closes angular#51328

(cherry picked from commit fc6a7eea68)
2026-04-15 10:31:33 -04:00
Kam 175343dfdb docs(docs-infra): add background to playground template dropdown
The template dropdown menu had no background color on the container,
causing page content to bleed through behind menu items.

(cherry picked from commit b2cff7918d)
2026-04-15 10:26:10 -04:00
Alan Agius e0b5078cf2 fix(platform-server): prevent SSRF bypasses via protocol-relative and backslash URLs
The `parseUrl` function in `ServerPlatformLocation` uses `new URL(urlStr, origin)` to parse incoming request URLs during SSR. Per the WHATWG URL specification, protocol-relative URLs (`//evil.com`) and backslash-prefixed URLs (`/\evil.com`) can override the hostname component of the base URL.

This vulnerability typically manifests in SSR setups (e.g., Express) where `req.url` is passed directly to `renderApplication` or `renderModule`:

```typescript
// Example usage in an Express server handling: http://localhost:4000//evil.com
app.get('*', async (req, res) => {
  const html = await renderApplication(bootstrap, {
    document: template,
    url: req.url, // req.url is "//evil.com"
  });
  res.send(html);
});
```

(cherry picked from commit ede7c58a2a)
2026-04-15 10:23:57 -04:00
Ben Hong 1e474f7cfa docs: add new signal forms schema guide
(cherry picked from commit 3eba900d3f)
2026-04-15 10:22:40 -04:00
Matthieu Riegler 63a857b874 fix(http): Don't on Passthru outside of reactive context
Priori to this change, the InMemory API threw when request was emited outside an injection context and that request hit the passThru.
This commit fixes this.

(cherry picked from commit d1cd97648a)
2026-04-15 10:20:53 -04:00
Kam c3d69aeaaa fix(docs-infra): prevent inline code wrapping in CLI reference table
Inline code elements inside table cells inherited `width: 100%` from
the global code styles, causing short codes like `s`, `dev` to stack
vertically instead of rendering on the same line. Add `min-width` to
table cells containing code to ensure proper inline layout.

(cherry picked from commit c8e23d3a9d)
2026-04-14 18:29:30 +03:00
arturovt 684e9fd53d fix(router): normalize multiple leading slashes in URL parser
URLs with three or more consecutive leading slashes (e.g. `///test`) were
parsed incorrectly by `DefaultUrlSerializer`. The parser consumed only two
leading slashes, leaving a third that caused `parseSegment()` to produce an
empty `UrlSegment`. When serialized back, that empty segment rendered as
`//test` — a protocol-relative URL that browsers resolve as a different
origin and reject with a `SecurityError` when passed to
`history.pushState`/`replaceState`.

The fix changes `parseRootSegment()` to consume all consecutive leading
slashes instead of just one, normalizing any number of leading slashes to
a single `/` before the path is parsed.

Closes #49610

(cherry picked from commit c90b6b398e)
2026-04-14 12:34:08 +03:00
Andrew Scott ff0af64ced refactor(compiler-cli): decouple SymbolReference from AST nodes in template checker
To support the need to resolve symbols without full AST access (e.g. when using virtual files), this commit decouples `ReferenceSymbol` from `ts.ClassDeclaration`.

Changes:
- Updated `ReferenceSymbol.target` to use `SymbolReference` instead of `ts.ClassDeclaration`.
- Removed `getReferenceTargetNode()` from `SymbolDirectiveMeta` and transitioned to `getSymbolReference()`.
- Refactored `getTsSymbolOfReference` in `checker.ts` to handle `SymbolReference` and resolve it to a `ts.Symbol` using a position-optimized AST traversal. This avoids using the private `getTokenAtPosition` API and avoids full file scans by only traversing nodes containing the target position.

(cherry picked from commit c2f4b2af7c)
2026-04-14 12:32:54 +03:00
Angular Robot bb8cdd9566 build: lock file maintenance
See associated pull request for more information.
2026-04-14 12:20:59 +03:00
AleksanderBodurri 17ffa19a2d docs(devtools): create router tree documentation
(cherry picked from commit cb19c69ea6)
2026-04-13 21:16:07 +03:00
Michael Small 6c341347b2 docs: add "Using Agent Skills" + command to skills README.md
(cherry picked from commit bb03878ae0)
2026-04-13 21:12:30 +03:00
Ben Hong 8c32f577f1 docs: add new signal forms cross field logic guide
(cherry picked from commit c879cecb45)
2026-04-13 21:07:51 +03:00
Jessica Janiuk aa5d23799b docs: draft PR spam policy addendum
This updates the spam policy to be clear about draft pull requests with regards to the 3 PR limit

(cherry picked from commit eb2b06f3d9)
2026-04-13 20:54:28 +03:00
YooLCD 540536c386 fix(http): add CSP nonce support to JsonpClientBackend
Add support for CSP nonces in JsonpClientBackend by injecting the CSP_NONCE token.
This ensures that dynamically created script tags for JSONP requests include the
required nonce attribute to comply with strict Content Security Policies.

(cherry picked from commit 39e382a756)
2026-04-13 16:01:16 +03:00
Jessica Janiuk f603d4714f fix(core): escape forward slashes in transfer state to prevent crawler indexing
This commit escapes forward slashes in the transfer state JSON output as \u002F to prevent search engine crawlers from aggressively indexing relative paths inside the inline script tag. It also updates related unit and integration tests across core and platform-server.

Fixes #65310

(cherry picked from commit 3c7641151c)
2026-04-13 13:55:00 +03:00
kirjs b72b6b4710 docs(forms): update signal forms migration guide
(cherry picked from commit f25c7ce6a6)
2026-04-13 13:25:44 +03:00
aparziale 3ed14c6354 refactor: Mobile layout api reference
Fix mobile layout shift in API reference

fix #67650

(cherry picked from commit 973ede6ccc)
2026-04-13 11:24:46 +03:00
Angular Robot 236b80b6f9 build: update cross-repo angular dependencies
See associated pull request for more information.
2026-04-13 11:22:33 +03:00
Michael Small ca5b3c4d3e docs: add dev-app section to contributing docs
docs: link directly to dev-app `README.md`

(cherry picked from commit 59513b740d)
2026-04-13 11:18:37 +03:00
Kam 245bcdd607 docs(docs-infra): fix card container overflow on mobile viewports
Override h2 min-width in docs-card-container-header for small screens
and add docs-content container query fallback to hide SVG illustrations.

(cherry picked from commit c3d4be4a61)
2026-04-13 11:09:54 +03:00
Kam e2e7211530 docs(docs-infra): consolidate tab menu margins for phone breakpoint
Replace separate margin-left/margin-right overrides with a single
margin shorthand in the phone-only media query, aligning spacing
with the base rule and preventing edge collision on small screens.

(cherry picked from commit b5b8631198)
2026-04-13 11:01:47 +03:00
Kam b351d493ea docs(docs-infra): fix essentials next-step navigation pills
Update the "Next step" pill in templates to point to signal-forms
instead of skipping it, and add a next-step pill in signal-forms
linking to dependency-injection.

(cherry picked from commit fda8d201bb)
2026-04-13 10:51:03 +03:00
Kam 096a5c2105 docs(docs-infra): add external links to W3C specs in Angular Aria overview
Link "W3C Accessibility Guidelines" to WCAG 2.2 and "WAI-ARIA patterns"
to the W3C APG patterns page, giving readers direct access to the
referenced specifications.

(cherry picked from commit e8eb179477)
2026-04-13 10:50:00 +03:00
Alan Agius b1407e1add build: update rules_angular setup in MODULE.bazel
Migrate from using use_repo_rule and override_repo directly to using the rules_angular.setup module extension for configuring configurable dependencies.

(cherry picked from commit a268547368)
2026-04-10 20:14:11 +03:00
Kam b4a747a94c docs(docs-infra): fix homepage nav overlay and banner visibility between 701–900px
The homepage navigation bar rendered with `height: 0` on viewports between
701–900px, causing its content to overflow on top of the announcement banner
and block scrolling. Reset nav height to `auto` at tablet sizes, center the
v21 banner, adjust its top margin, and hide the redundant search field since
the nav bar already provides one.

(cherry picked from commit 843f425ec8)
2026-04-10 17:39:36 +03:00
Angular Robot 3ae69406cc build: update cross-repo angular dependencies
See associated pull request for more information.
2026-04-10 17:36:09 +03:00
Doug Parker b52a6264ac refactor: add flaky tests workflow
I've had some success asking the Antigravity agent to find flaky tests and propose fixes for them, then just running it in the background and reviewing what it finds. Upstreaming this to the repo so others can use it, since it includes helpful notes like `--runs_per_test` and leveraging random seeds as well as an iteration loop.

I opted not to have the agent do anything with PRs just yet, but if this is useful and we build confidence in it, we can explore that in the future.

(cherry picked from commit 337e6e7d6e)
2026-04-10 16:45:37 +03:00
arturovt dc9581469f docs: add documentation for NG1002
Adds a documentation page for the NG01002 runtime error thrown by
FormGroup and FormArray when setValue is called with a value that is
missing an entry for one or more registered controls.

The error code is also changed from positive (1002) to negative (-1002)
so that Angular appends a link to the error reference page in dev mode,
consistent with how other documented errors (e.g. NG01101, NG01203) are
handled.

(cherry picked from commit 030422850b)
2026-04-10 10:54:46 +03:00
Angular Robot 05d9b97cf9 build: update cross-repo angular dependencies
See associated pull request for more information.
2026-04-09 14:17:44 +03:00
Andrew Scott 6aafd87038 release: cut the v21.2.8 release 2026-04-08 12:31:46 -07:00
Andrew Scott d4c8a9a887 refactor(compiler-cli): decouple SymbolBuilder from BoundTarget and minimize adapter surface
Decouple `SymbolBuilder` from the full `BoundTarget` interface by introducing a purpose-built `SymbolBoundTarget` interface containing only the 4 methods required for symbol resolution. This eliminates the need for the large, pass-through `BoundTargetAdapter` and further isolates `SymbolBuilder` from compiler-internal implementation details.

Also minimize `TypeCheckableDirectiveMetaAdapter` by redefining `SymbolDirectiveMeta` to not extend `DirectiveMeta`, exposing only the properties actually used by `SymbolBuilder`.

Removed dead code `getDirectiveMeta` in `template_symbol_builder.ts` which was unused.

These changes improve maintainability and ensure a cleaner architecture by strictly defining the boundaries of what `SymbolBuilder` needs from the rest of the system.
By limiting the required inputs to only what's necessary for the implementation, we make it easier to re-use
the implementation between different compiler architectures
2026-04-08 11:59:42 -07:00
N. Coury 057cc6d09d fix(core): remove obsolete iOS cursor pointer hack in event delegation
Since WebKit commit 67a62d98 (merged for iOS 13), WebKit no longer restricts
click event bubbling to interactable nodes like `div` or `body`. The
`cursor: pointer` hack polyfill is therefore obsolete and can safely be
removed without breaking JSAction behavior.

(cherry picked from commit a24179e125)
2026-04-08 18:31:34 +00:00
Angular Robot dea76c7a6e build: update cross-repo angular dependencies
See associated pull request for more information.
2026-04-08 10:06:53 -07:00
Alan Agius a07d64b406 docs: fix typos and formatting in spam policy and saved replies
- Enclose the spam saved reply in a code block in `saved-issue-replies.md`
- Correct spelling and punctuation in `saved-issue-replies.md` and `spam.md`

(cherry picked from commit c70b4fec8c)
2026-04-08 16:41:27 +00:00
Kam a6d6842467 docs: fix typos and grammar errors across documentation
Fix various typos, misspellings, and grammar issues across contributing
docs, adev content guides, and agent skills documentation.

- "an minimum" → "a minimum" (CONTRIBUTING.md)
- "GitHub accounts" → "GitHub account" (CONTRIBUTING.md)
- "decendants" → "descendants" (components/styling.md)
- "templates are using" → "templates is using" (hydration.md)
- "A automated" → "An automated" (branches-and-versioning.md)
- "Github" → "GitHub" (branches-and-versioning.md, commit-message-guidelines.md)
- "practices makes" → "practice makes" (caretaking.md)
- "corresponds" → "correspond" (triage-and-labelling.md)
- "one a line" → "a line" (documentation-authoring.md)
- "straight forward" → "straightforward" (using-fixup-commits.md)
- "or you decide" → "or you can decide" (anatomy-of-components.md)
- "whenver" → "whenever" (angular-new-app/SKILL.md)

(cherry picked from commit 4739bde9fb)
2026-04-08 16:40:51 +00:00
Andrew Scott 33a30e0e03 refactor(compiler-cli): Fix regressions caused by ts.typechecker removal
removing ts.typechecker in a prior refactor caused some regressions, particularly when multiple directives
appear on a single elemnt. this is now addressed by using an id for directives and storing that in the tcb comment

(cherry picked from commit 30c950f133)
2026-04-08 15:14:48 +00:00
Angular Robot a385743983 build: update cross-repo angular dependencies
See associated pull request for more information.
2026-04-07 16:42:35 -07:00
Andrew Scott c9f8f3afb6 test(core): add missing import of ChangeDetectionStrategy in query_spec
Add missing import of ChangeDetectionStrategy in query_spec.ts to fix compilation error.
2026-04-07 14:51:37 -07:00
Andrew Scott 910dcb6d6a refactor(compiler-cli): decouple TemplateSymbolBuilder from ts.TypeChecker
This updates the SymbolBuilder to no longer use ts.TypeChecker internally to
build symbols for the language service. These lookups are deferred/done later
using the newly expanded template type checker API.
2026-04-07 14:51:37 -07:00
Andrew Scott de12bc7e02 refactor(compiler-cli): tag host directives in TCB
Add HOST_DIRECTIVE expression identifier to TCB comments to identify host directives.
2026-04-07 14:51:37 -07:00
Kristiyan Kostadinov a4f312060c refactor(compiler): require a reference in DirectiveMeta
Requires the `DirectiveMeta` to have a `ref` so that we can find duplicates easily.
2026-04-07 14:51:37 -07:00
Kristiyan Kostadinov de533fe491 refactor(compiler-cli): move ClassPropertyMapping into compiler
Moves the `ClassPropertyMapping` into the compiler, rather than having to pass around the limited `InputOutputPropertySet` interface that is only implemented by `ClassPropertyMapping`.
2026-04-07 14:51:37 -07:00
Kristiyan Kostadinov ea1e34c4dd refactor(compiler): move matchSource into base metadata
Moves the `matchSource` into the base metadata so the binder can use it.
2026-04-07 14:51:37 -07:00
Kristiyan Kostadinov 304222014a refactor(compiler-cli): pre-compute key
Updates the TCB metadata to pre-compute and store the `TcbReferenceKey`, instead of computing it on the fly.
2026-04-07 14:51:37 -07:00
splincode 82192deda9 fix(core): handle missing serialized container hydration data
Simplify the hydration regression test by removing conditional early-return branches and relying on direct Jasmine expectations while keeping strict typing and OnPush configuration.

(cherry picked from commit 77f1ca08e4)
2026-04-07 18:22:07 +00:00
Angular Robot e8265f4593 build: lock file maintenance
See associated pull request for more information.
2026-04-07 09:49:43 -07:00
Kam c0496c1f39 docs(docs-infra): fix v21 event video not responsive on mobile
Replace raw iframe with docs-video component to fix YouTube embed
overflowing on mobile viewports.

(cherry picked from commit 1c9c4536d6)
2026-04-07 16:47:54 +00:00
Stephen Fluin 2d2ba938da docs(docs-infra): warning about upgrading to v21
(cherry picked from commit 64b52c1a30)
2026-04-07 16:47:17 +00:00
Kam ff8911fe52 docs(docs-infra): preserve navigation origin when clicking cross-category links
When a sidebar item links to a page in a different category (e.g., Route
transition animations under Animations links to a Routing page), clicking
back navigates to the main menu instead of the originating category.

Store the originating category in NavigationState when clicking a
cross-referenced item, so the back button returns to the correct section.

(cherry picked from commit 8132a96884)
2026-04-07 16:46:23 +00:00
Doug Parker 72d7a47dfd docs: add spam policy
This defines an initial policy with regard to issue / PR spam as well as a saved reply for bulk-closing large issue / PR counts at once.

(cherry picked from commit 19ce90fe9b)
2026-04-07 16:32:19 +00:00
Kristiyan Kostadinov 2c6781071f fix(compiler-cli): error for type parameter declarations
Fixes an error that was heppning when a generic param has type parameters of its own. There were a few different issues going on:
1. In #67707 I had changed a bit how we pass the `genericContextBehavior` which ended up ignoring the `useContextGenericType` option from the environment.
2. All directives depend on themselves, but we were overridding the `genericContextBehavior` for the directive being processed.
3. The type translator wasn't handling type parameter declarations. Technically we shouldn't be able to hit a code path that has a type parameter, however it's also easy enough to handle so we might as well.

Relates to #67704.

(cherry picked from commit ab061a7610)
2026-04-07 16:29:37 +00:00
Kristiyan Kostadinov e40d378f3e fix(compiler): handle nested brackets in host object bindings
Fixes that we were parsing bindings in the `host` object with a regex that didn't account for nested brackets which may come up with something like Tailwind.

Fixes #68039.

(cherry picked from commit 2ce0e98f79)
2026-04-06 20:21:54 +00:00
Kristiyan Kostadinov d715c366d9 refactor(compiler-cli): fix failing tests
Fixes some tests that started failing after recent changes.

(cherry picked from commit fd95735594)
2026-04-06 12:33:09 -07:00
Savio Dsouza c91bd074c0 ci: use immutable sha for checkout
Change to immutable SHA for checkout reference.

(cherry picked from commit 6e1e9766a5)
2026-04-06 11:47:06 -07:00
Angular Robot 2ec9ff087f docs: update cross-repo adev docs
Updated Angular adev cross repo docs files.
2026-04-06 11:35:04 -07:00
Doug Parker 8102331f82 test(http): disable XSRF and mock location in HttpClient tests to avoid Domino failures and state leakage
The `HttpClient` tests in `client_spec.ts` were failing intermittently in Node/Domino environment because `MockPlatformLocation` defaults to `http://_empty_/`. This valid URL satisfied the URL parser in `xsrfInterceptorFn`, causing it to proceed to cookie extraction which throws `NotYetImplemented` in Domino.

To fix this:
1. Disabled XSRF protection in `client_spec.ts` using `withNoXsrfProtection()`, as these tests are not for XSRF.
2. Provided `ɵprovideFakePlatformNavigation` to remove state leakage effects and ensure consistency.

(cherry picked from commit 1ef503e18e)
2026-04-06 18:05:58 +00:00
SkyZeroZx 44fc79b7f7 docs(docs-infra): Adds anchor links to class member headers for linking
Adds anchor links to class member headers for direct linking.
Add test to ensure anchors render correctly.

Fixes angular#67970
2026-04-06 11:05:20 -07:00
funsaized 39827a08fe fix(docs-infra): align search result icon with text on mobile
On narrow viewports, the search result icon was pushed to its own flex line when the title text was too long, causing vertical misalignment.

The fix wraps the title text and package badge in a single container that manages its own flex layout, preventing the icon from being separated from the text on narrow viewports.

Fixes #68005

(cherry picked from commit d95e856a25)
2026-04-06 17:57:13 +00:00
Doug Parker 04f5841eed test(platform-server): fix race condition in incremental hydration test
The test was using a brittle fixed timeout of 10ms to wait for change detection to run in Zoneless mode. This failed in CI sometimes presumably because CI can execute slower based on resource constraints. This commit replaces it with a polling approach which checks until the expected content is rendered.

(cherry picked from commit 4a174b89c8)
2026-04-03 23:47:00 +00:00
Doug Parker 958fc6ab8a test(platform-server): replace fixed timeout with polling in event replay test
This test appears to be flakey in CI, presumably because resource constrained environments can run unexpected slower and exceed the timeout. This switches to a polling approach, waiting for the queue to drain.

(cherry picked from commit b4a3abd095)
2026-04-03 13:50:09 -07:00
Angular Robot cbfd1ee571 build: update cross-repo angular dependencies
See associated pull request for more information.
2026-04-03 12:45:01 -07:00
Harmeet Singh b0dc2fbfcb docs(forms): clarify disabled FormArray value behavior
Document that FormArray.value includes only enabled child controls when the array is enabled, but includes all child values when the FormArray itself is disabled.

Fixes #67759

(cherry picked from commit 789c2cd9fb)
2026-04-03 18:22:37 +00:00
Angular Robot ef204bdf7a build: update bazel dependencies
See associated pull request for more information.
2026-04-03 10:59:22 -07:00
Angular Robot 4441a95119 build: update cross-repo angular dependencies to v21.2.5
See associated pull request for more information.
2026-04-02 15:50:14 -07:00
SkyZeroZx d150e2672e docs(docs-infra): Enhances update guide layout responsiveness
The version dropdown width now uses `clamp()` for better responsiveness.

(cherry picked from commit 164cf98879)
2026-04-02 22:49:32 +00:00
Kam 59f9912dc1 feat(docs-infra): update Stack Overflow icon to new logo
Updated the Stack Overflow icon in the navigation social menu to use the new Stack Overflow logo.

(cherry picked from commit a6a5e8a4a5)
2026-04-02 22:48:15 +00:00
Kam bce82bdc34 feat(docs-infra): add Stack Overflow link to navigation social menu
Added Stack Overflow icon and link to the navigation component.

(cherry picked from commit ba70e8ba0c)
2026-04-02 22:48:15 +00:00
Jessica Janiuk 2ae0912eea refactor(core): address review comments on NG0750 error message
This commit addresses review comments from AndrewKushnir regarding conditional formatting of error messages and updating tests.

(cherry picked from commit 30f63fc1c2)
2026-04-02 21:55:47 +00:00
Jessica Janiuk e583f4c808 refactor(core): Add more detail to NG0750 error message
This adds a bit more context to the NG0750 error message to provide details about which module failed to load when executing the dependencyResolverFn. This can help with debugging a failed lazy load in a defer block.

(cherry picked from commit 8218d2e34a)
2026-04-02 21:55:47 +00:00
Angular Robot 05bcc601ac build: update all github actions
See associated pull request for more information.
2026-04-02 13:00:00 -07:00
Angular Robot 9f61910923 build: update cross-repo angular dependencies
See associated pull request for more information.
2026-04-01 14:36:13 -07:00
Andrew Scott 7797671257 fix(language-service): get quick info at local var location to align with TS semantics and support type narrowing
Previously, the Language Service fetched Quick Info and definitions for template variables (such as `@let` declarations) using mapping to their `initializerLocation` (the right-hand side expression). This aggressively bubbled the type, JSDoc, and definition identity of the initializer backwards onto the variable itself.

This approach had two flaws:
1. It broke type narrowing because the LS read the original un-narrowed type from the source expression rather than the type of the narrowed intermediate variable in the Type Check Block.
2. It deviated from native TypeScript semantics, where a local `let` binding (`let address = hero.address`) does not inherit the docstrings or `(property)` kind of its initializer, acting solely as a local inferred variable.

By using `localVarLocation` rather than `initializerLocation` for LetDeclaration Quick Info and Type Definitions, these intermediate variables now properly preserve type narrowing within templates and flawlessly match the standard behavior expected of TypeScript block variables. `VariableSymbol.initializerLocation` is retained solely to map the value spans of structural directive contexts (e.g., `exportAs` strings).

fixes #65491

(cherry picked from commit 75ac120493)
2026-04-01 19:22:11 +00:00
SkyZeroZx 0b08e29f26 test(core): refactors test to use timeout utility
Replaces direct `setTimeout` wrapped in a Promise with the `timeout` helper from `@angular/private/testing`

(cherry picked from commit c15e3a005d)
2026-04-01 20:46:05 +02:00
Pawel Kozlowski 50761c8be4 release: cut the v21.2.7 release 2026-04-01 20:36:00 +02:00
Matthieu Riegler 8d56b17b95 ci: exclude test files from primitives review
Those files aren't synced into G3 and shouldn't require external reviews

(cherry picked from commit bf8105ef76)
2026-04-01 20:27:31 +02:00
Angular Robot 4ad6d582d2 build: update cross-repo angular dependencies to v21.2.6
See associated pull request for more information.
2026-04-01 18:25:52 +02:00
Angular Robot f363ab41f1 build: update cross-repo angular dependencies to 616a50d
See associated pull request for more information.
2026-04-01 13:21:58 +02:00
Andrew Scott 0960592d3d fix(router): pass outlet context to split to fix empty path named outlets
The `split` helper function in `packages/router/src/utils/config_matching.ts` was blind to the current outlet being processed. When encountering an empty path named outlet in the config, it would assume it needed to pull it in as a synthetic empty group, even if we were already in the process of resolving that very outlet!

When navigating to `/(secondary:component-copy)` with this config:

```typescript
{
  path: '',
  component: MainLayout,
  children: [
    { path: '', outlet: 'secondary', component: SecondaryComponent, children: [{path: 'component-copy'}] }
  ]
}
```

The router uses `MainLayout` as a pass-through and calls `split` on its children with segments `['component-copy']`.
`split` uses the `containsEmptyPathMatchesWithNamedOutlets` helper to determine if there are any candidate empty path named outlets to pull in. Because of this, it sees `{ path: '', outlet: 'secondary' }` and says: "Ah, an empty path named outlet! I must pull it in!"
Rather than falling through to standard segment matching, it returns `UrlSegmentGroup(segments: [], children: {secondary: emptyGroup})`.
The router then tries to process `primary` (with `[]` segments) and fails because the config only has `secondary`. It also tries to process `secondary` with the `emptyGroup`. While `{ path: '', outlet: 'secondary' }` matches the empty group, its child `{ path: 'component-copy' }` fails to match because the `emptyGroup` has no segments! So both branches fail, resulting in a `NoMatch` error for the entire navigation!

Pulling in empty path named outlets IS desired when they act as siblings to segments we are matching. This has worked before and continues to work!

```typescript
{
  path: 'a',
  children: [
    { path: 'b', component: ComponentB },
    { path: '', component: ComponentC, outlet: 'aux' }
  ]
}
```

When navigating to `a/b`, `split` sees segments `['b']` and the `aux` empty path. It pulls in `aux` so it gets instantiated alongside `b`. This is correct!

If we have a named outlet with a non-empty path under an empty path parent:

```typescript
{
  path: '',
  component: MainLayout,
  children: [
    { path: 'component-copy', outlet: 'secondary', component: ComponentE }
  ]
}
```

When we navigate to `/(secondary:component-copy)`:
- `split` uses `containsEmptyPathMatchesWithNamedOutlets` to see if there are any empty path named outlets. Since it only sees `path: 'component-copy'`, it returns `false`.
- It falls through to standard segment matching, which finds `component-copy` in the segments array and activates it flawlessly!

This worked perfectly before the fix because it didn't use `containsEmptyPathMatchesWithNamedOutlets`.

The fix passes the **current active outlet context** into `split`. If `split` finds an empty path named outlet that matches the outlet we are already processing, it ignores it as a pull-in candidate.

When evaluating `MainLayout` children for `secondary`:
- URL Segments left to process: `['component-copy']`
- Current Outlet: `secondary`
- `childConfig`: `[{ path: '', outlet: 'secondary' }]`

Previously, `split` saw the empty path and pulled it in as a synthetic empty group, breaking matching. Now, since `getOutlet(r) === outlet` (both are `secondary`), the fix ignores it. Instead of returning empty segments, it **falls through to standard segment matching**, which successfully find the `component-copy` segment!

When evaluating `ComponentA` children for `primary`:
- URL Segments left to process: `['b']`
- Current Outlet: `primary`
- `childConfig`: `[{ path: 'b' }, { path: '', outlet: 'aux' }]`

Since `getOutlet(aux) !== primary`, the fix **does not ignore it**. `split` pulls in `aux: emptyGroup` as a sibling, instantiating `ComponentC` alongside `ComponentB`. This preserves correct behavior for auxiliary outlets!

fixes #67708

(cherry picked from commit daa9b2a9d6)
2026-04-01 11:48:47 +02:00
Alan Agius d04ddd73df fix(core): prevent binding unsafe attributes on SVG animation elements (#67797)
SVG animation elements (`animate` and `set`) can be used to animate sensitive attributes like `href` or `xlink:href`. Binding to these animation attributes (like `to`, `from`, or `values`) with a sensitive target creates an XSS vector.

This change mitigates this risk by:
1. Classifying `to`, `from`, and `values` on `<animate>` and `<set>` elements as `ATTRIBUTE_NO_BINDING` in the DOM security schema to prevent standard dynamic bindings.
2. Adding runtime validations in `ɵɵvalidateAttribute` to verify that `attributeName` is not a sensitive attribute (such as `href` or `xlink:href`) when processed by a set of `SECURITY_SENSITIVE_ATTRIBUTE_NAMES`. If it is, a runtime error `UNSAFE_ATTRIBUTE_BINDING` is thrown.
3. Adding regression tests in `integration_spec.ts` to ensure unsafe bindings throw an error while safe ones pass correctly.

PR Close #67797
2026-04-01 11:43:59 +02:00
Alan Agius b682c62873 fix(core): treat object[data] as resource URL context (#67797)
Previously, the `data` attribute of the `<object>` tag was being sanitized as a regular URL instead of a `ResourceURL`, which is security-sensitive.
This commit updates the runtime sanitization logic to correctly identify `object[data]` as a `ResourceURL` context. Additionally, the sanitizer lookup logic has been refactored to use a more efficient lookup map (`RESOURCE_MAP`) instead of multiple `Set` lookups, providing better performance and maintainability.

Added tests to verify the correct sanitization of `object[data]` and its behavior with trusted values.

PR Close #67797
2026-04-01 11:43:59 +02:00
Alan Agius fea25d1a60 fix(compiler): register SVG animation attributes in URL security context (#67797)
This change is a security hardening measure to prevent potentially unsafe attribute value manipulation through SVG animations. By mapping `animate|to`, `animate|from`, `animate|values`, and `set|to` to the `SecurityContext.URL`,  Angular will now automatically sanitize these attributes.

PR Close #67797
2026-04-01 11:43:59 +02:00
Kam f916531ee1 docs(docs-infra): improve angular-new-app skill
Consolidate duplicate persona intro, add commonly useful ng new flags
and missing generators to scaffolding guidelines.

(cherry picked from commit f257f54967)
2026-04-01 09:26:41 +02:00
tomer953 8fd896e99a fix(core): resolve component import by exact specifier in route lazy-loading schematic
Avoid substring matching on importClause.getText() which caused suffix collisions (e.g., BarComponent vs FooBarComponent). Use AST-based matching for default and named (including aliased) imports to reliably resolve the correct import path when generating loadComponent.

(cherry picked from commit 8fa6617352)
2026-04-01 09:24:13 +02:00
Kam 895c576d49 refactor(forms): use strict equality for pending status getter
The `pending` getter in `AbstractControl` used loose equality (`==`)
while all other status getters (`valid`, `invalid`, `disabled`) use
strict equality (`===`). Both sides are strings so behavior is
identical, but this inconsistency would fail strict linting rules.

(cherry picked from commit ef7679b7a5)
2026-03-31 13:52:00 +02:00
Angular Robot 5fd4f0f854 build: lock file maintenance
See associated pull request for more information.
2026-03-31 13:42:53 +02:00
Angular Robot 429513bed0 build: update cross-repo angular dependencies
See associated pull request for more information.
2026-03-31 12:02:18 +02:00
Alan Agius f22d9a7098 docs: modernize CLI reference for build and test
Updates the Angular CLI reference to reflect current framework defaults:
- Changes the recommended application builder to @angular/build:application.
- Replaces Jasmine and Web Test Runner with Vitest as test runner examples.

(cherry picked from commit 1a6785874e)
2026-03-31 11:56:07 +02:00
Angular Robot cf7846bd62 build: update pnpm to v10.33.0
See associated pull request for more information.
2026-03-30 12:47:07 +02:00
Matthieu Riegler 9ea8cb6eea docs(docs-infra): sanitize markdown tooltip in Code editor
(cherry picked from commit 0f960a5514)
2026-03-30 12:16:16 +02:00
Alan Agius 3c41e74fdd fix(localize): validate locale in getOutputPathFn to prevent path traversal
The `localize-translate` CLI tool uses the `locale` field from translation files to expand the `{{LOCALE}}` placeholder in the output directory. It failed to sanitize `locale` input, allowing malicious translations to write files outside of the configured output directory.

This change mitigates this issue by combining.

Closes #67906

(cherry picked from commit 7871093822)
2026-03-30 12:15:31 +02:00
Shuaib Hasan Akib efbba945bc docs: align list formatting and improve emphasis
Aligns list formatting, replaces the “Helpful” block with an
IMPORTANT note for better visibility, and removes the separate
example file by inlining the single relevant line directly
in the documentation.
2026-03-30 11:12:29 +02:00
SkyZeroZx 7d54952e85 docs: update signal form tutorial
(cherry picked from commit 713eca74e7)
2026-03-30 11:11:28 +02:00
SkyZeroZx 94b1b530dc docs: update url from <base href>
(cherry picked from commit c6f73f4127)
2026-03-30 10:44:20 +02:00
marktechson 0c9c270552 docs: add agent skills documentation and new readme
(cherry picked from commit 86bbc9474c)
2026-03-27 11:39:57 -07:00
Georgi Serev 9b48cb2989 docs(docs-infra): introduce a custom UrlSerializer
The custom serializer should handle Adev-specific behavior like decoding encoded forward slash similarly to the app host.

(cherry picked from commit 9d79ec6866)
2026-03-27 17:17:55 +01:00
Andrei Chmelev bba5ed8e64 fix(compiler-cli): prevent recursive scope checks for invalid NgModule imports
Avoid recursive local scope lookups when invalid NgModule imports create import cycles.

(cherry picked from commit fcd0bb0db8)
2026-03-27 16:10:08 +01:00
Angular Robot 56c872dacc docs: update cross-repo adev docs
Updated Angular adev cross repo docs files.
2026-03-27 14:13:13 +01:00
Angular Robot c8902deb00 build: update all github actions
See associated pull request for more information.
2026-03-27 14:02:11 +01:00
Angular Robot 4b2008d0f2 build: update cross-repo angular dependencies
See associated pull request for more information.
2026-03-27 14:00:33 +01:00
Andrew Scott b453c0ce67 refactor(language-service): Update getTcbNodesOfTemplateAtPosition to be usable without compiler (#67898)
updates getTcbNodesOfTemplateAtPosition to be usable without ngCompiler instance
so it can be shared with more compiler types

PR Close #67898
2026-03-26 21:32:20 +00:00
Andrew Scott a9ca90fc11 refactor: fix types extraction for private (#67898)
fixes types extraction for private entrypoint of language service

PR Close #67898
2026-03-26 21:32:20 +00:00
Jessica Janiuk 807cabd9e1 fix(dev-infra): improve PR review skill handling of git worktrees
The current PR review skill blindly attempts to checkout branches using the `gh` CLI.
This creates fatal errors if the branch being checked out is already tied to a different git worktree.
Additionally, the logic didn't give the user a clear way to enforce a remote code review when they are the author.

This change updates the `Execution Workflow` to prioritize an explicit user instruction to review remotely, and updates the `Local Code Review` instructions to enforce checking for worktree conflicts before executing local checkouts.
2026-03-26 18:45:39 +01:00
Doug Parker 13f050d220 test: construct local Date objects to fix timezone flakiness
Replaced testing constructions of `Date` objects from `formatDate` tests from plain ISO strings over to 'new Date(year, month, date)'.

Instantiating 'new Date("2024-01-01")' parses the string strictly as UTC midnight ("2024-01-01T00:00:00.000Z"). When local operations execute (such as calculating `getThursdayThisIsoWeek` boundaries), the UTC date shifts relative to the executing machine's timezone. For example, in PST (GMT-8), that date translates exactly to 'December 31st 16:00:00', pushing week boundaries backwards.

By wrapping date constructs explicitly as 'new Date(2024, 0, 1)', it natively guarantees local midnight execution and prevents boundaries shifting on global CI Remote Build Execution (RBE) workers.

Example (from a machine in PST):

```javascript
> new Date('2024-01-01')
Sun Dec 31 2023 16:00:00 GMT-0800 (Pacific Standard Time)
> new Date(2024, 0, 1)
Mon Jan 01 2024 00:00:00 GMT-0800 (Pacific Standard Time)
```

(cherry picked from commit 61ee183fa7)
2026-03-26 18:34:13 +01:00
Doug Parker d0cf2995a2 test: remove unsupported timezone from formatDate tests
Removes the 'America/New_York' timezone string test case from `formatDate` tests because the underlying `Date.parse` API does not support IANA timezone strings. This caused the timezone calculation to silently fall back to the local executing machine's timezone, leading to non-deterministic test flakiness on Remote Build Execution (RBE) workers operating in varying geographic locations.

(cherry picked from commit a1385ad977)
2026-03-26 18:34:13 +01:00
SkyZeroZx a35d572b22 docs: fix indentation that breaks roadmap rendering
(cherry picked from commit ecc616b94b)
2026-03-26 18:33:06 +01:00
cexbrayat 80fd08bcc8 docs: remove private utils from testing skills
(cherry picked from commit 9bb1e78969)
2026-03-26 18:29:24 +01:00
319 changed files with 18807 additions and 15007 deletions
+2 -2
View File
@@ -22,7 +22,7 @@ When reviewing a pull request for the `angular` repository, follow these essenti
- **Payload Size**: Pay attention to the impact of changes on the final client payload size.
3. **Execution Workflow**:
Determine the appropriate review method. If the user explicitly asks for a `local` or `remote` review in their request, that takes precedence. Otherwise, use the GitHub MCP or available scripts to determine if the review should be `local` or `remote`.
Determine the appropriate review method. If the user explicitly asks for a `remote` or `local` review in their request, that takes precedence (e.g. "leave comments on the PR" implies `remote`). Otherwise, use the GitHub MCP or available scripts to determine if the review should be `local` or `remote`.
**Common Review Practices (Applies to both Local and Remote)**
- **Preparation & Checklist**:
@@ -35,7 +35,7 @@ When reviewing a pull request for the `angular` repository, follow these essenti
- **Constructive Feedback**: Provide clear, actionable, and polite feedback. Explain the _why_ behind your suggestions or edits. Do **NOT** leave inline comments purely to praise, agree with, or acknowledge a correct implementation detail, as this clutters the review. If you want to praise the PR, do so in the single general PR comment.
**A. Local Code Review (If the PR is owned by the author requesting the review)**
- **Checkout**: Check out the PR branch locally (if it doesn't already exist, fetch it).
- **Checkout**: Check out the PR branch locally (if it doesn't already exist, fetch it). If checking out the branch fails due to a worktree claim (e.g. "fatal: '<branch>' is already used by worktree at '<path>'"), do the review in that directory.
- **Review & Edit**: Execute the review directly on the code. Instead of adding inline PR comments for suggestions, format the codebase or apply the edits directly to the files.
- **Feedback**: Summarize the review findings and the concrete changes you made in a message to the user, referencing the completed items from your checklist.
- **Do NOT Commit or Push**: Leave the changes uncommitted in the working directory so the user can easily review the pending edits locally. Let the user know the changes are ready for their review, but do not ask for approval to push.
+56
View File
@@ -0,0 +1,56 @@
---
description: Find and fix flaky tests in the repository
---
Investigate flaky tests in the repo and propose fixes to improve stability.
High-level process:
1. Run tests in the repo to look for flakes.
- Consider using Bazel's `--runs_per_test` flag to easily find
flakes.
- Be cognizant of not exhausting all the resources on the current
machine, run a subset of tests at a time such as
`bazel test //packages/core/...`.
2. Once you find some flakes, focus on one at a time.
3. Create a new branch named `flakes/${relevantNameFromTest}`.
4. Reproduce the flake to the best of your ability.
- Consider using `--test_env JASMINE_RANDOM_SEED=1234` to
replicate the broken test ordering.
5. Debug the test to understand the failure mode.
- Consider temporarily disabling / skipping other tests with `xit`
and `fit` to narrow down where the flake might be coming from if
multiple tests are influencing each other.
- Consider temporarily ignoring Firefox tests with
`--test_tag_filters -firefox` if the flake does not appear to be
browser specific.
- Consider using `--test_sharding_strategy disabled` to run the
test in a single shard.
- Try to understand why the test was _flaky_, not just why it
_failed_. Understanding the inconsistency is important to
finding the correct fix.
6. Attempt a fix and validate with `--runs_per_test`.
- Iterate on the fix until you have something which appears to
work.
- If you find yourself stuck and not making meaningful progress,
note down what you've learned/where you're struggling, commit
what you have, look for another flake to fix, and continue. At
the end, surface to the user what you failed to fix.
- Don't try to make significant changes to Angular's runtime
behavior, focus just on making the test pass/fail consistently.
7. Commit the change with relevant details in the commit message and
move on to the next test.
- Be sure to include your theory of why the test was flaky and
how this fix eliminates or reduces that flakiness.
8. Iterate as many times as the user requests you to (default 5
branches if not otherwise specified).
9. Once you can't find any flaky tests or have iterated as many times
as requested, stop and inform the user what you found and fixed.
Additional notes:
- Multiple fixes including the same/related files can go in the same
commit or multiple commits on the same branch.
- Distinct test fixes should go in different branches, make a new one
for each investigation.
- You may push these branches to `origin`, but do not create PRs for
them.
+92 -26
View File
@@ -19248,7 +19248,7 @@ var ChildProcess = class {
return new Promise((resolve5, reject) => {
const commandText = `${command2} ${args.join(" ")}`;
Log.debug(`Executing command: ${commandText}`);
const childProcess = _spawn(command2, args, { ...options, shell: true, stdio: "inherit" });
const childProcess = _spawn(command2, args, { ...options, stdio: "inherit" });
childProcess.on("close", (status) => status === 0 ? resolve5() : reject(status));
});
}
@@ -19256,7 +19256,7 @@ var ChildProcess = class {
const commandText = `${command2} ${args.join(" ")}`;
const env22 = getEnvironmentForNonInteractiveCommand(options.env);
Log.debug(`Executing command: ${commandText}`);
const { status: exitCode, signal, stdout, stderr } = _spawnSync(command2, args, { ...options, env: env22, encoding: "utf8", shell: true, stdio: "pipe" });
const { status: exitCode, signal, stdout, stderr } = _spawnSync(command2, args, { ...options, env: env22, encoding: "utf8", stdio: "pipe" });
const status = statusFromExitCodeAndSignal(exitCode, signal);
if (status === 0 || options.suppressErrorOnFailingExitCode) {
return { status, stdout, stderr };
@@ -19266,7 +19266,7 @@ var ChildProcess = class {
static spawn(command2, args, options = {}) {
const commandText = `${command2} ${args.join(" ")}`;
const env22 = getEnvironmentForNonInteractiveCommand(options.env);
return processAsyncCmd(commandText, options, _spawn(command2, args, { ...options, env: env22, shell: true, stdio: "pipe" }));
return processAsyncCmd(commandText, options, _spawn(command2, args, { ...options, env: env22, stdio: "pipe" }));
}
static exec(command2, options = {}) {
const env22 = getEnvironmentForNonInteractiveCommand(options.env);
@@ -19321,7 +19321,7 @@ ${logOutput}`);
});
}
function determineRepoBaseDirFromCwd() {
const { stdout, stderr, status } = ChildProcess.spawnSync("git", ["rev-parse --show-toplevel"]);
const { stdout, stderr, status } = ChildProcess.spawnSync("git", ["rev-parse", "--show-toplevel"]);
if (status !== 0) {
throw Error(`Unable to find the path to the base directory of the repository.
Was the command run from inside of the repo?
@@ -22978,6 +22978,7 @@ var require_stringify = __commonJS2({
nullStr: "null",
simpleKeys: false,
singleQuote: null,
trailingComma: false,
trueStr: "true",
verifyAliasOrder: true
}, doc.schema.toStringOptions, options);
@@ -23483,12 +23484,19 @@ ${indent}${line}` : "\n";
if (comment)
reqNewline = true;
let str = stringify.stringify(item, itemCtx, () => comment = null);
if (i < items.length - 1)
reqNewline || (reqNewline = lines.length > linesAtValue || str.includes("\n"));
if (i < items.length - 1) {
str += ",";
} else if (ctx.options.trailingComma) {
if (ctx.options.lineWidth > 0) {
reqNewline || (reqNewline = lines.reduce((sum, line) => sum + line.length + 2, 2) + (str.length + 2) > ctx.options.lineWidth);
}
if (reqNewline) {
str += ",";
}
}
if (comment)
str += stringifyComment.lineComment(str, itemIndent, commentString(comment));
if (!reqNewline && (lines.length > linesAtValue || str.includes("\n")))
reqNewline = true;
lines.push(str);
linesAtValue = lines.length;
}
@@ -26418,17 +26426,22 @@ var require_compose_node = __commonJS2({
case "block-map":
case "block-seq":
case "flow-collection":
node = composeCollection.composeCollection(CN, ctx, token, props, onError);
if (anchor)
node.anchor = anchor.source.substring(1);
try {
node = composeCollection.composeCollection(CN, ctx, token, props, onError);
if (anchor)
node.anchor = anchor.source.substring(1);
} catch (error2) {
const message = error2 instanceof Error ? error2.message : String(error2);
onError(token, "RESOURCE_EXHAUSTION", message);
}
break;
default: {
const message = token.type === "error" ? token.message : `Unsupported token (type: ${token.type})`;
onError(token, "UNEXPECTED_TOKEN", message);
node = composeEmptyNode(ctx, token.offset, void 0, null, props, onError);
isSrcToken = false;
}
}
node ?? (node = composeEmptyNode(ctx, token.offset, void 0, null, props, onError));
if (anchor && node.anchor === "")
onError(anchor, "BAD_ALIAS", "Anchor cannot be an empty string");
if (atKey && ctx.options.stringKeys && (!identity.isScalar(node) || typeof node.value !== "string" || node.tag && node.tag !== "tag:yaml.org,2002:str")) {
@@ -32569,6 +32582,54 @@ var types = (
return types2;
}()
);
async function invokeWithRetry(fn, retries = 3, delay = 1e3) {
let attempt = 0;
while (attempt < retries) {
try {
return await fn();
} catch (e) {
attempt++;
if (attempt >= retries) {
throw e;
}
if (isGithubApiError(e) && e.status < 500) {
throw e;
}
if (e instanceof GraphqlResponseError2) {
if (!e.errors) {
throw e;
}
if (e.errors.every((err) => ["NOT_FOUND", "FORBIDDEN", "BAD_USER_INPUT", "UNAUTHENTICATED"].includes(err.type))) {
throw e;
}
}
Log.warn(`GitHub API call failed (attempt ${attempt}/${retries}). Retrying in ${delay}ms...`);
await new Promise((resolve22) => setTimeout(resolve22, delay));
}
}
throw new Error("Unreachable");
}
function createRetryProxy(target) {
return new Proxy(target, {
get(targetObj, prop, receiver) {
const value = Reflect.get(targetObj, prop, receiver);
if (typeof value === "function") {
return new Proxy(value, {
apply(targetFn, thisArg, argArray) {
return invokeWithRetry(() => targetFn.apply(targetObj, argArray));
}
});
}
if (typeof value === "object" && value !== null) {
return createRetryProxy(value);
}
return value;
},
apply(targetFn, thisArg, argArray) {
return invokeWithRetry(() => targetFn.apply(thisArg, argArray));
}
});
}
var GithubClient = class {
constructor(_octokitOptions) {
this._octokitOptions = _octokitOptions;
@@ -32581,18 +32642,18 @@ var GithubClient = class {
},
...this._octokitOptions
});
this.pulls = this._octokit.pulls;
this.orgs = this._octokit.orgs;
this.repos = this._octokit.repos;
this.issues = this._octokit.issues;
this.git = this._octokit.git;
this.rateLimit = this._octokit.rateLimit;
this.teams = this._octokit.teams;
this.search = this._octokit.search;
this.rest = this._octokit.rest;
this.paginate = this._octokit.paginate;
this.checks = this._octokit.checks;
this.users = this._octokit.users;
this.pulls = createRetryProxy(this._octokit.pulls);
this.orgs = createRetryProxy(this._octokit.orgs);
this.repos = createRetryProxy(this._octokit.repos);
this.issues = createRetryProxy(this._octokit.issues);
this.git = createRetryProxy(this._octokit.git);
this.rateLimit = createRetryProxy(this._octokit.rateLimit);
this.teams = createRetryProxy(this._octokit.teams);
this.search = createRetryProxy(this._octokit.search);
this.rest = createRetryProxy(this._octokit.rest);
this.paginate = createRetryProxy(this._octokit.paginate);
this.checks = createRetryProxy(this._octokit.checks);
this.users = createRetryProxy(this._octokit.users);
}
};
var AuthenticatedGithubClient = class extends GithubClient {
@@ -32604,9 +32665,14 @@ var AuthenticatedGithubClient = class extends GithubClient {
});
}
async graphql(queryObject, params2 = {}) {
return await this._graphql(query(queryObject).toString(), params2);
return invokeWithRetry(async () => {
return await this._graphql(query(queryObject).toString(), params2);
});
}
};
function isGithubApiError(obj) {
return obj instanceof Error && obj.constructor.name === "RequestError" && obj.request !== void 0;
}
function isDryRun() {
return process.env["DRY_RUN"] !== void 0;
}
@@ -33529,7 +33595,7 @@ tmp/lib/tmp.js:
(* v8 ignore next -- @preserve *)
(* v8 ignore else -- @preserve *)
@angular/ng-dev/bundles/chunk-YN3IWAKJ.mjs:
@angular/ng-dev/bundles/chunk-G7GMCCSS.mjs:
(*! Bundled license information:
yargs-parser/build/lib/string-utils.js:
@@ -33570,7 +33636,7 @@ tmp/lib/tmp.js:
*)
*)
@angular/ng-dev/bundles/chunk-GLEWX2LH.mjs:
@angular/ng-dev/bundles/chunk-PTDPQBIK.mjs:
(*! Bundled license information:
@octokit/request-error/dist-src/index.js:
+2 -2
View File
@@ -5,9 +5,9 @@ runs:
using: 'composite'
steps:
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Saucelabs Variables
uses: angular/dev-infra/github-actions/saucelabs@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/saucelabs@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Starting Saucelabs tunnel service
shell: bash
run: ./tools/saucelabs/sauce-service.sh run &
+4 -4
View File
@@ -21,17 +21,17 @@ jobs:
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'adev: preview'))
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Build adev
# `snapshot-build` config is used to stamp the exact version with sha in the footer.
run: pnpm bazel build //adev:build.production --config=snapshot-build
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@b9bca459f29f61177a829e69089730415c95ccde
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
workflow-artifact-name: 'adev-preview'
pull-number: '${{github.event.pull_request.number}}'
+5 -3
View File
@@ -32,15 +32,17 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: '${{secrets.GITHUB_TOKEN}}'
persist-credentials: false
- name: Configure Firebase deploy target
working-directory: ./
run: |
# We can use `npx` as the Firebase deploy actions uses it too.
npx -y firebase-tools@latest target:clear --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs
npx -y firebase-tools@latest target:apply --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs ${{env.PREVIEW_SITE}}
# Use stable version release
npx -y firebase-tools@15.15.0 target:clear --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs
npx -y firebase-tools@15.15.0 target:apply --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs ${{env.PREVIEW_SITE}}
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@b9bca459f29f61177a829e69089730415c95ccde
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
github-token: '${{secrets.GITHUB_TOKEN}}'
workflow-artifact-name: 'adev-preview'
@@ -17,6 +17,6 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: angular/dev-infra/github-actions/branch-manager@b9bca459f29f61177a829e69089730415c95ccde
- uses: angular/dev-infra/github-actions/branch-manager@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+2 -2
View File
@@ -34,11 +34,11 @@ jobs:
repository: ${{steps.comment-branch.outputs.head_owner}}/${{steps.comment-branch.outputs.head_repo}}
# Checkout the pull request and assume it being trusted given we've checked
# that the action was triggered by a team member.
ref: ${{steps.comment-branch.outputs.head_ref}}
ref: ${{steps.comment-branch.outputs.head_sha}}
- run: pnpm install --frozen-lockfile
- uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
bazelrc: ./.bazelrc.user
+26 -26
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Check code lint
@@ -41,13 +41,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
disable-package-manager-cache: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -57,7 +57,7 @@ jobs:
- name: Test build
run: pnpm devtools:build:chrome
- name: Cypress run
uses: cypress-io/github-action@ed2d88ccbbf936c5aac087dd43e10425abc3534f # v7.1.7
uses: cypress-io/github-action@783cb3f07983868532cabaedaa1e6c00ff4786a8 # v7.1.9
with:
command: pnpm devtools:test:e2e
start: pnpm bazel run //devtools/src:devserver
@@ -69,11 +69,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -85,11 +85,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -102,11 +102,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -121,11 +121,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -138,11 +138,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Install node modules
run: pnpm install --frozen-lockfile
- run: echo "https://${{secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN}}:@github.com" > ${HOME}/.git_credentials
@@ -154,11 +154,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -208,11 +208,11 @@ jobs:
runs-on: ubuntu-latest-8core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Build adev
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
ANGULAR_READONLY_GITHUB_TOKEN: ${{ secrets.READONLY_GITHUB_TOKEN }}
- name: Create a PR (if necessary)
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.ANGULAR_ROBOT_ACCESS_TOKEN }}
push-to-fork: 'angular-robot/angular'
+3 -3
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: angular/dev-infra/github-actions/labeling/pull-request@b9bca459f29f61177a829e69089730415c95ccde
- uses: angular/dev-infra/github-actions/labeling/pull-request@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
labels: '{"requires: TGP": ["packages/core/primitives/**/{*,.*}"]}'
@@ -25,14 +25,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: angular/dev-infra/github-actions/post-approval-changes@b9bca459f29f61177a829e69089730415c95ccde
- uses: angular/dev-infra/github-actions/post-approval-changes@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
issue_labels:
if: github.event_name == 'issues'
runs-on: ubuntu-latest
steps:
- uses: angular/dev-infra/github-actions/labeling/issue@b9bca459f29f61177a829e69089730415c95ccde
- uses: angular/dev-infra/github-actions/labeling/issue@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
google-generative-ai-key: ${{ secrets.GOOGLE_GENERATIVE_AI_KEY }}
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: angular/dev-infra/github-actions/google-internal-tests@b9bca459f29f61177a829e69089730415c95ccde
- uses: angular/dev-infra/github-actions/google-internal-tests@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
run-tests-guide-url: http://go/angular-g3sync-start
github-token: ${{ secrets.GITHUB_TOKEN }}
+4 -4
View File
@@ -13,15 +13,15 @@ jobs:
JOBS: 2
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Saucelabs Variables
uses: angular/dev-infra/github-actions/saucelabs@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/saucelabs@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Set up Sauce Tunnel Daemon
run: pnpm bazel run //tools/saucelabs-daemon/background-service -- $JOBS &
env:
+1 -1
View File
@@ -9,6 +9,6 @@ jobs:
status:
runs-on: ubuntu-latest
steps:
- uses: angular/dev-infra/github-actions/unified-status-check@b9bca459f29f61177a829e69089730415c95ccde
- uses: angular/dev-infra/github-actions/unified-status-check@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
workflows: ${{ steps.workflows.outputs.workflows }}
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Install node modules
run: pnpm install --frozen-lockfile
- id: workflows
@@ -36,9 +36,9 @@ jobs:
workflow: ${{ fromJSON(needs.list.outputs.workflows) }}
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Install node modules
run: pnpm install --frozen-lockfile
# We utilize the google-github-actions/auth action to allow us to get an active credential using workflow
+21 -21
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Check code lint
@@ -39,7 +39,7 @@ jobs:
- name: Check code format
run: pnpm ng-dev format changed --check ${{ github.event.pull_request.base.sha }}
- name: Check Package Licenses
uses: angular/dev-infra/github-actions/linting/licenses@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/linting/licenses@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
allow-dependencies-licenses: 'pkg:npm/google-protobuf@'
@@ -47,13 +47,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
disable-package-manager-cache: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Run unit tests
@@ -61,7 +61,7 @@ jobs:
- name: Test build
run: pnpm devtools:build:chrome
- name: Cypress run
uses: cypress-io/github-action@ed2d88ccbbf936c5aac087dd43e10425abc3534f # v7.1.7
uses: cypress-io/github-action@783cb3f07983868532cabaedaa1e6c00ff4786a8 # v7.1.9
with:
command: pnpm devtools:test:e2e
start: pnpm bazel run //devtools/src:devserver
@@ -73,11 +73,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Run CI tests for framework
@@ -97,11 +97,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Run integration CI tests for framework
@@ -112,11 +112,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Run tests
@@ -129,11 +129,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Run tests
@@ -144,11 +144,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@b9bca459f29f61177a829e69089730415c95ccde
uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
- name: Install node modules
run: pnpm install --frozen-lockfile
- run: |
+1 -1
View File
@@ -47,6 +47,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: 'Upload to code-scanning'
uses: github/codeql-action/upload-sarif@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
with:
sarif_file: results.sarif
+1 -1
View File
@@ -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:
+88
View File
@@ -1,3 +1,91 @@
<a name="21.2.10"></a>
# 21.2.10 (2026-04-22)
### docs
| Commit | Type | Description |
| -- | -- | -- |
| [0d5ee9ae1b](https://github.com/angular/angular/commit/0d5ee9ae1ba4b7acd8f27a059a778f0b4bd8a5bd) | fix | link formatting in "Animating your Application with CSS" |
### migrations
| Commit | Type | Description |
| -- | -- | -- |
| [5533ab4f56](https://github.com/angular/angular/commit/5533ab4f56f574bc9365cf0573c4a34a3ab5aaf1) | fix | fix NgClass leaving trailing comma after removal |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [580212c995](https://github.com/angular/angular/commit/580212c995751c4bf4ce8a49df4167498743e0ea) | fix | restore internal URL on popstate when `browserUrl` is used |
<!-- CHANGELOG SPLIT MARKER -->
<a name="21.2.9"></a>
# 21.2.9 (2026-04-15)
### core
| Commit | Type | Description |
| -- | -- | -- |
| [f603d4714f](https://github.com/angular/angular/commit/f603d4714fa184aad34a6f7f9ea4e79c8af3afac) | fix | escape forward slashes in transfer state to prevent crawler indexing |
### http
| Commit | Type | Description |
| -- | -- | -- |
| [540536c386](https://github.com/angular/angular/commit/540536c386f2c735a700c2c9e2697a88dcb3d4ec) | fix | add CSP nonce support to JsonpClientBackend |
| [63a857b874](https://github.com/angular/angular/commit/63a857b874172766451aa75ed3347ba50f0ee229) | fix | Don't on Passthru outside of reactive context |
### platform-server
| Commit | Type | Description |
| -- | -- | -- |
| [e0b5078cf2](https://github.com/angular/angular/commit/e0b5078cf2ebe79a6de85e9123148ae948b3d81d) | fix | prevent SSRF bypasses via protocol-relative and backslash URLs |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [684e9fd53d](https://github.com/angular/angular/commit/684e9fd53daacb9e910f42d98c6017f9e5cb4180) | fix | normalize multiple leading slashes in URL parser |
<!-- CHANGELOG SPLIT MARKER -->
<a name="21.2.8"></a>
# 21.2.8 (2026-04-08)
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [e40d378f3e](https://github.com/angular/angular/commit/e40d378f3e3e7e57a45c8fbd9565ee06a3a6a13f) | fix | handle nested brackets in host object bindings |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [2c6781071f](https://github.com/angular/angular/commit/2c6781071f52d6378a002fba6611bb283fbb2fde) | fix | error for type parameter declarations |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [82192deda9](https://github.com/angular/angular/commit/82192deda9c07113835e6c85af3f2c8c8218cda0) | fix | handle missing serialized container hydration data |
| [057cc6d09d](https://github.com/angular/angular/commit/057cc6d09d234f401a810cfdd3ad14127652b88b) | fix | remove obsolete iOS cursor pointer hack in event delegation |
### language-service
| Commit | Type | Description |
| -- | -- | -- |
| [7797671257](https://github.com/angular/angular/commit/7797671257350665e8b3ceb2bc6a0201829dd338) | fix | get quick info at local var location to align with TS semantics and support type narrowing |
<!-- CHANGELOG SPLIT MARKER -->
<a name="21.2.7"></a>
# 21.2.7 (2026-04-01)
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [fea25d1a60](https://github.com/angular/angular/commit/fea25d1a60ecaba1599d9cd9b8df27109ed195c5) | fix | register SVG animation attributes in URL security context ([#67797](https://github.com/angular/angular/pull/67797)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [bba5ed8e64](https://github.com/angular/angular/commit/bba5ed8e643b9c3f680e7e539c3d744ad6905e59) | fix | prevent recursive scope checks for invalid NgModule imports |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [d04ddd73df](https://github.com/angular/angular/commit/d04ddd73dfc03f420afbdde964c5119f338af135) | fix | prevent binding unsafe attributes on SVG animation elements ([#67797](https://github.com/angular/angular/pull/67797)) |
| [8fd896e99a](https://github.com/angular/angular/commit/8fd896e99a13855c6569f29efe7e578c301e13ee) | fix | resolve component import by exact specifier in route lazy-loading schematic |
| [b682c62873](https://github.com/angular/angular/commit/b682c628731b86a4884e50abb2f5fa73ac0ad057) | fix | treat `object[data]` as resource URL context ([#67797](https://github.com/angular/angular/pull/67797)) |
### localize
| Commit | Type | Description |
| -- | -- | -- |
| [3c41e74fdd](https://github.com/angular/angular/commit/3c41e74fdd279f683156b654699a9312a850add0) | fix | validate locale in getOutputPathFn to prevent path traversal |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [0960592d3d](https://github.com/angular/angular/commit/0960592d3d4fad110d5598144fda9f2488520826) | fix | pass outlet context to split to fix empty path named outlets |
<!-- CHANGELOG SPLIT MARKER -->
<a name="21.2.6"></a>
# 21.2.6 (2026-03-25)
### common
+2 -2
View File
@@ -71,7 +71,7 @@ You can file new issues by selecting from our [new issue templates](https://gith
### <a name="pr-quality"></a> Contribution Quality
We strongly value open source contribution and pull requests from community contributors. Please note that every pull request is reviewed and merged by an actual person on the team, which does take time and effort. That is time and effort that does take away from other valuable work. With that in mind we have an minimum set of expectations that are required of any community contribution pull request that is opened.
We strongly value open source contribution and pull requests from community contributors. Please note that every pull request is reviewed and merged by an actual person on the team, which does take time and effort. That is time and effort that does take away from other valuable work. With that in mind we have a minimum set of expectations that are required of any community contribution pull request that is opened.
1. Search [GitHub](https://github.com/angular/angular/pulls) for an open or closed PR that relates to your submission.
- You don't want to duplicate existing efforts.
@@ -238,7 +238,7 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise
- For corporations, we'll need you to
[print, sign and one of scan+email, fax or mail the form][corporate-cla].
If you have more than one GitHub accounts, or multiple email addresses associated with a single GitHub account, you must sign the CLA using the primary email address of the GitHub account used to author Git commits and send pull requests.
If you have more than one GitHub account, or multiple email addresses associated with a single GitHub account, you must sign the CLA using the primary email address of the GitHub account used to author Git commits and send pull requests.
The following documents can help you sort out issues with GitHub accounts and multiple email addresses:
+15 -17
View File
@@ -5,41 +5,41 @@ module(
)
bazel_dep(name = "rules_pkg", version = "1.2.0")
bazel_dep(name = "rules_nodejs", version = "6.7.3")
bazel_dep(name = "aspect_rules_ts", version = "3.8.7")
bazel_dep(name = "rules_nodejs", version = "6.7.4")
bazel_dep(name = "aspect_rules_ts", version = "3.8.8")
bazel_dep(name = "aspect_rules_js", version = "3.0.3")
bazel_dep(name = "aspect_rules_esbuild", version = "0.25.1")
bazel_dep(name = "aspect_rules_jasmine", version = "2.0.4")
bazel_dep(name = "aspect_rules_rollup", version = "2.0.1")
bazel_dep(name = "bazel_skylib", version = "1.9.0")
bazel_dep(name = "bazel_lib", version = "3.2.2")
bazel_dep(name = "tar.bzl", version = "0.9.0")
bazel_dep(name = "yq.bzl", version = "0.3.5")
bazel_dep(name = "tar.bzl", version = "0.10.1")
bazel_dep(name = "yq.bzl", version = "0.3.6")
bazel_dep(name = "rules_angular")
git_override(
module_name = "rules_angular",
commit = "af626f77ad610d1a9c47ee317af88e2c8edd66a4",
commit = "03dade2ea0ea355e13ca88c550eaa633191b16ec",
remote = "https://github.com/angular/rules_angular.git",
)
bazel_dep(name = "devinfra")
git_override(
module_name = "devinfra",
commit = "b9bca459f29f61177a829e69089730415c95ccde",
commit = "e04d90adad1a125b29fbc4d97f425798768a8cb1",
remote = "https://github.com/angular/dev-infra.git",
)
bazel_dep(name = "rules_sass")
git_override(
module_name = "rules_sass",
commit = "13918bec49cd183a591e3781d1d08044b4aa9f61",
commit = "dfb751533767caa759a7162a34cfe0852f988976",
remote = "https://github.com/angular/rules_sass.git",
)
bazel_dep(name = "rules_browsers")
git_override(
module_name = "rules_browsers",
commit = "46029652792eac7bb51b6abd04503af296db70ea",
commit = "4de93bbfdbee2cea5162ac8070eb15846b15133d",
remote = "https://github.com/angular/rules_browsers.git",
)
@@ -71,8 +71,8 @@ use_repo(node, "nodejs_windows_amd64")
pnpm = use_extension("@aspect_rules_js//npm:extensions.bzl", "pnpm")
pnpm.pnpm(
name = "pnpm",
pnpm_version = "10.32.1",
pnpm_version_integrity = "sha512-pwaTjw6JrBRWtlY+q07fHR+vM2jRGR/FxZeQ6W3JGORFarLmfWE94QQ9LoyB+HMD5rQNT/7KnfFe8a1Wc0jyvg==",
pnpm_version = "10.33.0",
pnpm_version_integrity = "sha512-EFaLtKavtYyes2MNqQzJUWQXq+vT+rvmc58K55VyjaFJHp21pUTHatjrdXD1xLs9bGN7LLQb/c20f6gjyGSTGQ==",
)
use_repo(pnpm, "pnpm")
@@ -125,14 +125,12 @@ use_repo(rules_ts_ext, **{"npm_typescript": "angular_npm_typescript"})
# TODO: Figure out how to make ng_project update whenever the packages/core::pkg target changes.
rules_angular = use_extension("@rules_angular//setup:extensions.bzl", "rules_angular")
use_repo_rule("@rules_angular//setup:repositories.bzl", "configurable_deps_repo")(
name = "rules_angular_configurable_deps",
angular_compiler_cli = "@angular//:node_modules/@angular/compiler-cli",
typescript = "@angular//:node_modules/typescript",
rules_angular.setup(
name = "angular_rules_angular_configurable_deps",
angular_compiler_cli = "//:node_modules/@angular/compiler-cli",
typescript = "//:node_modules/typescript",
)
override_repo(rules_angular, "rules_angular_configurable_deps")
use_repo(rules_angular, rules_angular_configurable_deps = "angular_rules_angular_configurable_deps")
register_toolchains(
"@devinfra//bazel/git-toolchain:git_linux_toolchain",
+33 -32
View File
@@ -28,8 +28,8 @@
"https://bcr.bazel.build/modules/aspect_rules_js/3.0.3/source.json": "bb8fff9a304452e1042af9522ad1d54d6f1d1fdf71c5127deadb6fd156654193",
"https://bcr.bazel.build/modules/aspect_rules_rollup/2.0.1/MODULE.bazel": "296e3a053658c2af989ba9bd62a205e6d1fa84bdd6dd5249196546e6b84770ec",
"https://bcr.bazel.build/modules/aspect_rules_rollup/2.0.1/source.json": "2fe8ac1ccb4de74bf884761e070010280b272d94e3997205b361b91c75409726",
"https://bcr.bazel.build/modules/aspect_rules_ts/3.8.7/MODULE.bazel": "830f8a53bb9f1139c24006a90ddc0230481326d69fa847eb00daf8eaae118724",
"https://bcr.bazel.build/modules/aspect_rules_ts/3.8.7/source.json": "95549d64e28f3e4e3648cc037cefdac01ec3b0f58fced2409c286784e82ad0f0",
"https://bcr.bazel.build/modules/aspect_rules_ts/3.8.8/MODULE.bazel": "b52b929a948438665809d49af610f58d1b14f63d6d21ab748f47b6050be4c1f6",
"https://bcr.bazel.build/modules/aspect_rules_ts/3.8.8/source.json": "5414530b761a45ab7ca6c49f0a2a9cf8dc0da772f5037cf05ca18aaa64bb1b19",
"https://bcr.bazel.build/modules/aspect_tools_telemetry/0.2.6/MODULE.bazel": "cafb8781ad591bc57cc765dca5fefab08cf9f65af363d162b79d49205c7f8af7",
"https://bcr.bazel.build/modules/aspect_tools_telemetry/0.2.8/MODULE.bazel": "aa975a83e72bcaac62ee61ab12b788ea324a1d05c4aab28aadb202f647881679",
"https://bcr.bazel.build/modules/aspect_tools_telemetry/0.3.3/MODULE.bazel": "37c764292861c2f70314efa9846bb6dbb44fc0308903b3285da6528305450183",
@@ -161,7 +161,8 @@
"https://bcr.bazel.build/modules/rules_nodejs/6.2.0/MODULE.bazel": "ec27907f55eb34705adb4e8257952162a2d4c3ed0f0b3b4c3c1aad1fac7be35e",
"https://bcr.bazel.build/modules/rules_nodejs/6.5.0/MODULE.bazel": "546d0cf79f36f9f6e080816045f97234b071c205f4542e3351bd4424282a8810",
"https://bcr.bazel.build/modules/rules_nodejs/6.7.3/MODULE.bazel": "c22a48b2a0dbf05a9dc5f83837bbc24c226c1f6e618de3c3a610044c9f336056",
"https://bcr.bazel.build/modules/rules_nodejs/6.7.3/source.json": "a3f966f4415a8a6545e560ee5449eac95cc633f96429d08e87c87775c72f5e09",
"https://bcr.bazel.build/modules/rules_nodejs/6.7.4/MODULE.bazel": "e6a241a55c82e999145553d2e00a08fc6ebadf62b63d108fb5e984696ffd0bd2",
"https://bcr.bazel.build/modules/rules_nodejs/6.7.4/source.json": "34e7a8a3b4c8d630ac0e0492b3fed9dba41fe008a0edf220b7d88fa38ac53698",
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
"https://bcr.bazel.build/modules/rules_pkg/1.2.0/MODULE.bazel": "c7db3c2b407e673c7a39e3625dc05dc9f12d6682cbd82a3a5924a13b491eda7e",
@@ -193,16 +194,16 @@
"https://bcr.bazel.build/modules/stardoc/0.7.1/MODULE.bazel": "3548faea4ee5dda5580f9af150e79d0f6aea934fc60c1cc50f4efdd9420759e7",
"https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5",
"https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216",
"https://bcr.bazel.build/modules/tar.bzl/0.10.1/MODULE.bazel": "bf5fda5b5ccef8c3c4a5f4886144377386e0baa382972f257acb42dcf40ea908",
"https://bcr.bazel.build/modules/tar.bzl/0.10.1/source.json": "3f1beb35acf53c270a9de493cdc775a985551d7069cfcf24e136b42f683bbb10",
"https://bcr.bazel.build/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468",
"https://bcr.bazel.build/modules/tar.bzl/0.5.1/MODULE.bazel": "7c2eb3dcfc53b0f3d6f9acdfd911ca803eaf92aadf54f8ca6e4c1f3aee288351",
"https://bcr.bazel.build/modules/tar.bzl/0.6.0/MODULE.bazel": "a3584b4edcfafcabd9b0ef9819808f05b372957bbdff41601429d5fd0aac2e7c",
"https://bcr.bazel.build/modules/tar.bzl/0.9.0/MODULE.bazel": "452a22d7f02b1c9d7a22ab25edf20f46f3e1101f0f67dc4bfbf9a474ddf02445",
"https://bcr.bazel.build/modules/tar.bzl/0.9.0/source.json": "c732760a374831a2cf5b08839e4be75017196b4d796a5aa55235272ee17cd839",
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
"https://bcr.bazel.build/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072",
"https://bcr.bazel.build/modules/yq.bzl/0.3.2/MODULE.bazel": "0384efa70e8033d842ea73aa4b7199fa099709e236a7264345c03937166670b6",
"https://bcr.bazel.build/modules/yq.bzl/0.3.5/MODULE.bazel": "130c603e54be717bdf84100210f06598a0d2b4b4e01888fb01b70f50f41767ec",
"https://bcr.bazel.build/modules/yq.bzl/0.3.5/source.json": "1ae7bdc03cb26aaa8bd2bceadf65e90d90f0b2d03008ba9a0564da2e21396c39",
"https://bcr.bazel.build/modules/yq.bzl/0.3.6/MODULE.bazel": "985c2a0cb4ad9994bb0e33cc7fae931c91105eeefe3faa355b8f4c258d0607c0",
"https://bcr.bazel.build/modules/yq.bzl/0.3.6/source.json": "678aaf6e291164f3cd761bb3e872e8a151248f413dbb63c5524a50b82a5bc890",
"https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
"https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca",
@@ -213,7 +214,7 @@
"moduleExtensions": {
"@@aspect_rules_esbuild+//esbuild:extensions.bzl%esbuild": {
"general": {
"bzlTransitiveDigest": "GnYkDpVOnWnYv+xwyhEi0qjk3Lvp4Wei30PeSIQF5vM=",
"bzlTransitiveDigest": "QxNkTCcD6yURsbqznKyC/WCjCyhHZbEipGXl2UQjjzY=",
"usagesDigest": "6We6zwGoawD9YXqMI0KPaxEKJTnamXBsuOekhFS2D40=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
@@ -428,8 +429,8 @@
},
"@@aspect_rules_ts+//ts:extensions.bzl%ext": {
"general": {
"bzlTransitiveDigest": "GbfZLeEI0W26fAQ89dUljyLGSAkzkr3CkF+VDQ0IFlI=",
"usagesDigest": "CnGVBnDYq2qAfYkDXgJlDcJckUt2NU51jQQX+igoGt8=",
"bzlTransitiveDigest": "dhTbv9E6UfT1WJmmu3ORRPO6AKFJvgBjBxu+BO+u1RY=",
"usagesDigest": "QJswGu07xQeMlf+NomctjP9AY7OKl/U5GqMvEr6U1g8=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -447,8 +448,8 @@
"rules_angular_npm_typescript": {
"repoRuleId": "@@aspect_rules_ts+//ts/private:npm_repositories.bzl%http_archive_version",
"attributes": {
"version": "5.9.3",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"version": "6.0.2",
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
"urls": [
"https://registry.npmjs.org/typescript/-/typescript-{}.tgz"
]
@@ -457,8 +458,8 @@
"npm_typescript": {
"repoRuleId": "@@aspect_rules_ts+//ts/private:npm_repositories.bzl%http_archive_version",
"attributes": {
"version": "5.9.3",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"version": "6.0.2",
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
"urls": [
"https://registry.npmjs.org/typescript/-/typescript-{}.tgz"
]
@@ -467,8 +468,8 @@
"npm_rules_browsers_typescript": {
"repoRuleId": "@@aspect_rules_ts+//ts/private:npm_repositories.bzl%http_archive_version",
"attributes": {
"version": "5.9.3",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"version": "6.0.2",
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
"urls": [
"https://registry.npmjs.org/typescript/-/typescript-{}.tgz"
]
@@ -492,7 +493,7 @@
"@@aspect_tools_telemetry+//:extension.bzl%telemetry": {
"general": {
"bzlTransitiveDigest": "cl5A2O84vDL6Tt+Qga8FCj1DUDGqn+e7ly5rZ+4xvcc=",
"usagesDigest": "mS7+2ykIUJH5qIIq/jathl5rcwdqyvYA8HOpHe8d+/Y=",
"usagesDigest": "XReaXejedAKAFKyp+2mfnFkPWXBJzM/VxiOYB4nyseY=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -501,7 +502,7 @@
"repoRuleId": "@@aspect_tools_telemetry+//:extension.bzl%tel_repository",
"attributes": {
"deps": {
"aspect_rules_ts": "3.8.7",
"aspect_rules_ts": "3.8.8",
"aspect_rules_js": "3.0.3",
"aspect_rules_esbuild": "0.25.1",
"aspect_rules_jasmine": "2.0.4",
@@ -560,24 +561,24 @@
},
"@@rules_angular+//setup:extensions.bzl%rules_angular": {
"general": {
"bzlTransitiveDigest": "fkaH7HMicL3g7/NDaFzlq39kcLopMyQ3KdbDn+5CRzA=",
"usagesDigest": "Li29KHtfoig0M7RQAoy9uoACa/RcVICtFkFkARsVOAA=",
"bzlTransitiveDigest": "aS7Uud1IzoU7PPLzH3s6IfFS4b2fa0SRWDi2/fS4bQU=",
"usagesDigest": "PaJB/TvnSzJTbqGUeIfiFAEjGkG4FEW7es6f6MFMtq8=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"angular_rules_angular_configurable_deps": {
"repoRuleId": "@@rules_angular+//setup:repositories.bzl%configurable_deps_repo",
"attributes": {
"angular_compiler_cli": "@@//:node_modules/@angular/compiler-cli",
"typescript": "@@//:node_modules/typescript"
}
},
"rules_angular_configurable_deps": {
"repoRuleId": "@@rules_angular+//setup:repositories.bzl%configurable_deps_repo",
"attributes": {
"angular_compiler_cli": "@@rules_angular+//:node_modules/@angular/compiler-cli",
"typescript": "@@rules_angular+//:node_modules/typescript"
}
},
"dev_infra_rules_angular_configurable_deps": {
"repoRuleId": "@@rules_angular+//setup:repositories.bzl%configurable_deps_repo",
"attributes": {
"angular_compiler_cli": "@@rules_angular+//:node_modules/@angular/compiler-cli",
"typescript": "@@rules_angular+//:node_modules/typescript"
"angular_compiler_cli": "@@//:node_modules/@angular/compiler-cli",
"typescript": "@@//:node_modules/typescript"
}
}
},
@@ -945,8 +946,8 @@
},
"@@rules_nodejs+//nodejs:extensions.bzl%node": {
"general": {
"bzlTransitiveDigest": "4pUxCNc22K4I+6+4Nxu52Hur12tFRfa1JMsN5mdDv60=",
"usagesDigest": "n80KYSy9Qdp3Z/R7I+m1NDH4klVgoxFaY608KacpRUQ=",
"bzlTransitiveDigest": "oZFClfRhTTwsYzpxVPkOpOt/r0+OzEfEV37au0jFZ0s=",
"usagesDigest": "dp2HPl9Y2BFhrnM0JALIsJHz02rKnRBpISBHQX4qV7E=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -4162,7 +4163,7 @@
"@@yq.bzl+//yq:extensions.bzl%yq": {
"general": {
"bzlTransitiveDigest": "UfFMy8CWK4/dVo/tfaSAIYUiDGNAPes5eRllx9O9Q9Q=",
"usagesDigest": "263D9xYtKhXWWCVTxT66bpT89HuZpdkB1AqEir45vbY=",
"usagesDigest": "5cUmZOEOibp2h65JoFppstHiXcjDFil/AG+HgD3avRk=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
+6 -6
View File
@@ -5,21 +5,21 @@
"@algolia/requester-browser-xhr": "5.48.0",
"@algolia/requester-node-http": "5.48.0",
"@angular/animations": "workspace:*",
"@angular/aria": "21.2.4",
"@angular/build": "21.2.3",
"@angular/cdk": "21.2.4",
"@angular/cli": "21.2.3",
"@angular/aria": "21.2.7",
"@angular/build": "21.2.8",
"@angular/cdk": "21.2.7",
"@angular/cli": "21.2.8",
"@angular/common": "workspace:*",
"@angular/compiler": "workspace:*",
"@angular/compiler-cli": "workspace:*",
"@angular/core": "workspace:*",
"@angular/docs": "workspace:*",
"@angular/forms": "workspace:*",
"@angular/material": "21.2.4",
"@angular/material": "21.2.7",
"@angular/platform-browser": "workspace:*",
"@angular/platform-server": "workspace:*",
"@angular/router": "workspace:*",
"@angular/ssr": "21.2.3",
"@angular/ssr": "21.2.8",
"@codemirror/autocomplete": "6.20.0",
"@codemirror/commands": "6.10.1",
"@codemirror/lang-angular": "0.1.4",
@@ -53,7 +53,7 @@
matrixParams: 'ignored',
fragment: 'ignored',
}"
(click)="emitClickOnLink()"
(click)="emitClickOnLink(item)"
[matTooltip]="item.label"
[matTooltipDisabled]="itemLabel.length < 27"
matTooltipPosition="after"
@@ -153,5 +153,6 @@ describe('NavigationList', () => {
class FakeNavigationListState {
isOpened = signal(true);
activeNavigationItem = signal(navigationItems.at(1));
crossCategoryOrigin = signal<NavigationItem | undefined>(undefined);
toggleItem(item: NavigationItem) {}
}
@@ -40,6 +40,7 @@ export class NavigationList {
readonly linkClicked = output<void>();
private readonly navigationState = inject(NavigationState);
private readonly crossCategoryOrigin = this.navigationState.crossCategoryOrigin;
readonly activeItem = this.navigationState.activeNavigationItem;
@@ -51,10 +52,19 @@ export class NavigationList {
) {
return;
}
const prevParentItem = this.crossCategoryOrigin();
if (prevParentItem) {
this.crossCategoryOrigin.set(undefined);
this.navigationState.toggleItem(prevParentItem);
return;
}
this.navigationState.toggleItem(item);
}
emitClickOnLink(): void {
emitClickOnLink(item: NavigationItem): void {
if (item.isCrossReferenced) {
this.crossCategoryOrigin.set(item.parent);
}
this.linkClicked.emit();
}
@@ -27,14 +27,16 @@
</i>
</span>
<!-- Page title -->
<span [innerHtml]="result.labelHtml"></span>
@if (result.package) {
<span
[innerHTML]="result.package"
class="docs-search-result__label__package"
></span>
}
<!-- Page title and package badge -->
<span class="docs-search-result__label__text">
<span [innerHtml]="result.labelHtml"></span>
@if (result.package) {
<span
[innerHTML]="result.package"
class="docs-search-result__label__package"
></span>
}
</span>
</p>
@if (result.subLabelHtml) {
@@ -46,6 +46,7 @@ dialog {
.docs-search-result-icon {
display: inline-block;
flex-shrink: 0;
i {
display: flex;
@@ -91,7 +92,20 @@ dialog {
&__label {
font-weight: 600;
flex-wrap: wrap;
&__text {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.25rem 0.5rem;
flex: 1;
min-width: 0;
overflow-wrap: break-word;
> * {
min-width: 0;
}
}
&__package {
font-size: 0.75rem;
@@ -17,5 +17,6 @@ export interface NavigationItem {
contentPath?: string;
status?: 'new' | 'updated';
category?: string;
isCrossReferenced?: boolean;
preserveOtherCategoryOrder?: boolean; // true by default
}
@@ -59,10 +59,16 @@ export function ClassMember(props: {member: MemberEntryRenderable}) {
const memberName = member.name;
const displayName = member.displayName;
const returnType = getMemberType(member);
const label = displayName ?? memberName;
return (
<div id={memberName} className={REFERENCE_MEMBER_CARD}>
<header className={REFERENCE_MEMBER_CARD_HEADER}>
<h3>{displayName ?? memberName}</h3>
<h3>
<a class="docs-anchor" href={'#' + memberName}>
{label}
</a>
</h3>
{isClassMethodEntry(member) && member.signatures.length > 1 ? (
<span>{member.signatures.length} overloads</span>
) : returnType ? (
@@ -13,6 +13,8 @@ import {setSymbols} from '../symbol-context.mjs';
import {resolve} from 'path';
import {initHighlighter} from '../../../shared/shiki.mjs';
import {setHighlighterInstance} from '../shiki/shiki.mjs';
import {renderEntry} from '../rendering.mjs';
import {JSDOM} from 'jsdom';
// Note: The tests will probably break if the schema of the api extraction changes.
// All entries in the fake-entries are extracted from Angular's api.
@@ -60,4 +62,21 @@ describe('renderable', () => {
expect(linkedSignal!.experimental).toBe(undefined);
expect(linkedSignal!.stable).toBe(undefined);
});
it('should render docs-anchor links in class member card headers', () => {
const viewRef = entries.get('ViewRef')!;
expect(viewRef).toBeDefined();
const html = renderEntry(viewRef);
const fragment = JSDOM.fragment(html);
const memberCards = fragment.querySelectorAll('.docs-reference-member-card');
expect(memberCards.length).toBeGreaterThan(0);
for (const card of Array.from(memberCards)) {
const id = card.getAttribute('id')!;
const anchor = card.querySelector('h3 a.docs-anchor') as HTMLAnchorElement;
expect(anchor.getAttribute('href')).toBe(`#${id}`);
}
});
});
@@ -95,6 +95,7 @@ export async function addRenderableCodeToc<T extends DocEntry & HasModuleName>(
{
language: 'typescript',
apiEntries: getSymbolsAsApiEntries(),
removeWhitespace: false,
},
);
+2 -1
View File
@@ -42,6 +42,7 @@ export function codeToHtml(
apiEntries?: ApiEntries;
language?: string;
highlight?: Set<number>;
removeWhitespace?: boolean;
},
): string {
const html = highlighter.codeToHtml(code, {
@@ -53,7 +54,7 @@ export function codeToHtml(
cssVariablePrefix: '--shiki-',
defaultColor: false,
transformers: [
removeWhitespaceTransformer(),
...(config.removeWhitespace ? [removeWhitespaceTransformer()] : []),
highlightTransformer(config.highlight),
linkApiEntriesTransformer(config.apiEntries),
],
@@ -21,6 +21,7 @@ export class NavigationState {
private readonly _isMobileNavVisible = signal<boolean>(false);
private readonly _level = linkedSignal(() => this._expandedItems().length);
readonly crossCategoryOrigin = signal<NavigationItem | undefined>(undefined);
readonly primaryActiveRouteItem = signal<string | null>(null);
activeNavigationItem = this._activeNavigationItem.asReadonly();
expandedItems = this._expandedItems.asReadonly();
+18 -2
View File
@@ -1,6 +1,8 @@
// Card Grid
@mixin docs-card() {
$compact-breakpoint: 550px;
.docs-card-container-wrapper {
border: 1px solid var(--senary-contrast);
border-radius: 0.25rem;
@@ -15,6 +17,11 @@
h2 {
padding: 2.5rem 1rem 2.5rem 2.5rem;
min-width: 350px;
@container docs-content (max-width: $compact-breakpoint) {
min-width: auto;
padding: 1.5rem;
}
}
.theme-fill-bg {
@@ -57,7 +64,11 @@
svg {
fill-opacity: 1;
@container header (max-width: 550px) {
@container header (max-width: $compact-breakpoint) {
display: none;
}
@container docs-content (max-width: $compact-breakpoint) {
display: none;
}
@@ -71,6 +82,11 @@
margin: 1rem;
padding: 1.5rem;
@container docs-content (max-width: $compact-breakpoint) {
margin: 0.5rem;
padding: 0.75rem;
}
.docs-card {
margin: 0;
@@ -252,7 +268,7 @@
.docs-nav-card-svg {
width: 350px;
@container header (max-width: 550px) {
@container header (max-width: $compact-breakpoint) {
display: none;
}
}
+3
View File
@@ -35,6 +35,9 @@
vertical-align: top;
min-width: 10ch;
}
td:has(code) {
min-width: 8ch;
}
&:not(:last-child) {
border-block-end: 1px solid var(--senary-contrast);
}
+6
View File
@@ -15,6 +15,7 @@ import {
provideZonelessChangeDetection,
provideEnvironmentInitializer,
} from '@angular/core';
import {UrlSerializer} from '@angular/router';
import {
DOCS_CONTENT_LOADER,
ENVIRONMENT,
@@ -33,6 +34,7 @@ import {CustomErrorHandler} from './core/services/errors-handling/error-handler'
import {ExampleContentLoader} from './core/services/example-content-loader.service';
import {routerProviders} from './routing/router_providers';
import {TYPESCRIPT_VFS_WORKER_PROVIDER} from './editor/code-editor/workers/factory-provider';
import {AdevUrlSerializer} from './core/services/routing/adev-url-serializer';
export const appConfig: ApplicationConfig = {
providers: [
@@ -53,5 +55,9 @@ export const appConfig: ApplicationConfig = {
deps: [DOCUMENT],
},
TYPESCRIPT_VFS_WORKER_PROVIDER,
{
provide: UrlSerializer,
useClass: AdevUrlSerializer,
},
],
};
@@ -436,6 +436,28 @@
</svg>
</a>
</li>
<li>
<a
[href]="ngLinks.STACKOVERFLOW"
cdkMenuItem
title="Angular Stack Overflow"
target="_blank"
rel="noopener"
>
<!-- Stack Overflow Icon -->
<svg
width="20"
height="20"
viewBox="0 0 26 31"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m22.18 16.32.05.02-.02.01za19 19 0 0 0-2.45 4.14l-.03.07q-.94 2.19-1.29 4.6v.03a18 18 0 0 0-.05 4.85H.01v-4.88h15.94l.1-.6L.69 20.48l1.28-4.7 15.55 4.1.22-.46-13.96-7.96 2.47-4.22 14.16 8.07.34-.4L9.15 3.47 12.65 0l11.78 11.64 1.25 1.23q-1.97 1.5-3.5 3.44"
/>
</svg>
</a>
</li>
</ul>
</ng-template>
</div>
@@ -94,6 +94,7 @@
width: 100%;
padding-inline: calc(var(--layout-padding) - 1.25rem);
height: auto;
scrollbar-width: none;
padding-block: 0;
}
@@ -169,6 +170,7 @@
@include mq.for-tablet {
flex-direction: row;
padding-block-start: 0;
}
// version dropdown button
@@ -233,7 +235,9 @@
@include mq.for-tablet {
flex-direction: row !important;
align-items: center;
margin-inline-end: 1.25rem;
margin-top: 0.25rem;
gap: 0.75rem;
}
@@ -289,6 +293,8 @@
@include mq.for-phone-only {
display: block;
position: relative;
top: 1rem;
}
}
@@ -0,0 +1,25 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {DefaultUrlSerializer, UrlTree} from '@angular/router';
/**
* Custom URL serializer extending the default behavior
* with Adev-specific behavior.
*/
export class AdevUrlSerializer extends DefaultUrlSerializer {
override parse(url: string): UrlTree {
// Since the app host/server is decoding encoded forward slashes,
// we perform this on the client as well in order to maintain
// a consistent behavior between the two environments and
// avoid opening a different page on client hydration (presumably, 404).
url = url.replaceAll(/%2(F|f)/g, '/');
return super.parse(url);
}
}
@@ -0,0 +1,36 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {TestBed} from '@angular/core/testing';
import {UrlSerializer} from '@angular/router';
import {AdevUrlSerializer} from './adev-url-serializer';
describe('AdevUrlSerializer', () => {
let serializer: UrlSerializer;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{
provide: UrlSerializer,
useClass: AdevUrlSerializer,
},
],
});
serializer = TestBed.inject(UrlSerializer);
});
it('should decode encoded forward slash (%2F)', () => {
// Uppercase hex
expect(serializer.parse('page%2Fabout').toString()).toBe('/page/about');
// Lowercase hex
expect(serializer.parse('page%2fabout').toString()).toBe('/page/about');
});
});
@@ -19,6 +19,8 @@ import {NodeRuntimeSandbox} from '../node-runtime-sandbox.service';
import {TypingsLoader} from '../typings-loader.service';
import {FileAndContentRecord} from '@angular/docs';
import {DomSanitizer} from '@angular/platform-browser';
import {NodeRuntimeState} from '../node-runtime-state.service';
import {CODE_EDITOR_EXTENSIONS} from './constants/code-editor-extensions';
import {LANGUAGES} from './constants/code-editor-languages';
import {getAutocompleteExtension} from './extensions/autocomplete';
@@ -26,10 +28,9 @@ import {getDiagnosticsExtension} from './extensions/diagnostics';
import {getTooltipExtension} from './extensions/tooltip';
import {DiagnosticsState} from './services/diagnostics-state.service';
import {TsVfsWorkerActions} from './workers/enums/actions';
import {TYPESCRIPT_VFS_WORKER_FACTORY} from './workers/factory-provider';
import {CodeChangeRequest} from './workers/interfaces/code-change-request';
import {ActionMessage} from './workers/interfaces/message';
import {NodeRuntimeState} from '../node-runtime-state.service';
import {TYPESCRIPT_VFS_WORKER_FACTORY} from './workers/factory-provider';
export interface EditorFile {
filename: string;
@@ -81,6 +82,7 @@ export class CodeMirrorEditor {
private readonly typingsLoader = inject(TypingsLoader);
private readonly destroyRef = inject(DestroyRef);
private readonly diagnosticsState = inject(DiagnosticsState);
private readonly domSanitizer = inject(DomSanitizer);
private readonly tsVfsWorkerFactory = inject(TYPESCRIPT_VFS_WORKER_FACTORY);
private tsVfsWorker: Worker | null = null;
@@ -448,7 +450,12 @@ export class CodeMirrorEditor {
this.sendRequestToTsVfs,
this.diagnosticsState,
),
getTooltipExtension(this.eventManager$, this.currentFile, this.sendRequestToTsVfs),
getTooltipExtension(
this.eventManager$,
this.currentFile,
this.sendRequestToTsVfs,
this.domSanitizer,
),
];
}
@@ -0,0 +1,41 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {DomSanitizer} from '@angular/platform-browser';
import {TestBed} from '@angular/core/testing';
import {getMarkedHtmlFromString, getTagsHtml} from './tooltip';
describe('getMarkedHtmlFromString', () => {
it('sanitizes markdown HTML content before assigning to innerHTML', () => {
const markdownContent = 'hello <img src=x onerror="alert(1)" />';
const domSanitizer = TestBed.inject(DomSanitizer);
const result = getMarkedHtmlFromString(markdownContent, domSanitizer);
expect(result.innerHTML.trim()).toBe('<p>hello <img src="x"></p>');
expect(result.innerHTML).not.toContain('onerror');
});
});
describe('getTagsHtml', () => {
it('sanitizes JSDoc tag content before assigning to innerHTML', () => {
const tags = [
{
name: 'example',
text: [{text: 'hello <img src=x onerror="alert(1)" />'}],
},
] as any[];
const domSanitizer = TestBed.inject(DomSanitizer);
const result = getTagsHtml(tags, domSanitizer);
expect(result.innerHTML).toContain('@example');
expect(result.innerHTML).not.toContain('onerror');
});
});
@@ -6,7 +6,9 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {Signal} from '@angular/core';
import {SecurityContext, Signal} from '@angular/core';
import {DomSanitizer} from '@angular/platform-browser';
import {Tooltip, hoverTooltip} from '@codemirror/view';
import {marked} from 'marked';
import {Subject, filter, take} from 'rxjs';
@@ -23,6 +25,7 @@ export const getTooltipExtension = (
emitter: Subject<ActionMessage<DisplayTooltipResponse>>,
currentFile: Signal<EditorFile>,
sendRequestToTsVfs: (request: ActionMessage<DisplayTooltipRequest>) => void,
domSanitizer: DomSanitizer,
) => {
return hoverTooltip(
async (_, pos: number): Promise<Tooltip | null> => {
@@ -58,9 +61,9 @@ export const getTooltipExtension = (
// use documentation if available as it's more informative than tags
if (documentation?.[0]?.text) {
tooltip.appendChild(getMarkedHtmlFromString(documentation[0]?.text));
tooltip.appendChild(getMarkedHtmlFromString(documentation[0]?.text, domSanitizer));
} else if (tags?.length) {
tooltip.appendChild(getTagsHtml(tags));
tooltip.appendChild(getTagsHtml(tags, domSanitizer));
}
return {
@@ -91,9 +94,13 @@ function forceTooltipScrollTop() {
}
}
function getMarkedHtmlFromString(content: string): HTMLDivElement {
export function getMarkedHtmlFromString(
content: string,
domSanitizer: DomSanitizer,
): HTMLDivElement {
const wrapper = document.createElement('div');
wrapper.innerHTML = marked(content) as string;
const sanitizedHtml = renderAndSanitizeMarkdownToHtml(content, domSanitizer);
wrapper.innerHTML = sanitizedHtml;
return wrapper;
}
@@ -123,7 +130,7 @@ function getHtmlFromDisplayParts(displayParts: ts.SymbolDisplayPart[]): HTMLDivE
return wrapper;
}
function getTagsHtml(tags: ts.JSDocTagInfo[]): HTMLDivElement {
export function getTagsHtml(tags: ts.JSDocTagInfo[], domSanitizer: DomSanitizer): HTMLDivElement {
const tagsWrapper = document.createElement('div');
let contentString = '';
@@ -138,7 +145,14 @@ function getTagsHtml(tags: ts.JSDocTagInfo[]): HTMLDivElement {
}
}
tagsWrapper.innerHTML = marked(contentString) as string;
const sanitizedHtml = renderAndSanitizeMarkdownToHtml(contentString, domSanitizer);
tagsWrapper.innerHTML = sanitizedHtml;
return tagsWrapper;
}
function renderAndSanitizeMarkdownToHtml(content: string, domSanitizer: DomSanitizer): string {
const markedHtml = marked(content) as string;
const sanitizedHtml = domSanitizer.sanitize(SecurityContext.HTML, markedHtml) ?? '';
return sanitizedHtml;
}
@@ -160,7 +160,7 @@
<section class="explore-section" id="learn-more">
<div class="title">
<h2>What to learn more about Angular?</h2>
<h2>Want to learn more about Angular?</h2>
<div class="pattern"></div>
</div>
+12 -3
View File
@@ -28,8 +28,12 @@
left: calc(var(--layout-padding) + var(--primary-nav-width));
z-index: 1;
@include mq.for-tablet-down {
justify-content: flex-start;
@include mq.for-tablet-landscape-down {
justify-content: center;
margin-top: 4rem;
}
@include mq.for-phone-only {
margin-top: 1rem;
}
}
@@ -182,7 +186,7 @@ section {
}
.search-field {
@include mq.for-tablet-down() {
@include mq.for-tablet-landscape-down() {
display: none;
}
}
@@ -269,6 +273,11 @@ section {
width: fit-content;
margin: 0 auto 2rem;
@include mq.for-phone-only {
width: auto;
margin: 0 0.5rem 1rem;
}
.tab-background {
position: absolute;
top: 4px;
@@ -69,6 +69,7 @@
border-radius: 0.25rem;
padding: 0;
transform: translateY(-0.7rem);
background: var(--page-background);
li {
list-style: none;
@@ -33,6 +33,7 @@ describe('TutorialPlayground', () => {
class FakeNodeRuntimeSandbox {
init() {}
reset() {}
}
TestBed.configureTestingModule({
@@ -56,4 +57,22 @@ describe('TutorialPlayground', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
it('should not call reset on the sandbox before it is initialized', async () => {
const fakeSandbox = {reset: jasmine.createSpy('reset')} as any;
component['nodeRuntimeSandbox'] = fakeSandbox;
component['isSandboxReady'].set(false);
spyOn<any>(component, 'loadTemplate').and.resolveTo();
await component.changeTemplate(component.templates[1]);
expect(fakeSandbox.reset).not.toHaveBeenCalled();
});
it('should call reset on the sandbox after it is initialized', async () => {
const fakeSandbox = {reset: jasmine.createSpy('reset')} as any;
component['nodeRuntimeSandbox'] = fakeSandbox;
component['isSandboxReady'].set(true);
spyOn<any>(component, 'loadTemplate').and.resolveTo();
await component.changeTemplate(component.templates[1]);
expect(fakeSandbox.reset).toHaveBeenCalled();
});
});
@@ -14,6 +14,7 @@ import {
DestroyRef,
EnvironmentInjector,
PLATFORM_ID,
signal,
Type,
effect,
inject,
@@ -55,6 +56,7 @@ export default class PlaygroundComponent {
protected nodeRuntimeSandbox?: NodeRuntimeSandbox;
protected embeddedEditorComponent?: Type<unknown>;
protected selectedTemplate: PlaygroundTemplate = this.defaultTemplate;
private readonly isSandboxReady = signal(false);
constructor() {
if (this.isServer) {
@@ -84,6 +86,7 @@ export default class PlaygroundComponent {
.subscribe(() => {
this.changeDetectorRef.markForCheck();
this.nodeRuntimeSandbox?.init();
this.isSandboxReady.set(true);
});
}
@@ -99,7 +102,9 @@ export default class PlaygroundComponent {
});
this.selectedTemplate = template;
await this.loadTemplate(template.path);
await this.nodeRuntimeSandbox?.reset();
if (this.isSandboxReady()) {
await this.nodeRuntimeSandbox?.reset();
}
}
private async loadTemplate(tutorialPath: string) {
@@ -28,10 +28,11 @@
-webkit-tap-highlight-color: transparent;
.adev-reference-list-type-filter-label {
margin-block: 2.5rem 1rem;
margin-block: 1rem;
}
.adev-reference-list-type-filter {
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(6, 1fr);
margin-block: 0;
@@ -45,6 +46,7 @@
}
@container api-ref-page (max-width: 600px) {
grid-template-columns: repeat(4, 1fr);
max-width: 500px;
}
@container api-ref-page (max-width: 500px) {
grid-template-columns: repeat(3, 1fr);
@@ -88,16 +90,70 @@
.adev-reference-list-query-filter {
display: flex;
gap: 1.5rem;
gap: 1rem;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
justify-content: space-between;
* {
box-sizing: border-box;
}
docs-text-field,
docs-select {
width: 100%;
max-width: 350px;
}
@container api-ref-page (max-width: 600px) {
flex-direction: column;
align-items: stretch;
docs-text-field,
docs-select {
width: 100%;
max-width: 500px;
}
}
}
.adev-reference-list-status {
display: flex;
align-items: center;
margin-top: 1rem;
::ng-deep .mat-mdc-chip-listbox {
@container api-ref-page (max-width: 600px) {
width: 100%;
max-width: 500px;
.mdc-evolution-chip-set__chips {
display: grid !important;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin-left: 0;
}
.mat-mdc-chip-option {
min-width: 0;
width: 100%;
margin: 0;
}
}
@container api-ref-page (max-width: 350px) {
.mdc-evolution-chip-set__chips {
grid-template-columns: 1fr;
}
}
}
}
}
.adev-reference-list-empty {
text-align: center;
margin-block-start: 2rem;
flex-basis: 100%;
p {
font-size: 1rem;
@@ -108,28 +164,3 @@
width: 100%;
}
}
.adev-reference-list-empty {
flex-basis: 100%;
p {
font-size: 1rem;
}
}
.docs-api-item-label-full {
white-space: nowrap;
}
.map-chip-option {
min-width: 190px;
}
.adev-reference-list-status {
display: flex;
align-items: center;
margin-top: 12px;
label {
margin-right: 8px;
}
}
@@ -1,12 +1,12 @@
@use '@angular/docs/styles/media-queries' as mq;
$ver-dropdown-width: 200px;
$ver-dropdown-width: clamp(165px, 20vw, 200px);
:host {
display: flex;
flex-flow: column;
align-items: center;
padding: var(--layout-padding) 0px;
padding: var(--layout-padding);
container: update-guide-page / inline-size;
.docs-viewer {
@@ -108,7 +108,7 @@ export default class UpdateComponent {
];
protected from = this.versions.find((version) => version.name === '20.0')!;
protected to = this.versions.find((version) => version.name === '21.0')!;
protected futureVersion = 2100;
protected futureVersion = 2200;
protected readonly steps: Step[] = RECOMMENDATIONS;
@@ -496,6 +496,27 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
category: 'Signal Forms',
status: 'new',
},
{
label: 'Cross-field logic',
path: 'guide/forms/signals/cross-field-logic',
contentPath: 'guide/forms/signals/cross-field-logic',
category: 'Signal Forms',
status: 'new',
},
{
label: 'Form submission',
path: 'guide/forms/signals/form-submission',
contentPath: 'guide/forms/signals/form-submission',
category: 'Signal Forms',
status: 'new',
},
{
label: 'Schemas',
path: 'guide/forms/signals/schemas',
contentPath: 'guide/forms/signals/schemas',
category: 'Signal Forms',
status: 'new',
},
{
label: 'Async operations',
path: 'guide/forms/signals/async-operations',
@@ -660,6 +681,7 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
path: 'guide/routing/testing',
contentPath: 'guide/routing/testing',
status: 'new',
isCrossReferenced: true,
},
{
label: 'Debugging tests',
@@ -863,6 +885,7 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
label: 'Route transition animations',
path: 'guide/routing/route-transition-animations',
contentPath: 'guide/routing/route-transition-animations',
isCrossReferenced: true,
},
],
},
@@ -875,7 +898,6 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
},
{
label: 'Build with AI',
status: 'new',
children: [
{
label: 'Get Started',
@@ -888,9 +910,10 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
contentPath: 'ai/develop-with-ai',
},
{
label: 'Design Patterns',
path: 'ai/design-patterns',
contentPath: 'ai/design-patterns',
label: 'Agent Skills',
path: 'ai/agent-skills',
contentPath: 'ai/agent-skills',
status: 'new',
},
{
label: 'Angular CLI MCP Server setup',
@@ -902,6 +925,11 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
path: 'ai/ai-tutor',
contentPath: 'ai/ai-tutor',
},
{
label: 'Design Patterns',
path: 'ai/design-patterns',
contentPath: 'ai/design-patterns',
},
],
},
{
@@ -1037,16 +1065,15 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
},
// TODO: create those guides
// The signal debugging docs should also be added to the signal section
// {
// label: 'Signals',
// path: 'tools/devtools/signals',
// contentPath: 'tools/devtools/signals',
// },
// {
// label: 'Router',
// path: 'tools/devtools/router',
// contentPath: 'tools/devtools/router',
// }
{
label: 'Router Tree',
path: 'tools/devtools/router',
contentPath: 'tools/devtools/router',
},
],
},
{
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

+24
View File
@@ -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
```
+1
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
{
"branchName": "refs/heads/21.2.x",
"sha": "fc8b94d631a5d105a87abc2a008a6e8f76d7f3f5"
"sha": "4e2c69bca091e2218fd456afe797826f033a76f2"
}
+99 -99
View File
@@ -482,19 +482,6 @@
"endLine": 19
}
},
{
"name": "CDK_DROP_LIST_GROUP",
"type": "any",
"entryType": "constant",
"rawComment": "/**\n * Injection token that can be used to reference instances of `CdkDropListGroup`. It serves as\n * alternative token to the actual `CdkDropListGroup` class which could cause unnecessary\n * retention of the class and its directive metadata.\n */",
"description": "Injection token that can be used to reference instances of `CdkDropListGroup`. It serves as\nalternative token to the actual `CdkDropListGroup` class which could cause unnecessary\nretention of the class and its directive metadata.",
"jsdocTags": [],
"source": {
"filePath": "/src/cdk/drag-drop/directives/drop-list-group.ts",
"startLine": 16,
"endLine": 18
}
},
{
"name": "CdkDragRelease",
"entryType": "interface",
@@ -533,6 +520,19 @@
"endLine": 26
}
},
{
"name": "CDK_DROP_LIST_GROUP",
"type": "any",
"entryType": "constant",
"rawComment": "/**\n * Injection token that can be used to reference instances of `CdkDropListGroup`. It serves as\n * alternative token to the actual `CdkDropListGroup` class which could cause unnecessary\n * retention of the class and its directive metadata.\n */",
"description": "Injection token that can be used to reference instances of `CdkDropListGroup`. It serves as\nalternative token to the actual `CdkDropListGroup` class which could cause unnecessary\nretention of the class and its directive metadata.",
"jsdocTags": [],
"source": {
"filePath": "/src/cdk/drag-drop/directives/drop-list-group.ts",
"startLine": 17,
"endLine": 19
}
},
{
"name": "CDK_DRAG_PLACEHOLDER",
"type": "any",
@@ -559,70 +559,6 @@
"endLine": 25
}
},
{
"name": "CdkDropListGroup",
"isAbstract": false,
"entryType": "undecorated_class",
"members": [
{
"name": "disabled",
"type": "boolean",
"memberType": "property",
"memberTags": [],
"description": "Whether starting a dragging sequence from inside this group is disabled.",
"jsdocTags": []
},
{
"name": "ngOnDestroy",
"signatures": [
{
"name": "ngOnDestroy",
"entryType": "function",
"description": "",
"generics": [],
"isNewType": false,
"jsdocTags": [],
"params": [],
"rawComment": "",
"returnType": "void"
}
],
"implementation": {
"params": [],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "ngOnDestroy",
"description": "",
"entryType": "function",
"jsdocTags": [],
"rawComment": ""
},
"entryType": "function",
"description": "",
"jsdocTags": [],
"rawComment": "",
"memberType": "method",
"memberTags": []
}
],
"generics": [
{
"name": "T"
}
],
"description": "Declaratively connects sibling `cdkDropList` instances together. All of the `cdkDropList`\nelements that are placed inside a `cdkDropListGroup` will be connected to each other\nautomatically. Can be used as an alternative to the `cdkDropListConnectedTo` input\nfrom `cdkDropList`.",
"jsdocTags": [],
"rawComment": "/**\n * Declaratively connects sibling `cdkDropList` instances together. All of the `cdkDropList`\n * elements that are placed inside a `cdkDropListGroup` will be connected to each other\n * automatically. Can be used as an alternative to the `cdkDropListConnectedTo` input\n * from `cdkDropList`.\n */",
"implements": [
"OnDestroy"
],
"source": {
"filePath": "/src/cdk/drag-drop/directives/drop-list-group.ts",
"startLine": 26,
"endLine": 42
}
},
{
"name": "CdkDragPlaceholder",
"isAbstract": false,
@@ -706,6 +642,70 @@
"endLine": 44
}
},
{
"name": "CdkDropListGroup",
"isAbstract": false,
"entryType": "undecorated_class",
"members": [
{
"name": "disabled",
"type": "boolean",
"memberType": "property",
"memberTags": [],
"description": "Whether starting a dragging sequence from inside this group is disabled.",
"jsdocTags": []
},
{
"name": "ngOnDestroy",
"signatures": [
{
"name": "ngOnDestroy",
"entryType": "function",
"description": "",
"generics": [],
"isNewType": false,
"jsdocTags": [],
"params": [],
"rawComment": "",
"returnType": "void"
}
],
"implementation": {
"params": [],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "ngOnDestroy",
"description": "",
"entryType": "function",
"jsdocTags": [],
"rawComment": ""
},
"entryType": "function",
"description": "",
"jsdocTags": [],
"rawComment": "",
"memberType": "method",
"memberTags": []
}
],
"generics": [
{
"name": "T"
}
],
"description": "Declaratively connects sibling `cdkDropList` instances together. All of the `cdkDropList`\nelements that are placed inside a `cdkDropListGroup` will be connected to each other\nautomatically. Can be used as an alternative to the `cdkDropListConnectedTo` input\nfrom `cdkDropList`.",
"jsdocTags": [],
"rawComment": "/**\n * Declaratively connects sibling `cdkDropList` instances together. All of the `cdkDropList`\n * elements that are placed inside a `cdkDropListGroup` will be connected to each other\n * automatically. Can be used as an alternative to the `cdkDropListConnectedTo` input\n * from `cdkDropList`.\n */",
"implements": [
"OnDestroy"
],
"source": {
"filePath": "/src/cdk/drag-drop/directives/drop-list-group.ts",
"startLine": 27,
"endLine": 43
}
},
{
"name": "CdkDragPreview",
"isAbstract": false,
@@ -6294,11 +6294,11 @@
"@angular/core"
],
[
"OnDestroy",
"Input",
"@angular/core"
],
[
"Input",
"OnDestroy",
"@angular/core"
],
[
@@ -6422,11 +6422,11 @@
"@angular/cdk/drag-drop"
],
[
"CDK_DROP_LIST_GROUP",
"CdkDragRelease",
"@angular/cdk/drag-drop"
],
[
"CdkDragRelease",
"CDK_DROP_LIST_GROUP",
"@angular/cdk/drag-drop"
],
[
@@ -6438,11 +6438,11 @@
"@angular/cdk/drag-drop"
],
[
"CdkDropListGroup",
"CdkDragPlaceholder",
"@angular/cdk/drag-drop"
],
[
"CdkDragPlaceholder",
"CdkDropListGroup",
"@angular/cdk/drag-drop"
],
[
@@ -6593,10 +6593,6 @@
"DropListOrientation",
"@angular/cdk/drag-drop"
],
[
"CDK_DROP_LIST_GROUP",
"@angular/cdk/drag-drop"
],
[
"CdkDragRelease",
"@angular/cdk/drag-drop"
@@ -6609,6 +6605,10 @@
"CdkDragRelease.event",
"@angular/cdk/drag-drop"
],
[
"CDK_DROP_LIST_GROUP",
"@angular/cdk/drag-drop"
],
[
"CDK_DRAG_PLACEHOLDER",
"@angular/cdk/drag-drop"
@@ -6617,18 +6617,6 @@
"CDK_DRAG_PREVIEW",
"@angular/cdk/drag-drop"
],
[
"CdkDropListGroup",
"@angular/cdk/drag-drop"
],
[
"CdkDropListGroup.disabled",
"@angular/cdk/drag-drop"
],
[
"CdkDropListGroup.ngOnDestroy",
"@angular/cdk/drag-drop"
],
[
"CdkDragPlaceholder",
"@angular/cdk/drag-drop"
@@ -6645,6 +6633,18 @@
"CdkDragPlaceholder.ngOnDestroy",
"@angular/cdk/drag-drop"
],
[
"CdkDropListGroup",
"@angular/cdk/drag-drop"
],
[
"CdkDropListGroup.disabled",
"@angular/cdk/drag-drop"
],
[
"CdkDropListGroup.ngOnDestroy",
"@angular/cdk/drag-drop"
],
[
"CdkDragPreview",
"@angular/cdk/drag-drop"
@@ -1142,8 +1142,8 @@
"implements": [],
"source": {
"filePath": "/src/cdk/testing/selenium-webdriver/selenium-web-driver-harness-environment.ts",
"startLine": 35,
"endLine": 38
"startLine": 34,
"endLine": 37
}
},
{
@@ -1194,8 +1194,8 @@
"rawComment": "/** Waits for angular to be ready after the page load. */",
"source": {
"filePath": "/src/cdk/testing/selenium-webdriver/selenium-web-driver-harness-environment.ts",
"startLine": 65,
"endLine": 68
"startLine": 66,
"endLine": 69
}
},
{
@@ -1635,8 +1635,8 @@
"implements": [],
"source": {
"filePath": "/src/cdk/testing/selenium-webdriver/selenium-web-driver-harness-environment.ts",
"startLine": 71,
"endLine": 150
"startLine": 72,
"endLine": 151
}
}
],
+1 -13
View File
@@ -22,16 +22,4 @@ Angular v21 is being delivered to you as a brand new release adventure. With mod
- Your first look at Signal Forms, our new streamlined, signal-based approach to forms in Angular
- Exciting new details about the Angular Aria package
<div style="display: block; width: 80%; margin: 0 auto; margin-top: 20px;">
<iframe
credentialless
width="560"
height="315"
src="https://www.youtube.com/embed/DDAHORVzQ5g?si=B9Uv5vXzIKJfcLGr"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
></iframe>
</div>
<docs-video src="https://www.youtube.com/embed/DDAHORVzQ5g?si=B9Uv5vXzIKJfcLGr" title="Angular v21 Developer Event"/>
@@ -1,3 +0,0 @@
<!-- #docregion unsupported -->
<p app:Highlight>This is invalid</p>
<!-- #enddocregion unsupported -->
@@ -1,7 +0,0 @@
// #docregion
import {Directive} from '@angular/core';
@Directive({
selector: '[appHighlight]',
})
export class HighlightDirective {}
+1 -1
View File
@@ -70,7 +70,7 @@ You can use css-grid to animate to auto height.
<docs-code header="auto-height.css" path="adev/src/content/examples/animations/src/app/native-css/auto-height.css" />
</docs-code-multifile>
If you don't have to worry about supporting all browsers, you can also check out `calc-size()`, which is the true solution to animating auto height. See [MDN's docs](https://developer.mozilla.org/en-US/docs/Web/CSS/calc-size) and (this tutorial)[https://frontendmasters.com/blog/one-of-the-boss-battles-of-css-is-almost-won-transitioning-to-auto/] for more information.
If you don't have to worry about supporting all browsers, you can also check out `calc-size()`, which is the true solution to animating auto height. See [MDN's docs](https://developer.mozilla.org/en-US/docs/Web/CSS/calc-size) and [this tutorial](https://frontendmasters.com/blog/one-of-the-boss-battles-of-css-is-almost-won-transitioning-to-auto/) for more information.
### Animate entering and leaving a view
+1 -1
View File
@@ -8,7 +8,7 @@
## Overview
The manubar is a horizontal navigation bar that provides persistent access to application menus. Menubars organize commands into logical categories like File, Edit, and View, helping users discover and execute application features through keyboard or mouse interaction.
The menubar is a horizontal navigation bar that provides persistent access to application menus. Menubars organize commands into logical categories like File, Edit, and View, helping users discover and execute application features through keyboard or mouse interaction.
<docs-tab-group>
<docs-tab label="Basic">
+2 -2
View File
@@ -3,9 +3,9 @@
## What is Angular Aria?
Building accessible components seems straightforward, but implementing them according to the W3C Accessibility Guidelines requires significant effort and accessibility expertise.
Building accessible components seems straightforward, but implementing them according to the [W3C Accessibility Guidelines](https://www.w3.org/TR/wcag/) requires significant effort and accessibility expertise.
Angular Aria is a collection of headless, accessible directives that implement common WAI-ARIA patterns. The directives handle keyboard interactions, ARIA attributes, focus management, and screen reader support. All you have to do is provide the HTML structure, CSS styling, and business logic!
Angular Aria is a collection of headless, accessible directives that implement common [WAI-ARIA patterns](https://www.w3.org/WAI/ARIA/apg/patterns/). The directives handle keyboard interactions, ARIA attributes, focus management, and screen reader support. All you have to do is provide the HTML structure, CSS styling, and business logic!
## Installation
@@ -51,7 +51,7 @@ You can alternatively choose to write your template and styles in separate files
export class ProfilePhoto {}
```
This can help separate the concerns of _presentation_ from _behavior_ in your project. You can choose one approach for your entire project, or you decide which to use for each component.
This can help separate the concerns of _presentation_ from _behavior_ in your project. You can choose one approach for your entire project, or you can decide which to use for each component.
Both `templateUrl` and `styleUrl` are relative to the directory in which the component resides.
+1 -1
View File
@@ -94,7 +94,7 @@ For example:
So, in effect, the `<a>` element may be in the component's template, or in any of its projected or child content.
- With `:host ::ng-deep p a`, both the `<a>` and `<p>` elements must be decendants of the component's host element.
- With `:host ::ng-deep p a`, both the `<a>` and `<p>` elements must be descendants of the component's host element.
They can come from the component's template or the views of its child components, but not elsewhere in the app.
@@ -14,7 +14,14 @@ This section walks you through creating a highlight directive that sets the back
The CLI creates `src/app/highlight.directive.ts`, a corresponding test file `src/app/highlight.directive.spec.ts`.
<docs-code header="highlight.directive.ts" path="adev/src/content/examples/attribute-directives/src/app/highlight.directive.0.ts"/>
```angular-ts
import {Directive} from '@angular/core';
@Directive({
selector: '[appHighlight]',
})
export class HighlightDirective {}
```
The `@Directive()` decorator's configuration property specifies the directive's CSS attribute selector, `[appHighlight]`.
@@ -25,11 +32,13 @@ This section walks you through creating a highlight directive that sets the back
1. Add logic to the `HighlightDirective` class that sets the background to yellow.
<docs-code header="highlight.directive.ts" path="adev/src/content/examples/attribute-directives/src/app/highlight.directive.1.ts"/>
<docs-code header="highlight.directive.ts" path="adev/src/content/examples/attribute-directives/src/app/highlight.directive.1.ts"/>
HELPFUL: Directives _do not_ support namespaces.
IMPORTANT: Directives _do not_ support namespaces.
<docs-code header="app.component.avoid.html (unsupported)" path="adev/src/content/examples/attribute-directives/src/app/app.component.avoid.html" region="unsupported"/>
```angular-html {avoid}
<p app:Highlight>This is invalid</p>
```
## Applying an attribute directive
@@ -76,7 +76,7 @@ Angular creates the directive class and specifies the CSS selector, `[select]`,
Import `TemplateRef`, and `ViewContainerRef`. Inject `TemplateRef` and `ViewContainerRef` in the directive as private properties.
```ts
import {Directive, TemplateRef, ViewContainerRef} from '@angular/core';
import {Directive, TemplateRef, ViewContainerRef, inject} from '@angular/core';
@Directive({
selector: '[select]',
@@ -9,12 +9,12 @@ A typical use-case is a questionnaire.
You might need to get input from users in different contexts.
The format and style of the forms a user sees should remain constant, while the actual questions you need to ask vary with the context.
In this tutorial you will build a dynamic form that presents a basic questionnaire.
In this tutorial, you will build a dynamic form that presents a basic questionnaire.
You build an online application for heroes seeking employment.
The agency is constantly tinkering with the application process, but by using the dynamic form
you can create the new forms on the fly without changing the application code.
The tutorial walks you through the following steps.
The tutorial walks you through the following steps:
1. Enable reactive forms for a project.
1. Establish a data model to represent form controls.
@@ -30,7 +30,7 @@ The basic version can evolve to support a richer variety of questions, more grac
Dynamic forms are based on reactive forms.
To give the application access reactive forms directives, import `ReactiveFormsModule` from the `@angular/forms` library into the necessary components.
To give the application access to reactive form directives, import `ReactiveFormsModule` from the `@angular/forms` package into the necessary components.
<docs-code-multifile>
<docs-code header="dynamic-form.component.ts" path="adev/src/content/examples/dynamic-form/src/app/dynamic-form.component.ts"/>
@@ -8,7 +8,7 @@ This page shows how to validate user input from the UI and display useful valida
To add validation to a template-driven form, you add the same validation attributes as you would with [native HTML form validation](https://developer.mozilla.org/docs/Web/Guide/HTML/HTML5/Constraint_validation).
Angular uses directives to match these attributes with validator functions in the framework.
Every time the value of a form control changes, Angular runs validation and generates either a list of validation errors that results in an `INVALID` status, or null, which results in a VALID status.
Every time the value of a form control changes, Angular runs validation and generates either a list of validation errors that results in an `INVALID` status, or `null`, which results in a `VALID` status.
You can then inspect the control's state by exporting `ngModel` to a local template variable.
The following example exports `NgModel` into a variable called `name`:
@@ -237,7 +237,7 @@ Asynchronous validators implement the `AsyncValidatorFn` and `AsyncValidator` in
These are very similar to their synchronous counterparts, with the following differences.
- The `validate()` functions must return a Promise or an observable,
- The observable returned must be finite, meaning it must complete at some point.
- The observable returned must be finite, meaning that it must complete at some point.
To convert an infinite observable into a finite one, pipe the observable through a filtering operator such as `first`, `last`, `take`, or `takeUntil`.
Asynchronous validation happens after the synchronous validation, and is performed only if the synchronous validation is successful.
@@ -369,7 +369,7 @@ onCountryChange(country: string) {
Use [`setValidators`](api/forms/AbstractControl#setValidators) to replace all existing synchronous validators on a control, or [`clearValidators`](api/forms/AbstractControl#clearValidators) to remove all validators.
```ts
toggleStrictNameValidation(isStenablerict: boolean) {
toggleStrictNameValidation(isStrict: boolean) {
const nameControl = this.profileForm.get('name');
if (enable) {
+4 -4
View File
@@ -6,7 +6,7 @@ Applications use forms to enable users to log in, to update a profile, to enter
Angular provides two different approaches to handling user input through forms: reactive and template-driven.
Both capture user input events from the view, validate the user input, create a form model and data model to update, and provide a way to track changes.
Both capture user input events from the view, validate the input, create a form and data model, and provide a way to track changes.
TIP: If you're looking for the new experimental Signal Forms, check out our [essential Signal Forms guide](/essentials/signal-forms)!
@@ -86,7 +86,7 @@ The following component implements the same input field for a single control, us
<docs-code language="angular-ts" path="adev/src/content/examples/forms-overview/src/app/template/favorite-color/favorite-color.component.ts"/>
IMPORTANT: In a template-driven form the source of truth is the template. The `NgModel` directive automatically manages the `FormControl` instance for you.
IMPORTANT: In a template-driven form, the source of truth is the template. The `NgModel` directive automatically manages the `FormControl` instance for you.
## Data flow in forms
@@ -99,10 +99,10 @@ The following diagrams illustrate both kinds of data flow for each type of form,
### Data flow in reactive forms
In reactive forms each form element in the view is directly linked to the form model (a `FormControl` instance).
In reactive forms, each form element in the view is directly linked to the form model (a `FormControl` instance).
Updates from the view to the model and from the model to the view are synchronous and do not depend on how the UI is rendered.
The view-to-model diagram shows how data flows when an input field's value is changed from the view through the following steps.
The view-to-model diagram shows how data flows when an input field's value is changed from the view through the following steps:
1. The user types a value into the input element, in this case the favorite color _Blue_.
1. The form input element emits an "input" event with the latest value.
@@ -1,28 +1,28 @@
# Reactive forms
Reactive forms provide a model-driven approach to handling form inputs whose values change over time.
This guide shows you how to create and update a basic form control, progress to using multiple controls in a group, validate form values, and create dynamic forms where you can add or remove controls at run time.
This guide shows you how to create and update a basic form control, use multiple controls in a group, validate form values, and create dynamic forms where you can add or remove controls at runtime.
## Overview of reactive forms
Reactive forms use an explicit and immutable approach to managing the state of a form at a given point in time.
Each change to the form state returns a new state, which maintains the integrity of the model between changes.
Reactive forms are built around observable streams, where form inputs and values are provided as streams of input values, which can be accessed synchronously.
Reactive forms are built around observable streams, where form inputs and values are provided as streams that can be accessed synchronously.
Reactive forms also provide a straightforward path to testing because you are assured that your data is consistent and predictable when requested.
Any consumers of the streams have access to manipulate that data safely.
Any consumers of these streams can safely manipulate the data.
Reactive forms differ from [template-driven forms](guide/forms/template-driven-forms) in distinct ways.
Reactive forms provide synchronous access to the data model, immutability with observable operators, and change tracking through observable streams.
Template-driven forms let direct access modify data in your template, but are less explicit than reactive forms because they rely on directives embedded in the template, along with mutable data to track changes asynchronously.
Template-driven forms allow direct access to modify data in your template, but are less explicit than reactive forms because they rely on directives embedded in the template, along with mutable data to track changes asynchronously.
See the [Forms Overview](guide/forms) for detailed comparisons between the two paradigms.
## Adding a basic form control
There are three steps to using form controls.
1. Generate a new component and register the reactive forms module. This module declares the reactive-form directives that you need to use reactive forms.
1. Generate a new component and register the reactive forms module. This module declares the reactive-form directives required to use reactive forms.
1. Instantiate a new `FormControl`.
1. Register the `FormControl` in the template.
@@ -62,7 +62,7 @@ The `FormControl` assigned to the `name` property is displayed when the `<app-na
### Displaying a form control value
You can display the value in the following ways.
You can display the value in the following ways:
- Through the `valueChanges` observable where you can listen for changes in the form's value in the template using `AsyncPipe` or in the component class using the `subscribe()` method
- With the `value` property, which gives you a snapshot of the current value
@@ -231,7 +231,7 @@ Simulate an update by adding a button to the template to update the user profile
When a user clicks the button, the `profileForm` model is updated with new values for `firstName` and `street`. Notice that `street` is provided in an object inside the `address` property.
This is necessary because the `patchValue()` method applies the update against the model structure.
`PatchValue()` only updates properties that the form model defines.
`patchValue()` only updates properties that the form model defines.
## Using the FormBuilder service to generate controls
@@ -437,7 +437,7 @@ Initially, the form contains one `Alias` field. To add another field, click the
## Unified control state change events
All form controls expose a single unified stream of **control state change events** through the `events` observable on `AbstractControl` (`FormControl`, `FormGroup`, `FormArray`, and `FormRecord`).
This unified stream lets you react to **value**, **status**, **pristine**, **touched** and **reset** state changes and also for **form-level actions** such as **submit** , allowing you to handle all updates with a one subscription instead of wiring multiple observables.
This unified stream lets you react to **value**, **status**, **pristine**, **touched**, and **reset** state changes, as well as **form-level actions** such as **submit**, allowing you to handle all updates with a single subscription instead of wiring multiple observables.
### Event types
@@ -329,13 +329,13 @@ export class Registration {
private createUsernameResource = (usernameSignal: Signal<string | undefined>) => {
return rxResource({
params: () => usernameSignal(),
stream: ({request: username}) => this.usernameService.checkUsername(username),
stream: ({params: username}) => this.usernameService.checkUsername(username),
});
};
registrationForm = form(this.registrationModel, (schemaPath) => {
validateAsync(schemaPath.username, {
params: ({value}) => value() || undefined,
params: ({value}) => value(),
factory: this.createUsernameResource,
onSuccess: (result) =>
result?.available ? null : {kind: 'usernameTaken', message: 'Username taken'},
@@ -0,0 +1,251 @@
# Cross-field logic
**Cross-field logic** is necessary when any rule, validation, or behavior of one field depends on another field's value or state.
Signal forms provide a **field context** to every rule function. The field context provides access to the current field's value and state, and lets you read other fields in the form using `valueOf()`, `stateOf()`, and `fieldTreeOf()`.
This guide covers the field context API in depth and shows common cross-field patterns. For single-field validation, see the [Validation guide](/guide/forms/signals/validation).
## Understanding the field context
Every rule function in signal forms receives a **field context** parameter, which is an object that describes the current field and provides access to the rest of the form.
There are three properties you can access for the current field:
| Property | Type | Description |
| ----------- | -------------------- | -------------------------------------------------------------------- |
| `value` | `Signal<TValue>` | The current field's value as a signal |
| `state` | `FieldState<TValue>` | The current field's state (such as validity, errors, touched, dirty) |
| `fieldTree` | `FieldTree<TValue>` | The current field's tree, for programmatic access to child fields |
For cross-field logic, the following three properties allow you to access other parts of the form:
| Property | Type | Description |
| --------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| `valueOf()` | `(path) => PValue` | Most common. Use when you need another field's raw value for comparisons or calculations. |
| `stateOf()` | `(path) => FieldState<PValue>` | Use when your logic depends on another field's state, such as whether it's valid, touched, or dirty. |
| `fieldTreeOf()` | `(path) => FieldTree<PModel>` | Use when you need programmatic access to another field's tree, such as pushing errors to a specific child field with validateTree. |
Here is an example of using `value` and `valueOf()` to validate that the current field (end date) comes after the start date in the form:
```ts
import {Component, signal} from '@angular/core';
import {form, validate} from '@angular/forms/signals';
@Component({
/* ... */
})
export class EventForm {
eventModel = signal({
startDate: new Date('2026-06-01'),
endDate: new Date('2026-06-05'),
});
eventForm = form(this.eventModel, (schemaPath) => {
validate(schemaPath.endDate, ({value, valueOf}) => {
if (value() <= valueOf(schemaPath.startDate)) {
return {
kind: 'invalidDateRange',
message: 'End date must be after start date',
};
}
return null;
});
});
}
```
NOTE: The `fieldContext` parameter is typically destructured to pull out only what the rule needs. The remaining examples in this guide use this pattern.
## Cross-field validation patterns
The date range example from the previous section validates the end date against the start date. Because the rule reads `valueOf(schemaPath.startDate)`, it re-evaluates automatically whenever either date changes. In other words, a single validator is enough to keep the error state correct.
However, that single validator only places the error on the end date field. If you want both fields to show an error when the range is invalid, add a matching validation rule to each field:
```ts
import {Component, signal} from '@angular/core';
import {form, validate} from '@angular/forms/signals';
@Component({
/* ... */
})
export class EventForm {
eventModel = signal({
startDate: new Date('2026-06-01'),
endDate: new Date('2026-06-05'),
});
eventForm = form(this.eventModel, (schemaPath) => {
validate(schemaPath.startDate, ({value, valueOf}) => {
if (value() >= valueOf(schemaPath.endDate)) {
return {
kind: 'invalidDateRange',
message: 'Start date must be before end date',
};
}
return null;
});
validate(schemaPath.endDate, ({value, valueOf}) => {
if (value() <= valueOf(schemaPath.startDate)) {
return {
kind: 'invalidDateRange',
message: 'End date must be after start date',
};
}
return null;
});
});
}
```
Both rules make use of `valueOf()` to read the other field. Because each rule is reactive, changing either date re-evaluates both validations automatically.
NOTE: When a rule involves multiple fields, you need to decide where the error belongs: on a specific field, on multiple fields, or on the parent. In general, place the error where the user would most likely go to fix the problem.
### Conditional requirements
In some forms, certain fields are only required under certain conditions. For example, a registration form might require a company name only when the user selects a business account type:
```ts
import {Component, signal} from '@angular/core';
import {form, required} from '@angular/forms/signals';
@Component({
/* ... */
})
export class RegistrationForm {
registrationModel = signal({
accountType: 'personal' as 'personal' | 'business',
companyName: '',
});
registrationForm = form(this.registrationModel, (schemaPath) => {
required(schemaPath.companyName, {
when: ({valueOf}) => valueOf(schemaPath.accountType) === 'business',
message: 'Company name is required for business accounts',
});
});
}
```
The `when` option receives the same field context as any other rule function, so `valueOf` works the same way. When the user switches back to `'personal'`, the condition re-evaluates and the requirement — along with its error — clears automatically.
Using `required()` with `when` instead of a manual `validate()` check also adds proper required metadata to the field, which enables accessibility features like marking the field as required for screen readers.
### Validating based on another field's state
The examples so far use `valueOf()` to read another field's value. Sometimes your logic depends on another field's _state_ instead — whether it's valid, touched, or dirty. Use `stateOf()` for this.
For example, a confirm-password field should only check for a match once the user has interacted with the password field. If the user hasn't touched the password yet, flagging a mismatch on the confirmation is premature:
```ts
import {Component, signal} from '@angular/core';
import {form, validate} from '@angular/forms/signals';
@Component({
/* ... */
})
export class PasswordForm {
passwordModel = signal({
password: '',
confirmPassword: '',
});
passwordForm = form(this.passwordModel, (schemaPath) => {
validate(schemaPath.confirmPassword, ({value, valueOf, stateOf}) => {
if (!stateOf(schemaPath.password).touched()) {
return null;
}
if (value() !== valueOf(schemaPath.password)) {
return {
kind: 'passwordMismatch',
message: 'Passwords do not match',
};
}
return null;
});
});
}
```
The `stateOf()` call returns the other field's [field state](api/forms/signals/FieldState), giving you access to signals like `invalid()`, `touched()`, and `dirty()`. Because these are signals, the rule re-evaluates whenever the password field's validity changes.
WARNING: Be careful not to read state which depends on your field's validation, as that creates a circular loop. For example, a validator which checks whether the parent field is valid will create an infinite loop because the parent's validity depends on its children's validity (which includes your validator).
## Using validateTree
The examples so far use `validate()` to check individual fields. Sometimes you need to validate a group of fields where the logic is inherently about multiple fields in a group, and direct errors to specific children within it. `validateTree` handles is ideal for these kinds of scenarios.
For example, in a Sudoku puzzle, each row must contain unique numbers. This is a group-level rule: you check the entire row, then flag the specific cells that violate it. This kind of validation can't be expressed cleanly with `validate` on individual fields, because each cell would need to know about every other cell.
```ts
import {Component, signal} from '@angular/core';
import {form, validateTree} from '@angular/forms/signals';
@Component({
/* ... */
})
export class SudokuRow {
rowModel = signal({
cell1: 1,
cell2: 3,
cell3: 1,
cell4: 4,
});
rowForm = form(this.rowModel, (schemaPath) => {
validateTree(schemaPath, ({value, fieldTreeOf}) => {
const row = value();
const entries = [
{val: row.cell1, fieldTree: fieldTreeOf(schemaPath.cell1)},
{val: row.cell2, fieldTree: fieldTreeOf(schemaPath.cell2)},
{val: row.cell3, fieldTree: fieldTreeOf(schemaPath.cell3)},
{val: row.cell4, fieldTree: fieldTreeOf(schemaPath.cell4)},
];
const counts = new Map<number, number>();
for (const {val} of entries) {
if (val !== 0) {
counts.set(val, (counts.get(val) ?? 0) + 1);
}
}
const errors = entries
.filter(({val}) => val !== 0 && (counts.get(val) ?? 0) > 1)
.map(({val, fieldTree}) => ({
kind: 'duplicateInRow',
message: `${val} already appears in this row`,
fieldTree,
}));
return errors.length > 0 ? errors : null;
});
});
}
```
The validator runs on the parent field (the row), reads all cell values, counts duplicates, and returns an error for each cell that contains a repeated number. The `fieldTree` property on each error tells Angular exactly which cell should show the error. Without `fieldTree`, the errors would apply to the row itself — not where the user needs to see them.
Because `validateTree` can return an array of errors, a single validator can flag multiple cells at once. Each error includes a `fieldTree` pointing to its target, so Angular routes the errors to the correct fields.
### When to use validateTree vs validate
Prefer `validate()` with `valueOf()` when the error belongs on the field being validated — even if the rule reads from other fields. Reach for `validateTree` when:
- The validation logic is inherently about a group of fields, not any single field
- The validator needs to return errors targeting different child fields
TIP: For an introduction to `validateTree` and its return type, see the [Validation guide](/guide/forms/signals/validation).
## Next steps
This guide covered the field context API and common cross-field patterns. To learn more about related Signal Forms guide, check out:
<docs-pill-row>
<docs-pill href="guide/forms/signals/validation" title="Validation" />
<docs-pill href="guide/forms/signals/field-state-management" title="Field state management" />
<docs-pill href="guide/forms/signals/custom-controls" title="Custom controls" />
</docs-pill-row>
@@ -516,7 +516,7 @@ import {form, FormField, min, max, validate} from '@angular/forms/signals';
@Component({
selector: 'app-custom',
imports: [formField],
imports: [FormField],
template: ` <input [formField]="customForm.score" /> `,
})
export class Custom {
@@ -643,7 +643,7 @@ import {form, FormField, max} from '@angular/forms/signals';
@Component({
selector: 'app-inventory',
imports: [formField],
imports: [FormField],
template: `
<label>
Item
@@ -742,7 +742,7 @@ import {
@Component({
selector: 'app-promo',
imports: [formField],
imports: [FormField],
template: `
@if (!promoForm.promoCode().hidden()) {
<label>
@@ -787,7 +787,7 @@ import {form, FormField, applyWhen, required, pattern} from '@angular/forms/sign
@Component({
selector: 'app-address',
imports: [formField],
imports: [FormField],
template: `
<label>
Country
@@ -0,0 +1,323 @@
# Form submission
When a user submits a form, your application typically needs to handle multiple concerns at once: surfacing validation errors, preventing duplicate submission, sending data to a server, and much more. Handling each of these manually can be tedious and prone to error.
Signal Forms provides a `submit()` function that helps you manage the form submission lifecycle. This guide walks through how to use it.
## What does `submit()` do?
The `submit()` function runs through a specific sequence:
1. **Mark interactive fields as touched** — Fields that display errors only after being touched will now show their validation errors. Hidden, disabled, and readonly fields are skipped.
1. **Check validation** — If any validation rules have failed, submission stops and the `action` function does not run.
1. **Run the action** — The `action` function executes with the form's current value. While it runs, `submitting()` returns `true`.
1. **Handle the result** — If the action returns errors, they are routed to their target fields. If it returns nothing, the submission is treated as successful.
The `submit()` function returns a `Promise<boolean>` that resolves to `true` when the action completes without errors, and `false` when validation fails or the action returns errors.
## Setting up form submission with `FormRoot`
The most common way to use the `submit()` function is through the `FormRoot` directive.
The `FormRoot` directive handles three things automatically when bound to a `<form>` element:
1. **Sets [`novalidate`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/form#novalidate)** — Disables the browser's built-in validation so Signal Forms manages validation instead
1. **Prevents default** — Stops the browser from navigating on form submission
1. **Calls `submit()`** — Triggers the submission flow when the user submits the form
NOTE: The `FormRoot` directive sets the `novalidate` attribute on the `form` element automatically. You do not need to add it manually when using `FormRoot`.
`FormRoot` handles the submission event, but you still need to tell it _what to do_ with the form data. That requires three things:
1. Bind your form to the `FormRoot` directive
1. Pass a `submission` option to the `form()` function
1. Define an `action` function within the `submission` option that manages the submitted data
```angular-ts
import {Component, signal} from '@angular/core';
import {form, FormField, FormRoot, required} from '@angular/forms/signals';
@Component({
selector: 'app-contact',
imports: [FormField, FormRoot],
template: `
<form [formRoot]="contactForm">
<label>
Name
<input [formField]="contactForm.name" />
</label>
<label>
Email
<input type="email" [formField]="contactForm.email" />
</label>
<button type="submit">Send</button>
</form>
`,
})
export class Contact {
contactModel = signal({
name: '',
email: '',
});
contactForm = form(
this.contactModel,
(schemaPath) => {
required(schemaPath.name);
required(schemaPath.email);
},
{
submission: {
action: async (field) => {
const result = await saveContact(field().value());
if (result.ok) return;
return {kind: 'serverError', message: 'Failed to submit form'};
},
},
},
);
}
```
The `action` function runs only when no validation rules have failed. By default, pending async validators do not block submission (see [Controlling validation gating](#controlling-validation-gating-with-ignorevalidators) for more details). The action receives the field tree and a `detail` object with `root` and `submitted` field trees, which is useful when submitting a sub-form.
After validation passes, the action itself may still fail due to scenarios such as a network error or duplicate entry. In those cases, you can surface the failure by returning the error(s). On the other hand, to indicate success, you only need to return `null` or `undefined`, or call an empty `return`.
## Showing submission state with `submitting()`
When you need to track whether the form is in the process of submitting, Signal Forms provides a `submitting()` signal that returns `true` while the `action` function is running. Use it to show loading indicators or disable the submit button to prevent duplicate submissions.
```angular-html
<button type="submit" [disabled]="contactForm().submitting()">
@if (contactForm().submitting()) {
Sending...
} @else {
Send
}
</button>
```
Once the `action` function succeeds or returns an error, the `submitting()` signal automatically resets back to `false`.
## Managing submission errors
### Server errors
When your `action` function communicates with a server, the server may return errors that need to appear on specific fields. Return these errors from the `action` to route them to their target fields.
#### Errors on the submitted field
By default, errors returned from the `action` are assigned to the submitted field (the field tree you passed to `submit()`):
```ts
action: async (field) => {
const result = await saveContact(field().value());
if (result.ok) return;
return {kind: 'serverError', message: 'Failed to submit form'};
};
```
#### Errors on specific fields
When you want to route an error to a specific field, include a `fieldTree` property pointing to that field:
```ts
action: async (field) => {
const result = await saveContact(field().value());
if (result.ok) return;
return {kind: 'taken', message: result.message, fieldTree: field.email};
};
```
#### Multiple errors
When you want to report errors on multiple fields, return an array:
```ts
action: async (field) => {
const result = await registerUser(field().value());
if (result.ok) return;
return result.errors.map((err: {field: string; message: string}) => ({
kind: 'serverError',
message: err.message,
fieldTree: field[err.field as keyof typeof field],
}));
};
```
### Auto-clearing submission errors
Submission errors clear automatically when the user edits the field. If the `action` returns an error on the email field, that error disappears as soon as the user changes the email value.
This differs from validation errors, which recompute reactively. Validation rules run again on each change and may produce the same error. Submission errors are one-time results from the server — once cleared, they do not reappear unless the form is submitted again.
TIP: Submission errors appear alongside validation errors in the field's `errors()` signal. For guidance on displaying errors in your template, see the [Field State Management guide](guide/forms/signals/field-state-management).
## Handling invalid submissions with `onInvalid`
When validation fails, the `action` function does not run. If you need to respond to a failed submission attempt — such as scrolling to the first error, showing a toast, or focusing an invalid field — use the `onInvalid` callback.
```ts
contactForm = form(
this.contactModel,
(schemaPath) => {
required(schemaPath.name);
required(schemaPath.email);
},
{
submission: {
action: async (field) => {
await saveContact(field().value());
},
onInvalid: (field) => {
const firstError = field().errorSummary()[0];
firstError?.fieldTree().focusBoundControl();
},
},
},
);
```
The `onInvalid` callback receives the same `(field, detail)` parameters as `action`. It runs after all interactive fields are marked as touched, so validation errors are already visible in the UI when it executes.
## Controlling validation gating with `ignoreValidators`
By default, `submit()` ignores pending validators. If no validators have failed, the action runs even if some async validators are still in progress. The `ignoreValidators` option gives you control over this behavior.
| Value | Behavior |
| ----------- | ------------------------------------------------------------------------ |
| `'pending'` | Submit if no validators have failed, even if some are pending (default) |
| `'none'` | Submit only if all validators pass — pending validators block submission |
| `'all'` | Always submit regardless of validation state |
```ts
contactForm = form(
this.contactModel,
(schemaPath) => {
required(schemaPath.name);
required(schemaPath.email);
},
{
submission: {
action: async (field) => {
await saveContact(field().value());
},
ignoreValidators: 'none',
},
},
);
```
Use `'none'` when your form has async validators (such as checking username availability) and you need all validation to complete before submitting. Use `'all'` for draft-saving scenarios where you want to persist data regardless of validation state.
## Manual submission with `submit()`
The `FormRoot` directive is the most common way to trigger submission, but you can also call `submit()` directly. This is useful for multi-step wizards, auto-save, or triggering submission from outside the form element.
```angular-ts
import {Component, signal} from '@angular/core';
import {form, FormField, required, submit} from '@angular/forms/signals';
@Component({
selector: 'app-contact',
imports: [FormField],
template: `
<label>
Name
<input [formField]="contactForm.name" />
</label>
<label>
Email
<input type="email" [formField]="contactForm.email" />
</label>
<button (click)="onSave()">Save</button>
`,
})
export class Contact {
contactModel = signal({
name: '',
email: '',
});
contactForm = form(this.contactModel, (schemaPath) => {
required(schemaPath.name);
required(schemaPath.email);
});
async onSave() {
// When calling `submit()` directly, you pass the action as the second argument
// instead of configuring it in `FormOptions`.
const success = await submit(this.contactForm, async (field) => {
const result = await saveContact(field().value());
if (result.ok) return;
return {kind: 'serverError', message: 'Failed to save'};
});
if (success) {
// Handle success — navigate, show confirmation, etc.
}
}
}
```
## Handling side effects
The `submit()` function returns a `Promise<boolean>` — `true` when the action completes without errors, `false` when validation fails or the action returns errors. Use this to trigger side effects like navigation or notifications.
```ts
async onSave() {
const success = await submit(this.contactForm, async (field) => {
await saveContact(field().value());
});
if (success) {
await this.router.navigate(['/confirmation']);
}
}
```
When the action produces data that a side effect needs, such as a server-generated ID, handle the side effect inside the action:
```ts
async onSave() {
await submit(this.contactForm, async (field) => {
const contact = await createContact(field().value());
await this.router.navigate(['/confirmation', contact.id]);
});
}
```
When using `FormRoot`, side effects also go inside the `action` since `FormRoot` calls `submit()` internally:
```ts
submission: {
action: async (field) => {
const result = await saveContact(field().value());
if (result.ok) {
await this.router.navigate(['/confirmation']);
return;
}
return {kind: 'serverError', message: 'Failed to submit form'};
},
}
```
## Next steps
This guide covered submitting forms and handling form submission errors. Related guides explore other aspects of Signal Forms:
<docs-pill-row>
<docs-pill href="guide/forms/signals/validation" title="Validation" />
<docs-pill href="guide/forms/signals/field-state-management" title="Field state management" />
<docs-pill href="guide/forms/signals/form-logic" title="Adding form logic" />
</docs-pill-row>
@@ -235,13 +235,79 @@ export class UserProfile {
}
```
The `SignalFormControl` synchronizes values and validation status bi-directionally:
The `SignalFormControl` synchronizes values bi-directionally between the **Signal Forms** system and the **Reactive Forms** system:
- **Signal -> Control**: Changing `email.set(...)` updates `emailControl.value` and the parent `form.value`.
- **Control -> Signal**: Typing in the input (updating `emailControl`) updates the `email` signal.
- **Validation**: Schema validators (like `required`) propagate errors to `emailControl.errors`.
- **Signal -> Reactive**: Updating the value via Signal Forms updates the Reactive Form control immediately.
### Disabling/Enabling control.
```typescript
// Signal Forms update
this.emailControl.fieldTree().value.set('new@example.com');
// Reactive Forms reflects the change
console.log(this.form.value); // {email: 'new@example.com'}
```
- **Reactive -> Signal**: Updating the value via the parent `FormGroup` updates the Signal Forms state.
```typescript
// Reactive Forms update
this.form.patchValue({email: 'other@example.com'});
// Signal Forms reflects the change
console.log(this.emailControl.fieldTree().value()); // 'other@example.com'
```
### Binding `SignalFormControl`
To use `SignalFormControl` in a `FormGroup`, pass it as a control and bind it in the template using `.fieldTree`:
```typescript
readonly emailControl = new SignalFormControl('', (p) => { required(p); });
readonly form = new FormGroup({
name: new FormControl('Alice'),
email: this.emailControl,
});
```
```angular-html {prefer}
<form [formGroup]="form">
<!-- Standard control -->
<input formControlName="name" />
<!-- Signal control -->
<input [formField]="emailControl.fieldTree" />
</form>
```
```angular-html {avoid}
<!-- Avoid: Using formControlName or [formControl] for SignalFormControl -->
<input formControlName="email" />
<input [formControl]="emailControl" />
```
### Why `SignalFormControl` takes a value instead of a signal
In standard Signal Forms, you create a form by passing a signal: `form(mySignal)`.
However, `SignalFormControl` takes a **raw value** (like a string or object) as its first argument:
```typescript
// Takes a raw value, not a signal
const userControl = new SignalFormControl({
email: 'pirojok@example.com',
});
```
`SignalFormControl` creates the signal internally to intercept writes and trigger the **synchronous updates** expected by Reactive Forms.
You can still access the internal signal via `.sourceValue`:
```typescript
const value = userControl.sourceValue();
```
### Disabling/Enabling control
Imperative APIs for changing the enabled/disabled state (like `enable()`, `disable()`) are intentionally not supported
in `SignalFormControl`. This is because the state of the control should be derived from the signal state and rules.
@@ -383,6 +449,3 @@ bootstrapApplication(App, {
],
});
```
<!-- TODO: include some high level usage comment about how people should mostly interact with this via the signal forms API exposed on .fieldTree, not via the reactive forms methods. -->
<!-- TODO: Elaborate on why the value taken is not a signal. -->
@@ -0,0 +1,243 @@
# Schemas and schema composability
Signal Forms uses a two-layer architecture to separate _how your form is structured_ from _how it behaves at runtime_.
When you pass a schema function to `form()`, that function _runs once_ during form creation. Its job is to set up the form's logic tree by declaring which fields have validation, which fields are disabled, and which fields depend on other fields. This is the **structural layer** of your form.
Inside a schema function, you call rule functions such as `disabled()` and `validate()`. These rule functions accept reactive logic that recomputes whenever the signals they reference change. Other rules like `required()` accept optional configuration, including a `when` function that conditionally activates the rule. Together, these form the **behavioral layer** of your form during runtime.
```ts
contactForm = form(this.contactModel, (schemaPath) => {
// Schema function: runs ONCE during form creation
required(schemaPath.name);
disabled(schemaPath.couponCode, ({valueOf}) => valueOf(schemaPath.total) < 50);
// ^^^ Reactive logic: recomputes when total changes
});
```
```mermaid
graph TD
A["form(model, schemaFn)"] --> B["Schema function runs ONCE"]
B --> C["required(path)"]
B --> D["disabled(path, logicFn)"]
B --> E["validate(path, logicFn)"]
B --> F["apply(path, schema)"]
C --> G["Reactive: recomputes on signal change"]
D --> G
E --> G
F --> B2["Nested schema function runs ONCE"]
B2 --> G
```
This distinction is important when you compose schemas because functions like `apply()`, `applyWhen()`, and `schema()` all operate at the structural layer. Schemas control _which_ rules exist and _whether_ they're active, while rule functions define _what_ those rules evaluate.
## Create reusable schemas with `schema()`
When multiple forms share the same rules for a common data shape, you can use the `schema()` function to extract those rules into a reusable schema.
```ts
import {schema, required, minLength} from '@angular/forms/signals';
const nameSchema = schema<{first: string; last: string}>((name) => {
required(name.first);
required(name.last);
minLength(name.first, 2);
minLength(name.last, 2);
});
```
The `schema()` function wraps a function and converts it into a reusable `Schema<T>` object. Like any schema function, it _runs once_ per form, but the object itself can be shared across as many forms as you need.
TIP: If rules only appear in one place, an inline schema function works just as well. Use `schema()` when you want to reuse the same schema across multiple forms or apply the same schema to multiple paths. Reusable `Schema` objects are cached per form compilation.
### Using the schema with `apply()`
You can apply a reusable schema to a specific path in a form by using the `apply()` function. When you call `apply()`, the schema receives a scoped path that only sees the fields within that sub-path:
```ts
import {apply} from '@angular/forms/signals';
profileForm = form(this.profileModel, (schemaPath) => {
apply(schemaPath.name, nameSchema);
});
registrationForm = form(this.registrationModel, (schemaPath) => {
apply(schemaPath.name, nameSchema);
});
```
## Conditional schemas with `applyWhen()`
NOTE: The [Adding form logic guide](guide/forms/signals/form-logic) introduced `applyWhen()` for conditional rules with inline logic. This section covers how to compose `applyWhen()` with reusable schemas.
Some rules should only apply under certain conditions. For example, a zip code field might require validation only when the selected country is the United States.
The `applyWhen()` function applies a schema conditionally based on reactive state. It accepts three arguments:
1. A path to apply the schema to
1. A reactive logic function that returns `true` when the schema should be active
1. A schema or schema function containing the conditional rules
```ts
import {form, applyWhen, required, pattern} from '@angular/forms/signals';
addressForm = form(this.addressModel, (schemaPath) => {
applyWhen(
schemaPath,
({valueOf}) => valueOf(schemaPath.country) === 'US',
(schemaPath) => {
required(schemaPath.zipCode);
pattern(schemaPath.zipCode, /^\d{5}(-\d{4})?$/);
},
);
});
```
The logic function receives a `FieldContext`, which provides access to `value`, `valueOf`, `stateOf`, and other reactive helpers. Because it's reactive, the condition is re-evaluated whenever the signals it reads change. When the condition becomes `false`, the rules inside the schema deactivate. When it becomes `true` again, they reactivate.
The schema itself is still structural — the schema function runs once during form creation. The condition controls whether those rules are _active_, not whether they _exist_.
Inside the conditional schema, use the scoped path parameter passed to that schema function. Paths from an outer schema are not valid inside a nested schema.
### Combining `applyWhen()` with reusable schemas
Since `applyWhen()` accepts a `Schema` object, you can pair it with `schema()` to conditionally apply reusable schemas:
```ts
const usZipCodeSchema = schema<{zipCode: string}>((address) => {
required(address.zipCode);
pattern(address.zipCode, /^\d{5}(-\d{4})?$/);
});
const caPostalCodeSchema = schema<{postalCode: string}>((address) => {
required(address.postalCode);
pattern(address.postalCode, /^[A-Z]\d[A-Z] \d[A-Z]\d$/);
});
shippingForm = form(this.shippingModel, (schemaPath) => {
applyWhen(
schemaPath.address,
({valueOf}) => valueOf(schemaPath.country) === 'US',
usZipCodeSchema,
);
applyWhen(
schemaPath.address,
({valueOf}) => valueOf(schemaPath.country) === 'CA',
caPostalCodeSchema,
);
});
```
NOTE: The logic function accesses `valueOf(schemaPath.country)` even though the path argument is `schemaPath.address`. This is because the `valueOf` helper can access any field in the form, not just fields within the scoped path.
This pattern keeps validation logic modular — each country's address rules live in their own schema, and the form selects which one to activate based on the user's choice.
## Type-narrowing with `applyWhenValue()`
The `applyWhenValue()` function simplifies conditions that only need to check the field's value. Instead of receiving a `FieldContext`, the condition function receives the field's raw value directly.
```ts {header: "applyWhen — logic function receives FieldContext"}
applyWhen(schemaPath.payment, ({value}) => value().type === 'credit-card', creditCardSchema);
```
```ts {header: "applyWhenValue — condition receives the value directly"}
applyWhenValue(schemaPath.payment, (payment) => payment.type === 'credit-card', creditCardSchema);
```
The main advantage of `applyWhenValue()` is TypeScript type guard support. When the condition function is a type guard, the schema's type parameter narrows to the guarded type. This is especially useful for discriminated unions, where each variant has different fields that need different rules.
```ts
import {form, applyWhenValue, required} from '@angular/forms/signals';
interface CreditCard {
type: 'credit-card';
cardNumber: string;
expiry: string;
cvv: string;
}
interface BankTransfer {
type: 'bank-transfer';
accountNumber: string;
routingNumber: string;
}
type PaymentMethod = CreditCard | BankTransfer;
function isCreditCard(value: PaymentMethod): value is CreditCard {
return value.type === 'credit-card';
}
function isBankTransfer(value: PaymentMethod): value is BankTransfer {
return value.type === 'bank-transfer';
}
paymentForm = form(this.paymentModel, (schemaPath) => {
applyWhenValue(schemaPath.payment, isCreditCard, (payment) => {
// TypeScript knows payment is scoped to CreditCard
required(payment.cardNumber);
required(payment.expiry);
required(payment.cvv);
});
applyWhenValue(schemaPath.payment, isBankTransfer, (payment) => {
// TypeScript knows payment is scoped to BankTransfer
required(payment.accountNumber);
required(payment.routingNumber);
});
});
```
Without the type guard, TypeScript would not know which fields are available inside each schema function. The type narrowing ensures that accessing `payment.cardNumber` is type-safe in the credit card branch and `payment.accountNumber` is type-safe in the bank transfer branch.
## Array items with `applyEach()`
When a form contains an array of objects, you often need the same rules applied to every item. The `applyEach()` function applies a schema to each item in an array field, regardless of how many items exist.
```ts
import {form, applyEach, required, min} from '@angular/forms/signals';
type LineItem = {name: string; quantity: number};
orderForm = form(this.orderModel, (schemaPath) => {
required(schemaPath.title);
applyEach(schemaPath.items, (item) => {
required(item.name);
min(item.quantity, 1);
});
});
```
The schema function passed to `applyEach()` receives a `SchemaPathTree` scoped to a single array item. Rules declared inside apply to every item in the array, including items added after form creation.
### Combining `applyEach()` with reusable schemas
Since `applyEach()` accepts a `Schema` object, you can extract item-level rules into a reusable schema and share them across forms:
```ts
const lineItemSchema = schema<LineItem>((item) => {
required(item.name);
min(item.quantity, 1);
});
orderForm = form(this.orderModel, (schemaPath) => {
required(schemaPath.title);
applyEach(schemaPath.items, lineItemSchema);
});
invoiceForm = form(this.invoiceModel, (schemaPath) => {
required(schemaPath.invoiceNumber);
applyEach(schemaPath.lineItems, lineItemSchema);
});
```
TIP: For more on validating array items, including custom error messages per field, see the [Validation guide](guide/forms/signals/validation).
## Next steps
To learn more about Signal Forms, check out these related guides:
- [Adding form logic](guide/forms/signals/form-logic) - Learn how to add conditional logic, dynamic behavior, and metadata to your forms
- [Validation](guide/forms/signals/validation) - Learn about validation rules and error handling
- [Async operations](guide/forms/signals/async-operations) - Learn how to handle form submission and async validation
@@ -10,7 +10,7 @@ Angular supports two design approaches for interactive forms. Template-driven fo
Template-driven forms are a great choice for small or simple forms, while reactive forms are more scalable and suitable for complex forms. For a comparison of the two approaches, see [Choosing an approach](guide/forms#choosing-an-approach)
</docs-callout>
You can build almost any kind of form with an Angular template —login forms, contact forms, and pretty much any business form.
You can build almost any kind of form with an Angular template — login forms, contact forms, and pretty much any business form.
You can lay out the controls creatively and bind them to the data in your object model.
You can specify validation rules and display validation errors, conditionally allow input from specific controls, trigger built-in visual feedback, and much more.
@@ -182,8 +182,7 @@ The following table describes the class names that Angular applies based on the
| The control's value has changed. | `ng-dirty` | `ng-pristine` |
| The control's value is valid. | `ng-valid` | `ng-invalid` |
Angular also applies the `ng-submitted` class to `form` elements upon submission,
but not to the controls inside the `form` element.
Angular also applies the `ng-submitted` class to `form` elements upon submission, but not to the controls inside the `form` element.
You use these CSS classes to define the styles for your control based on its status.
@@ -336,7 +335,7 @@ You will bind the form property that indicates its overall validity to the **Sub
</docs-step>
<docs-step title="Run the application">
Notice that the button is enabled —although it doesn't do anything useful yet.
Notice that the button is enabled — although it doesn't do anything useful yet.
</docs-step>
<docs-step title="Delete the Name value">
+3 -3
View File
@@ -27,7 +27,7 @@ const emailDomain = login.value.email.domain;
With strictly typed reactive forms, the above code does not compile, because there is no `domain` property on `email`.
In addition to the added safety, the types enable a variety of other improvements, such as better autocomplete in IDEs, and an explicit way to specify form structure.
In addition to the added safety, the types enable a variety of other improvements, such as better autocomplete in IDEs and an explicit way to specify form structure.
These improvements currently apply only to _reactive_ forms (not [_template-driven_ forms](guide/forms/template-driven-forms)).
@@ -42,7 +42,7 @@ const login = new UntypedFormGroup({
});
```
Each `Untyped` symbol has exactly the same semantics as in previous Angular version. By removing the `Untyped` prefixes, you can incrementally enable the types.
Each `Untyped` symbol has exactly the same semantics as in previous Angular versions. By removing the `Untyped` prefixes, you can incrementally enable the types.
## `FormControl`: Getting Started
@@ -52,7 +52,7 @@ The simplest possible form consists of a single control:
const email = new FormControl('angularrox@gmail.com');
```
This control will be automatically inferred to have the type `FormControl<string|null>`. TypeScript will automatically enforce this type throughout the [`FormControl` API](api/forms/FormControl), such as `email.value`, `email.valueChanges`, `email.setValue(...)`, etc.
This control will be automatically inferred to have the type `FormControl<string|null>`. TypeScript will automatically enforce this type throughout the [`FormControl` API](api/forms/FormControl), such as `email.value`, `email.valueChanges`, and `email.setValue(...)`.
### Nullability
+1 -1
View File
@@ -142,7 +142,7 @@ Providing a custom or a "noop" Zone.js implementation may lead to a different ti
## Errors
There are several hydration related errors you may encounter ranging from node mismatches to cases when the `ngSkipHydration` was used on an invalid host node. The most common error case that may occur is due to direct DOM manipulation using native APIs that results in hydration being unable to find or match the expected DOM tree structure on the client that was rendered by the server. The other case you may encounter this type of error was mentioned in the [Valid HTML structure](#valid-html-structure) section earlier. So, make sure the HTML in your templates are using valid structure, and you'll avoid that error case.
There are several hydration related errors you may encounter ranging from node mismatches to cases when the `ngSkipHydration` was used on an invalid host node. The most common error case that may occur is due to direct DOM manipulation using native APIs that results in hydration being unable to find or match the expected DOM tree structure on the client that was rendered by the server. The other case you may encounter this type of error was mentioned in the [Valid HTML structure](#valid-html-structure) section earlier. So, make sure the HTML in your templates is using valid structure, and you'll avoid that error case.
For a full reference on hydration related errors, visit the [Errors Reference Guide](/errors).
@@ -147,6 +147,29 @@ export class UserDetail {
}
```
When navigating multiple levels up, all `..` segments must be in the **first element** of the commands array. The router only parses `..` from the first command string — subsequent array elements are treated as literal path segments.
```angular-ts {prefer}
// From: /team/123/users/456
// Result: /team/123/settings
this.router.navigate(['../../settings'], {relativeTo: this.route});
```
When using `relativeTo`, never prefix the first command with `/`. A leading `/` makes the navigation absolute and ignores `relativeTo` entirely.
```angular-ts {prefer}
// From: /team/123/users/456
// Result: /team/123/users/456/edit
this.router.navigate(['edit'], {relativeTo: this.route});
```
```angular-ts {avoid}
// From: /team/123/users/456
// Leading '/' causes absolute navigation — relativeTo is ignored
// Result: /edit
this.router.navigate(['/edit'], {relativeTo: this.route});
```
### `router.navigateByUrl()`
The `router.navigateByUrl()` method provides a direct way to programmatically navigate using URL path strings rather than array segments. This method is ideal when you have a full URL path and need to perform absolute navigation, especially when working with externally provided URLs or deep linking scenarios.
@@ -269,3 +269,7 @@ To learn more about Signal Forms and how it works, check out the in-depth guides
- [Form models](guide/forms/signals/models) - Creating and managing form data with signals
- [Field state management](guide/forms/signals/field-state-management) - Working with validation state, interaction tracking, and field visibility
- [Validation](guide/forms/signals/validation) - Built-in validators, custom validation rules, and async validation
<docs-pill-row>
<docs-pill title="Modular design with dependency injection" href="essentials/dependency-injection" />
</docs-pill-row>
@@ -147,6 +147,6 @@ TIP: Want to know more about Angular templates? See the [In-depth Templates guid
Now that you have dynamic data and templates in the application, it's time to learn how to enhance templates by conditionally hiding or showing certain elements, looping over elements, and more.
<docs-pill-row>
<docs-pill title="Modular design with dependency injection" href="essentials/dependency-injection" />
<docs-pill title="Forms with Signals" href="essentials/signal-forms" />
<docs-pill title="In-depth template guide" href="guide/templates" />
</docs-pill-row>
@@ -51,7 +51,7 @@
Angular CLI's `ng update` runs automated code transformations that automatically handle routine breaking changes, dramatically simplifying major version updates. Keeping up with the latest version keeps your app as fast and secure as possible.
</docs-card>
<docs-card title="Language Service" href="tools/language-service" link="Language Service" iconImgSrc="adev/src/assets/icons/language-service.svg">
Angular's IDE language services powers code completion, navigation, refactoring, and real-time diagnostics in your favorite editor.
Angular's IDE language service powers code completion, navigation, refactoring, and real-time diagnostics in your favorite editor.
</docs-card>
</docs-card-container>
@@ -0,0 +1,46 @@
# Missing Control Value
This error occurs when you call `setValue` on a `FormGroup` or `FormArray` but the value you pass is missing an entry for one of the registered controls.
`setValue` is strict — it expects a value for every control. If you want to update only some controls, use `patchValue` instead.
## Debugging the error
Check which control is named in the error message, then make sure your value object includes it.
A common source of this error is spreading an object that doesn't have all the keys:
```typescript
const someValue = {first: 'Nancy'}; // 'last' is missing
form.setValue({...someValue}); // throws NG01002
```
This can happen if `someValue` comes from an API response, a partial state update, or a type that doesn't fully match the form structure. In those cases, either fill in the missing keys explicitly or switch to `patchValue`.
### FormGroup
```typescript
const form = new FormGroup({
first: new FormControl(''),
last: new FormControl(''),
});
// 'last' is not in the value — throws NG01002
form.setValue({first: 'Nancy'});
// both controls are covered — works fine
form.setValue({first: 'Nancy', last: 'Drew'});
```
### FormArray
```typescript
const formArray = new FormArray([new FormControl(''), new FormControl('')]);
// only one value for two controls — throws NG01002
formArray.setValue(['Nancy']);
// one value per control — works fine
formArray.setValue(['Nancy', 'Drew']);
```
+1 -1
View File
@@ -17,7 +17,7 @@ class Test {
}
```
In the provided example the `item.key` tracking expression will find two duplicate keys `a` (at index 0 and 2).
In the provided example the `item.value` tracking expression will find two duplicate keys `a` (at index 0 and 2).
Duplicate keys are problematic from the correctness point of view: since the `@for` loop can't uniquely identify items it might choose DOM nodes corresponding to _another_ item (with the same key) when performing DOM moves or destroy.
@@ -36,6 +36,7 @@
| `NG0951` | [Child query result is required but no value is available](errors/NG0951) |
| `NG0955` | [Track expression resulted in duplicated keys for a given collection](errors/NG0955) |
| `NG0956` | [Tracking expression caused re-creation of the DOM structure](errors/NG0956) |
| `NG01002` | [Missing Control Value](errors/NG01002) |
| `NG01101` | [Wrong Async Validator Return Type](errors/NG01101) |
| `NG01203` | [Missing value accessor](errors/NG01203) |
| `NG02200` | [Missing Iterable Differ](errors/NG02200) |
+18 -17
View File
@@ -38,7 +38,7 @@ Start developing with the latest Angular features from our roadmap. This list re
### Bringing the best of AI to Angular
<docs-card-container>
<docs-card title="AI Powered Angular" href="">
<docs-card title="AI Powered Angular">
AI continues to shape the development landscape. It has changed how we develop apps and the types of user experiences that are possible. We plan to best support the developer community in AI-assisted coding and integrating AI in their applications.
</docs-card>
<docs-card title="AI Development">
@@ -73,11 +73,11 @@ As part of this project, we'll explore the requirement space of cross framework
<docs-card title="Signal Forms" href="/guide/forms/signals/overview">
In Angular v21, we landed an experimental version of Signal Forms. This new approach allows developers to manage form state using signals, providing an ergonomic forms creation experience. Next, our plans include promoting Signal Forms to stable and enhancing interoperability with reactive forms - enabling teams to progressively migrate large forms at their own pace.
</docs-card>
<docs-card title="Reactivity" href="">
<docs-card title="Reactivity">
We introduced experimental signal APIs, resource() and httpResource(), for flexible asynchronous data handling. We plan to promote these APIs to developer preview/stable based on community feedback.
We are also evaluating new APIs for unhandled use cases, considering community benefits and tradeoffs before implementation after careful consideration.
</docs-card>
<docs-card title="Change Detection" href="">
<docs-card title="Change Detection">
With Zoneless being stable and default, we are also planning to shift the default change detection strategy to OnPush, to follow current best practices. [See the RFC discussion for details](https://github.com/angular/angular/discussions/66779).
</docs-card>
<docs-card title="Components">
@@ -90,7 +90,7 @@ For accessibility, we are continuously evaluating the components and patterns ag
### Improve tooling
<docs-card-container>
<docs-card title="Modernize unit testing tooling with ng test" href="">
<docs-card title="Modernize unit testing tooling with ng test">
Following the stable release of Vitest in Angular v21, it is now our primary test runner. We are now focused on promoting our experimental Karma to Vitest migration tool to stable as well as investigating new features to further refine and improve the developer testing workflow.
</docs-card>
</docs-card-container>
@@ -98,7 +98,7 @@ For accessibility, we are continuously evaluating the components and patterns ag
## Completed projects
<docs-card-container>
<docs-card title="Signal debugging in Angular DevTools" href="" link="Completed in 2025">
<docs-card title="Signal debugging in Angular DevTools" link="Completed in 2025">
With the evolution of Signals in Angular, we are working on a better tooling for debugging them. High on the priority list is a UI for inspecting and debugging signals.
</docs-card>
<docs-card title="Improve HMR (Hot Module Reload)" href="https://github.com/angular/angular/issues/39367#issuecomment-1439537306" link="Completed in 2025">
@@ -106,19 +106,20 @@ For accessibility, we are continuously evaluating the components and patterns ag
In Angular v19 we shipped initial support for CSS and template HMR and in v20 we graduated template HMR to stable. We'll continue collecting feedback to make sure we're addressing developers' needs before we mark this project as complete.
</docs-card>
<docs-card title="Zoneless Angular" href="" link="Completed in Q4 2025">
<docs-card title="Zoneless Angular" link="Completed in Q4 2025">
In v18 we shipped experimental zoneless support in Angular. It enables developers to use the framework without including zone.js in their bundle, which improves performance, debugging experience, and interoperability. As part of the initial release we also introduced zoneless support to the Angular CDK and Angular Material.
In v19 we introduced zoneless support in server-side rendering, addressed some edge cases, and created a schematic to scaffold zoneless projects. We transitioned <a href="https://fonts.google.com/">Google Fonts</a> to zoneless which improved performance, developer experience, and allowed us to identify gaps that we need to address before moving this feature to developer preview.
In v19 we introduced zoneless support in server-side rendering, addressed some edge cases, and created a schematic to scaffold zoneless projects. We transitioned <a href="https://fonts.google.com/">Google Fonts</a> to zoneless which improved performance, developer experience, and allowed us to identify gaps that we need to address before moving this feature to developer preview.
As of Angular v20.2, Zoneless Angular is now stable and includes improvements in error handling and server-side rendering.
</docs-card>
<docs-card title="Server route configuration" link="Completed in Q2 2025" href="">
We're working towards enabling a more ergonomic route configuration on the server. We want to make it trivial to declare which routes should be server-side rendered, prerendered or client-side rendered.
As of Angular v20.2, Zoneless Angular is now stable and includes improvements in error handling and server-side rendering.
</docs-card>
<docs-card title="Server route configuration" link="Completed in Q2 2025" >
We're working towards enabling a more ergonomic route configuration on the server. We want to make it trivial to declare which routes should be server-side rendered, prerendered or client-side rendered.
In Angular v19 we shipped developer preview of route-level render mode which allows you to granularly configure which routes you want Angular to prerender, server-side render or client-side render. In Angular v20 we graduated it to stable.
</docs-card>
<docs-card title="Enable incremental hydration" link="Completed in Q2 2025" href="">
<docs-card title="Enable incremental hydration" link="Completed in Q2 2025">
In v17 we graduated hydration from developer preview and we've been consistently observing 40-50% improvements in LCP. Since then we started prototyping incremental hydration and shared a demo on stage at ng-conf.
In v19 we shipped the incremental hydration in developer preview mode, powered by `@defer` blocks. In Angular v20 we graduated it to stable!
@@ -131,15 +132,15 @@ In Angular v20 we graduated all the fundamental reactivity primitives to stable
<docs-card title="Support two-dimensional drag-and-drop" link="Completed in Q2 2024" href="https://github.com/angular/components/issues/13372">
As part of this project, we implemented mixed orientation support for the Angular CDK drag and drop. This is one of the repository's most highly requested features.
</docs-card>
<docs-card title="Event replay with SSR and prerendering" link="Completed in Q4 2024" href="https://angular.dev/api/platform-browser/withEventReplay">
<docs-card title="Event replay with SSR and prerendering" link="Completed in Q4 2024" href="api/platform-browser/withEventReplay">
In v18 we introduced an event replay functionality when using server-side rendering or prerendering. For this feature we depend on the event dispatch primitive (previously known as jsaction) that is running on Google.com.
In Angular v19 we graduated event replay to stable and enabled it by default for all new projects.
</docs-card>
<docs-card title="Integrate Angular Language Service with Schematics" link="Completed in Q4 2024" href="">
<docs-card title="Integrate Angular Language Service with Schematics" link="Completed in Q4 2024">
To make it easier for developers to use modern Angular APIs, we enabled integration between the Angular language service and schematics which allows you to refactor your app with a single click.
</docs-card>
<docs-card title="Streamline standalone imports with Language Service" link="Completed in Q4 2024" href="">
<docs-card title="Streamline standalone imports with Language Service" link="Completed in Q4 2024">
As part of this initiative, the language service automatically imports components and pipes in standalone and NgModule-based apps. Additionally, we've added a template diagnostic to highlight unused imports in standalone components, which should help make application bundles smaller.
</docs-card>
<docs-card title="Local template variables" link="Completed in Q3 2024">
@@ -153,7 +154,7 @@ In v17.2 we shared experimental support for Angular Material 3 and in v18 we gra
<docs-card title="Introduce deferred loading" link="Completed in Q2 2024" href="https://next.angular.dev/guide/templates/defer">
In v17 we shipped deferrable views in developer preview, which provide an ergonomic API for deferred code loading. In v18 we enabled deferrable views for library developers and graduated the API to stable.
</docs-card>
<docs-card title="iframe support in Angular DevTools" link="Completed in Q2 2024" href="">
<docs-card title="iframe support in Angular DevTools" link="Completed in Q2 2024">
We enabled debugging and profiling of Angular apps embedded within an iframe on the page.
</docs-card>
<docs-card title="Automation for transition of existing hybrid rendering projects to esbuild and vite" link="Completed in Q2 2024" href="tools/cli/build-system-migration">
@@ -165,7 +166,7 @@ Angular.dev is the new site, domain and home for Angular development. The new si
<docs-card title="Introduce built-in control flow" link="Completed in Q2 2024" href="https://next.angular.dev/essentials/conditionals-and-loops">
In v17 we shipped a developer preview version of a new control flow. It brings significant performance improvements and better ergonomics for template authoring. We also provided a migration of existing `*ngIf`, `*ngFor`, and `*ngSwitch` which you can run to move your project to the new implementation. As of v18 the built-in control flow is now stable.
</docs-card>
<docs-card title="Modernize getting started tutorial" link="Completed Q4 2023" href="">
<docs-card title="Modernize getting started tutorial" link="Completed Q4 2023">
Over the past two quarters, we developed a new [video](https://www.youtube.com/watch?v=xAT0lHYhHMY&list=PL1w1q3fL4pmj9k1FrJ3Pe91EPub2_h4jF) and [textual](/tutorials/learn-angular) tutorial based on standalone components.
</docs-card>
<docs-card title="Investigate modern bundlers" link="Completed Q4 2023" href="guide/hydration">
@@ -130,7 +130,7 @@ The following list discusses all the `browser` builder options that will need to
- `resourcesOutputPath` should be removed, this is now always `media`.
- `vendorChunk` should be removed, as this was a performance optimization which is no longer needed.
- `commonChunk` should be removed, as this was a performance optimization which is no longer needed.
- `deployUrl` should be removed and is not supported. Prefer [`<base href>`](guide/routing/common-router-tasks) instead. See [deployment documentation](tools/cli/deployment#--deploy-url) for more information.
- `deployUrl` should be removed and is not supported. Prefer [`<base href>`](guide/routing/router-reference#base-href) instead. See [deployment documentation](tools/cli/deployment#--deploy-url) for more information.
- `ngswConfigPath` should be renamed to `serviceWorker`.
If the application is not using SSR currently, this should be the final step to allow `ng build` to function.
+7 -7
View File
@@ -8,13 +8,13 @@ Angular CLI includes four builders typically used as `build` targets:
| Builder | Purpose |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@angular-devkit/build-angular:application` | Builds an application with a client-side bundle, a Node server, and build-time prerendered routes with [esbuild](https://esbuild.github.io/). |
| `@angular/build:application` | Builds an application with a client-side bundle, a Node server, and build-time prerendered routes with [esbuild](https://esbuild.github.io/). |
| `@angular-devkit/build-angular:browser-esbuild` | Bundles a client-side application for use in a browser with [esbuild](https://esbuild.github.io/). See [`browser-esbuild` documentation](tools/cli/build-system-migration#manual-migration-to-the-compatibility-builder) for more information. |
| `@angular-devkit/build-angular:browser` | Bundles a client-side application for use in a browser with [webpack](https://webpack.js.org/). |
| `@angular-devkit/build-angular:ng-packagr` | Builds an Angular library adhering to [Angular Package Format](tools/libraries/angular-package-format). |
| `@angular/build:ng-packagr` | Builds an Angular library adhering to [Angular Package Format](tools/libraries/angular-package-format). |
Applications generated by `ng new` use `@angular-devkit/build-angular:application` by default.
Libraries generated by `ng generate library` use `@angular-devkit/build-angular:ng-packagr` by default.
Applications generated by `ng new` use `@angular/build:application` by default.
Libraries generated by `ng generate library` use `@angular/build:ng-packagr` by default.
You can determine which builder is being used for a particular project by looking up the `build` target for that project.
@@ -25,7 +25,7 @@ You can determine which builder is being used for a particular project by lookin
"architect": {
// `ng build` invokes the Architect target named `build`.
"build": {
"builder": "@angular-devkit/build-angular:application",
"builder": "@angular/build:application",
…
},
"serve": { … }
@@ -37,7 +37,7 @@ You can determine which builder is being used for a particular project by lookin
}
```
This page discusses usage and options of `@angular-devkit/build-angular:application`.
This page discusses usage and options of `@angular/build:application`.
## Output directory
@@ -142,7 +142,7 @@ If the best option is to use a CommonJS dependency, you can disable these warnin
```json
"build": {
"builder": "@angular-devkit/build-angular:browser",
"builder": "@angular/build:application",
"options": {
"allowedCommonJsDependencies": [
"lodash"
+5 -7
View File
@@ -147,8 +147,7 @@ In the `package.json` file, add a `builders` key that tells the Architect tool w
"description": "Builder for copying files",
"builders": "builders.json",
"dependencies": {
"@angular-devkit/architect": "~0.1200.0",
"@angular-devkit/core": "^12.0.0"
"@angular/build": "^21.2.0"
}
}
```
@@ -169,7 +168,7 @@ A target specifies the builder to use, its default options configuration, and na
Architect in the Angular CLI uses the target definition to resolve input options for a given run.
The `angular.json` file has a section for each project, and the "architect" section of each project configures targets for builders used by CLI commands such as 'build', 'test', and 'serve'.
By default, for example, the `ng build` command runs the builder `@angular-devkit/build-angular:browser` to perform the build task, and passes in default option values as specified for the `build` target in `angular.json`.
By default, for example, the `ng build` command runs the builder `@angular/build:application` to perform the build task, and passes in default option values as specified for the `build` target in `angular.json`.
```json {header: "angular.json"}
{
@@ -177,7 +176,7 @@ By default, for example, the `ng build` command runs the builder `@angular-devki
"...": "...",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/myApp",
"index": "src/index.html",
@@ -267,7 +266,7 @@ If you create a new project with `ng new builder-test`, the generated `angular.j
"builder-test": {
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/builder-test",
"index": "src/index.html",
@@ -278,8 +277,7 @@ If you create a new project with `ng new builder-test`, the generated `angular.j
"configurations": {
"production": {
"optimization": true,
"aot": true,
"buildOptimizer": true
"aot": true
}
}
}
+2 -2
View File
@@ -44,7 +44,7 @@ To manually deploy your application, create a production build and copy the outp
By default, `ng build` uses the `production` configuration.
If you have customized your build configurations, you may want to confirm [production optimizations](tools/cli/deployment#production-optimizations) are being applied before deploying.
`ng build` outputs the built artifacts to `dist/my-app/` by default, however this path can be configured with the `outputPath` option in the `@angular-devkit/build-angular:browser` builder.
`ng build` outputs the built artifacts to `dist/my-app/` by default, however this path can be configured with the `outputPath` option in the `@angular/build:application` builder.
Copy this directory to the server and configure it to serve the directory.
While this is a minimal deployment solution, there are a few requirements for the server to serve your Angular application correctly.
@@ -128,7 +128,7 @@ ng build --deploy-url /my/assets
```
The effect and purpose of `--deploy-url` overlaps with [`<base href>`](guide/routing/common-router-tasks). Both can be used for initial scripts, stylesheets, lazy scripts, and css resources.
The effect and purpose of `--deploy-url` overlaps with [`<base href>`](guide/routing/router-reference#base-href). Both can be used for initial scripts, stylesheets, lazy scripts, and css resources.
Unlike `<base href>` which can be defined in a single place at runtime, the `--deploy-url` needs to be hard-coded into an application at build time.
Prefer `<base href>` where possible.
+5 -3
View File
@@ -16,7 +16,7 @@ Angular CLI builders support a `configurations` object, which allows overwriting
"my-app": {
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"builder": "@angular/build:application",
"options": {
// By default, disable source map generation.
"sourceMap": false
@@ -54,7 +54,7 @@ ng build --configuration debug,production,customer-facing
## Configure environment-specific defaults
`@angular-devkit/build-angular:browser` supports file replacements, an option for substituting source files before executing a build.
`@angular/build:application` supports file replacements, an option for substituting source files before executing a build.
Using this in combination with `--configuration` provides a mechanism for configuring environment-specific data in your application.
Start by [generating environments](cli/generate/environments) to create the `src/environments/` directory and configure the project to use file replacements.
@@ -99,6 +99,8 @@ export const environment = {
};
```
CRITICAL: Files in `src/environments/` are bundled into your client-side application and visible to anyone who loads the page. Never store secrets such as API keys here. Use a server-side proxy or a secrets manager instead.
You can add target-specific configuration files, such as `environment.development.ts`.
The following content sets default values for the development build target:
@@ -186,7 +188,7 @@ You can also configure `ng serve` to use the targeted build configuration if you
```json
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"builder": "@angular/build:dev-server",
"options": { … },
"configurations": {
"development": {
+3 -2
View File
@@ -15,15 +15,16 @@ HELPFUL: Chrome's new tab page does not run installed extensions, so the Angular
## Open your application
When you open the extension, you'll see three additional tabs:
When you open the extension, you'll see four additional tabs:
| Tabs | Details |
| :---------------------------------------- | :--------------------------------------------------------------------------------------------------------------------- |
| [Components](tools/devtools/component) | Lets you explore the components and directives in your application and preview or edit their state. |
| [Profiler](tools/devtools/profiler) | Lets you profile your application and understand what the performance bottleneck is during change detection execution. |
| [Injector Tree](tools/devtools/injectors) | Lets you visualize the Environment and Element Injector hierarchy |
| [Router Tree](tools/devtools/router) | Lets you visualize the routing tree of your application. |
Other tabs like `Router Tree` or `Transfer State` are experimental and can be enabled via the devtools settings and are not documented yet.
Other tabs like `Transfer State` are experimental and can be enabled via the devtools settings and are not documented yet.
HELPFUL: For users of Chromium based browsers, you might be interested in the [Performance panel integration](/best-practices/profiling-with-chrome-devtools).
+30
View File
@@ -0,0 +1,30 @@
# Inspect the Router Tree
The **Router Tree** tab lets you visualize the routing tree of your application. You can explore how routes are nested and view details about specific routes.
<img src="assets/images/guide/devtools/router-tree.png" alt="A screenshot of the 'Router Tree' tab in Angular DevTools showing a tree of configured routes. The active routes are highlighted in green, while inactive ones are white.">
### View route details
When you select a specific route in the tree, Angular DevTools displays its properties in the sidebar on the right. This information includes:
- **Path**: The URL path for the route. If the route uses a custom URL matcher, DevTools displays the **Matcher** instead.
- **Component**: The component rendered for this route. If the route is a redirect, DevTools displays the **Redirect to** target instead.
- **Path Match**: The path matching strategy (`prefix` or `full`), if configured.
- **Data**: Static data associated with the route, displayed as a JSON tree.
- **Resolvers**: Route resolvers, displayed as key-value pairs.
- **Guards**: Any guards configured on the route, grouped by type — `canActivate`, `canActivateChild`, `canDeactivate`, and `canMatch`.
- **Providers**: Route-level providers, if configured.
- **Title**: The route title, if configured.
- **RunGuardsAndResolvers**: The re-run strategy for guards and resolvers, if configured.
- **Active**: Whether this route is currently active.
- **Auxiliary**: Indicates if the route is an auxiliary route (e.g., in a named outlet).
- **Lazy**: Indicates if the route is lazily loaded.
Note: Properties like Path Match, Data, Resolvers, Guards, Providers, Title, and RunGuardsAndResolvers only appear in the sidebar when they are configured on the selected route.
### Navigate to a specific route
You can easily trigger navigation directly from the DevTools. While inspecting a route's details in the right sidebar, click on the **Navigate** icon next to the path string. This triggers the Angular router to navigate to that URL in your application.
<img src="assets/images/guide/devtools/router-tree-navigate.png" alt="A screenshot showing the 'Navigate to' tooltip on the route path in the 'Routes Details' sidebar.">
@@ -49,7 +49,7 @@ When you generate a new library, the workspace configuration file, `angular.json
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"builder": "@angular/build:ng-packagr",
…
```
@@ -228,7 +228,7 @@ ng build my-lib --watch
IMPORTANT: The CLI `build` command uses a different builder and invokes a different build tool for libraries than it does for applications.
- The build system for applications, `@angular-devkit/build-angular`, is based on `webpack`, and is included in all new Angular CLI projects
- The build system for applications, `@angular/build`, is based on `esbuild`, and is included in all new Angular CLI projects
- The build system for libraries is based on `ng-packagr`.
It is only added to your dependencies when you add a library using `ng generate library my-lib`.
@@ -274,7 +274,7 @@ To use linked libraries, you need to configure your application's `angular.json`
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"builder": "@angular/build:dev-server",
"options": {
"prebundle": {
"exclude": ["my-lib"]
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More