Compare commits

...

106 Commits

Author SHA1 Message Date
Alon Mishne ef9669c1f8 release: cut the v21.2.11 release 2026-04-29 15:38:40 -07:00
Kam 6c6db5b604 docs(docs-infra): open update guide external links in new tabs
External links in the update guide opened inconsistently. Override
marked's link renderer to add `target="_blank" rel="noopener noreferrer"`
to external anchors and apply the `external-link-with-icon` mixin for
the icon. Convert raw HTML and bare URLs in recommendations.ts to
markdown so they all flow through the renderer.

(cherry picked from commit 13c0422029)
2026-04-29 20:59:46 +00:00
Kam 185862ef76 docs: document allowedHosts SSR requirement in v21 update guide
The GHSA-x288-3778-4hhx patch requires `allowedHosts` on
`CommonEngine` or SSR silently falls back to CSR. Add a checklist
item to the v21 update guide.

(cherry picked from commit 2101b13653)
2026-04-29 20:59:17 +00:00
Bhuvansh855 631e303dd9 docs: improve clarity in dependency injection guide
(cherry picked from commit 9f7d41f296)
2026-04-29 20:38:21 +00:00
Alan Agius 41e4851928 docs: update documentation for platform server URL token options
This `baseUrl` option is not available.

(cherry picked from commit 0f7086add4)
2026-04-29 20:37:04 +00:00
Andrew Scott a24dcfd1ec refactor(compiler-cli): remove reflectionhost from environment
all necessary info is already available in the tcb meta objects. environments without full ts program no longer need a reflectionhost for tcb generation

(cherry picked from commit c70625e806)
2026-04-29 20:36:25 +00:00
Ben Hong 1dde3827e9 docs: add debouncing section to signal forms async operations
Co-authored-by: Matthieu Riegler <kyro38@gmail.com>

Co-authored-by: Matthieu Riegler <kyro38@gmail.com>

Co-authored-by: Matthieu Riegler <kyro38@gmail.com>
(cherry picked from commit 18826de489)
2026-04-29 20:35:22 +00:00
Andrew Scott b40b67cdc1 fix(vscode-extension): Look for tsdk override in the new js/ts.tsdk.path setting
recent versions of vscode use js/ts.tsdk.path rather than typescript.tsdk

relates to #68423
2026-04-29 13:32:02 -07:00
Angular Robot 4900e453e1 build: update cross-repo angular dependencies
See associated pull request for more information.
2026-04-29 13:29:44 -07:00
Matthieu Riegler 42d57c3578 refactor(common): fix viewport tests
10ad3c0 broke the 21.2.x branch
2026-04-28 16:38:11 -07:00
SkyZeroZx 10ad3c0692 fix(common): prevent focus from scrollToAnchor
Focus the target element using `focus({preventScroll: true})` after scrolling, so the browser doesn’t adjust the scroll position when applying focus.

Fixes #65938

(cherry picked from commit 97cac1cf4d)
2026-04-28 19:39:31 +00:00
Denis Balan d07f502946 docs: Fix links to Firebase AI Logic Angular example
(cherry picked from commit 5dfe37df8e)
2026-04-28 19:10:52 +00:00
Suraj Yadav 600da64ba4 docs(forms): add NG01902 error reference and link to docs
Add the NG01902 (Orphan field in signal forms) documentation page
to the Error Encyclopedia and change the ORPHAN_FIELD_PROPERTY
error code to -1902 so Angular's RuntimeError automatically appends
a link to angular.dev/errors/NG01902 in the thrown error message.

(cherry picked from commit f2c6445681)
2026-04-28 19:07:50 +00:00
Matthieu Riegler a40e2cebc8 fix(core): fix ordering of view queries metadata in JIT mode
AOT was generating an array that was ordered as signal queries first, then the decorator queries.
Aligning JIT with AOT fixes the issue illustrated by the test.

fixes #68404

(cherry picked from commit 8c11816490)
2026-04-28 19:03:45 +00:00
Kam 9ed1b6c045 docs: use contentChildren() in component harness example
The example already uses the signal-based input() but still declares
items with the @ContentChildren decorator. Convert to the signal-based
contentChildren() query for consistency.

(cherry picked from commit a6eb55642c)
2026-04-28 19:03:13 +00:00
Angular Robot 2b9b27e882 build: update cross-repo angular dependencies
See associated pull request for more information.
2026-04-28 12:00:21 -07:00
Angular Robot 85f1c0a268 build: lock file maintenance
See associated pull request for more information.
2026-04-28 11:59:30 -07:00
Kam fb7e67ca9d docs: use inject() in @Self example in hierarchical DI guide
The surrounding @SkipSelf and @Host examples already use inject(),
and the section intro recommends it. Align the @Self example to match.

(cherry picked from commit 273ff07469)
2026-04-28 17:31:02 +00:00
Herdiyan IT Dev 316d49a8ae refactor(dev-infra): use shell: false and quote args in benchmark-compare workflow
Currently, the exec() utility uses childProcess.spawn() with shell: true. This commit changes the spawn option to shell: false to prevent OS command injection vulnerabilities and quotes the benchmark target in the github action.

(cherry picked from commit f219e65841)
2026-04-28 17:29:28 +00:00
Matthieu Riegler 9bcbf37641 refactor(core): fix bundling symbol test
The golden needed an update.
2026-04-28 09:44:58 -07:00
Simon a3d14fc11b docs: remove duplicated text
(cherry picked from commit c8aad6acc6)
2026-04-28 00:10:24 +00:00
Simon 0612c8c53b docs: remove spaces to correct the indentation
(cherry picked from commit 38c352766a)
2026-04-28 00:09:50 +00:00
Kristiyan Kostadinov 4f5d8a2c0b fix(compiler): let declaration span not including end character
Fixes that the span for `@let` declarations didn't include the end token.

(cherry picked from commit 6bd1721662)
2026-04-28 00:09:19 +00:00
Alan Agius be1f80a253 fix(platform-server): ensure origin has a trailing slash when parsing url
The origin did not have a trailing slash, which caused parsing issues for relative URLs.

Fixes #68322

(cherry picked from commit 2a6b6fafb0)
2026-04-28 00:08:40 +00:00
aparziale 76cf531002 docs: Fix typo in doc
Fix typo in documentation. Changed "create workspace" to "create a workspace"

Fixed #68375

(cherry picked from commit 982e3cce52)
2026-04-28 00:08:06 +00:00
Sonu Kapoor 885a1a1d97 fix(core): guard against non-object events and avoid listener wrapper identity mismatch
Two issues caused browser test failures after the event replay fix:

1. `markEventHandledForElement` used the event object as a WeakMap key, but
   `DebugElement.triggerEventHandler` can pass null or primitive values as the
   event argument. Added an early return for non-object values.

2. Registering a separate `domListener` closure with `renderer.listen` instead of
   `wrappedListener` caused `DebugElement.triggerEventHandler` to invoke the
   handler twice: once via `this.listeners` (which holds `wrappedListener`) and
   once via Zone.js's `eventListeners` (which holds the unwrapped `domListener`).
   The existing dedup logic in `triggerEventHandler` checks if the unwrapped
   Zone.js listener is already in `invokedListeners`, but with two different
   function objects that check always fails.

   Replaced the `domListener` wrapper with a property (`__ngNativeEl__`) stored
   directly on `wrappedListener`. `wrapListenerIn_markDirtyAndPreventDefault` reads
   this property and calls `markEventHandledForElement` when the listener fires,
   while `renderer.listen` receives the same `wrappedListener` function that
   Angular stores in `lCleanup`, preserving the dedup invariant.

(cherry picked from commit 3583c01bf9)
2026-04-28 00:07:41 +00:00
Sonu Kapoor 7a64aff9b5 fix(core): prevent event replay double-invocation when element hydrates before app stability
When `withEventReplay()` is enabled and a component hydrates before the
application becomes stable (e.g. while a pending HTTP request is in
flight), a user interaction on the hydrated element triggers both the
real DOM listener registered by Angular and the jsaction replay path.
This causes the event handler to be invoked twice.

The root cause is that `listenToDomEvent` registers the same
`wrappedListener` both as a stashed jsaction handler (via
`stashEventListenerImpl`) and as a native DOM listener (via
`renderer.listen`). When the user interacts after hydration but before
app stability, jsaction queues the event because no dispatcher is
registered yet. Once the app stabilises and `initEventReplay` runs,
jsaction replays the queued event through `invokeListeners`, which
calls the stashed handler a second time.

The fix tracks dispatched `(event, element)` pairs in a
`WeakMap<Event, WeakSet<Element>>`. The native DOM listener wrapper
records each pair via `markEventHandledForElement`, and `invokeListeners`
skips replay for any pair already present. Keying by element (rather
than event alone) preserves incremental hydration behaviour, where
jsaction legitimately replays the same event on a different element
(the deferred block content) from the one that originally triggered
hydration.

Fixes #67328

(cherry picked from commit d5fd51e956)
2026-04-28 00:07:40 +00:00
Kam cbaba8afe6 docs: document moduleResolution bundler change in v20 update guide
The v20 update guide doesn't flag that `ng update` switches
`moduleResolution` to `'bundler'`. Add a checklist item so manual
upgrades don't miss it.

(cherry picked from commit 67a5f00cd8)
2026-04-28 00:06:39 +00:00
Andrew Scott fbe080f829 docs(router): Fix method name for retrieving stored handles
method name was documented incorrectly. this fixes the name

(cherry picked from commit f17f32c351)
2026-04-28 00:03:25 +00:00
aparziale 1be52ad880 docs: Align Router API docs with inject based DI
Updates Router documentation examples to reflect modern inject based dependency injection instead of constructor injection.

Fixed #68378

(cherry picked from commit ded5a0ed62)
2026-04-28 00:02:32 +00:00
SkyZeroZx fa4eff36cb docs(docs-infra): Validate case-sensitive API symbol links in @link
Adds build-time validation for case-sensitive API symbols in `@link`. Avoid broken links

(cherry picked from commit d2c7b4e111)
2026-04-28 00:01:44 +00:00
Andrew Scott 27da56ee9d refactor: use stronger language for adev writing guide
use stronger language to improve skill usage hits for adev writing.

(cherry picked from commit 357cb15208)
2026-04-27 22:29:08 +00:00
Angular Robot 7b8faccc34 build: update cross-repo angular dependencies
See associated pull request for more information.
2026-04-27 15:27:28 -07:00
Kam 6662724c99 docs(docs-infra): improve Playground card on installation page
Updates the Playground card copy and adds a `titleInline` attribute on
<docs-card> so the icon and title sit on the same row. Existing cards
are unaffected.

(cherry picked from commit 29081f7765)
2026-04-24 17:34:42 +00:00
Angular Robot 8ce8b9342a build: update pnpm to v10.33.2
See associated pull request for more information.
2026-04-24 10:11:20 -07:00
Angular Robot 131c422da2 build: update devinfra digest to c4d0c37
See associated pull request for more information.
2026-04-24 10:06:35 -07:00
Alan Agius fa33854d24 docs: document trustProxyHeaders and update X-Forwarded-Prefix validation
Update the security guide to explain how to configure `trustProxyHeaders` when initializing the application engine. Also, update the validation rules for `X-Forwarded-Prefix` to reflect that it must start with `/` and contain only alphanumeric characters, hyphens, and underscores.

(cherry picked from commit 0399115a82)
2026-04-24 17:04:00 +00:00
Angular Robot f3308dc1f7 build: update cross-repo angular dependencies
See associated pull request for more information.
2026-04-23 14:14:19 -07:00
Angular Robot f9b70a0887 docs: update cross-repo adev docs
Updated Angular adev cross repo docs files.
2026-04-23 12:41:14 -07:00
Kam a8dd801856 docs: link Angular CLI in installation prerequisites
Links the "Angular CLI" mention in the Terminal prerequisite to the CLI
overview page.

(cherry picked from commit 6120d3196a)
2026-04-23 18:39:42 +00:00
Angular Robot 2ec753cb77 build: update devinfra digest to e9c1452
See associated pull request for more information.
2026-04-23 11:13:23 -07:00
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
220 changed files with 13703 additions and 11828 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: adev-writing-guide
description: Comprehensive writing guide for Angular documentation (adev). Covers Google Technical Writing standards, Angular-specific markdown extensions, code blocks, and components. Use when authoring or reviewing content in adev/src/content.
description: Comprehensive writing guide for Angular documentation (adev). Covers Google Technical Writing standards, Angular-specific markdown extensions, code blocks, and components. You MUST use this skill any time you plan to create, edit, or review documentation files in `adev/` or `adev/src/content`.
---
# Angular Documentation (adev) Writing Guide
+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.
+6 -6
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?
@@ -33595,7 +33595,7 @@ tmp/lib/tmp.js:
(* v8 ignore next -- @preserve *)
(* v8 ignore else -- @preserve *)
@angular/ng-dev/bundles/chunk-YN3IWAKJ.mjs:
@angular/ng-dev/bundles/chunk-G7GMCCSS.mjs:
(*! Bundled license information:
yargs-parser/build/lib/string-utils.js:
@@ -33636,7 +33636,7 @@ tmp/lib/tmp.js:
*)
*)
@angular/ng-dev/bundles/chunk-TUTTLTAK.mjs:
@angular/ng-dev/bundles/chunk-PTDPQBIK.mjs:
(*! Bundled license information:
@octokit/request-error/dist-src/index.js:
+4 -2
View File
@@ -32,13 +32,15 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: '${{secrets.GITHUB_TOKEN}}'
persist-credentials: false
- name: Configure Firebase deploy target
working-directory: ./
run: |
# We can use `npx` as the Firebase deploy actions uses it too.
npx -y firebase-tools@latest target:clear --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs
npx -y firebase-tools@latest target:apply --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs ${{env.PREVIEW_SITE}}
# Use stable version release
npx -y firebase-tools@15.15.0 target:clear --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs
npx -y firebase-tools@15.15.0 target:apply --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs ${{env.PREVIEW_SITE}}
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@ba726e7bca0b08b125ccc6f93c233749e1213c17
with:
+2 -1
View File
@@ -49,7 +49,8 @@ jobs:
COMMENT_BODY: ${{ github.event.comment.body }}
run: pnpm benchmarks prepare-for-github-action "$COMMENT_BODY"
- run: pnpm benchmarks run-compare ${{steps.info.outputs.compareSha}} ${{steps.info.outputs.benchmarkTarget}}
- run: pnpm benchmarks run-compare ${{steps.info.outputs.compareSha}} "${{steps.info.outputs.benchmarkTarget}}"
id: benchmark
name: Running benchmark
+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'
+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@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
with:
sarif_file: results.sarif
+62
View File
@@ -1,3 +1,65 @@
<a name="21.2.11"></a>
# 21.2.11 (2026-04-29)
### common
| Commit | Type | Description |
| -- | -- | -- |
| [10ad3c0692](https://github.com/angular/angular/commit/10ad3c06923453ae0ec06b06e664ce05900a4ff6) | fix | prevent focus from scrollToAnchor |
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [4f5d8a2c0b](https://github.com/angular/angular/commit/4f5d8a2c0b5e38d4debc4293945270cea4a9590d) | fix | let declaration span not including end character |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [a40e2cebc8](https://github.com/angular/angular/commit/a40e2cebc878965c3e21bfb61658f3f80cbd2ebf) | fix | fix ordering of view queries metadata in JIT mode |
| [885a1a1d97](https://github.com/angular/angular/commit/885a1a1d9757adfa8766d9b369c848a277438c31) | fix | guard against non-object events and avoid listener wrapper identity mismatch |
| [7a64aff9b5](https://github.com/angular/angular/commit/7a64aff9b59999077ea915486a7fa0b97a286659) | fix | prevent event replay double-invocation when element hydrates before app stability |
### platform-server
| Commit | Type | Description |
| -- | -- | -- |
| [be1f80a253](https://github.com/angular/angular/commit/be1f80a253b8ee27ed7d8de2287d6895c4821909) | fix | ensure origin has a trailing slash when parsing url |
<!-- CHANGELOG SPLIT MARKER -->
<a name="21.2.10"></a>
# 21.2.10 (2026-04-22)
### docs
| Commit | Type | Description |
| -- | -- | -- |
| [0d5ee9ae1b](https://github.com/angular/angular/commit/0d5ee9ae1ba4b7acd8f27a059a778f0b4bd8a5bd) | fix | link formatting in "Animating your Application with CSS" |
### migrations
| Commit | Type | Description |
| -- | -- | -- |
| [5533ab4f56](https://github.com/angular/angular/commit/5533ab4f56f574bc9365cf0573c4a34a3ab5aaf1) | fix | fix NgClass leaving trailing comma after removal |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [580212c995](https://github.com/angular/angular/commit/580212c995751c4bf4ce8a49df4167498743e0ea) | fix | restore internal URL on popstate when `browserUrl` is used |
<!-- CHANGELOG SPLIT MARKER -->
<a name="21.2.9"></a>
# 21.2.9 (2026-04-15)
### core
| Commit | Type | Description |
| -- | -- | -- |
| [f603d4714f](https://github.com/angular/angular/commit/f603d4714fa184aad34a6f7f9ea4e79c8af3afac) | fix | escape forward slashes in transfer state to prevent crawler indexing |
### http
| Commit | Type | Description |
| -- | -- | -- |
| [540536c386](https://github.com/angular/angular/commit/540536c386f2c735a700c2c9e2697a88dcb3d4ec) | fix | add CSP nonce support to JsonpClientBackend |
| [63a857b874](https://github.com/angular/angular/commit/63a857b874172766451aa75ed3347ba50f0ee229) | fix | Don't on Passthru outside of reactive context |
### platform-server
| Commit | Type | Description |
| -- | -- | -- |
| [e0b5078cf2](https://github.com/angular/angular/commit/e0b5078cf2ebe79a6de85e9123148ae948b3d81d) | fix | prevent SSRF bypasses via protocol-relative and backslash URLs |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [684e9fd53d](https://github.com/angular/angular/commit/684e9fd53daacb9e910f42d98c6017f9e5cb4180) | fix | normalize multiple leading slashes in URL parser |
<!-- CHANGELOG SPLIT MARKER -->
<a name="21.2.8"></a>
# 21.2.8 (2026-04-08)
### compiler
+13 -15
View File
@@ -5,7 +5,7 @@ module(
)
bazel_dep(name = "rules_pkg", version = "1.2.0")
bazel_dep(name = "rules_nodejs", version = "6.7.3")
bazel_dep(name = "rules_nodejs", version = "6.7.4")
bazel_dep(name = "aspect_rules_ts", version = "3.8.8")
bazel_dep(name = "aspect_rules_js", version = "3.0.3")
bazel_dep(name = "aspect_rules_esbuild", version = "0.25.1")
@@ -14,32 +14,32 @@ bazel_dep(name = "aspect_rules_rollup", version = "2.0.1")
bazel_dep(name = "bazel_skylib", version = "1.9.0")
bazel_dep(name = "bazel_lib", version = "3.2.2")
bazel_dep(name = "tar.bzl", version = "0.10.1")
bazel_dep(name = "yq.bzl", version = "0.3.5")
bazel_dep(name = "yq.bzl", version = "0.3.6")
bazel_dep(name = "rules_angular")
git_override(
module_name = "rules_angular",
commit = "6c36180c2efebc6526ef0e6a55a6d738c7de6909",
commit = "c6f7e15470934f7c2fe46ff5bae7d68be0230501",
remote = "https://github.com/angular/rules_angular.git",
)
bazel_dep(name = "devinfra")
git_override(
module_name = "devinfra",
commit = "ba726e7bca0b08b125ccc6f93c233749e1213c17",
commit = "f018eca84d10c594e16e9517ade1254a36dc8733",
remote = "https://github.com/angular/dev-infra.git",
)
bazel_dep(name = "rules_sass")
git_override(
module_name = "rules_sass",
commit = "b5ddaa8e77509bcce35158ad20009636d3da4fbc",
commit = "dfb751533767caa759a7162a34cfe0852f988976",
remote = "https://github.com/angular/rules_sass.git",
)
bazel_dep(name = "rules_browsers")
git_override(
module_name = "rules_browsers",
commit = "652b57c41218be318f33fc92032696f53d3aa0ef",
commit = "af35c89a5a099c4bbc3f1a3495688cf51bd6a8da",
remote = "https://github.com/angular/rules_browsers.git",
)
@@ -71,8 +71,8 @@ use_repo(node, "nodejs_windows_amd64")
pnpm = use_extension("@aspect_rules_js//npm:extensions.bzl", "pnpm")
pnpm.pnpm(
name = "pnpm",
pnpm_version = "10.33.0",
pnpm_version_integrity = "sha512-EFaLtKavtYyes2MNqQzJUWQXq+vT+rvmc58K55VyjaFJHp21pUTHatjrdXD1xLs9bGN7LLQb/c20f6gjyGSTGQ==",
pnpm_version = "10.33.2",
pnpm_version_integrity = "sha512-qQ+vb+6rca1sblf5Tg/hoS9dzCLNdU20CulZPraj4LaxLjVAIYuzeuCDQEsfLObbKkEh6XmCm0r/lLmfSdoc+A==",
)
use_repo(pnpm, "pnpm")
@@ -125,14 +125,12 @@ use_repo(rules_ts_ext, **{"npm_typescript": "angular_npm_typescript"})
# TODO: Figure out how to make ng_project update whenever the packages/core::pkg target changes.
rules_angular = use_extension("@rules_angular//setup:extensions.bzl", "rules_angular")
use_repo_rule("@rules_angular//setup:repositories.bzl", "configurable_deps_repo")(
name = "rules_angular_configurable_deps",
angular_compiler_cli = "@angular//:node_modules/@angular/compiler-cli",
typescript = "@angular//:node_modules/typescript",
rules_angular.setup(
name = "angular_rules_angular_configurable_deps",
angular_compiler_cli = "//:node_modules/@angular/compiler-cli",
typescript = "//:node_modules/typescript",
)
override_repo(rules_angular, "rules_angular_configurable_deps")
use_repo(rules_angular, rules_angular_configurable_deps = "angular_rules_angular_configurable_deps")
register_toolchains(
"@devinfra//bazel/git-toolchain:git_linux_toolchain",
+25 -24
View File
@@ -161,7 +161,8 @@
"https://bcr.bazel.build/modules/rules_nodejs/6.2.0/MODULE.bazel": "ec27907f55eb34705adb4e8257952162a2d4c3ed0f0b3b4c3c1aad1fac7be35e",
"https://bcr.bazel.build/modules/rules_nodejs/6.5.0/MODULE.bazel": "546d0cf79f36f9f6e080816045f97234b071c205f4542e3351bd4424282a8810",
"https://bcr.bazel.build/modules/rules_nodejs/6.7.3/MODULE.bazel": "c22a48b2a0dbf05a9dc5f83837bbc24c226c1f6e618de3c3a610044c9f336056",
"https://bcr.bazel.build/modules/rules_nodejs/6.7.3/source.json": "a3f966f4415a8a6545e560ee5449eac95cc633f96429d08e87c87775c72f5e09",
"https://bcr.bazel.build/modules/rules_nodejs/6.7.4/MODULE.bazel": "e6a241a55c82e999145553d2e00a08fc6ebadf62b63d108fb5e984696ffd0bd2",
"https://bcr.bazel.build/modules/rules_nodejs/6.7.4/source.json": "34e7a8a3b4c8d630ac0e0492b3fed9dba41fe008a0edf220b7d88fa38ac53698",
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
"https://bcr.bazel.build/modules/rules_pkg/1.2.0/MODULE.bazel": "c7db3c2b407e673c7a39e3625dc05dc9f12d6682cbd82a3a5924a13b491eda7e",
@@ -201,8 +202,8 @@
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
"https://bcr.bazel.build/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072",
"https://bcr.bazel.build/modules/yq.bzl/0.3.2/MODULE.bazel": "0384efa70e8033d842ea73aa4b7199fa099709e236a7264345c03937166670b6",
"https://bcr.bazel.build/modules/yq.bzl/0.3.5/MODULE.bazel": "130c603e54be717bdf84100210f06598a0d2b4b4e01888fb01b70f50f41767ec",
"https://bcr.bazel.build/modules/yq.bzl/0.3.5/source.json": "1ae7bdc03cb26aaa8bd2bceadf65e90d90f0b2d03008ba9a0564da2e21396c39",
"https://bcr.bazel.build/modules/yq.bzl/0.3.6/MODULE.bazel": "985c2a0cb4ad9994bb0e33cc7fae931c91105eeefe3faa355b8f4c258d0607c0",
"https://bcr.bazel.build/modules/yq.bzl/0.3.6/source.json": "678aaf6e291164f3cd761bb3e872e8a151248f413dbb63c5524a50b82a5bc890",
"https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
"https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca",
@@ -429,7 +430,7 @@
"@@aspect_rules_ts+//ts:extensions.bzl%ext": {
"general": {
"bzlTransitiveDigest": "dhTbv9E6UfT1WJmmu3ORRPO6AKFJvgBjBxu+BO+u1RY=",
"usagesDigest": "CnGVBnDYq2qAfYkDXgJlDcJckUt2NU51jQQX+igoGt8=",
"usagesDigest": "QJswGu07xQeMlf+NomctjP9AY7OKl/U5GqMvEr6U1g8=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -447,8 +448,8 @@
"rules_angular_npm_typescript": {
"repoRuleId": "@@aspect_rules_ts+//ts/private:npm_repositories.bzl%http_archive_version",
"attributes": {
"version": "5.9.3",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"version": "6.0.2",
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
"urls": [
"https://registry.npmjs.org/typescript/-/typescript-{}.tgz"
]
@@ -457,8 +458,8 @@
"npm_typescript": {
"repoRuleId": "@@aspect_rules_ts+//ts/private:npm_repositories.bzl%http_archive_version",
"attributes": {
"version": "5.9.3",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"version": "6.0.2",
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
"urls": [
"https://registry.npmjs.org/typescript/-/typescript-{}.tgz"
]
@@ -467,8 +468,8 @@
"npm_rules_browsers_typescript": {
"repoRuleId": "@@aspect_rules_ts+//ts/private:npm_repositories.bzl%http_archive_version",
"attributes": {
"version": "5.9.3",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"version": "6.0.2",
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
"urls": [
"https://registry.npmjs.org/typescript/-/typescript-{}.tgz"
]
@@ -560,24 +561,24 @@
},
"@@rules_angular+//setup:extensions.bzl%rules_angular": {
"general": {
"bzlTransitiveDigest": "fkaH7HMicL3g7/NDaFzlq39kcLopMyQ3KdbDn+5CRzA=",
"usagesDigest": "Li29KHtfoig0M7RQAoy9uoACa/RcVICtFkFkARsVOAA=",
"bzlTransitiveDigest": "aS7Uud1IzoU7PPLzH3s6IfFS4b2fa0SRWDi2/fS4bQU=",
"usagesDigest": "PaJB/TvnSzJTbqGUeIfiFAEjGkG4FEW7es6f6MFMtq8=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"angular_rules_angular_configurable_deps": {
"repoRuleId": "@@rules_angular+//setup:repositories.bzl%configurable_deps_repo",
"attributes": {
"angular_compiler_cli": "@@//:node_modules/@angular/compiler-cli",
"typescript": "@@//:node_modules/typescript"
}
},
"rules_angular_configurable_deps": {
"repoRuleId": "@@rules_angular+//setup:repositories.bzl%configurable_deps_repo",
"attributes": {
"angular_compiler_cli": "@@rules_angular+//:node_modules/@angular/compiler-cli",
"typescript": "@@rules_angular+//:node_modules/typescript"
}
},
"dev_infra_rules_angular_configurable_deps": {
"repoRuleId": "@@rules_angular+//setup:repositories.bzl%configurable_deps_repo",
"attributes": {
"angular_compiler_cli": "@@rules_angular+//:node_modules/@angular/compiler-cli",
"typescript": "@@rules_angular+//:node_modules/typescript"
"angular_compiler_cli": "@@//:node_modules/@angular/compiler-cli",
"typescript": "@@//:node_modules/typescript"
}
}
},
@@ -945,8 +946,8 @@
},
"@@rules_nodejs+//nodejs:extensions.bzl%node": {
"general": {
"bzlTransitiveDigest": "4pUxCNc22K4I+6+4Nxu52Hur12tFRfa1JMsN5mdDv60=",
"usagesDigest": "o//mYtRPOfTZZh8pDGhbI7WGo7UOnSiSLgWYaJCEB+0=",
"bzlTransitiveDigest": "oZFClfRhTTwsYzpxVPkOpOt/r0+OzEfEV37au0jFZ0s=",
"usagesDigest": "dp2HPl9Y2BFhrnM0JALIsJHz02rKnRBpISBHQX4qV7E=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -4162,7 +4163,7 @@
"@@yq.bzl+//yq:extensions.bzl%yq": {
"general": {
"bzlTransitiveDigest": "UfFMy8CWK4/dVo/tfaSAIYUiDGNAPes5eRllx9O9Q9Q=",
"usagesDigest": "263D9xYtKhXWWCVTxT66bpT89HuZpdkB1AqEir45vbY=",
"usagesDigest": "5cUmZOEOibp2h65JoFppstHiXcjDFil/AG+HgD3avRk=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
+1 -1
View File
@@ -67,7 +67,7 @@ Install the Angular CLI globally:
npm install -g @angular/cli
```
Create workspace:
Create a workspace:
```
ng new [PROJECT NAME]
+6 -6
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.5",
"@angular/build": "21.2.6",
"@angular/cdk": "21.2.5",
"@angular/cli": "21.2.6",
"@angular/aria": "21.2.9",
"@angular/build": "21.2.9",
"@angular/cdk": "21.2.9",
"@angular/cli": "21.2.9",
"@angular/common": "workspace:*",
"@angular/compiler": "workspace:*",
"@angular/compiler-cli": "workspace:*",
"@angular/core": "workspace:*",
"@angular/docs": "workspace:*",
"@angular/forms": "workspace:*",
"@angular/material": "21.2.5",
"@angular/material": "21.2.9",
"@angular/platform-browser": "workspace:*",
"@angular/platform-server": "workspace:*",
"@angular/router": "workspace:*",
"@angular/ssr": "21.2.6",
"@angular/ssr": "21.2.9",
"@codemirror/autocomplete": "6.20.0",
"@codemirror/commands": "6.10.1",
"@codemirror/lang-angular": "0.1.4",
@@ -199,6 +199,23 @@ describe('jsdoc transforms', () => {
expect(entryFn).toThrowError(/Forbidden relative link: cli\/build ng build/);
});
it('should throw on a miscased absolute @link to a known API symbol', () => {
setSymbols({RouterModule: 'router'});
const entryFn = () =>
addHtmlAdditionalLinks({
jsdocTags: [
{
name: 'see',
comment: '{@link /api/router/routerModule#forRoot forRoot}',
},
],
moduleName: 'test',
});
expect(entryFn).toThrowError(/Broken @link.*Did you mean \/api\/router\/RouterModule/);
});
});
describe('addHtmlDescription', () => {
@@ -223,6 +223,27 @@ function parseAtLink(link: string): {label: string; url: string} | undefined {
);
}
// Validate absolute `/api/...` links against the known symbol registry. This catches
// miscased symbol names (e.g. `/api/router/routerModule` instead of
// `/api/router/RouterModule`) at build time.
if (rawSymbol.startsWith('/api/')) {
const [pathPart] = rawSymbol.split('#');
const segments = pathPart.split('/').filter((s) => s.length > 0);
const symbolName = segments[segments.length - 1];
// Case-insensitive lookup: find the canonical symbol name in the registry.
const knownSymbols = Object.keys(getSymbolsAsApiEntries());
const canonicalSymbol = knownSymbols.find(
(s) => s.toLowerCase() === symbolName.toLowerCase(),
);
if (canonicalSymbol && canonicalSymbol !== symbolName) {
const expectedUrl = getSymbolUrl(canonicalSymbol);
throw Error(
`Broken @link: ${link}. Did you mean ${expectedUrl}? ` +
`Symbol names in API URLs are case-sensitive.`,
);
}
}
return {
url: rawSymbol,
label: description ?? rawSymbol.split('/').pop()!,
@@ -18,6 +18,7 @@ interface DocsCardToken extends Tokens.Generic {
href?: string;
imgSrc?: string;
iconImgSrc?: string; // Need image since icons are custom
titleInline?: boolean;
tokens: Token[];
}
@@ -30,6 +31,7 @@ const linkRule = /link="([^"]*)"/;
const hrefRule = /href="([^"]*)"/;
const imgSrcRule = /imgSrc="([^"]*)"/;
const iconImgSrcRule = /iconImgSrc="([^"]*)"/;
const titleInlineRule = /(?:^|\s)titleInline(?=\s|$|=)/;
export const docsCardExtension = {
name: 'docs-card',
@@ -47,6 +49,7 @@ export const docsCardExtension = {
const href = hrefRule.exec(attr);
const imgSrc = imgSrcRule.exec(attr);
const iconImgSrc = iconImgSrcRule.exec(attr);
const titleInline = titleInlineRule.test(attr);
const body = match[2].trim();
@@ -59,6 +62,7 @@ export const docsCardExtension = {
link: link ? link[1] : undefined,
imgSrc: imgSrc ? imgSrc[1] : undefined,
iconImgSrc: iconImgSrc ? iconImgSrc[1] : undefined,
titleInline,
tokens: [],
};
this.lexer.blockTokens(token.body, token.tokens);
@@ -79,12 +83,14 @@ function getStandardCard(renderer: AdevDocsRenderer, token: DocsCardToken) {
// We need to read svg content, instead of renering svg with `img`,
// cause we would like to use CSS variables to support dark and light mode.
const icon = loadWorkspaceRelativeFile(token.iconImgSrc);
const header = token.titleInline
? `<div class="docs-card-header-inline">${icon}<h3>${token.title}</h3></div>`
: `${icon}<h3>${token.title}</h3>`;
return `
<a href="${token.href}" ${anchorTarget(token.href)} class="docs-card">
<div>
${icon}
<h3>${token.title}</h3>
${header}
${renderer.parser.parse(token.tokens)}
</div>
<span>${token.link ? token.link : 'Learn more'}</span>
+33 -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;
@@ -143,6 +159,21 @@
margin-block: 1.5rem;
}
.docs-card-header-inline {
display: flex;
align-items: center;
gap: 0.5rem;
svg {
margin-block-end: 0;
flex-shrink: 0;
}
h3 {
margin-block: 0;
}
}
&.docs-card-with-svg {
padding: 0;
@@ -252,7 +283,7 @@
.docs-nav-card-svg {
width: 350px;
@container header (max-width: 550px) {
@container header (max-width: $compact-breakpoint) {
display: none;
}
}
+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);
}
@@ -94,6 +94,7 @@
width: 100%;
padding-inline: calc(var(--layout-padding) - 1.25rem);
height: auto;
scrollbar-width: none;
padding-block: 0;
}
@@ -169,6 +170,7 @@
@include mq.for-tablet {
flex-direction: row;
padding-block-start: 0;
}
// version dropdown button
@@ -233,7 +235,9 @@
@include mq.for-tablet {
flex-direction: row !important;
align-items: center;
margin-inline-end: 1.25rem;
margin-top: 0.25rem;
gap: 0.75rem;
}
@@ -289,6 +293,8 @@
@include mq.for-phone-only {
display: block;
position: relative;
top: 1rem;
}
}
@@ -160,7 +160,7 @@
<section class="explore-section" id="learn-more">
<div class="title">
<h2>What to learn more about Angular?</h2>
<h2>Want to learn more about Angular?</h2>
<div class="pattern"></div>
</div>
+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;
}
}
+56 -35
View File
@@ -14,6 +14,12 @@ export enum ApplicationComplexity {
export interface Step {
step: string;
/**
* Action text rendered as Markdown. Use Markdown link syntax `[text](url)`
* for any links — raw `<a>` HTML tags bypass the custom link renderer in
* `update.component.ts` and will not pick up `target="_blank"` or the
* external-link icon.
*/
action: string;
possibleIn: number;
necessaryAsOf: number;
@@ -155,7 +161,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Advanced,
step: 'gendir',
action:
'Do not rely on `gendir`, instead look at using `skipTemplateCodeGen`. <a href=https://github.com/angular/angular/issues/19339#issuecomment-332607471" target="_blank">Read More</a>',
'Do not rely on `gendir`, instead look at using `skipTemplateCodeGen`. [Read More](https://github.com/angular/angular/issues/19339#issuecomment-332607471)',
},
{
possibleIn: 220,
@@ -239,7 +245,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'node 8',
action:
'Make sure you are using <a href="http://www.hostingadvice.com/how-to/update-node-js-latest-version/" target="_blank">Node 8 or later</a>',
'Make sure you are using [Node 8 or later](http://www.hostingadvice.com/how-to/update-node-js-latest-version/)',
},
{
possibleIn: 600,
@@ -248,7 +254,7 @@ export const RECOMMENDATIONS: Step[] = [
windows: false,
step: 'Update to CLI v6',
action:
'Update your Angular CLI, and migrate the configuration to the <a href="https://github.com/angular/angular-cli/wiki/angular-workspace" target="_blank">new angular.json format</a> by running the following:<br/><br/>`NG_DISABLE_VERSION_CHECK=1 npx @angular/cli@6 update @angular/cli@6`<br/>',
'Update your Angular CLI, and migrate the configuration to the [new angular.json format](https://github.com/angular/angular-cli/wiki/angular-workspace) by running the following:<br/><br/>`NG_DISABLE_VERSION_CHECK=1 npx @angular/cli@6 update @angular/cli@6`<br/>',
},
{
possibleIn: 600,
@@ -257,7 +263,7 @@ export const RECOMMENDATIONS: Step[] = [
windows: true,
step: 'Update to CLI v6',
action:
'Update your Angular CLI, and migrate the configuration to the <a href="https://github.com/angular/angular-cli/wiki/angular-workspace" target="_blank">new angular.json format</a> by running the following:<br/><br/>`cmd /C "set "NG_DISABLE_VERSION_CHECK=1" && npx @angular/cli@6 update @angular/cli@6 @angular/core@6"`<br/>',
'Update your Angular CLI, and migrate the configuration to the [new angular.json format](https://github.com/angular/angular-cli/wiki/angular-workspace) by running the following:<br/><br/>`cmd /C "set "NG_DISABLE_VERSION_CHECK=1" && npx @angular/cli@6 update @angular/cli@6 @angular/core@6"`<br/>',
},
{
possibleIn: 600,
@@ -343,7 +349,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Medium,
step: 'strictPropertyInitializer',
action:
'If you have TypeScript configured to be strict (if you have set `strict` to `true` in your `tsconfig.json` file), update your `tsconfig.json` to disable `strictPropertyInitialization` or move property initialization from `ngOnInit` to your constructor. You can learn more about this flag on the <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#strict-class-initialization">TypeScript 2.7 release notes</a>.',
'If you have TypeScript configured to be strict (if you have set `strict` to `true` in your `tsconfig.json` file), update your `tsconfig.json` to disable `strictPropertyInitialization` or move property initialization from `ngOnInit` to your constructor. You can learn more about this flag on the [TypeScript 2.7 release notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#strict-class-initialization).',
},
{
possibleIn: 600,
@@ -351,7 +357,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'update to RxJS 6',
action:
'Remove deprecated RxJS 5 features using <a href="https://github.com/ReactiveX/rxjs-tslint" target="_blank">rxjs-tslint auto update rules</a><br/><br/>For most applications this will mean running the following two commands:<br/><br/>`npx rxjs-tslint`<br/>`rxjs-5-to-6-migrate -p src/tsconfig.app.json`',
'Remove deprecated RxJS 5 features using [rxjs-tslint auto update rules](https://github.com/ReactiveX/rxjs-tslint)<br/><br/>For most applications this will mean running the following two commands:<br/><br/>`npx rxjs-tslint`<br/>`rxjs-5-to-6-migrate -p src/tsconfig.app.json`',
},
{
possibleIn: 600,
@@ -374,7 +380,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'TypeScript 3.1',
action:
'Angular now uses TypeScript 3.1, read more about any potential breaking changes: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-1.html',
'Angular now uses TypeScript 3.1, read more about [any potential breaking changes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-1.html).',
},
{
possibleIn: 700,
@@ -382,7 +388,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'Node 10',
action:
'Angular has now added support for Node 10: https://nodejs.org/en/blog/release/v10.0.0/',
'Angular has now added support for [Node 10](https://nodejs.org/en/blog/release/v10.0.0/).',
},
{
possibleIn: 700,
@@ -464,7 +470,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'use ::ng-deep instead of /deep/',
action:
'Replace `/deep/` with `::ng-deep` in your styles, [read more about angular component styles and ::ng-deep](https://angular.io/guide/component-styles#deprecated-deep--and-ng-deep). `/deep/` and `::ng-deep` both are deprecated but using `::ng-deep` is preferred until the shadow-piercing descendant combinator is [removed from browsers and tools](https://www.chromestatus.com/features/6750456638341120) completely.',
'Replace `/deep/` with `::ng-deep` in your styles, [read more about angular component styles and ::ng-deep](https://angular.io/guide/component-styles#deprecated-deep--and-ng-deep). `/deep/` and `::ng-deep` both are deprecated but using `::ng-deep` is preferred until the shadow-piercing descendant combinator is [removed from browsers and tools](https://chromestatus.com/feature/5045542597951488) completely.',
},
{
possibleIn: 800,
@@ -480,7 +486,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'node 10',
action:
'Make sure you are using <a href="http://www.hostingadvice.com/how-to/update-node-js-latest-version/" target="_blank">Node 10 or later</a>.',
'Make sure you are using [Node 10 or later](http://www.hostingadvice.com/how-to/update-node-js-latest-version/).',
},
{
possibleIn: 800,
@@ -573,7 +579,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'node 10.13',
action:
'Make sure you are using <a href="http://www.hostingadvice.com/how-to/update-node-js-latest-version/" target="_blank">Node 10.13 or later</a>.',
'Make sure you are using [Node 10.13 or later](http://www.hostingadvice.com/how-to/update-node-js-latest-version/).',
},
{
possibleIn: 900,
@@ -782,8 +788,7 @@ export const RECOMMENDATIONS: Step[] = [
necessaryAsOf: 1000,
level: ApplicationComplexity.Basic,
step: 'v10 NodeJS 12',
action:
'Make sure you are using <a href="https://nodejs.org/dist/latest-v12.x/" target="_blank">Node 12 or later</a>.',
action: 'Make sure you are using [Node 12 or later](https://nodejs.org/dist/latest-v12.x/).',
},
{
possibleIn: 1000,
@@ -871,7 +876,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Medium,
step: 'esm5-bundles',
action:
"The [Angular Package Format](https://g.co/ng/apf) has been updated to remove `esm5` and `fesm5` formats. These are no longer distributed in our npm packages. If you don't use the CLI, you may need to downlevel Angular code to ES5 yourself.",
"The [Angular Package Format](https://angular.dev/tools/libraries/angular-package-format) has been updated to remove `esm5` and `fesm5` formats. These are no longer distributed in our npm packages. If you don't use the CLI, you may need to downlevel Angular code to ES5 yourself.",
},
{
possibleIn: 1000,
@@ -1335,7 +1340,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'TypeScript 4.4',
action:
'Angular now uses TypeScript 4.4, read more about any potential breaking changes: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-4.html',
'Angular now uses TypeScript 4.4, read more about [any potential breaking changes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-4.html).',
},
{
possibleIn: 1300,
@@ -1343,7 +1348,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'v13 node',
action:
'Make sure you are using <a href="http://www.hostingadvice.com/how-to/update-node-js-latest-version/" target="_blank">Node 12.20.0 or later</a>',
'Make sure you are using [Node 12.20.0 or later](http://www.hostingadvice.com/how-to/update-node-js-latest-version/)',
},
{
possibleIn: 1300,
@@ -1457,7 +1462,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'TypeScript 4.6',
action:
'Angular now uses TypeScript 4.6, read more about any potential breaking changes: https://devblogs.microsoft.com/typescript/announcing-typescript-4-6/',
'Angular now uses TypeScript 4.6, read more about [any potential breaking changes](https://devblogs.microsoft.com/typescript/announcing-typescript-4-6/).',
},
{
@@ -1466,7 +1471,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'v14 node',
action:
'Make sure you are using <a href="http://www.hostingadvice.com/how-to/update-node-js-latest-version/" target="_blank">Node 14.15.0 or later</a>',
'Make sure you are using [Node 14.15.0 or later](http://www.hostingadvice.com/how-to/update-node-js-latest-version/)',
},
{
possibleIn: 1400,
@@ -1669,7 +1674,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'v15 node support',
action:
'Make sure that you are using a supported version of node.js before you upgrade your application. Angular v15 supports node.js versions: 14.20.x, 16.13.x and 18.10.x. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-01" alt="Link to more information about this change">Read further</a>',
'Make sure that you are using a supported version of node.js before you upgrade your application. Angular v15 supports node.js versions: 14.20.x, 16.13.x and 18.10.x. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-01)',
},
{
possibleIn: 1500,
@@ -1677,7 +1682,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'v15 ts support',
action:
'Make sure that you are using a supported version of TypeScript before you upgrade your application. Angular v15 supports TypeScript version 4.8 or later. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-02" alt="Link to more information about this change">Read further</a>',
'Make sure that you are using a supported version of TypeScript before you upgrade your application. Angular v15 supports TypeScript version 4.8 or later. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-02)',
},
{
possibleIn: 1500,
@@ -1701,7 +1706,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Medium,
step: 'v15 keyframe',
action:
'In v15, the Angular compiler prefixes `@keyframes` in CSS with the component\'s scope. This means that any TypeScript code that relies on `keyframes` names no longer works in v15. Update any such instances to: define keyframes programmatically, use global stylesheets, or change the component\'s view encapsulation. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-03" alt="Link to more information about this change">Read further</a>',
"In v15, the Angular compiler prefixes `@keyframes` in CSS with the component's scope. This means that any TypeScript code that relies on `keyframes` names no longer works in v15. Update any such instances to: define keyframes programmatically, use global stylesheets, or change the component's view encapsulation. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-03)",
},
{
possibleIn: 1500,
@@ -1717,7 +1722,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Medium,
step: 'v15 base-decorators',
action:
'Make sure to use decorators in base classes with child classes that inherit constructors and use dependency injection. Such base classes should be decorated with either `@Injectable` or `@Directive` or the compiler returns an error. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-05" alt="Link to more information about this change">Read further</a>',
'Make sure to use decorators in base classes with child classes that inherit constructors and use dependency injection. Such base classes should be decorated with either `@Injectable` or `@Directive` or the compiler returns an error. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-05)',
},
{
possibleIn: 1500,
@@ -1725,7 +1730,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Medium,
step: 'v15 setDisabledState',
action:
'In v15, `setDisabledState` is always called when a `ControlValueAccessor` is attached. To opt-out of this behavior, use `FormsModule.withConfig` or `ReactiveFormsModule.withConfig`. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-06" alt="Link to more information about this change">Read further</a>',
'In v15, `setDisabledState` is always called when a `ControlValueAccessor` is attached. To opt-out of this behavior, use `FormsModule.withConfig` or `ReactiveFormsModule.withConfig`. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-06)',
},
{
possibleIn: 1500,
@@ -1733,7 +1738,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Advanced,
step: 'v15 canParse',
action:
'Applications that use `canParse` should use `analyze` from `@angular/localize/tools` instead. In v15, the `canParse` method was removed from all translation parsers in `@angular/localize/tools`. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-07" alt="Link to more information about this change">Read further</a>',
'Applications that use `canParse` should use `analyze` from `@angular/localize/tools` instead. In v15, the `canParse` method was removed from all translation parsers in `@angular/localize/tools`. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-07)',
},
{
possibleIn: 1500,
@@ -1741,7 +1746,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'v15 ActivatedRoutSnapshot',
action:
'Make sure that all `ActivatedRouteSnapshot` objects have a `title` property. In v15, the `title` property is a required property of `ActivatedRouteSnapshot`. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-08" alt="Link to more information about this change">Read further</a>',
'Make sure that all `ActivatedRouteSnapshot` objects have a `title` property. In v15, the `title` property is a required property of `ActivatedRouteSnapshot`. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-08)',
},
{
possibleIn: 1500,
@@ -1749,7 +1754,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Advanced,
step: 'v15 RouterOutlet',
action:
'If your tests with `RouterOutlet` break, make sure they don\'t depend on the instantiation order of the corresponding component relative to change detection. In v15, `RouterOutlet` instantiates the component after change detection. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-09" alt="Link to more information about this change">Read further</a>',
"If your tests with `RouterOutlet` break, make sure they don't depend on the instantiation order of the corresponding component relative to change detection. In v15, `RouterOutlet` instantiates the component after change detection. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-09)",
},
{
possibleIn: 1500,
@@ -1757,7 +1762,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'v15 relativeLinkResolution',
action:
'In v15, `relativeLinkResolution` is not configurable in the Router. It was used to opt out of an earlier bug fix that is now standard. <a href="https://v15.angular.io/guide/update-to-version-15#v15-bc-10" alt="Link to more information about this change">Read further</a>',
'In v15, `relativeLinkResolution` is not configurable in the Router. It was used to opt out of an earlier bug fix that is now standard. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-bc-10)',
},
{
possibleIn: 1500,
@@ -1765,7 +1770,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Medium,
step: 'v15 DATE_PIPE_DEFAULT_OPTIONS',
action:
'Change instances of the `DATE_PIPE_DEFAULT_TIMEZONE` token to use `DATE_PIPE_DEFAULT_OPTIONS` to configure time zones. In v15, the `DATE_PIPE_DEFAULT_TIMEZONE` token is deprecated. <a href="https://v15.angular.io/guide/update-to-version-15#v15-dp-01" alt="Link to more information about this change">Read further</a>',
'Change instances of the `DATE_PIPE_DEFAULT_TIMEZONE` token to use `DATE_PIPE_DEFAULT_OPTIONS` to configure time zones. In v15, the `DATE_PIPE_DEFAULT_TIMEZONE` token is deprecated. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-dp-01)',
},
{
possibleIn: 1500,
@@ -1781,7 +1786,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Medium,
step: 'v15 Injector.get',
action:
'Update instances of `Injector.get()` that use an `InjectFlags` parameter to use an `InjectOptions` parameter. The `InjectFlags` parameter of `Injector.get()` is deprecated in v15. <a href="https://v15.angular.io/guide/update-to-version-15#v15-dp-02" alt="Link to more information about this change">Read further</a>',
'Update instances of `Injector.get()` that use an `InjectFlags` parameter to use an `InjectOptions` parameter. The `InjectFlags` parameter of `Injector.get()` is deprecated in v15. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-dp-02)',
},
{
possibleIn: 1500,
@@ -1789,7 +1794,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'v15 TestBed.inject',
action:
'Update instances of `TestBed.inject()` that use an `InjectFlags` parameter to use an `InjectOptions` parameter. The `InjectFlags` parameter of `TestBed.inject()` is deprecated in v15. <a href="https://v15.angular.io/guide/update-to-version-15#v15-dp-01" alt="Link to more information about this change">Read further</a>',
'Update instances of `TestBed.inject()` that use an `InjectFlags` parameter to use an `InjectOptions` parameter. The `InjectFlags` parameter of `TestBed.inject()` is deprecated in v15. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-dp-01)',
},
{
possibleIn: 1500,
@@ -1797,7 +1802,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Medium,
step: 'v15 ngModule in providedIn',
action:
'Using `providedIn: ngModule` for an `@Injectable` and `InjectionToken` is deprecated in v15. <a href="https://v15.angular.io/guide/update-to-version-15#v15-dp-04" alt="Link to more information about this change">Read further</a>',
'Using `providedIn: ngModule` for an `@Injectable` and `InjectionToken` is deprecated in v15. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-dp-04)',
},
{
possibleIn: 1500,
@@ -1805,7 +1810,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'v15 providedIn any',
action:
'Using `providedIn: \'any\'` for an `@Injectable` or `InjectionToken` is deprecated in v15. <a href="https://v15.angular.io/guide/update-to-version-15#v15-dp-05" alt="Link to more information about this change">Read further</a>',
"Using `providedIn: 'any'` for an `@Injectable` or `InjectionToken` is deprecated in v15. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-dp-05)",
},
{
possibleIn: 1500,
@@ -1813,7 +1818,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Medium,
step: 'v15 RouterLinkWithHref',
action:
'Update instances of the `RouterLinkWithHref`directive to use the `RouterLink` directive. The `RouterLinkWithHref` directive is deprecated in v15. <a href="https://v15.angular.io/guide/update-to-version-15#v15-dp-06" alt="Link to more information about this change">Read further</a>',
'Update instances of the `RouterLinkWithHref`directive to use the `RouterLink` directive. The `RouterLinkWithHref` directive is deprecated in v15. [Read further](https://v15.angular.io/guide/update-to-version-15#v15-dp-06)',
},
{
possibleIn: 1500,
@@ -1822,7 +1827,7 @@ export const RECOMMENDATIONS: Step[] = [
material: true,
step: 'v15 mat refactor',
action:
'In Angular Material v15, many of the components have been refactored to be based on the official Material Design Components for Web (MDC). This change affected the DOM and CSS classes of many components. <a href="https://rc.material.angular.dev/guide/mdc-migration" alt="Link to more information about this change">Read further</a>',
'In Angular Material v15, many of the components have been refactored to be based on the official Material Design Components for Web (MDC). This change affected the DOM and CSS classes of many components. [Read further](https://rc.material.angular.dev/guide/mdc-migration)',
},
{
possibleIn: 1500,
@@ -2669,7 +2674,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: '20.0.0_update_nodejs_version',
action:
'Ensure your Node.js version is at least 20.11.1 and not v18 or v22.0-v22.10 before upgrading to Angular v20. Check https://angular.dev/reference/versions for the full list of supported Node.js versions.',
'Ensure your Node.js version is at least 20.11.1 and not v18 or v22.0-v22.10 before upgrading to Angular v20. Check the [full list of supported Node.js versions](https://angular.dev/reference/versions).',
},
{
possibleIn: 2000,
@@ -2703,6 +2708,14 @@ export const RECOMMENDATIONS: Step[] = [
action:
"Upgrade your project's TypeScript version to at least 5.8 before upgrading to Angular v20 to ensure compatibility.",
},
{
possibleIn: 2000,
necessaryAsOf: 2000,
level: ApplicationComplexity.Medium,
step: '20.0.0_set_moduleResolution_to_bundler',
action:
"Set `moduleResolution` to `'bundler'` in your `tsconfig.json`. Angular CLI's `ng update` migration applies this change automatically; if you upgrade manually or override the option in a base tsconfig, set it explicitly so imports of secondary entry-points such as `@angular/core/rxjs-interop` continue to resolve correctly.",
},
{
possibleIn: 2000,
necessaryAsOf: 2000,
@@ -2912,4 +2925,12 @@ export const RECOMMENDATIONS: Step[] = [
action:
'The `lastSuccessfulNavigation` property on the Router has been converted to a signal. To get its value, you now need to invoke it as a function: `router.lastSuccessfulNavigation()`.',
},
{
possibleIn: 2100,
necessaryAsOf: 2100,
level: ApplicationComplexity.Medium,
step: '21.0.0-configure-commonengine-allowed-hosts',
action:
"Starting `@angular/ssr` 21.1.5, if your application uses SSR with `CommonEngine`, set the `allowedHosts` option in your `server.ts` (for example, `new CommonEngine({allowedHosts: ['localhost', '*.yourdomain.com']})`). Without it, SSR silently falls back to client-side rendering. This requirement comes from security advisory [GHSA-x288-3778-4hhx](https://github.com/angular/angular-cli/security/advisories/GHSA-x288-3778-4hhx) (also backported to 20.3.17 and 19.2.21).",
},
];
@@ -1,4 +1,5 @@
@use '@angular/docs/styles/media-queries' as mq;
@use '@angular/docs/styles/links' as links;
$ver-dropdown-width: clamp(165px, 20vw, 200px);
@@ -172,7 +173,14 @@ h4 {
}
// Code blocks are generable from the markdown, we need to opt-out of the scoping
::ng-deep code {
cursor: pointer;
::ng-deep {
code {
cursor: pointer;
}
// External links (rendered with target="_blank") get an "open in new" icon
a[target='_blank'] {
@include links.external-link-with-icon;
}
}
}
@@ -13,7 +13,7 @@ import {provideHttpClientTesting} from '@angular/common/http/testing';
import {By} from '@angular/platform-browser';
import UpdateComponent from './update.component';
import {ApplicationComplexity} from './recommendations';
import {ApplicationComplexity, RECOMMENDATIONS} from './recommendations';
describe('UpdateComponent', () => {
let component: UpdateComponent;
@@ -91,4 +91,14 @@ describe('UpdateComponent', () => {
}
});
});
describe('RECOMMENDATIONS data', () => {
it('should use Markdown links instead of raw <a> HTML tags', () => {
for (const step of RECOMMENDATIONS) {
expect(step.action)
.withContext(`step "${step.step}" should use [text](url), not raw <a> HTML`)
.not.toMatch(/<a\s/i);
}
});
});
});
@@ -17,6 +17,20 @@ import {ActivatedRoute, Router} from '@angular/router';
import {marked} from 'marked';
import {MatSnackBar} from '@angular/material/snack-bar';
/**
* Configure marked with a custom link renderer so external links in the
* update guide open in a new tab, matching the convention applied elsewhere
* in adev via the `ExternalLink` directive.
*/
marked.use({
renderer: {
link({href, title, text}) {
const titleAttr = title ? ` title="${title}"` : '';
return `<a href="${href}"${titleAttr} target="_blank" rel="noopener noreferrer">${text}</a>`;
},
},
});
interface Option {
id: keyof Step;
name: string;
@@ -496,6 +496,27 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
category: 'Signal Forms',
status: 'new',
},
{
label: 'Cross-field logic',
path: 'guide/forms/signals/cross-field-logic',
contentPath: 'guide/forms/signals/cross-field-logic',
category: 'Signal Forms',
status: 'new',
},
{
label: 'Form submission',
path: 'guide/forms/signals/form-submission',
contentPath: 'guide/forms/signals/form-submission',
category: 'Signal Forms',
status: 'new',
},
{
label: 'Schemas',
path: 'guide/forms/signals/schemas',
contentPath: 'guide/forms/signals/schemas',
category: 'Signal Forms',
status: 'new',
},
{
label: 'Async operations',
path: 'guide/forms/signals/async-operations',
@@ -1044,16 +1065,15 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
},
// TODO: create those guides
// The signal debugging docs should also be added to the signal section
// {
// label: 'Signals',
// path: 'tools/devtools/signals',
// contentPath: 'tools/devtools/signals',
// },
// {
// label: 'Router',
// path: 'tools/devtools/router',
// contentPath: 'tools/devtools/router',
// }
{
label: 'Router Tree',
path: 'tools/devtools/router',
contentPath: 'tools/devtools/router',
},
],
},
{
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 -960 960 960" fill="#F637E3">
<path d="M450.001-611.691v-32.386q-39.385-9.923-64.692-41.897-25.308-31.975-25.308-74.025 0-49.922 35.038-84.96 35.039-35.038 84.961-35.038t84.961 35.038q35.038 35.038 35.038 84.96 0 42.05-25.308 74.025-25.307 31.974-64.692 41.897v32.386l273.846 157.538q17.173 9.912 26.663 26.582 9.491 16.671 9.491 36.495v62.152q0 19.824-9.491 36.495-9.49 16.67-26.663 26.582L516.154-111.771q-17.203 9.846-36.217 9.846t-36.091-9.846L176.155-265.847q-17.173-9.912-26.663-26.582-9.491-16.671-9.491-36.495v-62.152q0-19.824 9.491-36.495 9.49-16.67 26.663-26.582l273.846-157.538Zm-6.155 364.537L200-387.461v58.537q0 3.078 1.539 5.962 1.538 2.885 4.615 4.808l267.692 154.692q3.077 1.923 6.154 1.923t6.154-1.923l267.692-154.692q3.077-1.923 4.615-4.808 1.539-2.884 1.539-5.962v-58.537L516.154-247.154q-17.203 9.847-36.217 9.847t-36.091-9.847Zm6.155-162.847V-542.77L250.46-427.691l223.386 128.846q3.077 1.924 6.154 1.924t6.154-1.924l223.001-128.846L509.999-542.77v132.769h-59.998ZM480-699.999q25 0 42.5-17.5t17.5-42.5q0-25-17.5-42.5t-42.5-17.5q-25 0-42.5 17.5t-17.5 42.5q0 25 17.5 42.5t42.5 17.5Zm-2.308 538.61Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

+3 -3
View File
@@ -59,7 +59,7 @@ Here are examples of how to build with Genkit and Angular:
Here is an example of how to build with Firebase AI Logic and Angular:
- [Firebase AI Logic x Angular Starter Kit](https://github.com/angular/examples/tree/main/vertex-ai-firebase-angular-example) - Use this starter-kit to build an e-commerce application with a chat agent that can perform tasks. Start here if you do not have experience building with Firebase AI Logic and Angular.
- [Firebase AI Logic x Angular Starter Kit](https://github.com/angular/examples/tree/main/firebase-ai-logic-angular-example) - Use this starter-kit to build an e-commerce application with a chat agent that can perform tasks. Start here if you do not have experience building with Firebase AI Logic and Angular.
This example includes an [in-depth video walkthrough explaining the functionality and demonstrates how to add new features](https://youtube.com/live/4vfDz2al_BI).
@@ -79,7 +79,7 @@ When connecting to model providers, it is important to keep your API secrets saf
Your application's architecture determines which AI APIs and tools to choose. Specifically, choose based on whether or not your application is client-side or server-side. Tools such as Firebase AI Logic provide a secure connection to the model APIs for client-side code. If you want to use a different API than Firebase AI Logic or prefer to use a different model provider, consider creating a proxy-server or even [Cloud Functions for Firebase](https://firebase.google.com/docs/functions) to serve as a proxy and not expose your API keys.
For an example of connecting using a client-side app, see the code: [Firebase AI Logic Angular example repository](https://github.com/angular/examples/tree/main/vertex-ai-firebase-angular-example).
For an example of connecting using a client-side app, see the code: [Firebase AI Logic Angular example repository](https://github.com/angular/examples/tree/main/firebase-ai-logic-angular-example).
For server-side connections to model APIs that require API keys, prefer using a secrets manager or environment variable, not `environments.ts`. You should follow standard best practices for securing API keys and credentials. Firebase now provides a new secrets manager with the latest updates from Firebase App Hosting. To learn more, [check out the official documentation](https://firebase.google.com/docs/app-hosting/configure).
@@ -91,7 +91,7 @@ If you want to build agentic workflows, where agents are able to act and use too
Tool calling further enhances your web apps by expanding your AI integration further than a question and answer style chat bot. In fact, you can empower your model to request function calls using the function calling API of your model provider. The available tools can be used to perform more complex actions within the context of your application.
In the [e-commerce example](https://github.com/angular/examples/blob/main/vertex-ai-firebase-angular-example/src/app/ai.service.ts#L88) of the [Angular examples repository](https://github.com/angular/examples), the LLM requests to make calls to functions for inventory in order to gain the necessary context to perform more complex tasks such as calculating how much a group of items in the store will cost. The scope of the available API is up to you as a developer just as is whether or not to call a function requested by the LLM. You remain in control of the flow of execution. You can expose specific functions of a service for example but not all functions of that service.
In the [e-commerce example](https://github.com/angular/examples/blob/main/firebase-ai-logic-angular-example/src/app/ai.service.ts#L88) of the [Angular examples repository](https://github.com/angular/examples), the LLM requests to make calls to functions for inventory in order to gain the necessary context to perform more complex tasks such as calculating how much a group of items in the store will cost. The scope of the available API is up to you as a developer just as is whether or not to call a function requested by the LLM. You remain in control of the flow of execution. You can expose specific functions of a service for example but not all functions of that service.
### Handling non-deterministic responses
+1 -1
View File
@@ -1,4 +1,4 @@
{
"branchName": "refs/heads/21.2.x",
"sha": "0707c69d63e9962bab077dbc45770c25c8d26b7c"
"sha": "faea5e033fd1bc0a5dec7e430434414d1cf6243d"
}
+1 -1
View File
@@ -792,7 +792,7 @@
"source": {
"filePath": "/src/aria/menu/menu.ts",
"startLine": 58,
"endLine": 194
"endLine": 201
}
}
],
@@ -37,7 +37,7 @@ The `outputToObservable` function lets you create an RxJS observable from a comp
import {outputToObservable} from '@angular/core/rxjs-interop';
@Component(/*...*/)
class CustomSlider {
class CustomSlider {
valueChange = output<number>();
}
+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
@@ -134,7 +134,7 @@ In cases like this, the following rules determine which value wins:
## Styling with CSS custom properties
Developers often rely on [CSS Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties) to enable a flexible configuration of their component's styles.
You can set such custom properties on a host element with a [style binding][style binding](guide/templates/binding#css-style-properties).
You can set such custom properties on a host element with a [style binding](guide/templates/binding#css-style-properties).
```angular-ts
@Component({
@@ -1,20 +1,20 @@
# Creating and using services
Services are reusable pieces of code that can be shared across your Angular application. They typically handle data fetching, business logic, or other functionality that multiple components need to access.
Services are reusable pieces of code that you can share across your Angular application. You commonly use them to handle data fetching, business logic, or other functionality that multiple components need to access.
## Creating a service
You can create a service with the [Angular CLI](tools/cli) with the following command:
You can create a service using the [Angular CLI](tools/cli) with the following command:
```bash
ng generate service CUSTOM_NAME
```
This creates a dedicated `CUSTOM_NAME.ts` file in your `src` directory.
This command creates a dedicated `CUSTOM_NAME.ts` file in your `src` directory.
You can also manually create a service by adding the `@Injectable()` decorator to a TypeScript class. This tells Angular that the service can be injected as a dependency.
You can also manually create a service by adding the `@Injectable()` decorator to a TypeScript class. This tells Angular that you can use the class as an injectable dependency.
Here is an example of a service that allows users to add and request data:
The following example defines a service that allows users to add and retrieve data:
```ts
// 📄 src/app/basic-data-store.ts
@@ -38,9 +38,9 @@ export class BasicDataStore {
When you use `@Injectable({ providedIn: 'root' })` in your service, Angular:
- **Creates a single instance** (singleton) for your entire application
- **Makes it available everywhere** without any additional configuration
- **Enables tree-shaking** so the service is only included in your JavaScript bundle if it's actually used
- **Creates a single instance** (a singleton) for the entire application
- **Makes it available throughout your application** without additional configuration
- **Enables tree-shaking** so Angular only includes the service in your JavaScript bundle if you actually use it
This is the recommended approach for most services.
@@ -93,7 +93,7 @@ export class BasicDataStore {
## Next steps
While `providedIn: 'root'` covers most use cases, Angular offers additional ways to provide services for specialized scenarios:
While `providedIn: 'root'` covers most use cases, Angular also provides additional ways you can configure services for more specialized scenarios:
- **Component-specific instances** - When components need their own isolated service instances
- **Manual configuration** - For services that require runtime configuration
@@ -1,21 +1,21 @@
# Creating an injectable service
Service is a broad category encompassing any value, function, or feature that an application needs.
A service is typically a class with a narrow, well-defined purpose.
A component is one type of class that can use DI.
A service is a broad category that encompasses any value, function, or feature that your application needs.
A service is typically a class with a focused and well-defined purpose.
A component is one type of class that you can use with dependency injection (DI).
Angular distinguishes components from services to increase modularity and reusability.
By separating a component's view-related features from other kinds of processing, you can make your component classes lean and efficient.
Angular distinguishes components from services to improve modularity and reusability.
By separating a component's view-related features from other types of processing, you can keep your component classes lean and efficient.
Ideally, a component's job is to enable the user experience and nothing more.
Ideally, your component's responsibility is to enable the user experience and nothing more.
A component should present properties and methods for data binding, to mediate between the view (rendered by the template) and the application logic (which often includes some notion of a model).
A component can delegate certain tasks to services, such as fetching data from the server, validating user input, or logging directly to the console.
By defining such processing tasks in an injectable service class, you make those tasks available to any component.
You can also make your application more adaptable by configuring different providers of the same kind of service, as appropriate in different circumstances.
You can delegate tasks from a component to services, such as fetching data from a server, validating user input, or logging to the console.
By defining such tasks in an injectable service class, you make those capabilities available to any component.
You can also make your application more adaptable by configuring different providers for the same type of service based on different circumstances.
Angular does not enforce these principles.
Angular helps you follow these principles by making it easy to factor your application logic into services and make those services available to components through DI.
Angular does not strictly enforce these principles.
Angular helps you follow these principles by making it easy to organize your application logic into services and make those services available to components through DI.
## Service examples
@@ -82,7 +82,7 @@ export class HeroService {}
```
The `@Injectable()` decorator specifies that Angular can use this class in the DI system.
The metadata, `providedIn: 'root'`, means that the `HeroService` is provided throughout the application.
The `providedIn: 'root'` metadata specifies that the `HeroService` is available throughout your application.
Add a `getHeroes()` method that returns the heroes from `mock.heroes.ts` to get the hero mock data:
@@ -106,7 +106,7 @@ For clarity and maintainability, it is recommended that you define components an
## Injecting services
To inject a service as a dependency into a component, you can declare a class field representing the dependency and use Angular's [`inject`](/api/core/inject) function to initialize it.
To inject a service into a component, declare a class field for the dependency and use Angular's [`inject`](/api/core/inject) function to initialize it.
The following example specifies the `HeroService` in the `HeroList`.
The type of `heroService` is `HeroService`.
@@ -125,7 +125,7 @@ It is also possible to inject a service into a component using the component's c
constructor(private heroService: HeroService)
```
The [`inject`](/api/core/inject) method can be used in both classes and functions, while the constructor method can naturally only be used in a class constructor. However, in either case a dependency may only be injected in a valid [injection context](guide/di/dependency-injection-context), usually in the construction or initialization of a component.
The [`inject`](/api/core/inject) method can be used in both classes and functions, while the constructor method can naturally only be used in a class constructor. However, in both cases, you can only inject a dependency within a valid [injection context](guide/di/dependency-injection-context), typically during the construction or initialization of a component.
## Injecting services in other services
@@ -1,24 +1,24 @@
# Injection context
The dependency injection (DI) system relies internally on a runtime context where the current injector is available.
The dependency injection (DI) system relies on a runtime context where the current injector is available.
This means that injectors can only work when code is executed in such a context.
This means that injectors only work when you execute code within this context.
The injection context is available in these situations:
You have an injection context available in the following situations:
- During construction (via the `constructor`) of a class being instantiated by the DI system, such as an `@Injectable` or `@Component`.
- In the initializer for fields of such classes.
- During construction (via the `constructor`) of a class instantiated by the DI system, such as an `@Injectable` or `@Component`.
- In field initializers of such classes.
- In the factory function specified for `useFactory` of a `Provider` or an `@Injectable`.
- In the `factory` function specified for an `InjectionToken`.
- Within a stack frame that runs in an injection context.
Knowing when you are in an injection context will allow you to use the [`inject`](api/core/inject) function to inject instances.
Knowing when you are in an injection context allows you to use the [`inject`](api/core/inject) function to retrieve dependencies.
NOTE: For basic examples of using `inject()` in class constructors and field initializers, see the [overview guide](/guide/di#where-can-inject-be-used).
## Stack frame in context
Some APIs are designed to be run in an injection context. This is the case, for example, with router guards. This allows the use of [`inject`](api/core/inject) within the guard function to access a service.
Some APIs are designed to run within an injection context. This is the case, for example, with router guards. This allows you to use [`inject`](api/core/inject) within the guard function to access services.
Here is an example for `CanActivateFn`
@@ -33,8 +33,8 @@ const canActivateTeam: CanActivateFn = (
## Run within an injection context
When you want to run a given function in an injection context without already being in one, you can do so with `runInInjectionContext`.
This requires access to a given injector, like the `EnvironmentInjector`, for example:
If you need to run a function within an injection context without already being in one, you can use `runInInjectionContext`.
This requires access to an injector, such as the `EnvironmentInjector`:
```ts {highlight: [9], header"hero.service.ts"}
@Injectable({
@@ -51,11 +51,11 @@ export class HeroService {
}
```
Note that [`inject`](/api/core/inject) will return an instance only if the injector can resolve the required token.
Note that [`inject`](/api/core/inject) returns an instance only if the injector can resolve the requested token.
## Asserts the context
Angular provides the `assertInInjectionContext` helper function to assert that the current context is an injection context and throws a clear error if not. Pass a reference to the calling function so the error message points to the correct API entry point. This produces a clearer, more actionable message than the default generic injection error.
Angular provides the `assertInInjectionContext` helper function to verify that the current context is an injection context and throw a clear error if it is not. Pass a reference to the calling function so the error message points to the correct API entry point. This produces a clearer, more actionable message than the default generic injection error.
```ts
import {ElementRef, assertInInjectionContext, inject} from '@angular/core';
@@ -86,4 +86,4 @@ export class PreviewCard {
## Using DI outside of a context
Calling [`inject`](api/core/inject) or calling `assertInInjectionContext` outside of an injection context will throw [error NG0203](/errors/NG0203).
If you call [`inject`](api/core/inject) or `assertInInjectionContext` outside of an injection context, Angular throws [error NG0203](/errors/NG0203).
+10 -10
View File
@@ -1,15 +1,15 @@
# DI in action
This guide explores additional features of dependency injection in Angular.
This guide explores additional features of dependency injection (DI) in Angular.
NOTE: For comprehensive coverage of InjectionToken and custom providers, see the [defining dependency providers guide](guide/di/defining-dependency-providers#injection-tokens).
## Inject the component's DOM element
Although developers strive to avoid it, some visual effects and third-party tools require direct DOM access.
As a result, you might need to access a component's DOM element.
Although developers generally avoid it, some visual effects and third-party tools require you to access the DOM directly.
In such cases, you may need to access a component's DOM element.
Angular exposes the underlying element of a `@Component` or `@Directive` via injection using the `ElementRef` injection token:
Angular exposes the underlying DOM element of a `@Component` or `@Directive` through injection using the `ElementRef` token:
```ts {highlight:[7]}
import {Directive, ElementRef, inject} from '@angular/core';
@@ -28,7 +28,7 @@ export class HighlightDirective {
## Inject the host element's tag name
When you need the tag name of a host element, inject it using the `HOST_TAG_NAME` token.
To get the tag name of a host element, inject it using the `HOST_TAG_NAME` token.
```ts
import {Directive, HOST_TAG_NAME, inject} from '@angular/core';
@@ -59,19 +59,19 @@ NOTE: If the host element might not have a tag name (e.g., `ng-container` or `ng
## Resolve circular dependencies with a forward reference
The order of class declaration matters in TypeScript.
You can't refer directly to a class until it's been defined.
In TypeScript, the order of class declarations matters.
You cannot reference a class directly until you define it.
This isn't usually a problem, especially if you adhere to the recommended _one class per file_ rule.
But sometimes circular references are unavoidable.
For example, when class 'A' refers to class 'B' and 'B' refers to 'A', one of them has to be defined first.
However, in some cases, circular references are unavoidable.
For example, if class 'A' refers to class 'B' and class 'B' refers to class 'A', one of them must be defined first.
The Angular `forwardRef()` function creates an _indirect_ reference that Angular can resolve later.
You face a similar problem when a class makes _a reference to itself_.
For example, in its `providers` array.
The `providers` array is a property of the `@Component()` decorator function, which must appear before the class definition.
You can break such circular references by using `forwardRef`.
Such circular references can be resolved using `forwardRef`.
```typescript {header: 'app.component.ts', highlight: [4]}
providers: [
@@ -232,7 +232,7 @@ In this case, the injector looks no further than the current `ElementInjector` b
providers: [{provide: FlowerService, useValue: {emoji: '🌷'}}],
})
export class Self {
constructor(@Self() public flower: FlowerService) {}
public flower = inject(FlowerService, {self: true});
}
```
+12 -12
View File
@@ -1,38 +1,38 @@
<docs-decorative-header title="Dependency injection in Angular" imgSrc="adev/src/assets/images/dependency_injection.svg"> <!-- markdownlint-disable-line -->
Dependency Injection (DI) is a design pattern used to organize and share code across an application.
Dependency Injection (DI) is a design pattern you use to organize and share code across your application by supplying dependencies to a class instead of creating them inside it.
</docs-decorative-header>
TIP: Check out Angular's [Essentials](essentials/dependency-injection) before diving into this comprehensive guide.
As an application grows, developers often need to reuse and share features across different parts of the codebase. [Dependency Injection (DI)](https://en.wikipedia.org/wiki/Dependency_injection) is a design pattern used to organize and share code across an application by allowing you to "inject" features into different parts.
As an application grows, developers often need to reuse and share functionality across different parts of the codebase. [Dependency Injection (DI)](https://en.wikipedia.org/wiki/Dependency_injection) helps you achieve this by allowing you to provide dependencies to a class instead of creating them directly inside it. This makes different parts of the application more reusable and easier to manage.
Dependency injection is a popular pattern because it allows developers to address common challenges such as:
- **Improved code maintainability**: Dependency injection allows cleaner separation of concerns which enables easier refactoring and reducing code duplication.
- **Scalability**: Modular functionality can be reused across multiple contexts and allows for easier scaling.
- **Better testing**: DI allows unit tests to easily use [test doubles](https://en.wikipedia.org/wiki/Test_double) for situations when using a real implementation is not practical.
- **Improved code maintainability**: Dependency injection promotes a clear separation of concerns, making code easier to refactor and reducing duplication.
- **Scalability**: You can reuse modular functionality across different parts of an application, making it easier to scale.
- **Better testing**: DI allows unit tests to use [test doubles](https://en.wikipedia.org/wiki/Test_double) in place of real implementations when needed.
## How does dependency injection work in Angular?
A dependency is any object, value, function or service that a class needs to work but does not create itself. In other words, it creates a relationship between different parts of your application since it wouldn't work without the dependency.
A dependency is any object, value, function, or service that a class requires to work but does not create itself. Instead, you provide it from the outside, creating a clear relationship between different parts of the application.
There are two ways that code interacts with any dependency injection system:
You interact with a dependency injection system in two main ways:
- Code can _provide_, or make available, values.
- Code can _inject_, or ask for, those values as dependencies.
- You can _provide_, or make available, values.
- You can _inject_, or ask for, those values as dependencies.
"Values," in this context, can be any JavaScript value, including objects and functions. Common types of injected dependencies include:
In this context, "values" can refer to any JavaScript value, including objects, functions, or class instances. Common types of injected dependencies include:
- **Configuration values**: Environment-specific constants, API URLs, feature flags, etc.
- **Factories**: Functions that create objects or values based on runtime conditions
- **Services**: Classes that provide common functionality, business logic, or state
Angular components and directives automatically participate in DI, meaning that they can inject dependencies _and_ they are available to be injected.
Angular components and directives automatically participate in DI, meaning that you can inject dependencies into them and make them available for injection.
## What are services?
An Angular _service_ is a TypeScript class decorated with `@Injectable`, which makes an instance of the class available to be injected as a dependency. Services are the most common way of sharing data and functionality across an application.
An Angular _service_ is a TypeScript class decorated with `@Injectable`, which allows you to inject an instance of the class as a dependency. Services are the most common way of sharing data and functionality across an application.
Common types of services include:
@@ -76,7 +76,7 @@ Angular creates the directive class and specifies the CSS selector, `[select]`,
Import `TemplateRef`, and `ViewContainerRef`. Inject `TemplateRef` and `ViewContainerRef` in the directive as private properties.
```ts
import {Directive, TemplateRef, ViewContainerRef} from '@angular/core';
import {Directive, TemplateRef, ViewContainerRef, inject} from '@angular/core';
@Directive({
selector: '[select]',
@@ -9,12 +9,12 @@ A typical use-case is a questionnaire.
You might need to get input from users in different contexts.
The format and style of the forms a user sees should remain constant, while the actual questions you need to ask vary with the context.
In this tutorial you will build a dynamic form that presents a basic questionnaire.
In this tutorial, you will build a dynamic form that presents a basic questionnaire.
You build an online application for heroes seeking employment.
The agency is constantly tinkering with the application process, but by using the dynamic form
you can create the new forms on the fly without changing the application code.
The tutorial walks you through the following steps.
The tutorial walks you through the following steps:
1. Enable reactive forms for a project.
1. Establish a data model to represent form controls.
@@ -30,7 +30,7 @@ The basic version can evolve to support a richer variety of questions, more grac
Dynamic forms are based on reactive forms.
To give the application access reactive forms directives, import `ReactiveFormsModule` from the `@angular/forms` library into the necessary components.
To give the application access to reactive form directives, import `ReactiveFormsModule` from the `@angular/forms` package into the necessary components.
<docs-code-multifile>
<docs-code header="dynamic-form.component.ts" path="adev/src/content/examples/dynamic-form/src/app/dynamic-form.component.ts"/>
@@ -8,7 +8,7 @@ This page shows how to validate user input from the UI and display useful valida
To add validation to a template-driven form, you add the same validation attributes as you would with [native HTML form validation](https://developer.mozilla.org/docs/Web/Guide/HTML/HTML5/Constraint_validation).
Angular uses directives to match these attributes with validator functions in the framework.
Every time the value of a form control changes, Angular runs validation and generates either a list of validation errors that results in an `INVALID` status, or null, which results in a VALID status.
Every time the value of a form control changes, Angular runs validation and generates either a list of validation errors that results in an `INVALID` status, or `null`, which results in a `VALID` status.
You can then inspect the control's state by exporting `ngModel` to a local template variable.
The following example exports `NgModel` into a variable called `name`:
@@ -237,7 +237,7 @@ Asynchronous validators implement the `AsyncValidatorFn` and `AsyncValidator` in
These are very similar to their synchronous counterparts, with the following differences.
- The `validate()` functions must return a Promise or an observable,
- The observable returned must be finite, meaning it must complete at some point.
- The observable returned must be finite, meaning that it must complete at some point.
To convert an infinite observable into a finite one, pipe the observable through a filtering operator such as `first`, `last`, `take`, or `takeUntil`.
Asynchronous validation happens after the synchronous validation, and is performed only if the synchronous validation is successful.
@@ -369,7 +369,7 @@ onCountryChange(country: string) {
Use [`setValidators`](api/forms/AbstractControl#setValidators) to replace all existing synchronous validators on a control, or [`clearValidators`](api/forms/AbstractControl#clearValidators) to remove all validators.
```ts
toggleStrictNameValidation(isStenablerict: boolean) {
toggleStrictNameValidation(isStrict: boolean) {
const nameControl = this.profileForm.get('name');
if (enable) {
+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'},
@@ -350,6 +350,229 @@ export class Registration {
The `rxResource` function works directly with Observables and handles subscription cleanup automatically when the field value changes.
## Debouncing
The `debounce` rule delays when a user's input is committed to the form model. You can think of it as the rule holding back values until the user pauses typing. This is useful when downstream behavior shouldn't react to every keystroke, such as expensive derived computations, validation that flashes errors mid-word, or search filters that reapply on each character.
Add the `debounce` rule inside a schema to delay how a form field's UI changes reach the form model. In its simplest form, `debounce(path, ms)` holds each UI change for the given number of milliseconds before writing it to the model. A new change within that window resets the timer.
The following example applies `debounce` and `validateHttp` to the username field to delay the username availability check in a registration form until the user pauses typing:
```angular-ts
import {Component, signal} from '@angular/core';
import {form, debounce, validateHttp, FormField} from '@angular/forms/signals';
@Component({
selector: 'app-registration',
imports: [FormField],
template: `
<label>
Username:
<input [formField]="registrationForm.username" />
</label>
@if (registrationForm.username().pending()) {
<span class="checking">Checking availability...</span>
}
`,
})
export class Registration {
registrationModel = signal({username: ''});
registrationForm = form(this.registrationModel, (schemaPath) => {
// Hold UI updates for 300 ms before writing to the model
debounce(schemaPath.username, 300);
// Runs against the debounced model value, not every keystroke
validateHttp(schemaPath.username, {
request: ({value}) => {
const username = value();
// Skip the request for blank values
return username ? `/api/users/check?username=${username}` : undefined;
},
onSuccess: (response) =>
response.available ? null : {kind: 'usernameTaken', message: 'Username is already taken'},
onError: () => ({
kind: 'serverError',
message: 'Could not verify username availability',
}),
});
});
}
```
With a 300 ms debounce, the model updates and validates only after the user pauses typing longer than the configured duration. For example, typing "signal forms" in a quick burst fires one validation request instead of twelve.
### Touch flushes the model
Regardless of the debounce duration, the framework writes the field's `controlValue()` to the model immediately when the field becomes touched. Native inputs become touched on blur, so a user who finishes typing and tabs away doesn't have to wait for the debounce timer to expire. Custom controls can mark the field as touched in response to any event they choose.
In the typical case, this matters for form submission. When the user clicks a submit button, the focused input blurs, which touches that field and flushes its pending debounce before the submission handler runs.
### Commit only on blur
Some fields shouldn't update mid-typing at all, and instead should only update after the user has finished entering a value. For example, if you have a search filter that reapplies on every change or a form that triggers expensive derived state, it is often better for the model to wait until the user finishes typing.
In these scenarios, pass `'blur'` instead of a duration to defer all updates until the field becomes touched:
```ts
form(this.registrationModel, (schemaPath) => {
debounce(schemaPath.username, 'blur');
});
```
With `'blur'`, the model keeps its previous value while the user is typing. Sync and async validation, derived signals, and any reactive rules reading the field all see the previous value until the field becomes touched. This commonly occurs when the user blurs a native input, or when a custom control signals touch on its own.
### Custom timing logic
For timing logic that a duration or `'blur'` can't express, pass a `Debouncer` function. The function receives the field context and an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal), and returns a `Promise<void>` that resolves when the model should update:
```ts
import {debounce, type Debouncer} from '@angular/forms/signals';
const shorterWhenLonger: Debouncer<string> = ({value}, abortSignal) => {
// Shorter queries get a longer delay since the user is likely still typing.
const ms = value().length < 3 ? 500 : 200;
return new Promise((resolve) => {
const timeoutId = setTimeout(resolve, ms);
// Abort fires when this field is touched or its value changes, so the pending timer is cleared
abortSignal.addEventListener(
'abort',
() => {
clearTimeout(timeoutId);
resolve();
},
{once: true},
);
});
};
form(this.registrationModel, (schemaPath) => {
debounce(schemaPath.username, shorterWhenLonger);
});
```
The `abortSignal` fires when the field is touched, or when its value changes before the debounce resolves. Resolve the promise on abort so your debouncer releases any pending timers. The framework writes the pending value to the model on touch, and discards it when a newer value arrives. See the [`debounce` API reference](api/forms/signals/debounce) for the full `Debouncer` signature.
### Debouncing a single async validator
The `debounce` rule holds back every reaction to the field, from sync validation to derived signals to async validation. However, there are times when you want the opposite: cheap sync validators like `required` or `email` running immediately for instant feedback, while only the expensive async call waits for the user to settle. Both `validateHttp()` and `validateAsync()` accept their own [`debounce` option](api/forms/signals/validateAsync) that throttles just that validator:
```ts
form(this.registrationModel, (schemaPath) => {
validateHttp(schemaPath.username, {
// Throttles only this HTTP call
debounce: 300,
request: ({value}) => {
const username = value();
// Skip the request for blank values
return username ? `/api/users/check?username=${username}` : undefined;
},
onSuccess: (response) =>
response.available ? null : {kind: 'usernameTaken', message: 'Username is already taken'},
onError: () => ({
kind: 'serverError',
message: 'Could not verify username availability',
}),
});
});
```
The model still updates on every keystroke, and any other rules attached to the field still react immediately. Only the HTTP request is debounced: each change waits 300 ms of quiet before firing, so a request only goes out once the user has paused typing.
Choose between the two layers based on scope:
| Option | When to use |
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `debounce()` rule | Sync validation, derived state, and submission should all wait until the field commits. The whole field shouldn't react mid-typing. |
| `validateHttp({ debounce })` or `validateAsync({ debounce })` | Cheap sync validators should give immediate feedback, but expensive async calls should wait for the user to pause. |
Both options accept a duration in milliseconds. Their custom-timing callbacks differ: the form-level rule takes a `Debouncer`, and the validator-level option takes a `DebounceTimer` from `@angular/core`. The two signatures are not interchangeable.
## Composing resources in async validation with a factory
The built-in [`debounce` option](api/forms/signals/validateAsync) covers throttling, but `validateAsync()` exposes a deeper composition point: the `factory` function. The factory receives the params as a signal and returns a resource. Between those two points, you're free to compose whatever you need.
In its simplest form, a factory wraps a single resource. A username-availability check can live as a method on the component class, and then be wired into `validateAsync` by reference:
```ts
export class Registration {
registrationModel = signal({username: ''});
private usernameValidator = inject(UsernameValidator);
// Factory function
checkUsernameAvailable = (username: Signal<string | undefined>) =>
resource({
params: () => username(),
loader: async ({params: name}) => this.usernameValidator.checkAvailability(name),
});
registrationForm = form(this.registrationModel, (schemaPath) => {
validateAsync(schemaPath.username, {
params: ({value}) => {
const username = value();
// Skip validation for short usernames
return username.length >= 3 ? username : undefined;
},
debounce: 300,
// Reference to the factory defined above
factory: this.checkUsernameAvailable,
onSuccess: (result) =>
result?.available ? null : {kind: 'usernameTaken', message: 'Username taken'},
onError: () => ({kind: 'serverError', message: 'Could not verify'}),
});
});
}
```
The `params` callback returns `undefined` for short usernames, signaling that validation should skip. With `debounce: 300` applied, the resource waits until the user pauses typing for 300 ms before acting on each change. It then runs the loader for valid usernames and stays idle once the debounced value settles to `undefined`.
### Combining debounce with additional logic
When you need logic beyond a plain duration debounce, use a custom factory to combine debouncing with that logic. A common case is caching validated responses. For example, once the server has confirmed a username, you don't need to ask again on subsequent keystrokes that revisit the same value.
```ts
export class Registration {
registrationModel = signal({username: ''});
private usernameValidator = inject(UsernameValidator);
registrationForm = form(this.registrationModel, (schemaPath) => {
validateAsync(schemaPath.username, {
params: ({value}) => {
const username = value();
return username.length >= 3 ? username : undefined;
},
factory: (username) => {
// Core primitive: settles 300 ms after the source stops changing
const debouncedUsername = debounced(username, 300);
// Cache lives in the factory's closure and persists for the field's lifetime
const cache = new Map<string, {available: boolean}>();
return resource({
// Read from the debounced signal, not the raw one
params: () => debouncedUsername.value(),
loader: async ({params: name}) => {
const cached = cache.get(name);
if (cached) return cached;
const result = await this.usernameValidator.checkAvailability(name);
cache.set(name, result);
return result;
},
});
},
onSuccess: (result) =>
result?.available ? null : {kind: 'usernameTaken', message: 'Username taken'},
onError: () => ({
kind: 'serverError',
message: 'Could not verify username',
}),
});
});
}
```
The `cache` lives in the factory's closure, so it persists for the field's lifetime. Once the user has typed a username the server has already checked, the loader reads from the cache instead of making a new network request.
## Understanding pending state
When async validation runs, the field's `pending()` signal returns `true`. During this time:
@@ -0,0 +1,251 @@
# Cross-field logic
**Cross-field logic** is necessary when any rule, validation, or behavior of one field depends on another field's value or state.
Signal forms provide a **field context** to every rule function. The field context provides access to the current field's value and state, and lets you read other fields in the form using `valueOf()`, `stateOf()`, and `fieldTreeOf()`.
This guide covers the field context API in depth and shows common cross-field patterns. For single-field validation, see the [Validation guide](/guide/forms/signals/validation).
## Understanding the field context
Every rule function in signal forms receives a **field context** parameter, which is an object that describes the current field and provides access to the rest of the form.
There are three properties you can access for the current field:
| Property | Type | Description |
| ----------- | -------------------- | -------------------------------------------------------------------- |
| `value` | `Signal<TValue>` | The current field's value as a signal |
| `state` | `FieldState<TValue>` | The current field's state (such as validity, errors, touched, dirty) |
| `fieldTree` | `FieldTree<TValue>` | The current field's tree, for programmatic access to child fields |
For cross-field logic, the following three properties allow you to access other parts of the form:
| Property | Type | Description |
| --------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| `valueOf()` | `(path) => PValue` | Most common. Use when you need another field's raw value for comparisons or calculations. |
| `stateOf()` | `(path) => FieldState<PValue>` | Use when your logic depends on another field's state, such as whether it's valid, touched, or dirty. |
| `fieldTreeOf()` | `(path) => FieldTree<PModel>` | Use when you need programmatic access to another field's tree, such as pushing errors to a specific child field with validateTree. |
Here is an example of using `value` and `valueOf()` to validate that the current field (end date) comes after the start date in the form:
```ts
import {Component, signal} from '@angular/core';
import {form, validate} from '@angular/forms/signals';
@Component({
/* ... */
})
export class EventForm {
eventModel = signal({
startDate: new Date('2026-06-01'),
endDate: new Date('2026-06-05'),
});
eventForm = form(this.eventModel, (schemaPath) => {
validate(schemaPath.endDate, ({value, valueOf}) => {
if (value() <= valueOf(schemaPath.startDate)) {
return {
kind: 'invalidDateRange',
message: 'End date must be after start date',
};
}
return null;
});
});
}
```
NOTE: The `fieldContext` parameter is typically destructured to pull out only what the rule needs. The remaining examples in this guide use this pattern.
## Cross-field validation patterns
The date range example from the previous section validates the end date against the start date. Because the rule reads `valueOf(schemaPath.startDate)`, it re-evaluates automatically whenever either date changes. In other words, a single validator is enough to keep the error state correct.
However, that single validator only places the error on the end date field. If you want both fields to show an error when the range is invalid, add a matching validation rule to each field:
```ts
import {Component, signal} from '@angular/core';
import {form, validate} from '@angular/forms/signals';
@Component({
/* ... */
})
export class EventForm {
eventModel = signal({
startDate: new Date('2026-06-01'),
endDate: new Date('2026-06-05'),
});
eventForm = form(this.eventModel, (schemaPath) => {
validate(schemaPath.startDate, ({value, valueOf}) => {
if (value() >= valueOf(schemaPath.endDate)) {
return {
kind: 'invalidDateRange',
message: 'Start date must be before end date',
};
}
return null;
});
validate(schemaPath.endDate, ({value, valueOf}) => {
if (value() <= valueOf(schemaPath.startDate)) {
return {
kind: 'invalidDateRange',
message: 'End date must be after start date',
};
}
return null;
});
});
}
```
Both rules make use of `valueOf()` to read the other field. Because each rule is reactive, changing either date re-evaluates both validations automatically.
NOTE: When a rule involves multiple fields, you need to decide where the error belongs: on a specific field, on multiple fields, or on the parent. In general, place the error where the user would most likely go to fix the problem.
### Conditional requirements
In some forms, certain fields are only required under certain conditions. For example, a registration form might require a company name only when the user selects a business account type:
```ts
import {Component, signal} from '@angular/core';
import {form, required} from '@angular/forms/signals';
@Component({
/* ... */
})
export class RegistrationForm {
registrationModel = signal({
accountType: 'personal' as 'personal' | 'business',
companyName: '',
});
registrationForm = form(this.registrationModel, (schemaPath) => {
required(schemaPath.companyName, {
when: ({valueOf}) => valueOf(schemaPath.accountType) === 'business',
message: 'Company name is required for business accounts',
});
});
}
```
The `when` option receives the same field context as any other rule function, so `valueOf` works the same way. When the user switches back to `'personal'`, the condition re-evaluates and the requirement — along with its error — clears automatically.
Using `required()` with `when` instead of a manual `validate()` check also adds proper required metadata to the field, which enables accessibility features like marking the field as required for screen readers.
### Validating based on another field's state
The examples so far use `valueOf()` to read another field's value. Sometimes your logic depends on another field's _state_ instead — whether it's valid, touched, or dirty. Use `stateOf()` for this.
For example, a confirm-password field should only check for a match once the user has interacted with the password field. If the user hasn't touched the password yet, flagging a mismatch on the confirmation is premature:
```ts
import {Component, signal} from '@angular/core';
import {form, validate} from '@angular/forms/signals';
@Component({
/* ... */
})
export class PasswordForm {
passwordModel = signal({
password: '',
confirmPassword: '',
});
passwordForm = form(this.passwordModel, (schemaPath) => {
validate(schemaPath.confirmPassword, ({value, valueOf, stateOf}) => {
if (!stateOf(schemaPath.password).touched()) {
return null;
}
if (value() !== valueOf(schemaPath.password)) {
return {
kind: 'passwordMismatch',
message: 'Passwords do not match',
};
}
return null;
});
});
}
```
The `stateOf()` call returns the other field's [field state](api/forms/signals/FieldState), giving you access to signals like `invalid()`, `touched()`, and `dirty()`. Because these are signals, the rule re-evaluates whenever the password field's validity changes.
WARNING: Be careful not to read state which depends on your field's validation, as that creates a circular loop. For example, a validator which checks whether the parent field is valid will create an infinite loop because the parent's validity depends on its children's validity (which includes your validator).
## Using validateTree
The examples so far use `validate()` to check individual fields. Sometimes you need to validate a group of fields where the logic is inherently about multiple fields in a group, and direct errors to specific children within it. `validateTree` handles is ideal for these kinds of scenarios.
For example, in a Sudoku puzzle, each row must contain unique numbers. This is a group-level rule: you check the entire row, then flag the specific cells that violate it. This kind of validation can't be expressed cleanly with `validate` on individual fields, because each cell would need to know about every other cell.
```ts
import {Component, signal} from '@angular/core';
import {form, validateTree} from '@angular/forms/signals';
@Component({
/* ... */
})
export class SudokuRow {
rowModel = signal({
cell1: 1,
cell2: 3,
cell3: 1,
cell4: 4,
});
rowForm = form(this.rowModel, (schemaPath) => {
validateTree(schemaPath, ({value, fieldTreeOf}) => {
const row = value();
const entries = [
{val: row.cell1, fieldTree: fieldTreeOf(schemaPath.cell1)},
{val: row.cell2, fieldTree: fieldTreeOf(schemaPath.cell2)},
{val: row.cell3, fieldTree: fieldTreeOf(schemaPath.cell3)},
{val: row.cell4, fieldTree: fieldTreeOf(schemaPath.cell4)},
];
const counts = new Map<number, number>();
for (const {val} of entries) {
if (val !== 0) {
counts.set(val, (counts.get(val) ?? 0) + 1);
}
}
const errors = entries
.filter(({val}) => val !== 0 && (counts.get(val) ?? 0) > 1)
.map(({val, fieldTree}) => ({
kind: 'duplicateInRow',
message: `${val} already appears in this row`,
fieldTree,
}));
return errors.length > 0 ? errors : null;
});
});
}
```
The validator runs on the parent field (the row), reads all cell values, counts duplicates, and returns an error for each cell that contains a repeated number. The `fieldTree` property on each error tells Angular exactly which cell should show the error. Without `fieldTree`, the errors would apply to the row itself — not where the user needs to see them.
Because `validateTree` can return an array of errors, a single validator can flag multiple cells at once. Each error includes a `fieldTree` pointing to its target, so Angular routes the errors to the correct fields.
### When to use validateTree vs validate
Prefer `validate()` with `valueOf()` when the error belongs on the field being validated — even if the rule reads from other fields. Reach for `validateTree` when:
- The validation logic is inherently about a group of fields, not any single field
- The validator needs to return errors targeting different child fields
TIP: For an introduction to `validateTree` and its return type, see the [Validation guide](/guide/forms/signals/validation).
## Next steps
This guide covered the field context API and common cross-field patterns. To learn more about related Signal Forms guide, check out:
<docs-pill-row>
<docs-pill href="guide/forms/signals/validation" title="Validation" />
<docs-pill href="guide/forms/signals/field-state-management" title="Field state management" />
<docs-pill href="guide/forms/signals/custom-controls" title="Custom controls" />
</docs-pill-row>
@@ -516,7 +516,7 @@ import {form, FormField, min, max, validate} from '@angular/forms/signals';
@Component({
selector: 'app-custom',
imports: [formField],
imports: [FormField],
template: ` <input [formField]="customForm.score" /> `,
})
export class Custom {
@@ -643,7 +643,7 @@ import {form, FormField, max} from '@angular/forms/signals';
@Component({
selector: 'app-inventory',
imports: [formField],
imports: [FormField],
template: `
<label>
Item
@@ -742,7 +742,7 @@ import {
@Component({
selector: 'app-promo',
imports: [formField],
imports: [FormField],
template: `
@if (!promoForm.promoCode().hidden()) {
<label>
@@ -787,7 +787,7 @@ import {form, FormField, applyWhen, required, pattern} from '@angular/forms/sign
@Component({
selector: 'app-address',
imports: [formField],
imports: [FormField],
template: `
<label>
Country
@@ -0,0 +1,323 @@
# Form submission
When a user submits a form, your application typically needs to handle multiple concerns at once: surfacing validation errors, preventing duplicate submission, sending data to a server, and much more. Handling each of these manually can be tedious and prone to error.
Signal Forms provides a `submit()` function that helps you manage the form submission lifecycle. This guide walks through how to use it.
## What does `submit()` do?
The `submit()` function runs through a specific sequence:
1. **Mark interactive fields as touched** — Fields that display errors only after being touched will now show their validation errors. Hidden, disabled, and readonly fields are skipped.
1. **Check validation** — If any validation rules have failed, submission stops and the `action` function does not run.
1. **Run the action** — The `action` function executes with the form's current value. While it runs, `submitting()` returns `true`.
1. **Handle the result** — If the action returns errors, they are routed to their target fields. If it returns nothing, the submission is treated as successful.
The `submit()` function returns a `Promise<boolean>` that resolves to `true` when the action completes without errors, and `false` when validation fails or the action returns errors.
## Setting up form submission with `FormRoot`
The most common way to use the `submit()` function is through the `FormRoot` directive.
The `FormRoot` directive handles three things automatically when bound to a `<form>` element:
1. **Sets [`novalidate`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/form#novalidate)** — Disables the browser's built-in validation so Signal Forms manages validation instead
1. **Prevents default** — Stops the browser from navigating on form submission
1. **Calls `submit()`** — Triggers the submission flow when the user submits the form
NOTE: The `FormRoot` directive sets the `novalidate` attribute on the `form` element automatically. You do not need to add it manually when using `FormRoot`.
`FormRoot` handles the submission event, but you still need to tell it _what to do_ with the form data. That requires three things:
1. Bind your form to the `FormRoot` directive
1. Pass a `submission` option to the `form()` function
1. Define an `action` function within the `submission` option that manages the submitted data
```angular-ts
import {Component, signal} from '@angular/core';
import {form, FormField, FormRoot, required} from '@angular/forms/signals';
@Component({
selector: 'app-contact',
imports: [FormField, FormRoot],
template: `
<form [formRoot]="contactForm">
<label>
Name
<input [formField]="contactForm.name" />
</label>
<label>
Email
<input type="email" [formField]="contactForm.email" />
</label>
<button type="submit">Send</button>
</form>
`,
})
export class Contact {
contactModel = signal({
name: '',
email: '',
});
contactForm = form(
this.contactModel,
(schemaPath) => {
required(schemaPath.name);
required(schemaPath.email);
},
{
submission: {
action: async (field) => {
const result = await saveContact(field().value());
if (result.ok) return;
return {kind: 'serverError', message: 'Failed to submit form'};
},
},
},
);
}
```
The `action` function runs only when no validation rules have failed. By default, pending async validators do not block submission (see [Controlling validation gating](#controlling-validation-gating-with-ignorevalidators) for more details). The action receives the field tree and a `detail` object with `root` and `submitted` field trees, which is useful when submitting a sub-form.
After validation passes, the action itself may still fail due to scenarios such as a network error or duplicate entry. In those cases, you can surface the failure by returning the error(s). On the other hand, to indicate success, you only need to return `null` or `undefined`, or call an empty `return`.
## Showing submission state with `submitting()`
When you need to track whether the form is in the process of submitting, Signal Forms provides a `submitting()` signal that returns `true` while the `action` function is running. Use it to show loading indicators or disable the submit button to prevent duplicate submissions.
```angular-html
<button type="submit" [disabled]="contactForm().submitting()">
@if (contactForm().submitting()) {
Sending...
} @else {
Send
}
</button>
```
Once the `action` function succeeds or returns an error, the `submitting()` signal automatically resets back to `false`.
## Managing submission errors
### Server errors
When your `action` function communicates with a server, the server may return errors that need to appear on specific fields. Return these errors from the `action` to route them to their target fields.
#### Errors on the submitted field
By default, errors returned from the `action` are assigned to the submitted field (the field tree you passed to `submit()`):
```ts
action: async (field) => {
const result = await saveContact(field().value());
if (result.ok) return;
return {kind: 'serverError', message: 'Failed to submit form'};
};
```
#### Errors on specific fields
When you want to route an error to a specific field, include a `fieldTree` property pointing to that field:
```ts
action: async (field) => {
const result = await saveContact(field().value());
if (result.ok) return;
return {kind: 'taken', message: result.message, fieldTree: field.email};
};
```
#### Multiple errors
When you want to report errors on multiple fields, return an array:
```ts
action: async (field) => {
const result = await registerUser(field().value());
if (result.ok) return;
return result.errors.map((err: {field: string; message: string}) => ({
kind: 'serverError',
message: err.message,
fieldTree: field[err.field as keyof typeof field],
}));
};
```
### Auto-clearing submission errors
Submission errors clear automatically when the user edits the field. If the `action` returns an error on the email field, that error disappears as soon as the user changes the email value.
This differs from validation errors, which recompute reactively. Validation rules run again on each change and may produce the same error. Submission errors are one-time results from the server — once cleared, they do not reappear unless the form is submitted again.
TIP: Submission errors appear alongside validation errors in the field's `errors()` signal. For guidance on displaying errors in your template, see the [Field State Management guide](guide/forms/signals/field-state-management).
## Handling invalid submissions with `onInvalid`
When validation fails, the `action` function does not run. If you need to respond to a failed submission attempt — such as scrolling to the first error, showing a toast, or focusing an invalid field — use the `onInvalid` callback.
```ts
contactForm = form(
this.contactModel,
(schemaPath) => {
required(schemaPath.name);
required(schemaPath.email);
},
{
submission: {
action: async (field) => {
await saveContact(field().value());
},
onInvalid: (field) => {
const firstError = field().errorSummary()[0];
firstError?.fieldTree().focusBoundControl();
},
},
},
);
```
The `onInvalid` callback receives the same `(field, detail)` parameters as `action`. It runs after all interactive fields are marked as touched, so validation errors are already visible in the UI when it executes.
## Controlling validation gating with `ignoreValidators`
By default, `submit()` ignores pending validators. If no validators have failed, the action runs even if some async validators are still in progress. The `ignoreValidators` option gives you control over this behavior.
| Value | Behavior |
| ----------- | ------------------------------------------------------------------------ |
| `'pending'` | Submit if no validators have failed, even if some are pending (default) |
| `'none'` | Submit only if all validators pass — pending validators block submission |
| `'all'` | Always submit regardless of validation state |
```ts
contactForm = form(
this.contactModel,
(schemaPath) => {
required(schemaPath.name);
required(schemaPath.email);
},
{
submission: {
action: async (field) => {
await saveContact(field().value());
},
ignoreValidators: 'none',
},
},
);
```
Use `'none'` when your form has async validators (such as checking username availability) and you need all validation to complete before submitting. Use `'all'` for draft-saving scenarios where you want to persist data regardless of validation state.
## Manual submission with `submit()`
The `FormRoot` directive is the most common way to trigger submission, but you can also call `submit()` directly. This is useful for multi-step wizards, auto-save, or triggering submission from outside the form element.
```angular-ts
import {Component, signal} from '@angular/core';
import {form, FormField, required, submit} from '@angular/forms/signals';
@Component({
selector: 'app-contact',
imports: [FormField],
template: `
<label>
Name
<input [formField]="contactForm.name" />
</label>
<label>
Email
<input type="email" [formField]="contactForm.email" />
</label>
<button (click)="onSave()">Save</button>
`,
})
export class Contact {
contactModel = signal({
name: '',
email: '',
});
contactForm = form(this.contactModel, (schemaPath) => {
required(schemaPath.name);
required(schemaPath.email);
});
async onSave() {
// When calling `submit()` directly, you pass the action as the second argument
// instead of configuring it in `FormOptions`.
const success = await submit(this.contactForm, async (field) => {
const result = await saveContact(field().value());
if (result.ok) return;
return {kind: 'serverError', message: 'Failed to save'};
});
if (success) {
// Handle success — navigate, show confirmation, etc.
}
}
}
```
## Handling side effects
The `submit()` function returns a `Promise<boolean>` — `true` when the action completes without errors, `false` when validation fails or the action returns errors. Use this to trigger side effects like navigation or notifications.
```ts
async onSave() {
const success = await submit(this.contactForm, async (field) => {
await saveContact(field().value());
});
if (success) {
await this.router.navigate(['/confirmation']);
}
}
```
When the action produces data that a side effect needs, such as a server-generated ID, handle the side effect inside the action:
```ts
async onSave() {
await submit(this.contactForm, async (field) => {
const contact = await createContact(field().value());
await this.router.navigate(['/confirmation', contact.id]);
});
}
```
When using `FormRoot`, side effects also go inside the `action` since `FormRoot` calls `submit()` internally:
```ts
submission: {
action: async (field) => {
const result = await saveContact(field().value());
if (result.ok) {
await this.router.navigate(['/confirmation']);
return;
}
return {kind: 'serverError', message: 'Failed to submit form'};
},
}
```
## Next steps
This guide covered submitting forms and handling form submission errors. Related guides explore other aspects of Signal Forms:
<docs-pill-row>
<docs-pill href="guide/forms/signals/validation" title="Validation" />
<docs-pill href="guide/forms/signals/field-state-management" title="Field state management" />
<docs-pill href="guide/forms/signals/form-logic" title="Adding form logic" />
</docs-pill-row>
@@ -235,13 +235,79 @@ export class UserProfile {
}
```
The `SignalFormControl` synchronizes values and validation status bi-directionally:
The `SignalFormControl` synchronizes values bi-directionally between the **Signal Forms** system and the **Reactive Forms** system:
- **Signal -> Control**: Changing `email.set(...)` updates `emailControl.value` and the parent `form.value`.
- **Control -> Signal**: Typing in the input (updating `emailControl`) updates the `email` signal.
- **Validation**: Schema validators (like `required`) propagate errors to `emailControl.errors`.
- **Signal -> Reactive**: Updating the value via Signal Forms updates the Reactive Form control immediately.
### Disabling/Enabling control.
```typescript
// Signal Forms update
this.emailControl.fieldTree().value.set('new@example.com');
// Reactive Forms reflects the change
console.log(this.form.value); // {email: 'new@example.com'}
```
- **Reactive -> Signal**: Updating the value via the parent `FormGroup` updates the Signal Forms state.
```typescript
// Reactive Forms update
this.form.patchValue({email: 'other@example.com'});
// Signal Forms reflects the change
console.log(this.emailControl.fieldTree().value()); // 'other@example.com'
```
### Binding `SignalFormControl`
To use `SignalFormControl` in a `FormGroup`, pass it as a control and bind it in the template using `.fieldTree`:
```typescript
readonly emailControl = new SignalFormControl('', (p) => { required(p); });
readonly form = new FormGroup({
name: new FormControl('Alice'),
email: this.emailControl,
});
```
```angular-html {prefer}
<form [formGroup]="form">
<!-- Standard control -->
<input formControlName="name" />
<!-- Signal control -->
<input [formField]="emailControl.fieldTree" />
</form>
```
```angular-html {avoid}
<!-- Avoid: Using formControlName or [formControl] for SignalFormControl -->
<input formControlName="email" />
<input [formControl]="emailControl" />
```
### Why `SignalFormControl` takes a value instead of a signal
In standard Signal Forms, you create a form by passing a signal: `form(mySignal)`.
However, `SignalFormControl` takes a **raw value** (like a string or object) as its first argument:
```typescript
// Takes a raw value, not a signal
const userControl = new SignalFormControl({
email: 'pirojok@example.com',
});
```
`SignalFormControl` creates the signal internally to intercept writes and trigger the **synchronous updates** expected by Reactive Forms.
You can still access the internal signal via `.sourceValue`:
```typescript
const value = userControl.sourceValue();
```
### Disabling/Enabling control
Imperative APIs for changing the enabled/disabled state (like `enable()`, `disable()`) are intentionally not supported
in `SignalFormControl`. This is because the state of the control should be derived from the signal state and rules.
@@ -383,6 +449,3 @@ bootstrapApplication(App, {
],
});
```
<!-- TODO: include some high level usage comment about how people should mostly interact with this via the signal forms API exposed on .fieldTree, not via the reactive forms methods. -->
<!-- TODO: Elaborate on why the value taken is not a signal. -->
@@ -0,0 +1,243 @@
# Schemas and schema composability
Signal Forms uses a two-layer architecture to separate _how your form is structured_ from _how it behaves at runtime_.
When you pass a schema function to `form()`, that function _runs once_ during form creation. Its job is to set up the form's logic tree by declaring which fields have validation, which fields are disabled, and which fields depend on other fields. This is the **structural layer** of your form.
Inside a schema function, you call rule functions such as `disabled()` and `validate()`. These rule functions accept reactive logic that recomputes whenever the signals they reference change. Other rules like `required()` accept optional configuration, including a `when` function that conditionally activates the rule. Together, these form the **behavioral layer** of your form during runtime.
```ts
contactForm = form(this.contactModel, (schemaPath) => {
// Schema function: runs ONCE during form creation
required(schemaPath.name);
disabled(schemaPath.couponCode, ({valueOf}) => valueOf(schemaPath.total) < 50);
// ^^^ Reactive logic: recomputes when total changes
});
```
```mermaid
graph TD
A["form(model, schemaFn)"] --> B["Schema function runs ONCE"]
B --> C["required(path)"]
B --> D["disabled(path, logicFn)"]
B --> E["validate(path, logicFn)"]
B --> F["apply(path, schema)"]
C --> G["Reactive: recomputes on signal change"]
D --> G
E --> G
F --> B2["Nested schema function runs ONCE"]
B2 --> G
```
This distinction is important when you compose schemas because functions like `apply()`, `applyWhen()`, and `schema()` all operate at the structural layer. Schemas control _which_ rules exist and _whether_ they're active, while rule functions define _what_ those rules evaluate.
## Create reusable schemas with `schema()`
When multiple forms share the same rules for a common data shape, you can use the `schema()` function to extract those rules into a reusable schema.
```ts
import {schema, required, minLength} from '@angular/forms/signals';
const nameSchema = schema<{first: string; last: string}>((name) => {
required(name.first);
required(name.last);
minLength(name.first, 2);
minLength(name.last, 2);
});
```
The `schema()` function wraps a function and converts it into a reusable `Schema<T>` object. Like any schema function, it _runs once_ per form, but the object itself can be shared across as many forms as you need.
TIP: If rules only appear in one place, an inline schema function works just as well. Use `schema()` when you want to reuse the same schema across multiple forms or apply the same schema to multiple paths. Reusable `Schema` objects are cached per form compilation.
### Using the schema with `apply()`
You can apply a reusable schema to a specific path in a form by using the `apply()` function. When you call `apply()`, the schema receives a scoped path that only sees the fields within that sub-path:
```ts
import {apply} from '@angular/forms/signals';
profileForm = form(this.profileModel, (schemaPath) => {
apply(schemaPath.name, nameSchema);
});
registrationForm = form(this.registrationModel, (schemaPath) => {
apply(schemaPath.name, nameSchema);
});
```
## Conditional schemas with `applyWhen()`
NOTE: The [Adding form logic guide](guide/forms/signals/form-logic) introduced `applyWhen()` for conditional rules with inline logic. This section covers how to compose `applyWhen()` with reusable schemas.
Some rules should only apply under certain conditions. For example, a zip code field might require validation only when the selected country is the United States.
The `applyWhen()` function applies a schema conditionally based on reactive state. It accepts three arguments:
1. A path to apply the schema to
1. A reactive logic function that returns `true` when the schema should be active
1. A schema or schema function containing the conditional rules
```ts
import {form, applyWhen, required, pattern} from '@angular/forms/signals';
addressForm = form(this.addressModel, (schemaPath) => {
applyWhen(
schemaPath,
({valueOf}) => valueOf(schemaPath.country) === 'US',
(schemaPath) => {
required(schemaPath.zipCode);
pattern(schemaPath.zipCode, /^\d{5}(-\d{4})?$/);
},
);
});
```
The logic function receives a `FieldContext`, which provides access to `value`, `valueOf`, `stateOf`, and other reactive helpers. Because it's reactive, the condition is re-evaluated whenever the signals it reads change. When the condition becomes `false`, the rules inside the schema deactivate. When it becomes `true` again, they reactivate.
The schema itself is still structural — the schema function runs once during form creation. The condition controls whether those rules are _active_, not whether they _exist_.
Inside the conditional schema, use the scoped path parameter passed to that schema function. Paths from an outer schema are not valid inside a nested schema.
### Combining `applyWhen()` with reusable schemas
Since `applyWhen()` accepts a `Schema` object, you can pair it with `schema()` to conditionally apply reusable schemas:
```ts
const usZipCodeSchema = schema<{zipCode: string}>((address) => {
required(address.zipCode);
pattern(address.zipCode, /^\d{5}(-\d{4})?$/);
});
const caPostalCodeSchema = schema<{postalCode: string}>((address) => {
required(address.postalCode);
pattern(address.postalCode, /^[A-Z]\d[A-Z] \d[A-Z]\d$/);
});
shippingForm = form(this.shippingModel, (schemaPath) => {
applyWhen(
schemaPath.address,
({valueOf}) => valueOf(schemaPath.country) === 'US',
usZipCodeSchema,
);
applyWhen(
schemaPath.address,
({valueOf}) => valueOf(schemaPath.country) === 'CA',
caPostalCodeSchema,
);
});
```
NOTE: The logic function accesses `valueOf(schemaPath.country)` even though the path argument is `schemaPath.address`. This is because the `valueOf` helper can access any field in the form, not just fields within the scoped path.
This pattern keeps validation logic modular — each country's address rules live in their own schema, and the form selects which one to activate based on the user's choice.
## Type-narrowing with `applyWhenValue()`
The `applyWhenValue()` function simplifies conditions that only need to check the field's value. Instead of receiving a `FieldContext`, the condition function receives the field's raw value directly.
```ts {header: "applyWhen — logic function receives FieldContext"}
applyWhen(schemaPath.payment, ({value}) => value().type === 'credit-card', creditCardSchema);
```
```ts {header: "applyWhenValue — condition receives the value directly"}
applyWhenValue(schemaPath.payment, (payment) => payment.type === 'credit-card', creditCardSchema);
```
The main advantage of `applyWhenValue()` is TypeScript type guard support. When the condition function is a type guard, the schema's type parameter narrows to the guarded type. This is especially useful for discriminated unions, where each variant has different fields that need different rules.
```ts
import {form, applyWhenValue, required} from '@angular/forms/signals';
interface CreditCard {
type: 'credit-card';
cardNumber: string;
expiry: string;
cvv: string;
}
interface BankTransfer {
type: 'bank-transfer';
accountNumber: string;
routingNumber: string;
}
type PaymentMethod = CreditCard | BankTransfer;
function isCreditCard(value: PaymentMethod): value is CreditCard {
return value.type === 'credit-card';
}
function isBankTransfer(value: PaymentMethod): value is BankTransfer {
return value.type === 'bank-transfer';
}
paymentForm = form(this.paymentModel, (schemaPath) => {
applyWhenValue(schemaPath.payment, isCreditCard, (payment) => {
// TypeScript knows payment is scoped to CreditCard
required(payment.cardNumber);
required(payment.expiry);
required(payment.cvv);
});
applyWhenValue(schemaPath.payment, isBankTransfer, (payment) => {
// TypeScript knows payment is scoped to BankTransfer
required(payment.accountNumber);
required(payment.routingNumber);
});
});
```
Without the type guard, TypeScript would not know which fields are available inside each schema function. The type narrowing ensures that accessing `payment.cardNumber` is type-safe in the credit card branch and `payment.accountNumber` is type-safe in the bank transfer branch.
## Array items with `applyEach()`
When a form contains an array of objects, you often need the same rules applied to every item. The `applyEach()` function applies a schema to each item in an array field, regardless of how many items exist.
```ts
import {form, applyEach, required, min} from '@angular/forms/signals';
type LineItem = {name: string; quantity: number};
orderForm = form(this.orderModel, (schemaPath) => {
required(schemaPath.title);
applyEach(schemaPath.items, (item) => {
required(item.name);
min(item.quantity, 1);
});
});
```
The schema function passed to `applyEach()` receives a `SchemaPathTree` scoped to a single array item. Rules declared inside apply to every item in the array, including items added after form creation.
### Combining `applyEach()` with reusable schemas
Since `applyEach()` accepts a `Schema` object, you can extract item-level rules into a reusable schema and share them across forms:
```ts
const lineItemSchema = schema<LineItem>((item) => {
required(item.name);
min(item.quantity, 1);
});
orderForm = form(this.orderModel, (schemaPath) => {
required(schemaPath.title);
applyEach(schemaPath.items, lineItemSchema);
});
invoiceForm = form(this.invoiceModel, (schemaPath) => {
required(schemaPath.invoiceNumber);
applyEach(schemaPath.lineItems, lineItemSchema);
});
```
TIP: For more on validating array items, including custom error messages per field, see the [Validation guide](guide/forms/signals/validation).
## Next steps
To learn more about Signal Forms, check out these related guides:
- [Adding form logic](guide/forms/signals/form-logic) - Learn how to add conditional logic, dynamic behavior, and metadata to your forms
- [Validation](guide/forms/signals/validation) - Learn about validation rules and error handling
- [Async operations](guide/forms/signals/async-operations) - Learn how to handle form submission and async validation
@@ -10,7 +10,7 @@ Angular supports two design approaches for interactive forms. Template-driven fo
Template-driven forms are a great choice for small or simple forms, while reactive forms are more scalable and suitable for complex forms. For a comparison of the two approaches, see [Choosing an approach](guide/forms#choosing-an-approach)
</docs-callout>
You can build almost any kind of form with an Angular template —login forms, contact forms, and pretty much any business form.
You can build almost any kind of form with an Angular template — login forms, contact forms, and pretty much any business form.
You can lay out the controls creatively and bind them to the data in your object model.
You can specify validation rules and display validation errors, conditionally allow input from specific controls, trigger built-in visual feedback, and much more.
@@ -182,8 +182,7 @@ The following table describes the class names that Angular applies based on the
| The control's value has changed. | `ng-dirty` | `ng-pristine` |
| The control's value is valid. | `ng-valid` | `ng-invalid` |
Angular also applies the `ng-submitted` class to `form` elements upon submission,
but not to the controls inside the `form` element.
Angular also applies the `ng-submitted` class to `form` elements upon submission, but not to the controls inside the `form` element.
You use these CSS classes to define the styles for your control based on its status.
@@ -336,7 +335,7 @@ You will bind the form property that indicates its overall validity to the **Sub
</docs-step>
<docs-step title="Run the application">
Notice that the button is enabled —although it doesn't do anything useful yet.
Notice that the button is enabled — although it doesn't do anything useful yet.
</docs-step>
<docs-step title="Delete the Name value">
+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
@@ -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.
+21 -2
View File
@@ -376,9 +376,10 @@ The validation rules are:
- `Host` and `X-Forwarded-Host` headers are validated against a strict allowlist and cannot contain path separators.
- `X-Forwarded-Port` header must be numeric.
- `X-Forwarded-Proto` header must be `http` or `https`.
- `X-Forwarded-Prefix` header must not start with `\` or multiple `/` or contain `.`, `..` path segments.
- `X-Forwarded-Prefix` header must start with `/` and contain only alphanumeric characters, hyphens, and underscores, separated by single slashes.
- By default, all `X-Forwarded-*` headers are treated as untrusted and are removed from the request. To retain them, they must be explicitly allowed by configuring `trustProxyHeaders`.
Invalid or disallowed headers now trigger an error log. Requests with unrecognized hostnames will result in a Client-Side Rendered (CSR) page if `allowedHosts` is defined; if not, a `400 Bad Request` is issued. Note that in a future major release, all unrecognized hostnames will default to a `400 Bad Request` regardless of `allowedHosts` settings.
Invalid headers trigger an error log, and unallowed proxy headers are removed from the request. Requests with unrecognized hostnames will result in a `400 Bad Request` is issued.
NOTE: Most cloud providers and CDN providers perform automatic validation of these headers before a request ever reaches the application origin. This inherent filtering significantly reduces the practical attack surface.
@@ -433,6 +434,24 @@ export NG_ALLOWED_HOSTS="example.com,*.trusted-example.com"
IMPORTANT: You can use `*` as a value in `allowedHosts` to allow all hostnames, though this is generally discouraged and presents a security risk. Accepting any host header can expose your application to host header injection and [Server-Side Request Forgery (SSRF)](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/SSRF) attacks. This configuration should only be used when validation for `Host` and `X-Forwarded-Host` headers is performed in another layer, such as a load balancer or reverse proxy. For better security, we recommend using an explicit list of allowed hosts whenever possible. See [GHSA-x288-3778-4hhx](https://github.com/angular/angular-cli/security/advisories/GHSA-x288-3778-4hhx) for more details.
### Configuring trusted proxy headers
By default, Angular ignores all `X-Forwarded-*` headers. If your application is behind a trusted reverse proxy (like a load balancer) that sets these headers, you can configure Angular to trust them.
You can configure `trustProxyHeaders` when initializing the application engine:
```typescript
const appEngine = new AngularAppEngine({
trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'], // Trust specific headers
});
const nodeAppEngine = new AngularNodeAppEngine({
trustProxyHeaders: true, // Trust all X-Forwarded-* headers
});
```
IMPORTANT: Only enable `trustProxyHeaders` if your application is behind a trusted proxy that strictly validates or overrides these headers. Otherwise, attackers can spoof these headers to cause [Server-Side Request Forgery (SSRF)](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/SSRF) attacks.
## Auditing Angular applications
Angular applications must follow the same security principles as regular web applications, and must be audited as such.
@@ -133,7 +133,7 @@ class MyMenuItem {}
class MyMenu {
triggerText = input('');
@ContentChildren(MyMenuItem) items: QueryList<MyMenuItem>;
items = contentChildren(MyMenuItem);
}
```
@@ -15,6 +15,7 @@ generate_guides(
"//adev/src/assets/icons:forms.svg",
"//adev/src/assets/icons:language-service.svg",
"//adev/src/assets/icons:ng-update.svg",
"//adev/src/assets/icons:playground.svg",
"//adev/src/assets/icons:routing.svg",
"//adev/src/assets/icons:signals.svg",
"//adev/src/assets/icons:ssr.svg",
@@ -269,3 +269,7 @@ To learn more about Signal Forms and how it works, check out the in-depth guides
- [Form models](guide/forms/signals/models) - Creating and managing form data with signals
- [Field state management](guide/forms/signals/field-state-management) - Working with validation state, interaction tracking, and field visibility
- [Validation](guide/forms/signals/validation) - Built-in validators, custom validation rules, and async validation
<docs-pill-row>
<docs-pill title="Modular design with dependency injection" href="essentials/dependency-injection" />
</docs-pill-row>
@@ -147,6 +147,6 @@ TIP: Want to know more about Angular templates? See the [In-depth Templates guid
Now that you have dynamic data and templates in the application, it's time to learn how to enhance templates by conditionally hiding or showing certain elements, looping over elements, and more.
<docs-pill-row>
<docs-pill title="Modular design with dependency injection" href="essentials/dependency-injection" />
<docs-pill title="Forms with Signals" href="essentials/signal-forms" />
<docs-pill title="In-depth template guide" href="guide/templates" />
</docs-pill-row>
@@ -7,11 +7,9 @@ Get started with Angular quickly with online starters or locally with your termi
If you just want to play around with Angular in your browser without setting up a project, you can use our online sandbox:
<docs-card-container>
<docs-card title="" href="/playground" link="Open on Playground">
The fastest way to play with an Angular app. No setup required.
</docs-card>
</docs-card-container>
<docs-card title="Playground" href="/playground" link="Open on Playground" iconImgSrc="adev/src/assets/icons/playground.svg" titleInline>
The fastest way to play with an Angular app. No setup required.
</docs-card>
## Set up a new project locally
@@ -21,7 +19,7 @@ If you're starting a new project, you'll most likely want to create a local proj
- **Node.js** - [v20.19.0 or newer](/reference/versions)
- **Text editor** - We recommend [Visual Studio Code](https://code.visualstudio.com/)
- **Terminal** - Required for running Angular CLI commands
- **Terminal** - Required for running [Angular CLI](/tools/cli) commands
- **Development Tool** - To improve your development workflow, we recommend the [Angular Language Service](/tools/language-service)
### Instructions
@@ -51,7 +51,7 @@
Angular CLI's `ng update` runs automated code transformations that automatically handle routine breaking changes, dramatically simplifying major version updates. Keeping up with the latest version keeps your app as fast and secure as possible.
</docs-card>
<docs-card title="Language Service" href="tools/language-service" link="Language Service" iconImgSrc="adev/src/assets/icons/language-service.svg">
Angular's IDE language services powers code completion, navigation, refactoring, and real-time diagnostics in your favorite editor.
Angular's IDE language service powers code completion, navigation, refactoring, and real-time diagnostics in your favorite editor.
</docs-card>
</docs-card-container>
@@ -0,0 +1,46 @@
# Missing Control Value
This error occurs when you call `setValue` on a `FormGroup` or `FormArray` but the value you pass is missing an entry for one of the registered controls.
`setValue` is strict — it expects a value for every control. If you want to update only some controls, use `patchValue` instead.
## Debugging the error
Check which control is named in the error message, then make sure your value object includes it.
A common source of this error is spreading an object that doesn't have all the keys:
```typescript
const someValue = {first: 'Nancy'}; // 'last' is missing
form.setValue({...someValue}); // throws NG01002
```
This can happen if `someValue` comes from an API response, a partial state update, or a type that doesn't fully match the form structure. In those cases, either fill in the missing keys explicitly or switch to `patchValue`.
### FormGroup
```typescript
const form = new FormGroup({
first: new FormControl(''),
last: new FormControl(''),
});
// 'last' is not in the value — throws NG01002
form.setValue({first: 'Nancy'});
// both controls are covered — works fine
form.setValue({first: 'Nancy', last: 'Drew'});
```
### FormArray
```typescript
const formArray = new FormArray([new FormControl(''), new FormControl('')]);
// only one value for two controls — throws NG01002
formArray.setValue(['Nancy']);
// one value per control — works fine
formArray.setValue(['Nancy', 'Drew']);
```
@@ -0,0 +1,26 @@
# Orphan field in signal forms
This error indicates that a `Field` instance is no longer connected to the current model structure.
Angular signal forms throw `NG01902` when a field path that previously existed can no longer be resolved as the same property in the parent object.
A common trigger is unintentionally setting a field's value to `undefined`.
In signal forms, `undefined` means "field does not exist", so an existing field can become orphaned.
## Why this happens
Signal forms keep field instances in sync with your model shape.
If code still references an older field instance after the model shape changed, Angular detects the mismatch and throws this error.
This can happen when:
- a property is removed from the model object
- a property becomes `undefined` instead of remaining present
- code holds a stale field reference across structural updates
## How to fix it
- Avoid writing `undefined` to model fields. Use `null` or another explicit value when the field should still exist.
- Keep the model shape stable while a field is in use.
- Re-read fields from the current form tree after structural updates instead of reusing stale references.
For model design guidance, see [Avoid `undefined`](/guide/forms/signals/model-design#avoid-undefined).
+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,8 +36,10 @@
| `NG0951` | [Child query result is required but no value is available](errors/NG0951) |
| `NG0955` | [Track expression resulted in duplicated keys for a given collection](errors/NG0955) |
| `NG0956` | [Tracking expression caused re-creation of the DOM structure](errors/NG0956) |
| `NG01002` | [Missing Control Value](errors/NG01002) |
| `NG01101` | [Wrong Async Validator Return Type](errors/NG01101) |
| `NG01203` | [Missing value accessor](errors/NG01203) |
| `NG01902` | [Orphan field in signal forms](errors/NG01902) |
| `NG02200` | [Missing Iterable Differ](errors/NG02200) |
| `NG02800` | [JSONP support in HttpClient configuration](errors/NG02800) |
| `NG02802` | [Headers not transferred by HttpTransferCache](errors/NG02802) |
+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
}
}
}
+1 -1
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.
+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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -6,6 +6,7 @@ It also explains the basic mechanics of using `git`, `node`, and `pnpm`.
- [Building and Testing Angular](#building-and-testing-angular)
- [Prerequisite Software](#prerequisite-software)
- [Development in a Container](#development-in-a-container)
- [Experimenting in dev-app](#experimenting-in-dev-app)
- [Getting the Sources](#getting-the-sources)
- [Installing NPM Modules](#installing-npm-modules)
- [Building](#building)
@@ -57,6 +58,10 @@ following on your development machine:
You can also use the provided [Dev Container](https://containers.dev/) configuration to set up a development environment. This approach uses Docker to create a container with all the necessary tools (Node.js, pnpm, etc.) pre-installed.
## Experimenting in dev-app
For experimentation while developing Angular, consider running the [dev-app](../dev-app/README.md) in this repository.
**Prerequisites:**
- [Docker Desktop](https://www.docker.com/products/docker-desktop)
+4 -1
View File
@@ -5,7 +5,10 @@ particularly low-quality or low-value contributions, may see those contributions
automatically closed without consideration.
Community contributors should limit themselves to no more than 3 open PRs at a
single time.
single time. If you have more than 3 open PRs, we ask that you wait until some of those
PRs are merged or closed before opening more.
Draft pull requests are considered acceptable beyond the limit if 3, but within a reasonable limit (~10 or more within a short time frame). Beyond that reasonable limit, the team may still consider it spam and close them without consideration.
## Why?
+3 -3
View File
@@ -15,13 +15,13 @@
"@angular/platform-server": "workspace:*",
"@angular/localize": "workspace:*",
"@angular/router": "workspace:*",
"@angular/ssr": "21.2.6",
"@angular/ssr": "21.2.9",
"rxjs": "~7.8.0",
"tslib": "^2.3.0"
},
"devDependencies": {
"@angular/build": "21.2.6",
"@angular/cli": "21.2.6",
"@angular/build": "21.2.9",
"@angular/cli": "21.2.9",
"@angular/compiler-cli": "workspace:*",
"jsdom": "^28.0.0",
"typescript": "~5.9.2",
+1 -1
View File
@@ -21,7 +21,7 @@ export const enum RuntimeErrorCode {
// (undocumented)
MISSING_CONTROL = 1001,
// (undocumented)
MISSING_CONTROL_VALUE = 1002,
MISSING_CONTROL_VALUE = -1002,
// (undocumented)
NAME_AND_FORM_CONTROL_NAME_MUST_MATCH = 1202,
// (undocumented)
+6 -6
View File
@@ -11,7 +11,7 @@
},
"private": true,
"dependencies": {
"@angular/cdk": "21.2.5",
"@angular/cdk": "21.2.9",
"@angular/common": "link:./in-existing-linked-by-bazel",
"@angular/compiler": "link:./in-existing-linked-by-bazel",
"@angular/core": "link:./in-existing-linked-by-bazel",
@@ -19,15 +19,15 @@
"@angular/platform-browser": "link:./in-existing-linked-by-bazel",
"@angular/platform-browser-dynamic": "link:./in-existing-linked-by-bazel",
"@angular/router": "link:./in-existing-linked-by-bazel",
"@angular/ssr": "21.2.6",
"@angular/ssr": "21.2.9",
"rxjs": "^7.0.0",
"tslib": "^2.3.0",
"zone.js": "0.16.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "21.2.6",
"@angular/build": "21.2.6",
"@angular/cli": "21.2.6",
"@angular-devkit/build-angular": "21.2.9",
"@angular/build": "21.2.9",
"@angular/cli": "21.2.9",
"@angular/compiler-cli": "link:./in-existing-linked-by-bazel",
"@types/jasmine": "^6.0.0",
"@types/node": "^20.14.8",
@@ -38,5 +38,5 @@
"ts-node": "^10.9.1",
"typescript": "5.9.3"
},
"packageManager": "pnpm@10.33.0"
"packageManager": "pnpm@10.33.2"
}
+530 -556
View File
File diff suppressed because it is too large Load Diff
@@ -29,9 +29,9 @@
"zone.js": "0.16.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "21.2.6",
"@angular/build": "21.2.6",
"@angular/cli": "21.2.6",
"@angular-devkit/build-angular": "21.2.9",
"@angular/build": "21.2.9",
"@angular/cli": "21.2.9",
"@angular/compiler-cli": "link:./in-existing-linked-by-bazel",
"@types/jasmine": "^6.0.0",
"@types/jasminewd2": "^2.0.8",
@@ -45,5 +45,5 @@
"ts-node": "^10.9.1",
"typescript": "5.9.3"
},
"packageManager": "pnpm@10.33.0"
"packageManager": "pnpm@10.33.2"
}
File diff suppressed because it is too large Load Diff
@@ -13,19 +13,19 @@
"@angular/core": "link:./in-existing-linked-by-bazel",
"@angular/platform-browser": "link:./in-existing-linked-by-bazel",
"@angular/router": "link:./in-existing-linked-by-bazel",
"@angular/ssr": "21.2.6",
"@angular/ssr": "21.2.9",
"rxjs": "^7.0.0",
"tslib": "^2.3.0",
"zone.js": "0.16.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "21.2.6",
"@angular/build": "21.2.6",
"@angular/cli": "21.2.6",
"@angular-devkit/build-angular": "21.2.9",
"@angular/build": "21.2.9",
"@angular/cli": "21.2.9",
"@angular/compiler-cli": "link:./in-existing-linked-by-bazel",
"@types/node": "^20.14.8",
"ts-node": "^10.9.1",
"typescript": "5.9.3"
},
"packageManager": "pnpm@10.33.0"
"packageManager": "pnpm@10.33.2"
}
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -18,19 +18,19 @@
"@angular/forms": "link:./in-existing-linked-by-bazel",
"@angular/platform-browser": "link:./in-existing-linked-by-bazel",
"@angular/router": "link:./in-existing-linked-by-bazel",
"@angular/ssr": "21.2.6",
"@angular/ssr": "21.2.9",
"rxjs": "^7.0.0",
"tslib": "^2.3.0",
"zone.js": "0.16.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "21.2.6",
"@angular/build": "21.2.6",
"@angular/cli": "21.2.6",
"@angular-devkit/build-angular": "21.2.9",
"@angular/build": "21.2.9",
"@angular/cli": "21.2.9",
"@angular/compiler-cli": "link:./in-existing-linked-by-bazel",
"@types/node": "^20.14.8",
"ts-node": "^10.9.1",
"typescript": "5.9.3"
},
"packageManager": "pnpm@10.33.0"
"packageManager": "pnpm@10.33.2"
}
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -10,7 +10,7 @@
"lint": "ng lint",
"e2e": "ng build --configuration production && concurrently \"serve dist/browser -l 4210 --no-clipboard --single\" \"protractor e2e/protractor.conf.js --baseUrl=http://localhost:4210\" --kill-others --success first"
},
"packageManager": "pnpm@10.33.0",
"packageManager": "pnpm@10.33.2",
"private": true,
"dependencies": {
"@angular/animations": "link:./in-existing-linked-by-bazel",
@@ -25,8 +25,8 @@
"zone.js": "0.16.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "21.2.6",
"@angular/cli": "21.2.6",
"@angular-devkit/build-angular": "21.2.9",
"@angular/cli": "21.2.9",
"@angular/compiler-cli": "link:./in-existing-linked-by-bazel",
"@types/jasmine": "^6.0.0",
"@types/jasminewd2": "^2.0.8",
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -23,12 +23,12 @@
"zone.js": "0.16.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "21.2.6",
"@angular/build": "21.2.6",
"@angular/cli": "21.2.6",
"@angular-devkit/build-angular": "21.2.9",
"@angular/build": "21.2.9",
"@angular/cli": "21.2.9",
"@angular/compiler-cli": "link:./in-existing-linked-by-bazel",
"ts-node": "10.9.2",
"typescript": "5.9.3"
},
"packageManager": "pnpm@10.33.0"
"packageManager": "pnpm@10.33.2"
}

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